diff --git a/.vscode/launch.json b/.vscode/launch.json index 3e349f69..d1d68099 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,15 @@ "program": "${file}", "args": [], "preLaunchTask": "CMake: build ${input:target}" - } + }, + { + "type": "node", + "request": "launch", + "name": "wasi-threads test", + "runtimeArgs": [], + "program": "${workspaceFolder}/packages/wasi-threads/test/index.js", + "args": [] + }, ], "inputs": [ { diff --git a/packages/core/src/load.ts b/packages/core/src/load.ts index 0fe3ea61..1732db8c 100644 --- a/packages/core/src/load.ts +++ b/packages/core/src/load.ts @@ -1,4 +1,4 @@ -import { WASIThreads } from '@emnapi/wasi-threads' +import { WASIThreads, createInstanceProxy } from '@emnapi/wasi-threads' import { type InputType, load, loadSync } from './util' import { createNapiModule } from './emnapi/index' import type { CreateOptions, NapiModule } from './emnapi/index' @@ -131,54 +131,7 @@ function loadNapiModuleImpl (loadFn: Function, userNapiModule: NapiModule | unde const module = source.module if (wasi) { if (napiModule.childThread) { - // https://github.com/nodejs/help/issues/4102 - const createHandler = function (target: WebAssembly.Exports): ProxyHandler { - const handlers = [ - 'apply', - 'construct', - 'defineProperty', - 'deleteProperty', - 'get', - 'getOwnPropertyDescriptor', - 'getPrototypeOf', - 'has', - 'isExtensible', - 'ownKeys', - 'preventExtensions', - 'set', - 'setPrototypeOf' - ] - const handler: ProxyHandler = {} - for (let i = 0; i < handlers.length; i++) { - const name = handlers[i] as keyof ProxyHandler - handler[name] = function () { - const args = Array.prototype.slice.call(arguments, 1) - args.unshift(target) - return (Reflect[name] as any).apply(Reflect, args) - } - } - return handler - } - const handler = createHandler(originalExports) - const noop = (): void => {} - handler.get = function (_target, p, receiver) { - if (p === 'memory') { - return memory - } - if (p === '_initialize') { - return noop - } - return Reflect.get(originalExports, p, receiver) - } - const exportsProxy = new Proxy(Object.create(null), handler) - instance = new Proxy(instance, { - get (target, p, receiver) { - if (p === 'exports') { - return exportsProxy - } - return Reflect.get(target, p, receiver) - } - }) + instance = createInstanceProxy(instance, memory) } wasi.initialize(instance) } diff --git a/packages/wasi-threads/.gitignore b/packages/wasi-threads/.gitignore index 21498aa8..152d3668 100644 --- a/packages/wasi-threads/.gitignore +++ b/packages/wasi-threads/.gitignore @@ -2,3 +2,5 @@ node_modules /dist /src/emnapi/**/*.js +/test/**/*.wasm +/test/**/*.wat diff --git a/packages/wasi-threads/.npmignore b/packages/wasi-threads/.npmignore index f944142a..d6275066 100644 --- a/packages/wasi-threads/.npmignore +++ b/packages/wasi-threads/.npmignore @@ -7,3 +7,4 @@ node_modules .npmignore api-extractor.json tsconfig.json +/test diff --git a/packages/wasi-threads/src/index.ts b/packages/wasi-threads/src/index.ts index f81da414..3bfa0fed 100644 --- a/packages/wasi-threads/src/index.ts +++ b/packages/wasi-threads/src/index.ts @@ -6,3 +6,5 @@ export { WASIThreads } from './wasi-threads' export { MessageHandler } from './worker' export type { OnLoadData, HandleOptions } from './worker' + +export { ExecutionModel, createInstanceProxy } from './proxy' diff --git a/packages/wasi-threads/src/proxy.ts b/packages/wasi-threads/src/proxy.ts new file mode 100644 index 00000000..9ce9f1d2 --- /dev/null +++ b/packages/wasi-threads/src/proxy.ts @@ -0,0 +1,72 @@ +/** @public */ +export enum ExecutionModel { + Command = 'command', + Reactor = 'reactor' +} + +/** @public */ +export function createInstanceProxy ( + instance: WebAssembly.Instance, + memory: WebAssembly.Memory | (() => WebAssembly.Memory), + model: ExecutionModel = ExecutionModel.Reactor +): WebAssembly.Instance { + // https://github.com/nodejs/help/issues/4102 + const originalExports = instance.exports + const createHandler = function (target: WebAssembly.Exports): ProxyHandler { + const handlers = [ + 'apply', + 'construct', + 'defineProperty', + 'deleteProperty', + 'get', + 'getOwnPropertyDescriptor', + 'getPrototypeOf', + 'has', + 'isExtensible', + 'ownKeys', + 'preventExtensions', + 'set', + 'setPrototypeOf' + ] + const handler: ProxyHandler = {} + for (let i = 0; i < handlers.length; i++) { + const name = handlers[i] as keyof ProxyHandler + handler[name] = function () { + const args = Array.prototype.slice.call(arguments, 1) + args.unshift(target) + return (Reflect[name] as any).apply(Reflect, args) + } + } + return handler + } + const handler = createHandler(originalExports) + const _initialize = (): void => {} + const _start = (): number => 0 + handler.get = function (_target, p, receiver) { + if (p === 'memory') { + return typeof memory === 'function' ? memory() : memory + } + if (p === '_initialize') { + return model === ExecutionModel.Reactor ? _initialize : undefined + } + if (p === '_start') { + return model === ExecutionModel.Command ? _start : undefined + } + return Reflect.get(originalExports, p, receiver) + } + handler.has = function (_target, p) { + if (p === 'memory') return true + if (p === '_initialize') return model === ExecutionModel.Reactor + if (p === '_start') return model === ExecutionModel.Command + return Reflect.has(originalExports, p) + } + const exportsProxy = new Proxy(Object.create(null), handler) + return new Proxy(instance, { + get (target, p, receiver) { + if (p === 'exports') { + return exportsProxy + } + return Reflect.get(target, p, receiver) + } + }) +} diff --git a/packages/wasi-threads/test/index.js b/packages/wasi-threads/test/index.js index 70b786d1..9cbbfc57 100644 --- a/packages/wasi-threads/test/index.js +++ b/packages/wasi-threads/test/index.js @@ -1 +1,98 @@ -// TODO +const fs = require('node:fs') +const { join } = require('node:path') +const { spawnSync } = require('node:child_process') +const { Worker } = require('node:worker_threads') +const { WASI } = require('wasi') +const { WASIThreads, ThreadManager, ExecutionModel } = require('..') + +function build (model = ExecutionModel.Reactor) { + const bin = join(process.env.WASI_SDK_PATH, 'bin', 'clang') + (process.platform === 'win32' ? '.exe' : '') + const args = [ + '-o', join(__dirname, model === ExecutionModel.Command ? 'main.wasm' : 'lib.wasm'), + '-mbulk-memory', + '-matomics', + `-mexec-model=${model}`, + ...(model === ExecutionModel.Command + ? [ + '-D__WASI_COMMAND__=1' + ] + : [ + '-Wl,--no-entry' + ] + ), + '--target=wasm32-wasi-threads', + // '-O3', + '-g', + '-pthread', + '-Wl,--import-memory', + '-Wl,--shared-memory', + '-Wl,--export-memory', + '-Wl,--export-dynamic', + '-Wl,--max-memory=2147483648', + '-Wl,--export=malloc,--export=free', + join(__dirname, 'main.c') + ] + console.log(`> "${bin}" ${args.map(s => s.includes(' ') ? `"${s}"` : s).join(' ')}`) + spawnSync(bin, args, { + stdio: 'inherit', + env: process.env + }) +} + +function run (model = ExecutionModel.Reactor) { + const wasi = new WASI({ + version: 'preview1', + env: process.env + }) + const wasiThreads = new WASIThreads({ + threadManager: new ThreadManager({ + printErr: console.error.bind(console), + onCreateWorker: ({ name }) => { + return new Worker(join(__dirname, 'worker.js'), { + name, + workerData: { + name, + model + }, + env: process.env, + execArgv: ['--experimental-wasi-unstable-preview1'] + }) + } + }) + }) + const memory = new WebAssembly.Memory({ + initial: 16777216 / 65536, + maximum: 2147483648 / 65536, + shared: true + }) + const file = join(__dirname, model === ExecutionModel.Command ? 'main.wasm' : 'lib.wasm') + return WebAssembly.instantiate(fs.readFileSync(file), { + env: { + memory + }, + ...wasi.getImportObject(), + ...wasiThreads.getImportObject() + }).then(({ module, instance }) => { + wasiThreads.setup(instance, module, memory) + if (model === ExecutionModel.Command) { + return wasi.start(instance) + } else { + wasi.initialize(instance) + return instance.exports.fn() + } + }) +} + +async function main () { + build(ExecutionModel.Command) + build(ExecutionModel.Reactor) + console.log('-------- command --------') + await run(ExecutionModel.Command) + console.log('-------- reactor --------') + await run(ExecutionModel.Reactor) +} + +main().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/packages/wasi-threads/test/main.c b/packages/wasi-threads/test/main.c new file mode 100644 index 00000000..3bf3060d --- /dev/null +++ b/packages/wasi-threads/test/main.c @@ -0,0 +1,36 @@ +#include +#include +#include + +void *print_message_function(void *ptr) { + char *message; + message = (char *)ptr; + printf("%s \n", message); + return NULL; +} + +__attribute__((visibility("default"))) +int fn() { + pthread_t thread1, thread2; + const char *message1 = "Thread 1"; + const char *message2 = "Thread 2"; + int iret1, iret2; + + iret1 = pthread_create(&thread1, NULL, print_message_function, + (void *)message1); + iret2 = pthread_create(&thread2, NULL, print_message_function, + (void *)message2); + + pthread_join(thread1, NULL); + pthread_join(thread2, NULL); + + printf("Thread 1 returns: %d\n", iret1); + printf("Thread 2 returns: %d\n", iret2); + return 0; +} + +#ifdef __WASI_COMMAND__ +int main() { + return fn(); +} +#endif diff --git a/packages/wasi-threads/test/worker.js b/packages/wasi-threads/test/worker.js new file mode 100644 index 00000000..780d4113 --- /dev/null +++ b/packages/wasi-threads/test/worker.js @@ -0,0 +1,102 @@ +/* eslint-disable no-eval */ +/* eslint-disable no-undef */ + +(function () { + // const log = (...args) => { + // const str = require('util').format(...args) + // require('fs').writeSync(1, str + '\n') + // } + // const error = (...args) => { + // const str = require('util').format(...args) + // require('fs').writeSync(2, str + '\n') + // } + let WASI, wasiThreads, name, model + + const ENVIRONMENT_IS_NODE = + typeof process === 'object' && process !== null && + typeof process.versions === 'object' && process.versions !== null && + typeof process.versions.node === 'string' + + if (ENVIRONMENT_IS_NODE) { + const nodeWorkerThreads = require('worker_threads') + name = nodeWorkerThreads.workerData.name + model = nodeWorkerThreads.workerData.model + + const parentPort = nodeWorkerThreads.parentPort + + wasiThreads = require('..') + + parentPort.on('message', (data) => { + globalThis.onmessage({ data }) + }) + + Object.assign(globalThis, { + self: globalThis, + require, + Worker: nodeWorkerThreads.Worker, + importScripts: function (f) { + (0, eval)(fs.readFileSync(f, 'utf8') + '//# sourceURL=' + f) + }, + postMessage: function (msg) { + parentPort.postMessage(msg) + } + }) + + WASI = require('node:wasi').WASI + } else { + importScripts('../dist/wasi-threads.js') + WASI = globalThis.wasmUtil.WASI + name = globalThis.name + } + + console.log(`name: ${name}`) + + const { MessageHandler, WASIThreads, createInstanceProxy } = wasiThreads + const postMessage = typeof globalThis.postMessage === 'function' + ? globalThis.postMessage + : function (msg) { + parentPort.postMessage(msg) + } + + const handler = new MessageHandler({ + onLoad ({ wasmModule, wasmMemory }) { + const wasi = new WASI({ + version: 'preview1', + env: process.env, + print: ENVIRONMENT_IS_NODE + ? (...args) => { + const str = require('util').format(...args) + require('fs').writeSync(1, str + '\n') + } + : function () { console.log.apply(console, arguments) } + }) + + const wasiThreads = new WASIThreads({ + postMessage + }) + + const instance = createInstanceProxy(new WebAssembly.Instance(wasmModule, { + env: { + memory: wasmMemory + }, + ...wasi.getImportObject(), + ...wasiThreads.getImportObject() + }), wasmMemory, model) + + wasiThreads.setup(instance, wasmModule, wasmMemory) + if (model === 'reactor') { + wasi.initialize(instance) + } else { + wasi.start(instance) + } + + return { module: wasmModule, instance } + }, + postMessage + }) + + globalThis.onmessage = function (e) { + handler.handle(e) + // handle other messages + } +})()