socketsupply / socket

A cross-platform runtime for Web developers to build desktop & mobile apps for any OS using any frontend library.
https://socketsupply.co/guides
Other
1.6k stars 75 forks source link

Introduce VM module #914

Closed jwerle closed 7 months ago

jwerle commented 7 months ago

This pull request introduces the socket:vm module. It is similar in API surface to the node:vm module, but all exported functions are async as the internal semantics, context execution, and JavaScript environment are completely different.

import vm from 'socket:vm'

const result = await vm.runInContext('1 + 2 + 3')
console.log(result) // 6

Users of this module can supply a custom context object that is available in the globalThis scope of the execution context. Values that can be passed to a script virtual machine are the same that can be passed to a worker. We introduce a "reference proxy" that can be used to reference object instances in a VM to the caller. Functions are automatically detected, referenced, and made available as a proxy call.

import vm from 'socket:vm'

const context = {
  scope: {}
}

await vm.runInContext(
  `scope.decode = (buffer) => new TextDecoder().decode(buffer)`,
  { context }
)

const text = await scope.decode(new TextEncoder().encode('hello world')) // hello world

Virtual machine source scripts can be used in a module module or classic (default). When source scripts are modules, they can use import/export syntax. By default, all virtual machine scripts are executed in an async context granting "top level await" to the script.

import vm from 'socket:vm'

const source = `
import fs from 'socket:fs/promises'
export default fs.readdir('.')
`

const module = await rm.runInContext(source)
console.log(module.default) // [ { name: 'Credits.html' }, { name: 'icon.png' },  { name: 'index.html' }, ... ]

Virtual machine source scripts run in isolated contexts that are actually a mostly complete implementation of a Shadow Realm context. All environment intrinsics are preserved and non-configurable. Every script VM has a context and global scope that can the caller can supply and interact with.

import vm from 'socket:vm'

const channel = new MessageChannel()
const context = { scope: {} }
const source = `
let port = null

export function setMessagePort (messagePort) {
  port = messagePort
  port.start()
}

export function postMessage (message) {
  const transfer = vm.getTransferables(message)
  port.postMessage(message, { transfer })
}
`

const module = await rm.runInContext(source, { context })
const decoder = new TextDecoder()
const encoder = new TextEncoder()

channel.port1.start()
channel.port1.onmessage = (event) => {
  console.log(decoder.decode(event.data)) // 'hello world'
}

// send 'hello world' as buffer to VM module which
// will echo it back to us to decode above in the `onmessage`
// event handler for the `MessageChannel` we are sharing with
// the VM module
module.postMessage(encoder.encode('hello world'))