diff --git a/.eslintignore b/.eslintignore index 2b66ad91..83b7c2bb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,5 @@ **/dist +**/lib node_modules /lib/typings .eslintrc.js @@ -9,3 +10,4 @@ node_modules /packages/emnapi/lib /packages/emnapi/transformer/out /packages/core/src/index.js +/out diff --git a/.eslintrc.js b/.eslintrc.js index fb6b0fb0..5394cdf3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,6 +12,7 @@ module.exports = { 'spaced-comment': 'off', 'no-new-func': 'off', 'no-implied-eval': 'off', + 'no-var': 'off', '@typescript-eslint/no-implied-eval': 'off', '@typescript-eslint/no-unused-vars': 'error', '@typescript-eslint/no-namespace': 'off', @@ -24,6 +25,10 @@ module.exports = { '@typescript-eslint/no-dynamic-delete': 'off', '@typescript-eslint/method-signature-style': 'off', '@typescript-eslint/prefer-includes': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/consistent-type-imports': 'off', + '@typescript-eslint/consistent-generic-constructors': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'off', '@typescript-eslint/member-delimiter-style': ['error', { multiline: { diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f6bee0c7..08ba3350 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: - 'wasm64-unknown-emscripten' - 'wasm32-unknown-unknown' - 'wasm32-wasi' - # - 'wasm32-wasi-threads' + - 'wasm32-wasi-threads' steps: - uses: actions/checkout@v3 diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index a6229cba..36ad83c9 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -74,6 +74,16 @@ "cppStandard": "c++14", "includePath": ["${includePath}"] }, + { + "name": "WASI-THREADS", + "defines": ["${defines}", "_REENTRANT"], + "compilerPath": "${env:WASI_SDK_PATH}/bin/clang", + "intelliSenseMode": "clang-x86", + "cStandard": "c11", + "cppStandard": "c++14", + "includePath": ["${includePath}"], + "compilerArgs": ["--target=wasm32-wasi-threads"] + }, { "name": "WASM32", "defines": ["${defines}", "PAGESIZE=65536"], diff --git a/.vscode/launch.json b/.vscode/launch.json index 00c193f5..e73858a9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,10 +14,12 @@ "request": "launch", "name": "Launch Program", "env": { - "EMNAPI_TEST_WASI": "1" + "NODE_TEST_KNOWN_GLOBALS": "0", + "EMNAPI_TEST_WASI": "1", + "EMNAPI_TEST_WASI_THREADS": "1" }, "runtimeArgs": ["--experimental-wasi-unstable-preview1", "--expose-gc"], - "program": "${workspaceFolder}/packages/test/arg/arg.test.js", + "program": "${workspaceFolder}/packages/test/node-addon-api/async_worker.test.js", "args": [] }, { diff --git a/package.json b/package.json index fc4322bb..f7c80cf8 100644 --- a/package.json +++ b/package.json @@ -31,13 +31,13 @@ "@tybys/tsapi": "^0.6.0", "@types/fs-extra": "^9.0.13", "@types/node": "^16.11.21", - "@typescript-eslint/eslint-plugin": "^5.32.0", - "@typescript-eslint/parser": "^5.32.0", - "eslint": "^8.21.0", - "eslint-config-standard-with-typescript": "^22.0.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-n": "^15.2.4", - "eslint-plugin-promise": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^5.52.0", + "@typescript-eslint/parser": "^5.52.0", + "eslint": "^8.34.0", + "eslint-config-standard-with-typescript": "^34.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-n": "^15.6.1", + "eslint-plugin-promise": "^6.1.1", "fs-extra": "^10.1.0", "typescript": "~4.8.4" }, diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts index f25ddf45..f93d1d42 100644 --- a/packages/core/index.d.ts +++ b/packages/core/index.d.ts @@ -1,22 +1,36 @@ import type { Context } from '@emnapi/runtime' -export declare interface CreateOptions { - context: Context - filename?: string - nodeBinding?: { - node: { - emitAsyncInit: Function - emitAsyncDestroy: Function - makeCallback: Function - } - napi: { - asyncInit: Function - asyncDestroy: Function - makeCallback: Function - } +export declare interface NodeBinding { + node: { + emitAsyncInit: Function + emitAsyncDestroy: Function + makeCallback: Function + } + napi: { + asyncInit: Function + asyncDestroy: Function + makeCallback: Function } } +export declare type BaseCreateOptions = { + filename?: string + nodeBinding?: NodeBinding + reuseWorker?: boolean + onCreateWorker?: () => any + print?: (str: string) => void + printErr?: (str: string) => void + postMessage?: (msg: any) => any +} + +export declare type CreateOptions = BaseCreateOptions & ({ + context: Context + childThread?: boolean +} | { + context?: Context + childThread: true +}) + export declare interface PointerInfo { address: number ownership: 0 | 1 @@ -39,6 +53,7 @@ export declare interface NapiModule { exports: any loaded: boolean filename: string + childThread: boolean emnapi: { syncMemory ( js_to_wasm: boolean, @@ -50,6 +65,70 @@ export declare interface NapiModule { } init (options: InitOptions): any + spawnThread (startArg: number, errorOrTid?: number): number + startThread (tid: number, startArg: number): void + postMessage?: (msg: any) => any +} + +export declare function createNapiModule ( + options: CreateOptions +): NapiModule + +export declare interface ReactorWASI { + readonly wasiImport?: Record + initialize (instance: object): void + getImportObject? (): any +} + +export declare interface LoadOptions { + wasi?: ReactorWASI + overwriteImports?: (importObject: WebAssembly.Imports) => WebAssembly.Imports + getMemory?: (exports: WebAssembly.Exports) => WebAssembly.Memory + getTable?: (exports: WebAssembly.Exports) => WebAssembly.Table } -export function createNapiModule (options: CreateOptions): NapiModule +export declare type InstantiateOptions = CreateOptions & LoadOptions + +export declare interface InstantiatedSource extends WebAssembly.WebAssemblyInstantiatedSource { + napiModule: NapiModule +} + +export declare type InputType = string | URL | Response | BufferSource | WebAssembly.Module + +export declare function loadNapiModule ( + napiModule: NapiModule, + /** Only support `BufferSource` or `WebAssembly.Module` on Node.js */ + wasmInput: InputType | Promise, + options?: LoadOptions +): Promise + +export declare function loadNapiModuleSync ( + napiModule: NapiModule, + wasmInput: BufferSource | WebAssembly.Module, + options?: LoadOptions +): WebAssembly.WebAssemblyInstantiatedSource + +export declare function instantiateNapiModule ( + /** Only support `BufferSource` or `WebAssembly.Module` on Node.js */ + wasmInput: InputType | Promise, + options: InstantiateOptions +): Promise + +export declare function instantiateNapiModuleSync ( + wasmInput: BufferSource | WebAssembly.Module, + options: InstantiateOptions +): InstantiatedSource + +export declare interface OnLoadData { + wasmModule: WebAssembly.Module + wasmMemory: WebAssembly.Memory +} + +export declare interface HandleOptions { + onLoad (data: OnLoadData): InstantiatedSource | Promise +} + +export declare class MessageHandler { + constructor (options: HandleOptions) + handle (e: { data: any }): void +} diff --git a/packages/core/script/build.js b/packages/core/script/build.js index 24e29f7d..fb4da97a 100644 --- a/packages/core/script/build.js +++ b/packages/core/script/build.js @@ -5,8 +5,24 @@ const rollupNodeResolve = require('@rollup/plugin-node-resolve').default const rollupReplace = require('@rollup/plugin-replace').default const rollupTerser = require('rollup-plugin-terser').terser const dist = path.join(__dirname, '../dist') +const { compile } = require('@tybys/tsapi') function build () { + compile(path.join(__dirname, '../tsconfig.json'), { + optionsToExtend: { + target: require('typescript').ScriptTarget.ES5, + outDir: path.join(__dirname, '../lib/es5') + } + }) + compile(path.join(__dirname, '../tsconfig.json'), { + optionsToExtend: { + target: require('typescript').ScriptTarget.ES2019, + outDir: path.join(__dirname, '../lib/es2019'), + removeComments: true, + downlevelIteration: false + } + }) + /** * @param {'es5' | 'es2019'} esversion * @param {boolean=} minify @@ -14,7 +30,7 @@ function build () { */ function createInput (esversion, minify, options) { return { - input: path.join(__dirname, '../src/index.js'), + input: path.join(__dirname, '../lib', esversion, 'index.js'), plugins: [ rollupNodeResolve({ mainFields: ['module', 'main'], diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 1ac15369..08069ff0 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,3 +1,11 @@ export { createNapiModule } from './module.js' +export { + loadNapiModule, + loadNapiModuleSync, + instantiateNapiModule, + instantiateNapiModuleSync +} from './load.js' + +export { MessageHandler } from './worker.js' export const version = __VERSION__ diff --git a/packages/core/src/load.js b/packages/core/src/load.js new file mode 100644 index 00000000..32ad961f --- /dev/null +++ b/packages/core/src/load.js @@ -0,0 +1,192 @@ +import { load, loadSync } from './util.js' +import { createNapiModule } from './module.js' + +function loadNapiModuleImpl (loadFn, userNapiModule, wasmInput, options) { + options = options == null ? {} : options + + const getMemory = options.getMemory + const getTable = options.getTable + if (getMemory != null && typeof getMemory !== 'function') { + throw new TypeError('options.getMemory is not a function') + } + if (getTable != null && typeof getTable !== 'function') { + throw new TypeError('options.getTable is not a function') + } + + let napiModule + const isLoad = typeof userNapiModule === 'object' && userNapiModule !== null + if (isLoad) { + if (userNapiModule.loaded) { + throw new Error('napiModule has already loaded') + } + napiModule = userNapiModule + } else { + napiModule = createNapiModule(options) + } + + const wasi = options.wasi + let importObject = { + env: napiModule.imports.env, + napi: napiModule.imports.napi, + emnapi: napiModule.imports.emnapi, + wasi: { + // eslint-disable-next-line camelcase + 'thread-spawn': function __imported_wasi_thread_spawn (startArg, errorOrTid) { + return napiModule.spawnThread(startArg, errorOrTid) + } + } + } + + if (wasi) { + Object.assign( + importObject, + typeof wasi.getImportObject === 'function' + ? wasi.getImportObject() + : { wasi_snapshot_preview1: wasi.wasiImport } + ) + } + + const overwriteImports = options.overwriteImports + if (typeof overwriteImports === 'function') { + const newImportObject = overwriteImports(importObject) + if (typeof newImportObject === 'object' && newImportObject !== null) { + importObject = newImportObject + } + } + + return loadFn(wasmInput, importObject, (err, source) => { + if (err) { + throw err + } + + let instance = source.instance + const exports = instance.exports + + const exportMemory = 'memory' in exports + const importMemory = 'memory' in importObject.env + /** @type {WebAssembly.Memory} */ + const memory = getMemory + ? getMemory(exports) + : exportMemory + ? exports.memory + : importMemory + ? importObject.env.memory + : undefined + if (!memory) { + throw new Error('memory is neither exported nor imported') + } + const table = getTable ? getTable(exports) : exports.__indirect_function_table + if (wasi && !exportMemory) { + instance = { + exports: Object.assign({}, exports, { memory }) + } + } + const module = source.module + if (wasi) { + if (napiModule.childThread) { + // https://github.com/nodejs/help/issues/4102 + const createHandler = function (target) { + const handlers = [ + 'apply', + 'construct', + 'defineProperty', + 'deleteProperty', + 'get', + 'getOwnPropertyDescriptor', + 'getPrototypeOf', + 'has', + 'isExtensible', + 'ownKeys', + 'preventExtensions', + 'set', + 'setPrototypeOf' + ] + const handler = {} + for (let i = 0; i < handlers.length; i++) { + const name = handlers[i] + handler[name] = function () { + const args = Array.prototype.slice.call(arguments, 1) + args.unshift(target) + return Reflect[name].apply(Reflect, args) + } + } + return handler + } + const handler = createHandler(exports) + const noop = () => {} + handler.get = function (target, p, receiver) { + if (p === 'memory') { + return memory + } + if (p === '_initialize') { + return noop + } + return Reflect.get(exports, 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) + } + }) + } + wasi.initialize(instance) + } + + napiModule.init({ + instance, + module, + memory, + table + }) + + const ret = { instance, module } + if (!isLoad) { + ret.napiModule = napiModule + } + return ret + }) +} + +function loadCallback (wasmInput, importObject, callback) { + return load(wasmInput, importObject).then((source) => { + return callback(null, source) + }, err => { + return callback(err) + }) +} + +function loadSyncCallback (wasmInput, importObject, callback) { + let source + try { + source = loadSync(wasmInput, importObject) + } catch (err) { + return callback(err) + } + return callback(null, source) +} + +export function loadNapiModule (napiModule, wasmInput, options) { + if (typeof napiModule !== 'object' || napiModule === null) { + throw new TypeError('Invalid napiModule') + } + return loadNapiModuleImpl(loadCallback, napiModule, wasmInput, options) +} + +export function loadNapiModuleSync (napiModule, wasmInput, options) { + if (typeof napiModule !== 'object' || napiModule === null) { + throw new TypeError('Invalid napiModule') + } + return loadNapiModuleImpl(loadSyncCallback, napiModule, wasmInput, options) +} + +export function instantiateNapiModule (wasmInput, options) { + return loadNapiModuleImpl(loadCallback, undefined, wasmInput, options) +} + +export function instantiateNapiModuleSync (wasmInput, options) { + return loadNapiModuleImpl(loadSyncCallback, undefined, wasmInput, options) +} diff --git a/packages/core/src/util.js b/packages/core/src/util.js new file mode 100644 index 00000000..795c5768 --- /dev/null +++ b/packages/core/src/util.js @@ -0,0 +1,93 @@ +/* eslint-disable no-undef */ + +const _WebAssembly = typeof WebAssembly !== 'undefined' + ? WebAssembly + : typeof WXWebAssembly !== 'undefined' + ? WXWebAssembly + : undefined + +export { _WebAssembly } + +function validateImports (imports) { + if (imports && typeof imports !== 'object') { + throw new TypeError('imports must be an object or undefined') + } +} + +export function load (wasmInput, imports) { + if (!wasmInput) throw new TypeError('Invalid wasm source') + validateImports(imports) + imports = imports != null ? imports : {} + + // Promise + try { + const then = wasmInput.then + if (typeof then === 'function') { + return then.call(wasmInput, (input) => load(input, imports)) + } + } catch (_) {} + + // BufferSource + if (wasmInput instanceof ArrayBuffer || ArrayBuffer.isView(wasmInput)) { + return _WebAssembly.instantiate(wasmInput, imports) + } + + // WebAssembly.Module + if (wasmInput instanceof _WebAssembly.Module) { + return _WebAssembly.instantiate(wasmInput, imports).then((instance) => { + return { instance, module: wasmInput } + }) + } + + // Response + if (typeof Response !== 'undefined' && wasmInput instanceof Response) { + return wasmInput.arrayBuffer().then(buffer => { + return _WebAssembly.instantiate(buffer, imports) + }) + } + + // string | URL + const inputIsString = typeof wasmInput === 'string' + if (inputIsString || (typeof URL !== 'undefined' && wasmInput instanceof URL)) { + if (inputIsString && typeof wx !== 'undefined' && typeof __wxConfig !== 'undefined') { + return _WebAssembly.instantiate(wasmInput, imports) + } + if (typeof fetch !== 'function') { + throw new TypeError('wasm source can not be a string or URL in this environment') + } + if (typeof _WebAssembly.instantiateStreaming === 'function') { + try { + return _WebAssembly.instantiateStreaming(fetch(wasmInput), imports).catch(() => { + return load(fetch(wasmInput), imports) + }) + } catch (_) { + return load(fetch(wasmInput), imports) + } + } else { + return load(fetch(wasmInput), imports) + } + } + + throw new TypeError('Invalid wasm source') +} + +export function loadSync (wasmInput, imports) { + if (!wasmInput) throw new TypeError('Invalid wasm source') + validateImports(imports) + imports = imports != null ? imports : {} + + let module + + if ((wasmInput instanceof ArrayBuffer) || ArrayBuffer.isView(wasmInput)) { + module = new _WebAssembly.Module(wasmInput) + } else if (wasmInput instanceof WebAssembly.Module) { + module = wasmInput + } else { + throw new TypeError('Invalid wasm source') + } + + const instance = new _WebAssembly.Instance(module, imports) + const source = { instance, module } + + return source +} diff --git a/packages/core/src/worker.js b/packages/core/src/worker.js new file mode 100644 index 00000000..e8306755 --- /dev/null +++ b/packages/core/src/worker.js @@ -0,0 +1,79 @@ +export class MessageHandler { + constructor (options) { + const onLoad = options.onLoad + if (typeof onLoad !== 'function') { + throw new TypeError('options.onLoad is not a function') + } + this.onLoad = onLoad + this.instance = undefined + // this.module = undefined + this.napiModule = undefined + this.messagesBeforeLoad = [] + } + + handle (e) { + if (e && e.data && e.data.__emnapi__) { + const type = e.data.__emnapi__.type + const payload = e.data.__emnapi__.payload + + const onLoad = this.onLoad + if (type === 'load') { + if (this.instance !== undefined) return + const source = onLoad(payload) + const then = source && source.then + if (typeof then === 'function') { + then.call( + source, + (source) => { onLoaded.call(this, source) }, + (err) => { throw err } + ) + } else { + onLoaded.call(this, source) + } + } else if (type === 'start') { + handleAfterLoad.call(this, e, () => { + this.napiModule.startThread(payload.tid, payload.arg) + }) + } + } + } +} + +function handleAfterLoad (e, f) { + if (this.instance !== undefined) { + f.call(this, e) + } else { + this.messagesBeforeLoad.push(e.data) + } +} + +function onLoaded (source) { + if (source == null) { + throw new TypeError('onLoad should return an object') + } + + const instance = source.instance + const napiModule = source.napiModule + + if (!instance) throw new TypeError('onLoad should return an object which includes "instance"') + if (!napiModule) throw new TypeError('onLoad should return an object which includes "napiModule"') + if (!napiModule.childThread) throw new Error('napiModule should be created with `childThread: true`') + + this.instance = instance + this.napiModule = napiModule + + const postMessage = napiModule.postMessage + postMessage({ + __emnapi__: { + type: 'loaded', + payload: {} + } + }) + + const messages = this.messagesBeforeLoad + this.messagesBeforeLoad = [] + for (let i = 0; i < messages.length; i++) { + const data = messages[i] + this.handle({ data }) + } +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..0932cef1 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "../shared/tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "target": "ES5", + "module": "ESNext", + "noEmitHelpers": true, + "importHelpers": true, + "importsNotUsedAsValues": "error", + "paths": { + "tslib" : ["../../node_modules/tslib/tslib.d.ts"] + }, + "lib": [ + "ES5", + "ES2015", + "ES2020.BigInt", + "ES2021.WeakRef", + "ES2017.SharedMemory", + "DOM" + ], + "plugins": [ + { + "transform": "@tybys/ts-transform-pure-class", + "type": "raw", + "after": true + } + ] + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.js" + ] +} diff --git a/packages/emnapi/CMakeLists.txt b/packages/emnapi/CMakeLists.txt index 458f5143..457820b3 100644 --- a/packages/emnapi/CMakeLists.txt +++ b/packages/emnapi/CMakeLists.txt @@ -148,7 +148,7 @@ if(LIB_ARCH) endif() install(FILES - ${CMAKE_CURRENT_SOURCE_DIR}/include/common.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/emnapi_common.h ${CMAKE_CURRENT_SOURCE_DIR}/include/emnapi.h ${CMAKE_CURRENT_SOURCE_DIR}/include/js_native_api_types.h ${CMAKE_CURRENT_SOURCE_DIR}/include/js_native_api.h @@ -170,7 +170,7 @@ install(FILES if(EMNAPI_INSTALL_SRC) install(FILES ${EMNAPI_SRC} - "${CMAKE_CURRENT_SOURCE_DIR}/src/emnapi_common.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/emnapi_internal.h" DESTINATION "src/${PROJECT_NAME}") install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src/uv diff --git a/packages/emnapi/README.md b/packages/emnapi/README.md index 83597d54..7c1246cd 100644 --- a/packages/emnapi/README.md +++ b/packages/emnapi/README.md @@ -6,9 +6,7 @@ [![Build](https://github.com/toyobayashi/emnapi/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/toyobayashi/emnapi/actions/workflows/main.yml) -[Node-API](https://nodejs.org/docs/latest/api/n-api.html) implementation for [Emscripten](https://emscripten.org/index.html), [wasi-sdk](https://github.com/WebAssembly/wasi-sdk) and clang `wasm32-unknown-unknown` target, [napi-rs support is comming soon](https://github.com/napi-rs/napi-rs/issues/796). - -Emscripten is the first class support target, currently thread related APIs are unavailable on `wasm32-unknown-unknown` and `wasm32-wasi` target. +[Node-API](https://nodejs.org/docs/latest/api/n-api.html) implementation for [Emscripten](https://emscripten.org/index.html), [wasi-sdk](https://github.com/WebAssembly/wasi-sdk) and clang with wasm support. [napi-rs support is comming soon](https://github.com/napi-rs/napi-rs/tree/emnapi). This project aims to @@ -27,6 +25,16 @@ See documentation for more details: [How to build Node-API official examples](https://github.com/toyobayashi/node-addon-examples) +Emscripten is the first class support target. If your target is running addon on browser, +we strongly recommend you to use Emscripten instead of wasi-sdk. Async works and threadsafe +functions related APIs are only available on Emscripten or `wasm32-wasi-threads` target since +they are relying on pthread. Though today we have [WASI browser polyfill](https://github.com/toyobayashi/wasm-util), +`wasm32-wasi-threads` is in very early stage and WASI itself is not designed for browser. +There are some limitations on browser about wasi-libc's pthread implementation, for example +`pthread_mutex_lock` may call `__builtin_wasm_memory_atomic_wait32`(`memory.atomic.wait32`) +which is disallowed in browser JS main thread. While Emscripten's pthread implementation +has considered usage in browser. + ## Prerequests You will need to install: @@ -332,30 +340,19 @@ For non-emscripten, you need to use `@emnapi/core`. The initialization is simila ``` @@ -363,36 +360,25 @@ fetch('./hello.wasm').then(res => res.arrayBuffer()).then(wasmBuffer => { Using WASI on Node.js ```js -const { createNapiModule } = require('@emnapi/core') +const { instantiateNapiModule } = require('@emnapi/core') const { getDefaultContext } = require('@emnapi/runtime') const { WASI } = require('wasi') - -const napiModule = createNapiModule({ - context: getDefaultContext() -}) - -const wasi = new WASI({ /* ... */ }) - -WebAssembly.instantiate(require('fs').readFileSync('./hello.wasm'), { - wasi_snapshot_preview1: wasi.wasiImport, - env: { - ...napiModule.imports.env, - // Currently napi-rs imports all symbols from env module - ...napiModule.imports.napi, - ...napiModule.imports.emnapi - }, - // clang - napi: napiModule.imports.napi, - emnapi: napiModule.imports.emnapi -}).then(({ instance, module }) => { - wasi.initialize(instance) - const binding = napiModule.init({ - instance, - module, - memory: instance.exports.memory, - table: instance.exports.__indirect_function_table - }) - // binding === napiModule.exports +const fs = require('fs') + +instantiateNapiModule(fs.promises.readFile('./hello.wasm'), { + wasi: new WASI({ /* ... */ }), + context: getDefaultContext(), + overwriteImports (importObject) { + // importObject.env = { + // ...importObject.env, + // ...importObject.napi, + // ...importObject.emnapi, + // // ... + // } + } +}).then(({ instance, module, napiModule }) => { + const binding = napiModule.exports + // ... }) ``` @@ -400,38 +386,26 @@ Using WASI on browser, you can use WASI polyfill in [wasm-util](https://github.c and [memfs-browser](https://github.com/toyobayashi/memfs-browser) ```js -import { createNapiModule } from '@emnapi/core' +import { instantiateNapiModule } from '@emnapi/core' import { getDefaultContext } from '@emnapi/runtime' import { WASI } from '@tybys/wasm-util' -import { Volumn, createFsFromVolume } from 'memfs-browser' - -const napiModule = createNapiModule({ - context: getDefaultContext() -}) - -const fs = createFsFromVolume(Volume.from({ /* ... */ })) -const wasi = new WASI({ fs, /* ... */ }) - -WebAssembly.instantiate(wasmBuffer, { - wasi_snapshot_preview1: wasi.wasiImport, - env: { - ...napiModule.imports.env, - // Currently napi-rs imports all symbols from env module - ...napiModule.imports.napi, - ...napiModule.imports.emnapi - }, - // clang - napi: napiModule.imports.napi, - emnapi: napiModule.imports.emnapi -}).then(({ instance, module }) => { - wasi.initialize(instance) - const binding = napiModule.init({ - instance, - module, - memory: instance.exports.memory, - table: instance.exports.__indirect_function_table - }) - // binding === napiModule.exports +import { Volume, createFsFromVolume } from 'memfs-browser' + +const fs = createFsFromVolume(Volume.fromJSON({ /* ... */ })) +return instantiateNapiModule(fetch('./hello.wasm'), { + wasi: new WASI({ fs, /* ... */ }) + context: getDefaultContext(), + overwriteImports (importObject) { + // importObject.env = { + // ...importObject.env, + // ...importObject.napi, + // ...importObject.emnapi, + // // ... + // } + } +}).then(({ instance, module, napiModule }) => { + const binding = napiModule.exports + // ... }) ``` @@ -495,6 +469,7 @@ clang++ -O3 \ -L./node_modules/emnapi/lib/wasm32-wasi \ --target=wasm32-wasi \ --sysroot=$WASI_SDK_PATH/share/wasi-sysroot \ + -fno-exceptions \ -mexec-model=reactor \ -Wl,--initial-memory=16777216 \ -Wl,--export-dynamic \ @@ -522,6 +497,7 @@ clang++ -O3 \ -I./node_modules/emnapi/include \ -L./node_modules/emnapi/lib/wasm32 \ --target=wasm32 \ + -fno-exceptions \ -nostdlib \ -Wl,--no-entry \ -Wl,--initial-memory=16777216 \ @@ -752,35 +728,168 @@ pub unsafe extern "C" fn napi_register_wasm_v1(env: napi_env, exports: napi_valu -### Multithread (Emscripten Only) +### Multithread If you want to use async work or thread safe functions, there are additional C source file need to be compiled and linking. Recommend use CMake directly. +**This is EXPERIMENTAL on non-emscripten.** + ```cmake add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/node_modules/emnapi") add_executable(hello hello.c) target_link_libraries(hello emnapi-mt) -target_compile_options(hello PRIVATE "-sUSE_PTHREADS=1") -target_link_options(hello PRIVATE - "-sALLOW_MEMORY_GROWTH=1" - "-sEXPORTED_FUNCTIONS=['_malloc','_free']" - "-sUSE_PTHREADS=1" - "-sPTHREAD_POOL_SIZE=4" - # try to specify stack size if you experience pthread errors - "-sSTACK_SIZE=2MB" - "-sDEFAULT_PTHREAD_STACK_SIZE=2MB" -) + +if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + target_compile_options(hello PRIVATE "-sUSE_PTHREADS=1") + target_link_options(hello PRIVATE + "-sALLOW_MEMORY_GROWTH=1" + "-sEXPORTED_FUNCTIONS=['_malloc','_free']" + "-sUSE_PTHREADS=1" + "-sPTHREAD_POOL_SIZE=4" + # try to specify stack size if you experience pthread errors + "-sSTACK_SIZE=2MB" + "-sDEFAULT_PTHREAD_STACK_SIZE=2MB" + ) +elseif(CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-wasi-threads") + # Experimental + target_compile_options(hello PRIVATE "-fno-exceptions" "-pthread") + target_link_options(hello PRIVATE + "-pthread" + "-mexec-model=reactor" + "-Wl,--import-memory" + "-Wl,--max-memory=2147483648" + "-Wl,--export-dynamic" + "-Wl,--export=malloc" + "-Wl,--export=free" + "-Wl,--import-undefined" + "-Wl,--export-table" + ) +endif() ``` ```bash +# emscripten emcmake cmake -DCMAKE_BUILD_TYPE=Release -DEMNAPI_WORKER_POOL_SIZE=4 -G Ninja -H. -Bbuild + +# wasi-sdk with thread support (Experimental) +cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk-pthread.cmake \ + -DWASI_SDK_PREFIX=$WASI_SDK_PATH \ + -DCMAKE_BUILD_TYPE=Release \ + -G Ninja -H. -Bbuild + cmake --build build ``` +And additional work is required during instantiating wasm compiled with non-emscripten. + +```js +// emnapi main thread (could be in a Worker) +instantiateNapiModule(input, { + context: getDefaultContext(), + wasi: new WASI(/* ... */), + // reuseWorker: true, + onCreateWorker () { + return new Worker('./worker.js') + // Node.js + // return new Worker(join(__dirname, './worker.js'), { + // env: process.env, + // execArgv: ['--experimental-wasi-unstable-preview1'] + // }) + }, + overwriteImports (importObject) { + importObject.env.memory = new WebAssembly.Memory({ + initial: 16777216 / 65536, + maximum: 2147483648 / 65536, + shared: true + }) + } +}) +``` + +```js +// worker.js +(function () { + let fs, WASI, emnapiCore + + 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') + + const parentPort = nodeWorkerThreads.parentPort + + parentPort.on('message', (data) => { + globalThis.onmessage({ data }) + }) + + fs = require('fs') + + 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('./wasi').WASI + emnapiCore = require('@emnapi/core') + } else { + importScripts('./node_modules/memfs-browser/dist/memfs.js') + importScripts('./node_modules/@tybys/wasm-util/dist/wasm-util.min.js') + importScripts('./node_modules/@emnapi/core/dist/emnapi-core.js') + emnapiCore = globalThis.emnapiCore + + const { Volume, createFsFromVolume } = memfs + fs = createFsFromVolume(Volume.fromJSON({ + '/': null + })) + + WASI = globalThis.wasmUtil.WASI + } + + const { instantiateNapiModuleSync, MessageHandler } = emnapiCore + + const handler = new MessageHandler({ + onLoad ({ wasmModule, wasmMemory }) { + const wasi = new WASI({ + fs, + print: ENVIRONMENT_IS_NODE + ? (...args) => { + const str = require('util').format(...args) + fs.writeSync(1, str + '\n') + } + : function () { console.log.apply(console, arguments) } + }) + + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + overwriteImports (importObject) { + importObject.env.memory = wasmMemory + } + }) + } + }) + + globalThis.onmessage = function (e) { + handler.handle(e) + // handle other messages + } +})() +``` + ## Preprocess Macro Options ### `-DEMNAPI_WORKER_POOL_SIZE=4` @@ -794,6 +903,13 @@ Module.preRun.push(function () { ENV.UV_THREADPOOL_SIZE = '2'; } }); + +// wasi +new WASI({ + env: { + UV_THREADPOOL_SIZE: '2' + } +}) ``` It represent max of `EMNAPI_WORKER_POOL_SIZE` async work (`napi_queue_async_work`) can be executed in parallel. Default is not defined, read `UV_THREADPOOL_SIZE` at runtime. @@ -818,7 +934,7 @@ Tell emnapi how to delay async work in `uv_async_send` / `uv__async_close`. ### `-DEMNAPI_USE_PROXYING=1` -This option only has effect if you use `-sUSE_PTHREADS`. Default is `1` if emscripten version `>= 3.1.9`, else `0`. +This option only has effect if you use emscripten `-sUSE_PTHREADS`. Default is `1` if emscripten version `>= 3.1.9`, else `0`. - `0` diff --git a/packages/emnapi/include/emnapi.h b/packages/emnapi/include/emnapi.h index b926e6af..e9f3c5b2 100644 --- a/packages/emnapi/include/emnapi.h +++ b/packages/emnapi/include/emnapi.h @@ -2,7 +2,7 @@ #define EMNAPI_INCLUDE_EMNAPI_H_ #include "js_native_api.h" -#include "common.h" +#include "emnapi_common.h" typedef enum { emnapi_runtime, diff --git a/packages/emnapi/include/common.h b/packages/emnapi/include/emnapi_common.h similarity index 100% rename from packages/emnapi/include/common.h rename to packages/emnapi/include/emnapi_common.h diff --git a/packages/emnapi/include/js_native_api.h b/packages/emnapi/include/js_native_api.h index d57f24b4..92944c3a 100644 --- a/packages/emnapi/include/js_native_api.h +++ b/packages/emnapi/include/js_native_api.h @@ -22,7 +22,7 @@ #endif #include "js_native_api_types.h" -#include "common.h" +#include "emnapi_common.h" #define NAPI_AUTO_LENGTH SIZE_MAX diff --git a/packages/emnapi/script/build.js b/packages/emnapi/script/build.js index 74fa8d3b..1340fa66 100644 --- a/packages/emnapi/script/build.js +++ b/packages/emnapi/script/build.js @@ -67,7 +67,9 @@ async function build () { }) const parsedCode = compiler.parseCode(coreCode) fs.writeFileSync(path.join(__dirname, '../../core/src/module.js'), -`export function createNapiModule (options) { +`import { _WebAssembly as WebAssembly } from './util.js' + +export function createNapiModule (options) { ${parsedCode} return napiModule; } diff --git a/packages/emnapi/src/async_cleanup_hook.c b/packages/emnapi/src/async_cleanup_hook.c index 18f0bf9e..67b7d22f 100644 --- a/packages/emnapi/src/async_cleanup_hook.c +++ b/packages/emnapi/src/async_cleanup_hook.c @@ -1,4 +1,4 @@ -#include "emnapi_common.h" +#include "emnapi_internal.h" #include "node_api.h" EXTERN_C_START diff --git a/packages/emnapi/src/async_context.c b/packages/emnapi/src/async_context.c index 496253c1..8a5f7e48 100644 --- a/packages/emnapi/src/async_context.c +++ b/packages/emnapi/src/async_context.c @@ -1,5 +1,5 @@ #include -#include "emnapi_common.h" +#include "emnapi_internal.h" EXTERN_C_START diff --git a/packages/emnapi/src/async_work.c b/packages/emnapi/src/async_work.c index c0017f8b..958b03aa 100644 --- a/packages/emnapi/src/async_work.c +++ b/packages/emnapi/src/async_work.c @@ -1,5 +1,5 @@ #include "node_api.h" -#include "emnapi_common.h" +#include "emnapi_internal.h" #if EMNAPI_HAVE_THREADS diff --git a/packages/emnapi/src/core/async.ts b/packages/emnapi/src/core/async.ts new file mode 100644 index 00000000..f2e77951 --- /dev/null +++ b/packages/emnapi/src/core/async.ts @@ -0,0 +1,379 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ + +function terminateWorker (worker: any): void { + const tid = worker.__emnapi_tid + worker.terminate() + worker.onmessage = (e: any) => { + if (e.data.__emnapi__) { + err('received "' + e.data.__emnapi__.type + '" command from terminated worker: ' + tid) + } + } +} + +var PThread = { + unusedWorkers: [] as any[], + runningWorkers: [] as any[], + pthreads: Object.create(null), + nextWorkerID: 0, + init () {}, + returnWorkerToPool (worker: any) { + var tid = worker.__emnapi_tid + delete PThread.pthreads[tid] + PThread.unusedWorkers.push(worker) + PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker), 1) + delete worker.__emnapi_tid + if (ENVIRONMENT_IS_NODE) { + worker.unref() + } + }, + loadWasmModuleToWorker: (worker: any) => { + if (worker.whenLoaded) return worker.whenLoaded + worker.whenLoaded = new Promise((resolve, reject) => { + worker.onmessage = function (e: any) { + if (e.data.__emnapi__) { + const type = e.data.__emnapi__.type + const payload = e.data.__emnapi__.payload + if (type === 'loaded') { + worker.loaded = true + if (ENVIRONMENT_IS_NODE && !worker.__emnapi_tid) { + worker.unref() + } + resolve(worker) + // if (payload.err) { + // err('failed to load in child thread: ' + (payload.err.message || payload.err)) + // } + } else if (type === 'spawn-thread') { + spawnThread(payload.startArg, payload.errorOrTid) + } else if (type === 'cleanup-thread') { + if (reuseWorker) { + PThread.returnWorkerToPool(worker) + } else { + delete PThread.pthreads[payload.tid] + PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker), 1) + terminateWorker(worker) + delete worker.__emnapi_tid + } + } + } + } + worker.onerror = (e: any) => { + const message = 'worker sent an error!' + // if (worker.pthread_ptr) { + // message = 'Pthread ' + ptrToString(worker.pthread_ptr) + ' sent an error!' + // } + err(message + ' ' + e.message) + reject(e) + throw e + } + if (ENVIRONMENT_IS_NODE) { + worker.on('message', function (data: any) { + worker.onmessage({ + data + }) + }) + worker.on('error', function (e: any) { + worker.onerror(e) + }) + worker.on('detachedExit', function () {}) + } + // napiModule.emnapi.addSendListener(worker) + emnapiAddSendListener(worker) + worker.postMessage({ + __emnapi__: { + type: 'load', + payload: { + wasmModule, + wasmMemory + } + } + }) + }) + return worker.whenLoaded + }, + allocateUnusedWorker () { + if (typeof onCreateWorker !== 'function') { + throw new TypeError('createNapiModule `options.onCreateWorker` is not provided') + } + const worker = onCreateWorker() + PThread.unusedWorkers.push(worker) + return worker + }, + getNewWorker () { + if (reuseWorker) { + if (PThread.unusedWorkers.length === 0) { + const worker = PThread.allocateUnusedWorker() + PThread.loadWasmModuleToWorker(worker) + } + return PThread.unusedWorkers.pop() + } + const worker = PThread.allocateUnusedWorker() + PThread.loadWasmModuleToWorker(worker) + return worker + } +} + +function emnapiGetWorkerByPthreadPtr (pthreadPtr: number): any { + const view = new DataView(wasmMemory.buffer) + /** + * wasi-sdk-20.0+threads + * + * struct pthread { + * struct pthread *self; // 0 + * struct pthread *prev, *next; // 4, 8 + * uintptr_t sysinfo; // 12 + * uintptr_t canary; // 16 + * int tid; // 20 + * // ... + * } + */ + const tidOffset = 20 + const tid = view.getInt32(pthreadPtr + tidOffset, true) + const worker = PThread.pthreads[tid] + return worker +} + +function __emnapi_worker_unref (pthreadPtr: number): void { + if (ENVIRONMENT_IS_PTHREAD) return + const worker = emnapiGetWorkerByPthreadPtr(pthreadPtr) + if (worker && typeof worker.unref === 'function') { + worker.unref() + } +} + +function emnapiAddSendListener (worker: any): boolean { + if (!worker) return false + if (worker._emnapiSendListener) return true + const handler = function (e: any): void { + const data = ENVIRONMENT_IS_NODE ? e : e.data + const __emnapi__ = data.__emnapi__ + if (__emnapi__ && __emnapi__.type === 'async-send') { + if (ENVIRONMENT_IS_PTHREAD) { + const postMessage = napiModule.postMessage! + postMessage({ __emnapi__ }) + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const callback = __emnapi__.payload.callback + $makeDynCall('vp', 'callback')(__emnapi__.payload.data) + } + } + } + const dispose = function (): void { + if (ENVIRONMENT_IS_NODE) { + worker.off('message', handler) + } else { + worker.removeEventListener('message', handler, false) + } + delete worker._emnapiSendListener + } + worker._emnapiSendListener = { handler, dispose } + if (ENVIRONMENT_IS_NODE) { + worker.on('message', handler) + } else { + worker.addEventListener('message', handler, false) + } + return true +} + +function __emnapi_async_send_js (type: number, callback: number, data: number): void { + if (ENVIRONMENT_IS_PTHREAD) { + const postMessage = napiModule.postMessage! + postMessage({ + __emnapi__: { + type: 'async-send', + payload: { + callback, + data + } + } + }) + } else { + switch (type) { + case 0: __emnapi_set_immediate(callback, data); break + case 1: __emnapi_next_tick(callback, data); break + default: break + } + } +} + +// function ptrToString (ptr: number): string { +// return '0x' + ('00000000' + ptr.toString(16)).slice(-8) +// } + +function spawnThread (startArg: number, errorOrTid: number): number { + const isNewABI = errorOrTid !== undefined + if (!isNewABI) { + errorOrTid = $makeMalloc('spawnThread', '8') + if (!errorOrTid) { + return -48 /* ENOMEM */ + } + } + const struct = new Int32Array(wasmMemory.buffer, errorOrTid, 2) + Atomics.store(struct, 0, 0) + Atomics.store(struct, 1, 0) + + if (ENVIRONMENT_IS_PTHREAD) { + const postMessage = napiModule.postMessage! + postMessage({ + __emnapi__: { + type: 'spawn-thread', + payload: { + startArg, + errorOrTid + } + } + }) + Atomics.wait(struct, 1, 0) + const isError = Atomics.load(struct, 0) + const result = Atomics.load(struct, 1) + if (isNewABI) { + return isError + } + _free($to64('errorOrTid')) + return isError ? -result : result + } + + let worker: any + try { + worker = PThread.getNewWorker() + if (!worker) { + throw new Error('failed to get new worker') + } + } catch (err) { + const EAGAIN = 6 + + Atomics.store(struct, 0, 1) + Atomics.store(struct, 1, EAGAIN) + Atomics.notify(struct, 1) + + err(err.message) + if (isNewABI) { + return 1 + } + _free($to64('errorOrTid')) + return -EAGAIN + } + + const tid = PThread.nextWorkerID + 43 + + Atomics.store(struct, 0, 0) + Atomics.store(struct, 1, tid) + Atomics.notify(struct, 1) + + PThread.nextWorkerID = (PThread.nextWorkerID + 1) % (0xffffffff - 42) + PThread.pthreads[tid] = worker + worker.__emnapi_tid = tid + PThread.runningWorkers.push(worker) + if (ENVIRONMENT_IS_NODE) { + worker.ref() + } + + worker.postMessage({ + __emnapi__: { + type: 'start', + payload: { + tid, + arg: startArg + } + } + }) + + if (isNewABI) { + return 0 + } + _free($to64('errorOrTid')) + return tid +} + +function startThread (tid: number, startArg: number): void { + if (napiModule.childThread) { + const postMessage = napiModule.postMessage! + ;(wasmInstance.exports.wasi_thread_start as Function)(tid, startArg) + postMessage({ + __emnapi__: { + type: 'cleanup-thread', + payload: { + tid + } + } + }) + } else { + throw new Error('startThread is only available in child threads') + } +} + +napiModule.spawnThread = spawnThread +napiModule.startThread = startThread + +var uvThreadpoolReadyResolve: () => void +var uvThreadpoolReady: Promise & { ready: boolean } = new Promise((resolve) => { + uvThreadpoolReadyResolve = function () { + uvThreadpoolReady.ready = true + resolve() + } +}) as any +uvThreadpoolReady.ready = false + +function __emnapi_is_main_browser_thread (): number { + return (typeof window !== 'undefined' && typeof document !== 'undefined' && !ENVIRONMENT_IS_NODE) ? 1 : 0 +} + +function __emnapi_after_uvthreadpool_ready (callback: number, q: number, type: number): void { + if (uvThreadpoolReady.ready) { + $makeDynCall('vpi', 'callback')($to64('q'), type) + } else { + uvThreadpoolReady.then(() => { + $makeDynCall('vpi', 'callback')($to64('q'), type) + }) + } +} + +function __emnapi_tell_js_uvthreadpool (threads: number, size: number): void { + const p = [] as Array> + for (let i = 0; i < size; i++) { + const pthreadPtr = $makeGetValue('threads', 'i * ' + POINTER_SIZE, '*') + const worker = emnapiGetWorkerByPthreadPtr(pthreadPtr) + p.push(new Promise((resolve) => { + const handler = function (e: any): void { + const data = ENVIRONMENT_IS_NODE ? e : e.data + const __emnapi__ = data.__emnapi__ + if (__emnapi__ && __emnapi__.type === 'async-thread-ready') { + resolve() + if (worker && typeof worker.unref === 'function') { + worker.unref() + } + if (ENVIRONMENT_IS_NODE) { + worker.off('message', handler) + } else { + worker.removeEventListener('message', handler) + } + } + } + if (ENVIRONMENT_IS_NODE) { + worker.on('message', handler) + } else { + worker.addEventListener('message', handler) + } + })) + } + Promise.all(p).then(uvThreadpoolReadyResolve) +} + +function __emnapi_emit_async_thread_ready (): void { + if (!ENVIRONMENT_IS_PTHREAD) return + const postMessage = napiModule.postMessage! + postMessage({ + __emnapi__: { + type: 'async-thread-ready', + payload: {} + } + }) +} + +emnapiImplementInternal('_emnapi_is_main_browser_thread', 'i', __emnapi_is_main_browser_thread) +emnapiImplementInternal('_emnapi_after_uvthreadpool_ready', 'vppi', __emnapi_after_uvthreadpool_ready) +emnapiImplementInternal('_emnapi_tell_js_uvthreadpool', 'vpi', __emnapi_tell_js_uvthreadpool) +emnapiImplementInternal('_emnapi_emit_async_thread_ready', 'v', __emnapi_emit_async_thread_ready) +emnapiImplementInternal('_emnapi_worker_unref', 'vp', __emnapi_worker_unref) +emnapiImplementInternal('_emnapi_async_send_js', 'vipp', __emnapi_async_send_js) +emnapiImplementHelper('$emnapiAddSendListener', undefined, emnapiAddSendListener, undefined, 'addSendListener') diff --git a/packages/emnapi/src/core/init.ts b/packages/emnapi/src/core/init.ts index 400a331c..78770c05 100644 --- a/packages/emnapi/src/core/init.ts +++ b/packages/emnapi/src/core/init.ts @@ -1,10 +1,16 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ -/* eslint-disable no-var */ declare interface CreateOptions { context: Context filename?: string nodeBinding?: NodeBinding + childThread?: boolean + reuseWorker?: boolean + onCreateWorker?: () => any + print?: (str: string) => void + printErr?: (str: string) => void + postMessage?: (msg: any) => any } declare interface InitOptions { @@ -27,22 +33,28 @@ declare interface INapiModule { emnapi: any loaded: boolean filename: string + childThread: boolean envObject?: Env init (options: InitOptions): any + spawnThread (startArg: number, errorOrTid?: number): number + startThread (tid: number, startArg: number): void + postMessage?: (msg: any) => any } -// eslint-disable-next-line @typescript-eslint/no-unused-vars +var ENVIRONMENT_IS_NODE = typeof process === 'object' && process !== null && typeof process.versions === 'object' && process.versions !== null && typeof process.versions.node === 'string' +var ENVIRONMENT_IS_PTHREAD = Boolean(options.childThread) +var reuseWorker = Boolean(options.reuseWorker) + +var wasmInstance: WebAssembly.Instance var wasmModule: WebAssembly.Module -// eslint-disable-next-line @typescript-eslint/no-unused-vars var wasmMemory: WebAssembly.Memory -// eslint-disable-next-line @typescript-eslint/no-unused-vars + var wasmTable: WebAssembly.Table -// eslint-disable-next-line @typescript-eslint/no-unused-vars -var _malloc: any, _free: any +var _malloc: any +var _free: any -// eslint-disable-next-line @typescript-eslint/no-unused-vars function abort (msg: string): never { if (typeof WebAssembly.RuntimeError === 'function') { throw new WebAssembly.RuntimeError(msg) @@ -50,12 +62,10 @@ function abort (msg: string): never { throw Error(msg) } -// eslint-disable-next-line @typescript-eslint/no-unused-vars function runtimeKeepalivePush (): void {} -// eslint-disable-next-line @typescript-eslint/no-unused-vars + function runtimeKeepalivePop (): void {} -// eslint-disable-next-line no-var var napiModule: INapiModule = { imports: { env: {}, @@ -66,12 +76,17 @@ var napiModule: INapiModule = { emnapi: {}, loaded: false, filename: '', + childThread: Boolean(options.childThread), + + spawnThread: undefined!, + startThread: undefined!, init (options: InitOptions) { if (napiModule.loaded) return napiModule.exports if (!options) throw new TypeError('Invalid napi init options') const instance = options.instance if (!instance?.exports) throw new TypeError('Invalid wasm instance') + wasmInstance = instance const exports = instance.exports const module = options.module const memory = options.memory || exports.memory @@ -87,51 +102,86 @@ var napiModule: INapiModule = { _malloc = exports.malloc _free = exports.free - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const envObject = napiModule.envObject || (napiModule.envObject = emnapiCtx.createEnv( - (cb: Ptr) => $makeDynCall('vppp', 'cb'), - (cb: Ptr) => $makeDynCall('vp', 'cb') - )) - - const scope = emnapiCtx.openScope(envObject) - try { - envObject.callIntoModule((_envObject) => { - const exports = napiModule.exports - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const exportsHandle = scope.add(exports) - const napi_register_wasm_v1 = instance.exports.napi_register_wasm_v1 as Function - const napiValue = napi_register_wasm_v1($to64('_envObject.id'), $to64('exportsHandle.id')) - napiModule.exports = (!napiValue) ? exports : emnapiCtx.handleStore.get(napiValue)!.value - }) - } finally { - emnapiCtx.closeScope(envObject, scope) + if (!napiModule.childThread) { + // main thread only + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const envObject = napiModule.envObject || (napiModule.envObject = emnapiCtx.createEnv( + (cb: Ptr) => $makeDynCall('vppp', 'cb'), + (cb: Ptr) => $makeDynCall('vp', 'cb') + )) + + const scope = emnapiCtx.openScope(envObject) + try { + envObject.callIntoModule((_envObject) => { + const exports = napiModule.exports + const exportsHandle = scope.add(exports) + const napi_register_wasm_v1 = instance.exports.napi_register_wasm_v1 as Function + const napiValue = napi_register_wasm_v1($to64('_envObject.id'), $to64('exportsHandle.id')) + napiModule.exports = (!napiValue) ? exports : emnapiCtx.handleStore.get(napiValue)!.value + }) + } finally { + emnapiCtx.closeScope(envObject, scope) + } + napiModule.loaded = true + delete napiModule.envObject + return napiModule.exports + } else { + if (typeof exports.wasi_thread_start !== 'function') { + throw new TypeError('wasi_thread_start is not exported') + } } - napiModule.loaded = true - delete napiModule.envObject - return napiModule.exports } } var emnapiCtx: Context var emnapiNodeBinding: NodeBinding - -const context = options.context -if (typeof context !== 'object' || context === null) { - throw new TypeError("Invalid `options.context`. Use `import { getDefaultContext } from '@emnapi/runtime'`") +var onCreateWorker: () => any +var out: (str: string) => void +var err: (str: string) => void + +if (!ENVIRONMENT_IS_PTHREAD) { + const context = options.context + if (typeof context !== 'object' || context === null) { + throw new TypeError("Invalid `options.context`. Use `import { getDefaultContext } from '@emnapi/runtime'`") + } + emnapiCtx = context +} else { + emnapiCtx = options?.context + + const postMsg = typeof options.postMessage === 'function' + ? options.postMessage + : typeof postMessage === 'function' + ? postMessage + : undefined + if (typeof postMsg !== 'function') { + throw new TypeError('No postMessage found') + } + napiModule.postMessage = postMsg } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -emnapiCtx = context - if (typeof options.filename === 'string') { napiModule.filename = options.filename } +if (typeof options.onCreateWorker === 'function') { + onCreateWorker = options.onCreateWorker +} +if (typeof options.print === 'function') { + out = options.print +} else { + out = console.log.bind(console) +} +if (typeof options.printErr === 'function') { + err = options.printErr +} else { + err = console.warn.bind(console) +} + if ('nodeBinding' in options) { const nodeBinding = options.nodeBinding if (typeof nodeBinding !== 'object' || nodeBinding === null) { throw new TypeError('Invalid `options.nodeBinding`. Use @emnapi/node-binding package') } - // eslint-disable-next-line @typescript-eslint/no-unused-vars emnapiNodeBinding = nodeBinding } diff --git a/packages/emnapi/src/core/string.ts b/packages/emnapi/src/core/string.ts index 21e0cb04..b8e6f5d2 100644 --- a/packages/emnapi/src/core/string.ts +++ b/packages/emnapi/src/core/string.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-var */ /* eslint-disable @typescript-eslint/indent */ // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/emnapi/src/emnapi.c b/packages/emnapi/src/emnapi.c index 251da235..a8d2e00f 100644 --- a/packages/emnapi/src/emnapi.c +++ b/packages/emnapi/src/emnapi.c @@ -1,4 +1,4 @@ -#include "emnapi_common.h" +#include "emnapi_internal.h" EXTERN_C_START diff --git a/packages/emnapi/src/emnapi_common.h b/packages/emnapi/src/emnapi_internal.h similarity index 100% rename from packages/emnapi/src/emnapi_common.h rename to packages/emnapi/src/emnapi_internal.h diff --git a/packages/emnapi/src/emscripten/async.ts b/packages/emnapi/src/emscripten/async.ts index 59fc12d3..7c2f01b9 100644 --- a/packages/emnapi/src/emscripten/async.ts +++ b/packages/emnapi/src/emscripten/async.ts @@ -1,4 +1,4 @@ -declare const PThread: any +declare var PThread: any mergeInto(LibraryManager.library, { _emnapi_worker_unref__sig: 'vp', @@ -23,27 +23,36 @@ mergeInto(LibraryManager.library, { 'return r;' + '}; })();', $emnapiAddSendListener: function (worker: any) { - if (worker && !worker._emnapiSendListener) { - worker._emnapiSendListener = function _emnapiSendListener (e: any) { - const data = ENVIRONMENT_IS_NODE ? e : e.data - if (data.emnapiAsyncSend) { - if (ENVIRONMENT_IS_PTHREAD) { - postMessage({ - emnapiAsyncSend: data.emnapiAsyncSend - }) - } else { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const callback = data.emnapiAsyncSend.callback - $makeDynCall('vp', 'callback')(data.emnapiAsyncSend.data) - } + if (!worker) return false + if (worker._emnapiSendListener) return true + const handler = function (e: any): void { + const data = ENVIRONMENT_IS_NODE ? e : e.data + const __emnapi__ = data.__emnapi__ + if (__emnapi__ && __emnapi__.type === 'async-send') { + if (ENVIRONMENT_IS_PTHREAD) { + postMessage({ __emnapi__ }) + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const callback = __emnapi__.payload.callback + $makeDynCall('vp', 'callback')(__emnapi__.payload.data) } } + } + const dispose = function (): void { if (ENVIRONMENT_IS_NODE) { - worker.on('message', worker._emnapiSendListener) + worker.off('message', handler) } else { - worker.addEventListener('message', worker._emnapiSendListener, false) + worker.removeEventListener('message', handler, false) } + delete worker._emnapiSendListener } + worker._emnapiSendListener = { handler, dispose } + if (ENVIRONMENT_IS_NODE) { + worker.on('message', handler) + } else { + worker.addEventListener('message', handler, false) + } + return true }, _emnapi_async_send_js__sig: 'vipp', @@ -55,9 +64,12 @@ mergeInto(LibraryManager.library, { _emnapi_async_send_js: function (type: number, callback: number, data: number): void { if (ENVIRONMENT_IS_PTHREAD) { postMessage({ - emnapiAsyncSend: { - callback, - data + __emnapi__: { + type: 'async-send', + payload: { + callback, + data + } } }) } else { diff --git a/packages/emnapi/src/emscripten/init.ts b/packages/emnapi/src/emscripten/init.ts index 46f955e2..9fcb9209 100644 --- a/packages/emnapi/src/emscripten/init.ts +++ b/packages/emnapi/src/emscripten/init.ts @@ -7,9 +7,9 @@ // declare const process: any // declare const __webpack_public_path__: any -declare let emnapiCtx: Context +declare var emnapiCtx: Context // eslint-disable-next-line @typescript-eslint/no-unused-vars -declare let emnapiNodeBinding: NodeBinding +declare var emnapiNodeBinding: NodeBinding declare function _napi_register_wasm_v1 (env: Ptr, exports: Ptr): napi_value diff --git a/packages/emnapi/src/emscripten/runtime.d.ts b/packages/emnapi/src/emscripten/runtime.d.ts index 407fe410..f70c5686 100644 --- a/packages/emnapi/src/emscripten/runtime.d.ts +++ b/packages/emnapi/src/emscripten/runtime.d.ts @@ -3,12 +3,12 @@ declare const LibraryManager: { library: any } -declare function mergeInto (target: any, source: { [key: string]: any }): void +declare function mergeInto (target: any, source: Record): void // runtime -declare const wasmMemory: WebAssembly.Memory -declare const ENVIRONMENT_IS_NODE: boolean -declare const ENVIRONMENT_IS_PTHREAD: boolean +declare var wasmMemory: WebAssembly.Memory +declare var ENVIRONMENT_IS_NODE: boolean +declare var ENVIRONMENT_IS_PTHREAD: boolean declare function UTF8ToString (ptr: const_char_p, maxRead?: number): string declare function UTF16ToString (ptr: const_char16_t_p, maxRead?: number): string diff --git a/packages/emnapi/src/emscripten/string.ts b/packages/emnapi/src/emscripten/string.ts index dada08e6..238f351a 100644 --- a/packages/emnapi/src/emscripten/string.ts +++ b/packages/emnapi/src/emscripten/string.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/indent */ -declare let emnapiUtf8Decoder: { decode: (input: BufferSource) => string } -declare let emnapiUtf16leDecoder: { decode: (input: BufferSource) => string } +declare var emnapiUtf8Decoder: { decode: (input: BufferSource) => string } +declare var emnapiUtf16leDecoder: { decode: (input: BufferSource) => string } // eslint-disable-next-line @typescript-eslint/no-unused-vars declare function emnapiUtf8ToString (ptr: void_p, length: int): string // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/emnapi/src/emscripten/util.ts b/packages/emnapi/src/emscripten/util.ts index 01ad4029..bdcda61a 100644 --- a/packages/emnapi/src/emscripten/util.ts +++ b/packages/emnapi/src/emscripten/util.ts @@ -14,18 +14,18 @@ function emnapiImplement (name: string, sig: string | undefined, compilerTimeFun // emnapi_* function emnapiImplement2 (...args: Parameters): void function emnapiImplement2 (): void { - return emnapiImplement.apply(null, arguments as any) + emnapiImplement.apply(null, arguments as any) } // _emnapi_* function emnapiImplementInternal (...args: Parameters): void function emnapiImplementInternal (): void { - return emnapiImplement.apply(null, arguments as any) + emnapiImplement.apply(null, arguments as any) } // $emnapi* function emnapiImplementHelper (name: string, sig: string | undefined, compilerTimeFunction: Function, deps?: string[], _exportName?: string): void { - return emnapiImplement(name, sig, compilerTimeFunction, deps) + emnapiImplement(name, sig, compilerTimeFunction, deps) } function emnapiDefineVar (name: string, value: any, deps?: string[], postset?: string): void { diff --git a/packages/emnapi/src/internal.ts b/packages/emnapi/src/internal.ts index c67f0d5c..56935269 100644 --- a/packages/emnapi/src/internal.ts +++ b/packages/emnapi/src/internal.ts @@ -95,7 +95,7 @@ function emnapiDefineProperty (envObject: Env, obj: object, propertyName: string } } -function emnapiGetHandle (js_object: napi_value): { status: napi_status; handle?: Handle} { +function emnapiGetHandle (js_object: napi_value): { status: napi_status; handle?: Handle } { let handle = emnapiCtx.handleStore.get(js_object)! if (!(handle.isObject() || handle.isFunction())) { return { status: napi_status.napi_invalid_arg } diff --git a/packages/emnapi/src/js_native_api.c b/packages/emnapi/src/js_native_api.c index 995c38d8..b52a6882 100644 --- a/packages/emnapi/src/js_native_api.c +++ b/packages/emnapi/src/js_native_api.c @@ -1,4 +1,4 @@ -#include "emnapi_common.h" +#include "emnapi_internal.h" #ifdef __EMSCRIPTEN__ #include diff --git a/packages/emnapi/src/memory.ts b/packages/emnapi/src/memory.ts index 7cfee36d..f5d6f00d 100644 --- a/packages/emnapi/src/memory.ts +++ b/packages/emnapi/src/memory.ts @@ -118,7 +118,9 @@ const emnapiExternalMemory: { return view } - if (emnapiExternalMemory.isDetachedArrayBuffer(view.buffer) && emnapiExternalMemory.wasmMemoryViewTable.has(view)) { + const maybeOldWasmMemory = emnapiExternalMemory.isDetachedArrayBuffer(view.buffer) || + ((typeof SharedArrayBuffer === 'function') && (view.buffer instanceof SharedArrayBuffer)) + if (maybeOldWasmMemory && emnapiExternalMemory.wasmMemoryViewTable.has(view)) { const info = emnapiExternalMemory.wasmMemoryViewTable.get(view)! const Ctor = info.Ctor let newView: ArrayBufferView diff --git a/packages/emnapi/src/node_api.c b/packages/emnapi/src/node_api.c index a9b95538..a7002df8 100644 --- a/packages/emnapi/src/node_api.c +++ b/packages/emnapi/src/node_api.c @@ -1,4 +1,4 @@ -#include "emnapi_common.h" +#include "emnapi_internal.h" #include "node_api.h" #if EMNAPI_HAVE_THREADS diff --git a/packages/emnapi/src/threadsafe_function.c b/packages/emnapi/src/threadsafe_function.c index bc9ecfe4..c207f527 100644 --- a/packages/emnapi/src/threadsafe_function.c +++ b/packages/emnapi/src/threadsafe_function.c @@ -1,5 +1,5 @@ #include "node_api.h" -#include "emnapi_common.h" +#include "emnapi_internal.h" #if NAPI_VERSION >= 4 && EMNAPI_HAVE_THREADS #include diff --git a/packages/emnapi/src/uv/threadpool.c b/packages/emnapi/src/uv/threadpool.c index 2bed76ca..ea1b342f 100644 --- a/packages/emnapi/src/uv/threadpool.c +++ b/packages/emnapi/src/uv/threadpool.c @@ -27,7 +27,11 @@ #include #include #include "uv-common.h" -#include "common.h" +#include "emnapi_common.h" + +#if defined(__wasi__) && defined(_REENTRANT) +#define __EMNAPI_WASI_THREADS__ +#endif #define MAX_THREADPOOL_SIZE 1024 @@ -54,6 +58,15 @@ static void uv__cancelled(struct uv__work* w) { EMNAPI_INTERNAL_EXTERN void _emnapi_worker_unref(uv_thread_t pid); +#ifdef __EMNAPI_WASI_THREADS__ +EMNAPI_INTERNAL_EXTERN +void _emnapi_after_uvthreadpool_ready(void (*callback)(QUEUE* w, enum uv__work_kind kind), + QUEUE* w, + enum uv__work_kind kind); +EMNAPI_INTERNAL_EXTERN void _emnapi_tell_js_uvthreadpool(uv_thread_t* threads, unsigned int n); +EMNAPI_INTERNAL_EXTERN void _emnapi_emit_async_thread_ready(); +#endif + /* To avoid deadlock with uv_cancel() it's crucial that the worker * never holds the global mutex and the loop-local mutex at the same time. */ @@ -61,8 +74,11 @@ static void* worker(void* arg) { struct uv__work* w; QUEUE* q; int is_slow_work; - +#ifndef __EMNAPI_WASI_THREADS__ uv_sem_post((uv_sem_t*) arg); +#else + _emnapi_emit_async_thread_ready(); +#endif arg = NULL; uv_mutex_lock(&mutex); @@ -199,7 +215,9 @@ static void init_threads(void) { #if !defined(EMNAPI_WORKER_POOL_SIZE) || !(EMNAPI_WORKER_POOL_SIZE > 0) const char* val; #endif +#ifndef __EMNAPI_WASI_THREADS__ uv_sem_t sem; +#endif #if defined(EMNAPI_WORKER_POOL_SIZE) && EMNAPI_WORKER_POOL_SIZE > 0 nthreads = EMNAPI_WORKER_POOL_SIZE; @@ -233,24 +251,33 @@ static void init_threads(void) { QUEUE_INIT(&slow_io_pending_wq); QUEUE_INIT(&run_slow_work_message); +#ifndef __EMNAPI_WASI_THREADS__ if (uv_sem_init(&sem, 0)) abort(); +#endif for (i = 0; i < nthreads; i++) +#ifndef __EMNAPI_WASI_THREADS__ if (uv_thread_create(threads + i, (uv_thread_cb) worker, &sem)) +#else + if (uv_thread_create(threads + i, (uv_thread_cb) worker, NULL)) +#endif abort(); +#ifndef __EMNAPI_WASI_THREADS__ for (i = 0; i < nthreads; i++) uv_sem_wait(&sem); uv_sem_destroy(&sem); - for (i = 0; i < nthreads; i++) _emnapi_worker_unref(*(threads + i)); +#else + _emnapi_tell_js_uvthreadpool(threads, nthreads); +#endif } -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__wasi__) static void reset_once(void) { uv_once_t child_once = UV_ONCE_INIT; memcpy(&once, &child_once, sizeof(child_once)); @@ -259,7 +286,7 @@ static void reset_once(void) { static void init_once(void) { -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__wasi__) /* Re-initialize the threadpool after fork. * Note that this discards the global mutex and condition as well * as the work queue. @@ -280,7 +307,11 @@ void uv__work_submit(uv_loop_t* loop, w->loop = loop; w->work = work; w->done = done; +// #ifdef __EMNAPI_WASI_THREADS__ +// _emnapi_after_uvthreadpool_ready(post, &w->wq, kind); +// #else post(&w->wq, kind); +// #endif } diff --git a/packages/emnapi/src/uv/unix/async.c b/packages/emnapi/src/uv/unix/async.c index 30259569..1022ba7c 100644 --- a/packages/emnapi/src/uv/unix/async.c +++ b/packages/emnapi/src/uv/unix/async.c @@ -27,7 +27,7 @@ #include #include #include "../uv-common.h" -#include "common.h" +#include "emnapi_common.h" #if defined(__clang__) || \ defined(__GNUC__) || \ diff --git a/packages/emnapi/src/uv/unix/thread.c b/packages/emnapi/src/uv/unix/thread.c index cf80e34f..fda3a229 100644 --- a/packages/emnapi/src/uv/unix/thread.c +++ b/packages/emnapi/src/uv/unix/thread.c @@ -6,6 +6,15 @@ #include #include "uv.h" +// #define CHECK_RET(the_call) \ +// do { \ +// int r = (the_call); \ +// if (r) { \ +// fprintf(stderr, #the_call ": %d\n", r); \ +// abort(); \ +// } \ +// } while (0) + void uv_sem_post(sem_t* sem) { if (sem_post(sem)) abort(); @@ -39,7 +48,7 @@ void uv_once(pthread_once_t* guard, void (*callback)(void)) { } int uv_mutex_init(uv_mutex_t* mutex) { -#if defined(NDEBUG) || !defined(PTHREAD_MUTEX_ERRORCHECK) +#if defined(__wasi__) || defined(NDEBUG) || !defined(PTHREAD_MUTEX_ERRORCHECK) return pthread_mutex_init(mutex, NULL); #else pthread_mutexattr_t attr; diff --git a/packages/emnapi/tsconfig.core.json b/packages/emnapi/tsconfig.core.json index d89580f2..96e33b6a 100644 --- a/packages/emnapi/tsconfig.core.json +++ b/packages/emnapi/tsconfig.core.json @@ -7,7 +7,11 @@ ] }, "include": [ - "./src/core/**/*.ts", + "./src/core/init.ts", + "./src/core/async.ts", + "./src/core/miscellaneous.ts", + "./src/core/string.ts", + "./src/core/util.ts", "../runtime/src/typings/**/*.d.ts", "./src/typings/**/*.d.ts", "./src/*.ts", diff --git a/packages/runtime/src/Context.ts b/packages/runtime/src/Context.ts index 292001f1..4f765416 100644 --- a/packages/runtime/src/Context.ts +++ b/packages/runtime/src/Context.ts @@ -184,7 +184,7 @@ export class Context { } closeScope (envObject: Env, _scope?: HandleScope): void { - return this.scopeStore.closeScope(envObject) + this.scopeStore.closeScope(envObject) } ensureHandle (value: S): Handle { diff --git a/packages/test/CMakeLists.txt b/packages/test/CMakeLists.txt index b40bc1eb..96e3007e 100644 --- a/packages/test/CMakeLists.txt +++ b/packages/test/CMakeLists.txt @@ -33,6 +33,12 @@ else() set(IS_WASI OFF) endif() +if((CMAKE_SYSTEM_NAME STREQUAL "WASI") AND (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-wasi-threads")) + set(IS_WASI_THREADS ON) +else() + set(IS_WASI_THREADS OFF) +endif() + if((CMAKE_C_COMPILER_TARGET STREQUAL "wasm32") OR (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-unknown-unknown")) set(IS_WASM32 ON) else() @@ -88,7 +94,16 @@ if(IS_EMSCRIPTEN) "-sSAFE_HEAP=1" "-sMODULARIZE=1" ) +elseif(IS_WASI_THREADS) + add_compile_options("-fno-exceptions") + set(COMMON_LINK_OPTIONS + # "-v" + "-mexec-model=reactor" + # "-Wl,--import-memory" + "-Wl,--import-memory,--max-memory=2147483648,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table" + ) elseif(IS_WASI) + add_compile_options("-fno-exceptions") set(COMMON_LINK_OPTIONS # "-v" "-mexec-model=reactor" @@ -96,6 +111,7 @@ elseif(IS_WASI) "-Wl,--initial-memory=16777216,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table" ) elseif(IS_WASM32) + add_compile_options("-fno-exceptions") set(COMMON_LINK_OPTIONS # "-v" "-nostdlib" @@ -144,7 +160,7 @@ function(add_test NAME SOURCE_LIST NEED_ENTRY PTHREAD LINKOPTIONS) target_link_libraries(${NAME} PRIVATE "testcommon") if(IS_WASM) if(PTHREAD) - if(IS_WASI OR IS_WASM32) + if((IS_WASI AND NOT IS_WASI_THREADS) OR IS_WASM32) target_link_libraries(${NAME} PRIVATE "emnapi-basic") else() target_link_libraries(${NAME} PRIVATE "emnapi-mt") @@ -189,7 +205,7 @@ function(add_naa_test NAME SOURCE_LIST DEFINES ENABLE_EXCEPTION) set_target_properties(${NAME} PROPERTIES BUILD_RPATH "$ORIGIN") if(IS_WASM) - if(IS_WASI OR IS_WASM32) + if((IS_WASI AND NOT IS_WASI_THREADS) OR IS_WASM32) target_link_libraries(${NAME} PRIVATE "emnapi-basic") else() target_link_libraries(${NAME} PRIVATE "emnapi-mt") @@ -221,11 +237,11 @@ endfunction() add_test("env" "./env/binding.c" OFF OFF "") add_test("hello" "./hello/binding.c" OFF OFF "") -if((NOT IS_WASM) OR IS_WASI OR IS_EMSCRIPTEN) +if((NOT IS_WASM) OR IS_WASI OR IS_EMSCRIPTEN OR IS_WASI_THREADS) add_test("async" "./async/binding.c" OFF ON "") endif() -if((NOT IS_WASM) OR IS_EMSCRIPTEN) +if((NOT IS_WASM) OR IS_EMSCRIPTEN OR IS_WASI_THREADS) add_test("string_mt" "./string/binding.c" ON ON "") if(IS_EMSCRIPTEN) add_test("pool" "./pool/binding.c" OFF ON "--pre-js=../pool/pre.js") @@ -281,11 +297,12 @@ add_test("version" "./version/binding.c" OFF OFF "") add_test("make_callback" "./make_callback/binding.c" OFF OFF "") add_test("async_context" "./async_context/binding.c" OFF OFF "") +# TODO(wasm32-wasi-threads): OR IS_WASI_THREADS if(IS_EMSCRIPTEN) file(GLOB_RECURSE naa_binding_SRC "./node-addon-api/*.cc") - if(NOT IS_MEMORY64) + if(NOT IS_MEMORY64 AND NOT IS_WASI_THREADS) add_naa_test("naa_binding" "${naa_binding_SRC}" "" ON) endif() add_naa_test("naa_binding_noexcept" "${naa_binding_SRC}" "" OFF) diff --git a/packages/test/async/async-wasi.html b/packages/test/async/async-wasi.html new file mode 100644 index 00000000..cdaa6fae --- /dev/null +++ b/packages/test/async/async-wasi.html @@ -0,0 +1,19 @@ + + + + + + + async + + + + + + + + diff --git a/packages/test/async/async-wasi.js b/packages/test/async/async-wasi.js new file mode 100644 index 00000000..65b5cb1c --- /dev/null +++ b/packages/test/async/async-wasi.js @@ -0,0 +1,83 @@ +/* eslint-disable no-undef */ +/* eslint-disable camelcase */ +if (typeof self !== 'undefined') { + importScripts('../../../node_modules/@tybys/wasm-util/dist/wasm-util.min.js') + importScripts('../../core/dist/emnapi-core.js') + importScripts('../../runtime/dist/emnapi.js') +} + +;(async function main () { + const init = function () { + const { WASI } = wasmUtil + const { createNapiModule, loadNapiModule } = emnapiCore + const { getDefaultContext } = emnapi + const wasi = new WASI() + const napiModule = createNapiModule({ + context: getDefaultContext(), + onCreateWorker () { + return new Worker('../worker.js') + } + }) + const wasmMemory = new WebAssembly.Memory({ + initial: 16777216 / 65536, + maximum: 2147483648 / 65536, + shared: true + }) + + const p = new Promise((resolve, reject) => { + loadNapiModule(napiModule, '../.cgenbuild/Debug/async.wasm', { + wasi, + overwriteImports (importObject) { + importObject.env.memory = wasmMemory + } + }).then(() => { + resolve(napiModule.exports) + }).catch(reject) + }) + p.Module = napiModule + return p + } + const test_async = await init() + + const assert = { + strictEqual (a, b) { + if (a !== b) { + throw new Error('') + } + } + } + + await new Promise((resolve) => { + // Successful async execution and completion callback. + test_async.Test(5, {}, function (err, val) { + console.log('test_async.Test(5, {}, callback)') + assert.strictEqual(err, null) + assert.strictEqual(val, 10) + resolve() + }) + }) + + await new Promise((resolve) => { + // Async work item cancellation with callback. + test_async.TestCancel(() => { + console.log('test_async.TestCancel(callback)') + resolve() + }) + }) + + const iterations = 500 + let x = 0 + const workDone = (status) => { + assert.strictEqual(status, 0) + if (++x < iterations) { + setTimeout(() => test_async.DoRepeatedWork(workDone)) + } else { + console.log(x) + } + } + test_async.DoRepeatedWork(workDone) + + await new Promise((resolve) => { + setTimeout(resolve, 3000) + }) +})() diff --git a/packages/test/node-addon-api/common/index.js b/packages/test/node-addon-api/common/index.js index 67c0fc37..968499ee 100644 --- a/packages/test/node-addon-api/common/index.js +++ b/packages/test/node-addon-api/common/index.js @@ -84,7 +84,7 @@ async function runTest (test, buildType, buildPathRoot = process.env.BUILD_PATH // path.join(buildPathRoot, `../build/${buildType}/binding_noexcept.node`), // path.join(buildPathRoot, `../build/${buildType}/binding_noexcept_maybe.node`), // path.join(buildPathRoot, `../build/${buildType}/binding_custom_namespace.node`) - ...(!process.env.MEMORY64 ? ['naa_binding'] : []), + ...((!process.env.MEMORY64 && !process.env.EMNAPI_TEST_WASI_THREADS) ? ['naa_binding'] : []), 'naa_binding_noexcept', 'naa_binding_noexcept_maybe', 'naa_binding_custom_namespace' @@ -111,7 +111,7 @@ async function runTestWithBindingPath (test, buildType, buildPathRoot = process. // path.join(buildPathRoot, `../build/${buildType}/binding_noexcept.node`), // path.join(buildPathRoot, `../build/${buildType}/binding_noexcept_maybe.node`), // path.join(buildPathRoot, `../build/${buildType}/binding_custom_namespace.node`) - ...(!process.env.MEMORY64 ? [getEntry('naa_binding')] : []), + ...((!process.env.MEMORY64 && !process.env.EMNAPI_TEST_WASI_THREADS) ? [getEntry('naa_binding')] : []), getEntry('naa_binding_noexcept'), getEntry('naa_binding_noexcept_maybe'), getEntry('naa_binding_custom_namespace') diff --git a/packages/test/package.json b/packages/test/package.json index 2ad3deb5..3e54826e 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -5,10 +5,12 @@ "description": "emnapi test", "main": "index.js", "devDependencies": { + "@tybys/wasm-util": "^0.8.0", "chalk": "^4.1.1", "cmake-js": "^7.1.1", "cross-env": "^7.0.3", "glob": "^7.2.0", + "memfs-browser": "^3.4.13000", "node-addon-api": "6.0.0", "why-is-node-running": "^2.2.2" }, @@ -17,6 +19,8 @@ "rebuild:r": "node ./script/rm .cgenbuild && cross-env UV_THREADPOOL_SIZE=2 emcmake cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -H. -B.cgenbuild && cmake --build .cgenbuild --clean-first", "rebuild:w": "cross-env UV_THREADPOOL_SIZE=2 node ./script/build-wasi.js Debug", "rebuild:wr": "cross-env UV_THREADPOOL_SIZE=2 node ./script/build-wasi.js Release", + "rebuild:wt": "cross-env UV_THREADPOOL_SIZE=2 node ./script/build-wasi-threads.js Debug", + "rebuild:wtr": "cross-env UV_THREADPOOL_SIZE=2 node ./script/build-wasi-threads.js Release", "rebuild:wasm32": "cross-env UV_THREADPOOL_SIZE=2 node ./script/build-wasm32.js Debug", "rebuild:wasm32r": "cross-env UV_THREADPOOL_SIZE=2 node ./script/build-wasm32.js Release", "rebuild:n": "cmake-js rebuild -D -O .cgenbuild", @@ -25,6 +29,8 @@ "test:r": "cross-env NODE_TEST_KNOWN_GLOBALS=0 UV_THREADPOOL_SIZE=2 NODE_ENV=production node ./script/test.js", "test:w": "cross-env NODE_TEST_KNOWN_GLOBALS=0 EMNAPI_TEST_WASI=1 UV_THREADPOOL_SIZE=2 node ./script/test.js", "test:wr": "cross-env NODE_TEST_KNOWN_GLOBALS=0 EMNAPI_TEST_WASI=1 UV_THREADPOOL_SIZE=2 NODE_ENV=production node ./script/test.js", + "test:wt": "cross-env NODE_TEST_KNOWN_GLOBALS=0 EMNAPI_TEST_WASI=1 EMNAPI_TEST_WASI_THREADS=1 UV_THREADPOOL_SIZE=2 node ./script/test.js", + "test:wtr": "cross-env NODE_TEST_KNOWN_GLOBALS=0 EMNAPI_TEST_WASI=1 EMNAPI_TEST_WASI_THREADS=1 UV_THREADPOOL_SIZE=2 NODE_ENV=production node ./script/test.js", "test:wasm32": "cross-env NODE_TEST_KNOWN_GLOBALS=0 EMNAPI_TEST_WASM32=1 UV_THREADPOOL_SIZE=2 node ./script/test.js", "test:wasm32r": "cross-env NODE_TEST_KNOWN_GLOBALS=0 EMNAPI_TEST_WASM32=1 UV_THREADPOOL_SIZE=2 NODE_ENV=production node ./script/test.js", "test:n": "cross-env NODE_TEST_KNOWN_GLOBALS=0 EMNAPI_TEST_NATIVE=1 UV_THREADPOOL_SIZE=2 node ./script/test.js", diff --git a/packages/test/pool/pool-wasi.html b/packages/test/pool/pool-wasi.html new file mode 100644 index 00000000..2c7ccfb0 --- /dev/null +++ b/packages/test/pool/pool-wasi.html @@ -0,0 +1,19 @@ + + + + + + + pool + + + + + + + + diff --git a/packages/test/pool/pool-wasi.js b/packages/test/pool/pool-wasi.js new file mode 100644 index 00000000..c2a457df --- /dev/null +++ b/packages/test/pool/pool-wasi.js @@ -0,0 +1,45 @@ +/* eslint-disable no-undef */ +/* eslint-disable camelcase */ +if (typeof self !== 'undefined') { + importScripts('../../../node_modules/@tybys/wasm-util/dist/wasm-util.min.js') + importScripts('../../core/dist/emnapi-core.js') + importScripts('../../runtime/dist/emnapi.js') +} + +;(async function main () { + const init = function () { + const { WASI } = wasmUtil + const { createNapiModule, loadNapiModule } = emnapiCore + const { getDefaultContext } = emnapi + const wasi = new WASI() + const napiModule = createNapiModule({ + context: getDefaultContext(), + reuseWorker: true, + onCreateWorker () { + return new Worker('../worker.js') + } + }) + const wasmMemory = new WebAssembly.Memory({ + initial: 16777216 / 65536, + maximum: 2147483648 / 65536, + shared: true + }) + + const p = new Promise((resolve, reject) => { + loadNapiModule(napiModule, '../.cgenbuild/Debug/pool.wasm', { + wasi, + overwriteImports (importObject) { + importObject.env.memory = wasmMemory + } + }).then(() => { + resolve(napiModule.exports) + }).catch(reject) + }) + p.Module = napiModule + return p + } + const A = await init() + + await Promise.all(Array.from({ length: 4 }, () => A.async_method())) + console.log('done') +})() diff --git a/packages/test/rust/rust.test.js b/packages/test/rust/rust.test.js index 0c6e9f50..71f768c6 100644 --- a/packages/test/rust/rust.test.js +++ b/packages/test/rust/rust.test.js @@ -7,33 +7,24 @@ module.exports = new Promise((resolve, reject) => { const context = require('@emnapi/runtime').getDefaultContext() - const { createNapiModule } = require('@emnapi/core') - const napiModule = createNapiModule({ - context - }) + const { instantiateNapiModule } = require('@emnapi/core') const wasmBuffer = process.env.EMNAPI_TEST_WASI ? require('fs').readFileSync(require('path').join(__dirname, './target/wasm32-wasi/release/binding.wasm')) : require('fs').readFileSync(require('path').join(__dirname, './target/wasm32-unknown-unknown/release/binding.wasm')) - WebAssembly.instantiate(wasmBuffer, { - ...(process.env.EMNAPI_TEST_WASI ? { wasi_snapshot_preview1: wasi.wasiImport } : {}), - env: { - ...napiModule.imports.env, - ...napiModule.imports.napi, - ...napiModule.imports.emnapi + instantiateNapiModule(wasmBuffer, { + context, + wasi, + overwriteImports (importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi + } } - }).then(({ instance, module }) => { - console.log(instance.exports) - if (process.env.EMNAPI_TEST_WASI) { - wasi.initialize(instance) - } - const binding = napiModule.init({ - instance, - module, - memory: instance.exports.memory, - table: instance.exports.__indirect_function_table - }) + }).then(({ napiModule }) => { + const binding = napiModule.exports require('assert').strictEqual(binding.sum(1, 2), 3) resolve() }).catch(reject) diff --git a/packages/test/script/build-wasi-threads.js b/packages/test/script/build-wasi-threads.js new file mode 100644 index 00000000..4d05d77e --- /dev/null +++ b/packages/test/script/build-wasi-threads.js @@ -0,0 +1,41 @@ +const path = require('path') +const fs = require('fs') +const { spawn } = require('../../../script/spawn.js') +const { which } = require('../../../script/which.js') + +async function main () { + const buildDir = path.join(__dirname, '../.cgenbuild') + const cwd = path.join(__dirname, '..') + + fs.rmSync(buildDir, { force: true, recursive: true }) + fs.mkdirSync(buildDir, { recursive: true }) + let WASI_SDK_PATH = process.env.WASI_SDK_PATH + if (!WASI_SDK_PATH) { + throw new Error('process.env.WASI_SDK_PATH is falsy value') + } + if (!path.isAbsolute(WASI_SDK_PATH)) { + WASI_SDK_PATH = path.join(__dirname, '../../..', WASI_SDK_PATH) + } + WASI_SDK_PATH = WASI_SDK_PATH.replace(/\\/g, '/') + + await spawn('cmake', [ + ...( + which('ninja') + ? ['-G', 'Ninja'] + : (process.platform === 'win32' ? ['-G', 'MinGW Makefiles', '-DCMAKE_MAKE_PROGRAM=make'] : []) + ), + `-DCMAKE_TOOLCHAIN_FILE=${WASI_SDK_PATH}/share/cmake/wasi-sdk-pthread.cmake`, + `-DWASI_SDK_PREFIX=${WASI_SDK_PATH}`, + `-DCMAKE_BUILD_TYPE=${process.argv[2] || 'Debug'}`, + // '-DCMAKE_VERBOSE_MAKEFILE=1', + '-H.', + '-B', buildDir + ], cwd) + + await spawn('cmake', [ + '--build', + buildDir + ], cwd) +} + +main() diff --git a/packages/test/script/test.js b/packages/test/script/test.js index 83010213..1061825f 100644 --- a/packages/test/script/test.js +++ b/packages/test/script/test.js @@ -1,3 +1,7 @@ +if (process.env.EMNAPI_TEST_WASI_THREADS) { + process.env.EMNAPI_TEST_WASI = 1 +} + const { spawnSync } = require('child_process') const path = require('path') const glob = require('glob') @@ -24,7 +28,7 @@ if (process.env.EMNAPI_TEST_NATIVE) { 'rust/**/*', '**/{emnapitest,node-addon-api}/**/*' ])] -} else if (process.env.EMNAPI_TEST_WASI || process.env.EMNAPI_TEST_WASM32) { +} else if (!process.env.EMNAPI_TEST_WASI_THREADS && (process.env.EMNAPI_TEST_WASI || process.env.EMNAPI_TEST_WASM32)) { ignore = [...new Set([ ...ignore, ...(process.env.EMNAPI_TEST_WASI ? pthread.filter(item => (item !== 'async/**/*')) : pthread) @@ -36,6 +40,14 @@ if (process.env.EMNAPI_TEST_NATIVE) { ])] } +// # TODO(wasm32-wasi-threads): Remove this +if (process.env.EMNAPI_TEST_WASI_THREADS) { + ignore = [...new Set([ + ...ignore, + 'node-addon-api/**/*' + ])] +} + let files = glob.sync(subdir ? subdir.endsWith('.js') ? subdir diff --git a/packages/test/tsfn/binding.c b/packages/test/tsfn/binding.c index f36d42ca..25f4619d 100644 --- a/packages/test/tsfn/binding.c +++ b/packages/test/tsfn/binding.c @@ -6,11 +6,29 @@ #ifdef __EMSCRIPTEN__ #include #endif + +#ifdef __wasi__ +#include +#endif + #include #include #include #include "../common.h" +double now() { +#if defined(__EMSCRIPTEN__) + return emscripten_get_now(); +#elif defined(__wasi__) + struct timespec t; + timespec_get(&t, TIME_UTC); + return ((double)t.tv_sec * 1000) + ((double)t.tv_nsec / 1000000); +#else + uint64_t start = uv_hrtime(); + return (double)(start / 1000000); +#endif +} + #define ARRAY_LENGTH 10000 #define MAX_QUEUE_SIZE 2 @@ -80,13 +98,8 @@ static void* data_source_thread(void* data) { if (ts_fn_info->max_queue_size == 0 && (index % 1000 == 0)) { // Let's make this thread really busy for 200 ms to give the main thread a // chance to abort. -#ifdef __EMSCRIPTEN__ - double start = emscripten_get_now(); - for (; emscripten_get_now() - start < 200.0;); -#else - uint64_t start = uv_hrtime(); - for (; uv_hrtime() - start < 200000000;); -#endif + double start = now(); + for (; now() - start < 200.0;); } switch (status) { case napi_queue_full: @@ -127,6 +140,7 @@ static void* data_source_thread(void* data) { napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH, "napi_release_threadsafe_function failed", NAPI_AUTO_LENGTH); } + return NULL; } diff --git a/packages/test/util.js b/packages/test/util.js index 233128d5..0c99495a 100644 --- a/packages/test/util.js +++ b/packages/test/util.js @@ -1,4 +1,6 @@ +/* eslint-disable camelcase */ const { join } = require('path') +const fs = require('fs') const common = require('./common.js') const emnapi = require('../runtime') @@ -17,44 +19,51 @@ function loadPath (request, options) { } if (process.env.EMNAPI_TEST_WASI) { - const { WASI } = require('wasi') - const { createNapiModule } = require('@emnapi/core') - const wasi = new WASI() + const { WASI } = require('./wasi') + const { Worker } = require('worker_threads') + const { createNapiModule, loadNapiModule } = require('@emnapi/core') + const wasi = new WASI({ + fs + }) const napiModule = createNapiModule({ context, + filename: request, + reuseWorker: true, + onCreateWorker () { + return new Worker(join(__dirname, './worker.js'), { + env: process.env, + execArgv: ['--experimental-wasi-unstable-preview1'] + }) + }, ...(options || {}) }) - const p = new Promise((resolve, reject) => { - WebAssembly.instantiate(require('fs').readFileSync(request), { - wasi_snapshot_preview1: wasi.wasiImport, - env: napiModule.imports.env, - napi: napiModule.imports.napi, - emnapi: napiModule.imports.emnapi + let wasmMemory + if (process.env.EMNAPI_TEST_WASI_THREADS) { + wasmMemory = new WebAssembly.Memory({ + initial: 16777216 / 65536, + maximum: 2147483648 / 65536, + shared: true }) - .then(({ instance, module }) => { - wasi.initialize(instance) - let exports - try { - exports = napiModule.init({ - instance, - module, - memory: instance.exports.memory, - table: instance.exports.__indirect_function_table - }) - } catch (err) { - reject(err) - return + } + + const p = new Promise((resolve, reject) => { + loadNapiModule(napiModule, fs.readFileSync(request), { + wasi, + overwriteImports (importObject) { + if (process.env.EMNAPI_TEST_WASI_THREADS) { + importObject.env.memory = wasmMemory } - resolve(exports) - }) - .catch(reject) + } + }).then(() => { + resolve(napiModule.exports) + }).catch(reject) }) p.Module = napiModule return p } if (process.env.EMNAPI_TEST_WASM32) { - const { createNapiModule } = require('@emnapi/core') + const { createNapiModule, loadNapiModule } = require('@emnapi/core') const napiModule = createNapiModule({ context, ...(options || {}) @@ -70,35 +79,18 @@ function loadPath (request, options) { const shared = (typeof SharedArrayBuffer === 'function') && (wasmMemory.buffer instanceof SharedArrayBuffer) return new TextDecoder().decode(shared ? HEAPU8.slice(ptr, end) : HEAPU8.subarray(ptr, end)) } - WebAssembly.instantiate(require('fs').readFileSync(request), { - env: { - ...napiModule.imports.env, - console_log (fmt, ...args) { + loadNapiModule(napiModule, fs.readFileSync(request), { + overwriteImports (importObject) { + importObject.env.console_log = function (fmt, ...args) { const fmtString = UTF8ToString(fmt) console.log(fmtString, ...args) return 0 } - }, - napi: napiModule.imports.napi, - emnapi: napiModule.imports.emnapi - }) - .then(({ instance, module }) => { - wasmMemory = instance.exports.memory - let exports - try { - exports = napiModule.init({ - instance, - module, - memory: instance.exports.memory, - table: instance.exports.__indirect_function_table - }) - } catch (err) { - reject(err) - return - } - resolve(exports) - }) - .catch(reject) + } + }).then(({ instance }) => { + wasmMemory = instance.exports.memory + resolve(napiModule.exports) + }).catch(reject) }) p.Module = napiModule return p diff --git a/packages/test/wasi.js b/packages/test/wasi.js new file mode 100644 index 00000000..ad44693c --- /dev/null +++ b/packages/test/wasi.js @@ -0,0 +1,7 @@ +if (typeof window !== 'undefined') { + // eslint-disable-next-line no-undef + window.WASI = wasmUtil.WASI +} else { + // exports.WASI = require('wasi').WASI + exports.WASI = require('@tybys/wasm-util').WASI +} diff --git a/packages/test/worker.js b/packages/test/worker.js new file mode 100644 index 00000000..5186d5c7 --- /dev/null +++ b/packages/test/worker.js @@ -0,0 +1,87 @@ +/* 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 fs, WASI, emnapiCore + + 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') + + const parentPort = nodeWorkerThreads.parentPort + + parentPort.on('message', (data) => { + globalThis.onmessage({ data }) + }) + + fs = require('fs') + + 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('./wasi').WASI + emnapiCore = require('@emnapi/core') + } else { + importScripts('../../node_modules/memfs-browser/dist/memfs.js') + importScripts('../../node_modules/@tybys/wasm-util/dist/wasm-util.min.js') + importScripts('../../node_modules/@emnapi/core/dist/emnapi-core.js') + emnapiCore = globalThis.emnapiCore + + const { Volume, createFsFromVolume } = memfs + fs = createFsFromVolume(Volume.fromJSON({ + '/': null + })) + + WASI = globalThis.wasmUtil.WASI + } + + const { instantiateNapiModuleSync, MessageHandler } = emnapiCore + + const handler = new MessageHandler({ + onLoad ({ wasmModule, wasmMemory }) { + const wasi = new WASI({ + fs, + print: ENVIRONMENT_IS_NODE + ? (...args) => { + const str = require('util').format(...args) + fs.writeSync(1, str + '\n') + } + : function () { console.log.apply(console, arguments) } + }) + + return instantiateNapiModuleSync(wasmModule, { + childThread: true, + wasi, + overwriteImports (importObject) { + importObject.env.memory = wasmMemory + } + }) + } + }) + + globalThis.onmessage = function (e) { + handler.handle(e) + // handle other messages + } +})() diff --git a/script/release.js b/script/release.js index 3ae90466..864854dc 100644 --- a/script/release.js +++ b/script/release.js @@ -82,10 +82,17 @@ async function main () { sysroot ], cwd) + let WASI_THREADS_CMAKE_TOOLCHAIN_FILE = '' if (fs.existsSync(path.join(wasiSdkPath, 'share/cmake/wasi-sdk-pthread.cmake'))) { + WASI_THREADS_CMAKE_TOOLCHAIN_FILE = `${WASI_SDK_PATH}/share/cmake/wasi-sdk-pthread.cmake` + } else if (fs.existsSync(path.join(wasiSdkPath, 'share/cmake/wasi-sdk-threads.cmake'))) { + WASI_THREADS_CMAKE_TOOLCHAIN_FILE = `${WASI_SDK_PATH}/share/cmake/wasi-sdk-threads.cmake` + } + + if (WASI_THREADS_CMAKE_TOOLCHAIN_FILE) { await spawn('cmake', [ ...generatorOptions, - `-DCMAKE_TOOLCHAIN_FILE=${WASI_SDK_PATH}/share/cmake/wasi-sdk-pthread.cmake`, + `-DCMAKE_TOOLCHAIN_FILE=${WASI_THREADS_CMAKE_TOOLCHAIN_FILE}`, `-DWASI_SDK_PREFIX=${WASI_SDK_PATH}`, '-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_VERBOSE_MAKEFILE=1', diff --git a/tsconfig.json b/tsconfig.json index b3732944..4bf02f30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "ES2015", "ES2020.BigInt", "ES2021.WeakRef", + "ES2017.SharedMemory", "DOM" ] },