emnapi
Node-API implementation for Emscripten, wasi-sdk and clang with wasm support. napi-rs support is comming soon.
This project aims to
- Help users port their or existing Node-API native addons to wasm with code change as less as possible.
- Make runtime behavior matches native Node.js as much as possible.
See documentation for more details:
δΈζζζ‘£οΌ
Full API List
How to build Node-API official examples
Prerequests
You will need to install:
- Node.js
>= v16.15.0
- npm
>= v8
- Emscripten
>= v3.1.9
/ wasi-sdk / LLVM clang with wasm support - (Optional) CMake
>= v3.13
- (Optional) ninja
- (Optional) make
- (Optional) node-addon-api
>= 6.1.0
There are several choices to get make
for Windows user
Verify your environment:
node -v
npm -v
emcc -v
cmake --version
ninja --version
make -v
nmake /?
Build from source
You need to set EMSDK
and WASI_SDK_PATH
environment variables.
git clone https://github.com/toyobayashi/emnapi.git
cd ./emnapi
npm install -g node-gyp
npm install
npm run build
node ./script/release.js
npm run rebuild:test
npm test
See CONTRIBUTING for more details.
Quick Start
NPM Install
npm install -D emnapi
npm install @emnapi/runtime
npm install @emnapi/core
npm install node-addon-api
Each package should match the same version.
Using C
Create hello.c
.
#include <node_api.h>
#define NODE_API_CALL(env, the_call) \
do { \
if ((the_call) != napi_ok) { \
const napi_extended_error_info *error_info; \
napi_get_last_error_info((env), &error_info); \
bool is_pending; \
const char* err_message = error_info->error_message; \
napi_is_exception_pending((env), &is_pending); \
if (!is_pending) { \
const char* error_message = err_message != NULL ? \
err_message : \
"empty error message"; \
napi_throw_error((env), NULL, error_message); \
} \
return NULL; \
} \
} while (0)
static napi_value js_hello(napi_env env, napi_callback_info info) {
napi_value world;
const char* str = "world";
NODE_API_CALL(env, napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &world));
return world;
}
NAPI_MODULE_INIT() {
napi_value hello;
NODE_API_CALL(env, napi_create_function(env, "hello", NAPI_AUTO_LENGTH,
js_hello, NULL, &hello));
NODE_API_CALL(env, napi_set_named_property(env, exports, "hello", hello));
return exports;
}
The C code is equivalant to the following JavaScript:
module.exports = (function (exports) {
const hello = function hello () {
const world = 'world'
return world
}
exports.hello = hello
return exports
})(module.exports)
Building
emscripten
emcc -O3 \
-DBUILDING_NODE_EXTENSION \
"-DNAPI_EXTERN=__attribute__((__import_module__(\"env\")))" \
-I./node_modules/emnapi/include \
-L./node_modules/emnapi/lib/wasm32-emscripten \
--js-library=./node_modules/emnapi/dist/library_napi.js \
-sEXPORTED_FUNCTIONS="['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']" \
-o hello.js \
hello.c \
-lemnapi
wasi-sdk
clang -O3 \
-DBUILDING_NODE_EXTENSION \
-I./node_modules/emnapi/include \
-L./node_modules/emnapi/lib/wasm32-wasi \
--target=wasm32-wasi \
--sysroot=$WASI_SDK_PATH/share/wasi-sysroot \
-mexec-model=reactor \
-Wl,--initial-memory=16777216 \
-Wl,--export-dynamic \
-Wl,--export=malloc \
-Wl,--export=free \
-Wl,--export=napi_register_wasm_v1 \
-Wl,--export-if-defined=node_api_module_get_api_version_v1 \
-Wl,--import-undefined \
-Wl,--export-table \
-o hello.wasm \
hello.c \
-lemnapi
clang wasm32
Choose libdlmalloc.a
or libemmalloc.a
for malloc
and free
.
clang -O3 \
-DBUILDING_NODE_EXTENSION \
-I./node_modules/emnapi/include \
-L./node_modules/emnapi/lib/wasm32 \
--target=wasm32 \
-nostdlib \
-Wl,--no-entry \
-Wl,--initial-memory=16777216 \
-Wl,--export-dynamic \
-Wl,--export=malloc \
-Wl,--export=free \
-Wl,--export=napi_register_wasm_v1 \
-Wl,--export-if-defined=node_api_module_get_api_version_v1 \
-Wl,--import-undefined \
-Wl,--export-table \
-o hello.wasm \
hello.c \
-lemnapi \
-ldlmalloc
Initialization
To initialize emnapi, you need to import the emnapi runtime to create a Context
by createContext
or getDefaultContext
first.
Each context owns isolated Node-API object such as napi_env
, napi_value
, napi_ref
. If you have multiple emnapi modules, you should reuse the same Context
across them.
declare namespace emnapi {
export class Context { }
export function createContext (): Context
export function getDefaultContext (): Context
}
emscripten
then call Module.emnapiInit
after emscripten runtime initialized.
Module.emnapiInit
only do initialization once, it will always return the same binding exports after successfully initialized.
declare namespace Module {
interface EmnapiInitOptions {
context: emnapi.Context
filename?: string
nodeBinding?: typeof import('@emnapi/node-binding')
asyncWorkPoolSize?: number
}
export function emnapiInit (options: EmnapiInitOptions): any
}
<script src="node_modules/@emnapi/runtime/dist/emnapi.min.js"></script>
<script src="hello.js"></script>
<script>
Module.onRuntimeInitialized = function () {
var binding;
try {
binding = Module.emnapiInit({ context: emnapi.getDefaultContext() });
} catch (err) {
console.error(err);
return;
}
var msg = 'hello ' + binding.hello();
window.alert(msg);
};
Module({ }).then(function (Module) {
var binding = Module.emnapiInit({ context: emnapi.getDefaultContext() });
});
</script>
If you are using Visual Studio Code
and have Live Server
extension installed, you can right click the HTML file in Visual Studio Code source tree and click Open With Live Server
, then you can see the hello world alert!
Running on Node.js:
const emnapi = require('@emnapi/runtime')
const Module = require('./hello.js')
Module.onRuntimeInitialized = function () {
let binding
try {
binding = Module.emnapiInit({ context: emnapi.getDefaultContext() })
} catch (err) {
console.error(err)
return
}
const msg = `hello ${binding.hello()}`
console.log(msg)
}
Module({ }).then((Module) => {
const binding = Module.emnapiInit({ context: emnapi.getDefaultContext() })
})
wasi-sdk or clang wasm32
For non-emscripten, you need to use @emnapi/core
. The initialization is similar to emscripten.
<script src="node_modules/@emnapi/runtime/dist/emnapi.min.js"></script>
<script src="node_modules/@emnapi/core/dist/emnapi-core.min.js"></script>
<script>
emnapiCore.instantiateNapiModule(fetch('./hello.wasm'), {
context: emnapi.getDefaultContext(),
overwriteImports (importObject) {
}
}).then(({ instance, module, napiModule }) => {
const binding = napiModule.exports
})
</script>
Using WASI on Node.js
const { instantiateNapiModule } = require('@emnapi/core')
const { getDefaultContext } = require('@emnapi/runtime')
const { WASI } = require('wasi')
const fs = require('fs')
instantiateNapiModule(fs.promises.readFile('./hello.wasm'), {
wasi: new WASI({ }),
context: getDefaultContext(),
overwriteImports (importObject) {
}
}).then(({ instance, module, napiModule }) => {
const binding = napiModule.exports
})
Using WASI on browser, you can use WASI polyfill in wasm-util,
and memfs-browser
import { instantiateNapiModule } from '@emnapi/core'
import { getDefaultContext } from '@emnapi/runtime'
import { WASI } from '@tybys/wasm-util'
import { Volume, createFsFromVolume } from 'memfs-browser'
const fs = createFsFromVolume(Volume.fromJSON({ }))
instantiateNapiModule(fetch('./hello.wasm'), {
wasi: new WASI({ fs, })
context: getDefaultContext(),
overwriteImports (importObject) {
}
}).then(({ instance, module, napiModule }) => {
const binding = napiModule.exports
})
Using C++ and node-addon-api
Require node-addon-api
>= 6.1.0
npm install node-addon-api
Note: C++ wrapper can only be used to target Node.js v14.6.0+ and modern browsers those support FinalizationRegistry
and WeakRef
(v8 engine v8.4+)!
Create hello.cpp
.
#include <napi.h>
Napi::String Method(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "world");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "hello"),
Napi::Function::New(env, Method)).Check();
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
Compile hello.cpp
using em++
. C++ exception is disabled by Emscripten default, and not supported by wasi-sdk, so predefine -DNAPI_DISABLE_CPP_EXCEPTIONS
and -DNODE_ADDON_API_ENABLE_MAYBE
here. If you would like to enable C++ exception, use -sDISABLE_EXCEPTION_CATCHING=0
instead and remove .Check()
call. See official documentation here.
Building
emscripten
em++ -O3 \
-DBUILDING_NODE_EXTENSION \
"-DNAPI_EXTERN=__attribute__((__import_module__(\"env\")))" \
-DNAPI_DISABLE_CPP_EXCEPTIONS \
-DNODE_ADDON_API_ENABLE_MAYBE \
-I./node_modules/emnapi/include \
-I./node_modules/node-addon-api \
-L./node_modules/emnapi/lib/wasm32-emscripten \
--js-library=./node_modules/emnapi/dist/library_napi.js \
-sEXPORTED_FUNCTIONS="['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']" \
-o hello.js \
hello.cpp \
-lemnapi
wasi-sdk
clang++ -O3 \
-DBUILDING_NODE_EXTENSION \
-DNAPI_DISABLE_CPP_EXCEPTIONS \
-DNODE_ADDON_API_ENABLE_MAYBE \
-I./node_modules/emnapi/include \
-I./node_modules/node-addon-api \
-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 \
-Wl,--export=malloc \
-Wl,--export=free \
-Wl,--export=napi_register_wasm_v1 \
-Wl,--export-if-defined=node_api_module_get_api_version_v1 \
-Wl,--import-undefined \
-Wl,--export-table \
-o hello.wasm \
hello.cpp \
-lemnapi
clang wasm32
node-addon-api
is using the C++ standard libraries, so you must use WASI if you are using node-addon-api
.
You can still use wasm32-unknown-unknown
target if you use Node-API C API only in C++.
clang++ -O3 \
-DBUILDING_NODE_EXTENSION \
-I./node_modules/emnapi/include \
-L./node_modules/emnapi/lib/wasm32 \
--target=wasm32 \
-fno-exceptions \
-nostdlib \
-Wl,--no-entry \
-Wl,--initial-memory=16777216 \
-Wl,--export-dynamic \
-Wl,--export=malloc \
-Wl,--export=free \
-Wl,--export=napi_register_wasm_v1 \
-Wl,--export-if-defined=node_api_module_get_api_version_v1 \
-Wl,--import-undefined \
-Wl,--export-table \
-o node_api_c_api_only.wasm \
node_api_c_api_only.cpp \
-lemnapi \
-ldlmalloc
operator new
and operator delete
.
#include <stddef.h>
extern "C" void* malloc(size_t size);
extern "C" void free(void* p);
void* operator new(size_t size) {
return malloc(size);
}
void operator delete(void* p) noexcept {
free(p);
}
Using CMake
Create CMakeLists.txt
.
cmake_minimum_required(VERSION 3.13)
project(emnapiexample)
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/node_modules/emnapi")
add_executable(hello hello.c)
target_link_libraries(hello emnapi)
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
target_link_options(hello PRIVATE
"-sEXPORTED_FUNCTIONS=['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']"
)
elseif(CMAKE_SYSTEM_NAME STREQUAL "WASI")
set_target_properties(hello PROPERTIES SUFFIX ".wasm")
target_link_options(hello PRIVATE
"-mexec-model=reactor"
"-Wl,--export=napi_register_wasm_v1"
"-Wl,--export-if-defined=node_api_module_get_api_version_v1"
"-Wl,--initial-memory=16777216,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table"
)
elseif((CMAKE_C_COMPILER_TARGET STREQUAL "wasm32") OR (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-unknown-unknown"))
set_target_properties(hello PROPERTIES SUFFIX ".wasm")
target_link_options(hello PRIVATE
"-nostdlib"
"-Wl,--export=napi_register_wasm_v1"
"-Wl,--export-if-defined=node_api_module_get_api_version_v1"
"-Wl,--no-entry"
"-Wl,--initial-memory=16777216,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table"
)
target_link_libraries(hello dlmalloc)
# target_link_libraries(hello emmalloc)
endif()
If you use node-addon-api, you can use -DEMNAPI_FIND_NODE_ADDON_API=ON
or manually add node-addon-api directory to the include dir via include_directories()
or target_include_directories()
.
mkdir build
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
-DEMNAPI_FIND_NODE_ADDON_API=ON \
-G Ninja -H. -Bbuild
cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk.cmake \
-DWASI_SDK_PREFIX=$WASI_SDK_PATH \
-DEMNAPI_FIND_NODE_ADDON_API=ON \
-DCMAKE_BUILD_TYPE=Release \
-G Ninja -H. -Bbuild
cmake -DCMAKE_TOOLCHAIN_FILE=node_modules/emnapi/cmake/wasm32.cmake \
-DLLVM_PREFIX=$WASI_SDK_PATH \
-DCMAKE_BUILD_TYPE=Release \
-G Ninja -H. -Bbuild
cmake --build build
Output code can run in recent version modern browsers and Node.js latest LTS. IE is not supported.
Using Rust (Experimental)
Currently you can use napi-rs like this, more work is working in progress.
Note: WASI target require rust nightly toolchain.
Cargo.toml
[package]
edition = "2021"
name = "binding"
version = "0.0.0"
[[bin]]
name = "binding"
path = "src/main.rs"
[dependencies]
napi = { version = "2.12.1", default-features = false, features = ["napi8"] }
napi-sys = { version = "2.2.3", features = ["napi8"] }
napi-derive = "2.12.2"
[build-dependencies]
napi-build = "2.0.1"
[profile.release]
strip = "symbols"
.cargo/config.toml
[build]
target = [
"wasm32-unknown-unknown",
"wasm32-wasi"
]
[target.wasm32-unknown-unknown]
rustflags = [
"-L./node_modules/emnapi/lib/wasm32",
"-lemnapi",
"-ldlmalloc",
"-C", "link-arg=--no-entry",
"-C", "link-arg=--initial-memory=16777216",
"-C", "link-arg=--export-dynamic",
"-C", "link-arg=--export=malloc",
"-C", "link-arg=--export=free",
"-C", "link-arg=--export=napi_register_wasm_v1",
"-C", "link-arg=--export-if-defined=node_api_module_get_api_version_v1",
"-C", "link-arg=--export-table",
"-C", "link-arg=--import-undefined",
]
[target.wasm32-wasi]
rustflags = [
"-L./node_modules/emnapi/lib/wasm32-wasi",
"-lemnapi",
"-C", "link-arg=--initial-memory=16777216",
"-C", "link-arg=--export-dynamic",
"-C", "link-arg=--export=malloc",
"-C", "link-arg=--export=free",
"-C", "link-arg=--export=napi_register_wasm_v1",
"-C", "link-arg=--export-if-defined=node_api_module_get_api_version_v1",
"-C", "link-arg=--export-table",
"-C", "link-arg=--import-undefined",
"-Z", "wasi-exec-model=reactor",
]
src/main.rs
#![no_main]
use napi_derive::napi;
#[napi]
fn fibonacci(n: u32) -> u32 {
match n {
1 | 2 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
index.js
const fs = require('fs')
const path = require('path')
const useWASI = false
let wasi
if (useWASI) {
const { WASI } = require('wasi')
wasi = new WASI({ })
}
const { instantiateNapiModule } = require('@emnapi/core')
const wasmBuffer = useWASI
? fs.readFileSync(path.join(__dirname, './target/wasm32-wasi/release/binding.wasm'))
: fs.readFileSync(path.join(__dirname, './target/wasm32-unknown-unknown/release/binding.wasm'))
instantiateNapiModule(wasmBuffer, {
context: require('@emnapi/runtime').getDefaultContext(),
wasi,
beforeInit ({ instance }) {
for (const sym in instance.exports) {
if (sym.startsWith('__napi_register__')) {
instance.exports[sym]()
}
}
},
overwriteImports (importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi
}
}
}).then(({ napiModule }) => {
const binding = napiModule.exports
console.log(binding.fibonacci(5))
})
Multithread
Related API:
They are available in emnapi, but you need to know more details before you start to use them.
Now emnapi has 3 implementations of async work and 2 implementations of TSFN:
- Async work
- A. Libuv threadpool and pthread based implementation in C
- B. Single thread mock in JavaScript
- C. Web worker based implementation in C (stack allocation) and JavaScript
- TSFN
- D. Libuv and pthread based implementation in C
- E. Web worker based implementation in JavaScript
| Library to Link | wasm32-emscripten | wasm32 | wasm32-wasi | wasm32-wasi-threads |
---|
A | libemnapi-mt.a | β
| β | β | β
|
B | libemnapi-basic(-mt).a | β
| β
| β
| β
|
C | libemnapi-basic-mt.a | β | β
| β | β
|
D | libemnapi-mt.a | β
| β | β | β
|
E | libemnapi-basic(-mt).a | β
| β
| β
| β
|
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. If you need to run your addon with multithreaded features on browser,
we recommend you use Emscripten A & D, or bare wasm32 C & E.
Note: For browsers, all the multithreaded features relying on Web Workers (Emscripten pthread also relying on Web Workers)
require cross-origin isolation to enable SharedArrayBuffer
. You can make a page cross-origin isolated
by serving the page with these headers:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
If you would like to avoid SharedArrayBuffer
and cross-origin isolation, please use B & E (link against libemnapi-basic.a
), see the following table for more details.
About Prebuilt Libraries
Prebuilt libraries can be found in the lib
directory in emnapi
npm package.
Library | Description | wasm32-emscripten | wasm32 | wasm32-wasi | wasm32-wasi-threads |
---|
libemnapi.a | no atomics feature.
no libuv port.
napi_*_async_work and napi_*_threadsafe_function always return napi_generic_failure . | β
| β
| β
| β
|
libemnapi-mt.a | atomics feature enabled.
napi_*_async_work and napi_*_threadsafe_function are based on pthread and libuv port. | β
| β | β | β
|
libemnapi-basic.a | no atomics feature.
no libuv port.
napi_*_async_work and napi_*_threadsafe_function are imported from JavaScript land. | β
| β
| β
| β
|
libemnapi-basic-mt.a | atomics feature enabled.
no libuv port.
napi_*_async_work and napi_*_threadsafe_function are imported from JavaScript land.
include emnapi_async_worker_create and emnapi_async_worker_init for WebWorker based async work implementation. | β | β
| β
| β
|
libdlmalloc.a | no atomics feature, no thread safe garanteed. | β | β
| β | β |
libdlmalloc-mt.a | atomics feature enabled, thread safe. | β | β
| β | β |
libemmalloc.a | no atomics feature, no thread safe garanteed. | β | β
| β | β |
libemmalloc-mt.a | atomics feature enabled, thread safe. | β | β
| β | β |
Usage
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/node_modules/emnapi")
add_executable(hello hello.c)
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
target_link_libraries(hello emnapi-mt)
target_compile_options(hello PRIVATE "-pthread")
target_link_options(hello PRIVATE
"-sALLOW_MEMORY_GROWTH=1"
"-sEXPORTED_FUNCTIONS=['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']"
"-pthread"
"-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_link_libraries(hello emnapi-mt)
set_target_properties(hello PROPERTIES SUFFIX ".wasm")
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=napi_register_wasm_v1"
"-Wl,--export-if-defined=node_api_module_get_api_version_v1"
"-Wl,--export=malloc"
"-Wl,--export=free"
"-Wl,--import-undefined"
"-Wl,--export-table"
)
elseif((CMAKE_C_COMPILER_TARGET STREQUAL "wasm32") OR (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-unknown-unknown"))
target_link_libraries(hello emnapi-basic-mt)
set_target_properties(hello PROPERTIES SUFFIX ".wasm")
target_compile_options(hello PRIVATE "-fno-exceptions" "-matomics" "-mbulk-memory")
target_link_options(hello PRIVATE
"-nostdlib"
"-Wl,--no-entry"
"-Wl,--export=napi_register_wasm_v1"
"-Wl,--export-if-defined=node_api_module_get_api_version_v1"
"-Wl,--export=emnapi_async_worker_create"
"-Wl,--export=emnapi_async_worker_init"
"-Wl,--import-memory,--shared-memory,--max-memory=2147483648,--import-undefined"
"-Wl,--export-dynamic,--export=malloc,--export=free,--export-table"
)
endif()
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
-DEMNAPI_FIND_NODE_ADDON_API=ON \
-DEMNAPI_WORKER_POOL_SIZE=4 \
-G Ninja -H. -Bbuild
cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk-pthread.cmake \
-DWASI_SDK_PREFIX=$WASI_SDK_PATH \
-DEMNAPI_FIND_NODE_ADDON_API=ON \
-DCMAKE_BUILD_TYPE=Release \
-G Ninja -H. -Bbuild
cmake -DCMAKE_TOOLCHAIN_FILE=node_modules/emnapi/cmake/wasm32.cmake \
-DLLVM_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.
instantiateNapiModule(input, {
context: getDefaultContext(),
asyncWorkPoolSize: 4,
wasi: new WASI(),
onCreateWorker () {
return new Worker('./worker.js')
},
overwriteImports (importObject) {
importObject.env.memory = new WebAssembly.Memory({
initial: 16777216 / 65536,
maximum: 2147483648 / 65536,
shared: true
})
}
})
(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 })
return instantiateNapiModuleSync(wasmModule, {
childThread: true,
wasi,
overwriteImports (importObject) {
importObject.env.memory = wasmMemory
}
})
}
})
globalThis.onmessage = function (e) {
handler.handle(e)
}
})()
Preprocess Macro Options
-DEMNAPI_WORKER_POOL_SIZE=4
This is UV_THREADPOOL_SIZE
equivalent at compile time, if not predefined, emnapi will read asyncWorkPoolSize
option or UV_THREADPOOL_SIZE
from Emscripten environment variable at runtime:
Module.init({
asyncWorkPoolSize: 2
})
Module.preRun = Module.preRun || [];
Module.preRun.push(function () {
if (typeof ENV !== 'undefined') {
ENV.UV_THREADPOOL_SIZE = '2';
}
});
instantiateNapiModule({
asyncWorkPoolSize: 2
})
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.
You can set both PTHREAD_POOL_SIZE
and EMNAPI_WORKER_POOL_SIZE
to number of CPU cores
in general.
If you use another library function which may create N
child threads in async work,
then you need to set PTHREAD_POOL_SIZE
to EMNAPI_WORKER_POOL_SIZE * (N + 1)
.
This option only has effect if you use -pthread
.
Emnapi will create EMNAPI_WORKER_POOL_SIZE
threads when initializing,
it will throw error if PTHREAD_POOL_SIZE < EMNAPI_WORKER_POOL_SIZE && PTHREAD_POOL_SIZE_STRICT == 2
.
See Issue #8 for more detail.
-DEMNAPI_NEXTTICK_TYPE=0
This option only has effect if you use -pthread
, Default is 0
.
Tell emnapi how to delay async work in uv_async_send
/ uv__async_close
.
0
: Use setImmediate()
(Node.js native setImmediate
or browser MessageChannel
and port.postMessage
)1
: Use Promise.resolve().then()
-DEMNAPI_USE_PROXYING=1
This option only has effect if you use emscripten -pthread
. Default is 1
if emscripten version >= 3.1.9
, else 0
.
-
0
Use JavaScript implementation to send async work from worker threads, runtime code will access the Emscripten internal PThread
object to add custom worker message listener.
-
1
:
Use Emscripten proxying API to send async work from worker threads in C. If you experience something wrong, you can switch set this to 0
and feel free to create an issue.