Installations
npm install emnapi
Developer
Developer Guide
Module System
CommonJS
Min. Node Version
Typescript Support
Yes
Node Version
20.9.0
NPM Version
10.1.0
Statistics
149 Stars
987 Commits
6 Forks
2 Watching
8 Branches
4 Contributors
Updated on 28 Nov 2024
Languages
C (44.19%)
TypeScript (26.02%)
JavaScript (18.95%)
C++ (7.07%)
CMake (1.59%)
Python (1.15%)
HTML (0.91%)
Rust (0.09%)
Assembly (0.04%)
Total Downloads
Cumulative downloads
Total Downloads
641,186
Last day
25.6%
2,835
Compared to previous day
Last week
-10.5%
17,919
Compared to previous week
Last month
49.6%
84,278
Compared to previous month
Last year
7,984.7%
633,352
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
Peer Dependencies
1
emnapi
Sponsors
Node-API implementation for Emscripten, wasi-sdk and clang with wasm support.
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.
This project also powers the WebAssembly feature for napi-rs, and enables many Node.js native addons to run on StackBlitz's WebContainer.
Node-API changes will be synchronized into this repo.
See documentation for more details:
中文文档:
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) node-gyp
>= v10.2.0
- (Optional) ninja
- (Optional) make
- (Optional) node-addon-api
>= 6.1.0
There are several choices to get make
for Windows user
- Install mingw-w64, then use
mingw32-make
- Download MSVC prebuilt binary of GNU make, add to
%Path%
then rename it tomingw32-make
- Install Visual Studio 2022 C++ desktop workload, use
nmake
inVisual Studio Developer Command Prompt
- Install Visual C++ Build Tools, use
nmake
inVisual Studio Developer Command Prompt
Verify your environment:
1node -v 2npm -v 3emcc -v 4 5# clang -v 6# clang -print-targets # ensure wasm32 target exists 7 8cmake --version 9 10# if you use node-gyp 11node-gyp --version 12 13# if you use ninja 14ninja --version 15 16# if you use make 17make -v 18 19# if you use nmake in Visual Studio Developer Command Prompt 20nmake /?
Build from source
You need to set EMSDK
and WASI_SDK_PATH
environment variables.
1git clone https://github.com/toyobayashi/emnapi.git 2cd ./emnapi 3npm install -g node-gyp 4npm install 5npm run build # output ./packages/*/dist 6node ./script/release.js # output ./out 7 8# test 9npm run rebuild:test 10npm test
See CONTRIBUTING for more details.
Quick Start
NPM Install
1npm install -D emnapi 2npm install @emnapi/runtime 3 4# for non-emscripten 5npm install @emnapi/core 6 7# if you use node-addon-api 8npm install node-addon-api
Each package should match the same version.
Using C
Create hello.c
.
1#include <node_api.h> 2 3#define NODE_API_CALL(env, the_call) \ 4 do { \ 5 if ((the_call) != napi_ok) { \ 6 const napi_extended_error_info *error_info; \ 7 napi_get_last_error_info((env), &error_info); \ 8 bool is_pending; \ 9 const char* err_message = error_info->error_message; \ 10 napi_is_exception_pending((env), &is_pending); \ 11 if (!is_pending) { \ 12 const char* error_message = err_message != NULL ? \ 13 err_message : \ 14 "empty error message"; \ 15 napi_throw_error((env), NULL, error_message); \ 16 } \ 17 return NULL; \ 18 } \ 19 } while (0) 20 21static napi_value js_hello(napi_env env, napi_callback_info info) { 22 napi_value world; 23 const char* str = "world"; 24 NODE_API_CALL(env, napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &world)); 25 return world; 26} 27 28NAPI_MODULE_INIT() { 29 napi_value hello; 30 NODE_API_CALL(env, napi_create_function(env, "hello", NAPI_AUTO_LENGTH, 31 js_hello, NULL, &hello)); 32 NODE_API_CALL(env, napi_set_named_property(env, exports, "hello", hello)); 33 return exports; 34}
The C code is equivalant to the following JavaScript:
1module.exports = (function (exports) { 2 const hello = function hello () { 3 // native code in js_hello 4 const world = 'world' 5 return world 6 } 7 8 exports.hello = hello 9 return exports 10})(module.exports)
Building
emscripten
1emcc -O3 \ 2 -DBUILDING_NODE_EXTENSION \ 3 "-DNAPI_EXTERN=__attribute__((__import_module__(\"env\")))" \ 4 -I./node_modules/emnapi/include/node \ 5 -L./node_modules/emnapi/lib/wasm32-emscripten \ 6 --js-library=./node_modules/emnapi/dist/library_napi.js \ 7 -sEXPORTED_FUNCTIONS="['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']" \ 8 -o hello.js \ 9 hello.c \ 10 -lemnapi
wasi-sdk
1clang -O3 \ 2 -DBUILDING_NODE_EXTENSION \ 3 -I./node_modules/emnapi/include/node \ 4 -L./node_modules/emnapi/lib/wasm32-wasi \ 5 --target=wasm32-wasi \ 6 --sysroot=$WASI_SDK_PATH/share/wasi-sysroot \ 7 -mexec-model=reactor \ 8 -Wl,--initial-memory=16777216 \ 9 -Wl,--export-dynamic \ 10 -Wl,--export=malloc \ 11 -Wl,--export=free \ 12 -Wl,--export=napi_register_wasm_v1 \ 13 -Wl,--export-if-defined=node_api_module_get_api_version_v1 \ 14 -Wl,--import-undefined \ 15 -Wl,--export-table \ 16 -o hello.wasm \ 17 hello.c \ 18 -lemnapi
clang wasm32
Choose libdlmalloc.a
or libemmalloc.a
for malloc
and free
.
1clang -O3 \ 2 -DBUILDING_NODE_EXTENSION \ 3 -I./node_modules/emnapi/include/node \ 4 -L./node_modules/emnapi/lib/wasm32 \ 5 --target=wasm32 \ 6 -nostdlib \ 7 -Wl,--no-entry \ 8 -Wl,--initial-memory=16777216 \ 9 -Wl,--export-dynamic \ 10 -Wl,--export=malloc \ 11 -Wl,--export=free \ 12 -Wl,--export=napi_register_wasm_v1 \ 13 -Wl,--export-if-defined=node_api_module_get_api_version_v1 \ 14 -Wl,--import-undefined \ 15 -Wl,--export-table \ 16 -o hello.wasm \ 17 hello.c \ 18 -lemnapi \ 19 -ldlmalloc # -lemmalloc
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.
1declare namespace emnapi { 2 // module '@emnapi/runtime' 3 export class Context { /* ... */ } 4 /** Create a new context */ 5 export function createContext (): Context 6 /** Create or get */ 7 export function getDefaultContext (): Context 8 // ... 9}
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.
1declare namespace Module { 2 interface EmnapiInitOptions { 3 context: emnapi.Context 4 5 /** node_api_get_module_file_name */ 6 filename?: string 7 8 /** 9 * Support following async_hooks related things 10 * on Node.js runtime only 11 * 12 * napi_async_init, 13 * napi_async_destroy, 14 * napi_make_callback, 15 * async resource parameter of 16 * napi_create_async_work and napi_create_threadsafe_function 17 */ 18 nodeBinding?: typeof import('@emnapi/node-binding') 19 20 /** See Multithread part */ 21 asyncWorkPoolSize?: number 22 } 23 export function emnapiInit (options: EmnapiInitOptions): any 24}
1<script src="node_modules/@emnapi/runtime/dist/emnapi.min.js"></script> 2<script src="hello.js"></script> 3<script> 4Module.onRuntimeInitialized = function () { 5 var binding; 6 try { 7 binding = Module.emnapiInit({ context: emnapi.getDefaultContext() }); 8 } catch (err) { 9 console.error(err); 10 return; 11 } 12 var msg = 'hello ' + binding.hello(); 13 window.alert(msg); 14}; 15 16// if -sMODULARIZE=1 17Module({ /* Emscripten module init options */ }).then(function (Module) { 18 var binding = Module.emnapiInit({ context: emnapi.getDefaultContext() }); 19}); 20</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:
1const emnapi = require('@emnapi/runtime') 2const Module = require('./hello.js') 3 4Module.onRuntimeInitialized = function () { 5 let binding 6 try { 7 binding = Module.emnapiInit({ context: emnapi.getDefaultContext() }) 8 } catch (err) { 9 console.error(err) 10 return 11 } 12 const msg = `hello ${binding.hello()}` 13 console.log(msg) 14} 15 16// if -sMODULARIZE=1 17Module({ /* Emscripten module init options */ }).then((Module) => { 18 const binding = Module.emnapiInit({ context: emnapi.getDefaultContext() }) 19})
wasi-sdk or clang wasm32
For non-emscripten, you need to use @emnapi/core
. The initialization is similar to emscripten.
1<script src="node_modules/@emnapi/runtime/dist/emnapi.min.js"></script> 2<script src="node_modules/@emnapi/core/dist/emnapi-core.min.js"></script> 3<script> 4emnapiCore.instantiateNapiModule(fetch('./hello.wasm'), { 5 context: emnapi.getDefaultContext(), 6 overwriteImports (importObject) { 7 // importObject.env = { 8 // ...importObject.env, 9 // ...importObject.napi, 10 // ...importObject.emnapi, 11 // // ... 12 // } 13 } 14}).then(({ instance, module, napiModule }) => { 15 const binding = napiModule.exports 16 // ... 17}) 18</script>
Using WASI on Node.js
1const { instantiateNapiModule } = require('@emnapi/core') 2const { getDefaultContext } = require('@emnapi/runtime') 3const { WASI } = require('wasi') 4const fs = require('fs') 5 6instantiateNapiModule(fs.promises.readFile('./hello.wasm'), { 7 wasi: new WASI({ /* ... */ }), 8 context: getDefaultContext(), 9 overwriteImports (importObject) { 10 // importObject.env = { 11 // ...importObject.env, 12 // ...importObject.napi, 13 // ...importObject.emnapi, 14 // // ... 15 // } 16 } 17}).then(({ instance, module, napiModule }) => { 18 const binding = napiModule.exports 19 // ... 20})
Using WASI on browser, you can use WASI polyfill in wasm-util, and memfs-browser
1import { instantiateNapiModule } from '@emnapi/core' 2import { getDefaultContext } from '@emnapi/runtime' 3import { WASI } from '@tybys/wasm-util' 4import { Volume, createFsFromVolume } from 'memfs-browser' 5 6const fs = createFsFromVolume(Volume.fromJSON({ /* ... */ })) 7instantiateNapiModule(fetch('./hello.wasm'), { 8 wasi: new WASI({ fs, /* ... */ }) 9 context: getDefaultContext(), 10 overwriteImports (importObject) { 11 // importObject.env = { 12 // ...importObject.env, 13 // ...importObject.napi, 14 // ...importObject.emnapi, 15 // // ... 16 // } 17 } 18}).then(({ instance, module, napiModule }) => { 19 const binding = napiModule.exports 20 // ... 21})
Using C++ and node-addon-api
Require node-addon-api
>= 6.1.0
1npm 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
.
1#include <napi.h> 2 3Napi::String Method(const Napi::CallbackInfo& info) { 4 Napi::Env env = info.Env(); 5 return Napi::String::New(env, "world"); 6} 7 8Napi::Object Init(Napi::Env env, Napi::Object exports) { 9 exports.Set(Napi::String::New(env, "hello"), 10 Napi::Function::New(env, Method)).Check(); 11 return exports; 12} 13 14NODE_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
1em++ -O3 \ 2 -DBUILDING_NODE_EXTENSION \ 3 "-DNAPI_EXTERN=__attribute__((__import_module__(\"env\")))" \ 4 -DNAPI_DISABLE_CPP_EXCEPTIONS \ 5 -DNODE_ADDON_API_ENABLE_MAYBE \ 6 -I./node_modules/emnapi/include/node \ 7 -I./node_modules/node-addon-api \ 8 -L./node_modules/emnapi/lib/wasm32-emscripten \ 9 --js-library=./node_modules/emnapi/dist/library_napi.js \ 10 -sEXPORTED_FUNCTIONS="['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']" \ 11 -o hello.js \ 12 hello.cpp \ 13 -lemnapi
wasi-sdk
1clang++ -O3 \ 2 -DBUILDING_NODE_EXTENSION \ 3 -DNAPI_DISABLE_CPP_EXCEPTIONS \ 4 -DNODE_ADDON_API_ENABLE_MAYBE \ 5 -I./node_modules/emnapi/include/node \ 6 -I./node_modules/node-addon-api \ 7 -L./node_modules/emnapi/lib/wasm32-wasi \ 8 --target=wasm32-wasi \ 9 --sysroot=$WASI_SDK_PATH/share/wasi-sysroot \ 10 -fno-exceptions \ 11 -mexec-model=reactor \ 12 -Wl,--initial-memory=16777216 \ 13 -Wl,--export-dynamic \ 14 -Wl,--export=malloc \ 15 -Wl,--export=free \ 16 -Wl,--export=napi_register_wasm_v1 \ 17 -Wl,--export-if-defined=node_api_module_get_api_version_v1 \ 18 -Wl,--import-undefined \ 19 -Wl,--export-table \ 20 -o hello.wasm \ 21 hello.cpp \ 22 -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++.
1clang++ -O3 \ 2 -DBUILDING_NODE_EXTENSION \ 3 -I./node_modules/emnapi/include/node \ 4 -L./node_modules/emnapi/lib/wasm32 \ 5 --target=wasm32 \ 6 -fno-exceptions \ 7 -nostdlib \ 8 -Wl,--no-entry \ 9 -Wl,--initial-memory=16777216 \ 10 -Wl,--export-dynamic \ 11 -Wl,--export=malloc \ 12 -Wl,--export=free \ 13 -Wl,--export=napi_register_wasm_v1 \ 14 -Wl,--export-if-defined=node_api_module_get_api_version_v1 \ 15 -Wl,--import-undefined \ 16 -Wl,--export-table \ 17 -o node_api_c_api_only.wasm \ 18 node_api_c_api_only.cpp \ 19 -lemnapi \ 20 -ldlmalloc # -lemmalloc
operator new
and operator delete
.
1#include <stddef.h> 2 3extern "C" void* malloc(size_t size); 4extern "C" void free(void* p); 5 6void* operator new(size_t size) { 7 return malloc(size); 8} 9 10void operator delete(void* p) noexcept { 11 free(p); 12}
Using CMake
Create CMakeLists.txt
.
1cmake_minimum_required(VERSION 3.13) 2 3project(emnapiexample) 4 5add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/node_modules/emnapi") 6 7add_executable(hello hello.c) 8 9target_link_libraries(hello emnapi) 10if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") 11 target_link_options(hello PRIVATE 12 "-sEXPORTED_FUNCTIONS=['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']" 13 ) 14elseif(CMAKE_SYSTEM_NAME STREQUAL "WASI") 15 set_target_properties(hello PROPERTIES SUFFIX ".wasm") 16 target_link_options(hello PRIVATE 17 "-mexec-model=reactor" 18 "-Wl,--export=napi_register_wasm_v1" 19 "-Wl,--export-if-defined=node_api_module_get_api_version_v1" 20 "-Wl,--initial-memory=16777216,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table" 21 ) 22elseif((CMAKE_C_COMPILER_TARGET STREQUAL "wasm32") OR (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-unknown-unknown")) 23 set_target_properties(hello PROPERTIES SUFFIX ".wasm") 24 target_link_options(hello PRIVATE 25 "-nostdlib" 26 "-Wl,--export=napi_register_wasm_v1" 27 "-Wl,--export-if-defined=node_api_module_get_api_version_v1" 28 "-Wl,--no-entry" 29 "-Wl,--initial-memory=16777216,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table" 30 ) 31 target_link_libraries(hello dlmalloc) 32 # target_link_libraries(hello emmalloc) 33endif()
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()
.
1mkdir build 2 3# emscripten 4emcmake cmake -DCMAKE_BUILD_TYPE=Release \ 5 -DEMNAPI_FIND_NODE_ADDON_API=ON \ 6 -G Ninja -H. -Bbuild 7 8# wasi-sdk 9cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk.cmake \ 10 -DWASI_SDK_PREFIX=$WASI_SDK_PATH \ 11 -DEMNAPI_FIND_NODE_ADDON_API=ON \ 12 -DCMAKE_BUILD_TYPE=Release \ 13 -G Ninja -H. -Bbuild 14 15# wasm32 16cmake -DCMAKE_TOOLCHAIN_FILE=node_modules/emnapi/cmake/wasm32.cmake \ 17 -DLLVM_PREFIX=$WASI_SDK_PATH \ 18 -DCMAKE_BUILD_TYPE=Release \ 19 -G Ninja -H. -Bbuild 20 21cmake --build build
Output code can run in recent version modern browsers and Node.js latest LTS. IE is not supported.
Using node-gyp (Experimental)
Require node-gyp >= 10.2.0
See emnapi-node-gyp-test for examples.
- Variables
Arch: node-gyp configure --arch=<wasm32 | wasm64>
1// node-gyp configure -- -Dvariable_name=value 2 3declare var OS: 'emscripten' | 'wasi' | 'unknown' | 'wasm' | '' 4 5/** 6 * Enable async work and threadsafe-functions 7 * @default 0 8 */ 9declare var wasm_threads: 0 | 1 10 11/** @default 1048576 */ 12declare var stack_size: number 13 14/** @default 16777216 */ 15declare var initial_memory: number 16 17/** @default 2147483648 */ 18declare var max_memory: number 19 20/** @default path.join(path.dirname(commonGypiPath,'./dist/library_napi.js')) */ 21declare var emnapi_js_library: string 22 23/** @default 0 */ 24declare var emnapi_manual_linking: 0 | 1
- Create
binding.gyp
1{ 2 "targets": [ 3 { 4 "target_name": "hello", 5 "sources": [ 6 "hello.c" 7 ], 8 "conditions": [ 9 ["OS == 'emscripten'", { 10 "product_extension": "js", # required 11 12 "cflags": [], 13 "cflags_c": [], 14 "cflags_cc": [], 15 "ldflags": [] 16 }], 17 ["OS == 'wasi'", { 18 # ... 19 }], 20 ["OS in ' wasm unknown'", { 21 # ... 22 }] 23 ] 24 } 25 ] 26}
- Add the following environment variables.
1# Linux or macOS 2export GYP_CROSSCOMPILE=1 3 4# emscripten 5export AR_target="$EMSDK/upstream/emscripten/emar" 6export CC_target="$EMSDK/upstream/emscripten/emcc" 7export CXX_target="$EMSDK/upstream/emscripten/em++" 8 9# wasi-sdk 10export AR_target="$WASI_SDK_PATH/bin/ar" 11export CC_target="$WASI_SDK_PATH/bin/clang" 12export CXX_target="$WASI_SDK_PATH/bin/clang++"
1@REM Windows 2 3set GYP_CROSSCOMPILE=1 4 5@REM emscripten 6call set AR_target=%%EMSDK:\=/%%/upstream/emscripten/emar.bat 7call set CC_target=%%EMSDK:\=/%%/upstream/emscripten/emcc.bat 8call set CXX_target=%%EMSDK:\=/%%/upstream/emscripten/em++.bat 9 10@REM wasi-sdk 11call set AR_target=%%WASI_SDK_PATH:\=/%%/bin/ar.exe 12call set CC_target=%%WASI_SDK_PATH:\=/%%/bin/clang.exe 13call set CXX_target=%%WASI_SDK_PATH:\=/%%/bin/clang++.exe
- Build
1# Linux or macOS 2 3# emscripten 4emmake node-gyp rebuild \ 5 --arch=wasm32 \ 6 --nodedir=./node_modules/emnapi \ 7 -- -f make-emscripten # -Dwasm_threads=1 8 9# wasi 10node-gyp rebuild \ 11 --arch=wasm32 \ 12 --nodedir=./node_modules/emnapi \ 13 -- -f make-wasi # -Dwasm_threads=1 14 15# bare wasm32 16node-gyp rebuild \ 17 --arch=wasm32 \ 18 --nodedir=./node_modules/emnapi \ 19 -- -f make-wasm # -Dwasm_threads=1
1@REM Use make generator on Windows 2@REM Run the bat file in POSIX-like environment (e.g. Cygwin) 3 4@REM emscripten 5call npx.cmd node-gyp configure --arch=wasm32 --nodedir=./node_modules/emnapi -- -f make-emscripten 6call emmake.bat make -C %~dp0build 7 8@REM wasi 9call npx.cmd node-gyp configure --arch=wasm32 --nodedir=./node_modules/emnapi -- -f make-wasi 10make -C %~dp0build 11 12@REM bare wasm32 13call npx.cmd node-gyp configure --arch=wasm32 --nodedir=./node_modules/emnapi -- -f make-wasm 14make -C %~dp0build
Using Rust
See napi-rs
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
1add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/node_modules/emnapi") 2 3add_executable(hello hello.c) 4 5if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") 6 target_link_libraries(hello emnapi-mt) 7 target_compile_options(hello PRIVATE "-pthread") 8 target_link_options(hello PRIVATE 9 "-sALLOW_MEMORY_GROWTH=1" 10 "-sEXPORTED_FUNCTIONS=['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']" 11 "-pthread" 12 "-sPTHREAD_POOL_SIZE=4" 13 # try to specify stack size if you experience pthread errors 14 "-sSTACK_SIZE=2MB" 15 "-sDEFAULT_PTHREAD_STACK_SIZE=2MB" 16 ) 17elseif(CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-wasi-threads") 18 target_link_libraries(hello emnapi-mt) 19 set_target_properties(hello PROPERTIES SUFFIX ".wasm") 20 target_compile_options(hello PRIVATE "-fno-exceptions" "-pthread") 21 target_link_options(hello PRIVATE 22 "-pthread" 23 "-mexec-model=reactor" 24 "-Wl,--import-memory" 25 "-Wl,--max-memory=2147483648" 26 "-Wl,--export-dynamic" 27 "-Wl,--export=napi_register_wasm_v1" 28 "-Wl,--export-if-defined=node_api_module_get_api_version_v1" 29 "-Wl,--export=malloc" 30 "-Wl,--export=free" 31 "-Wl,--import-undefined" 32 "-Wl,--export-table" 33 ) 34elseif((CMAKE_C_COMPILER_TARGET STREQUAL "wasm32") OR (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-unknown-unknown")) 35 target_link_libraries(hello emnapi-basic-mt) 36 set_target_properties(hello PROPERTIES SUFFIX ".wasm") 37 target_compile_options(hello PRIVATE "-fno-exceptions" "-matomics" "-mbulk-memory") 38 target_link_options(hello PRIVATE 39 "-nostdlib" 40 "-Wl,--no-entry" 41 "-Wl,--export=napi_register_wasm_v1" 42 "-Wl,--export-if-defined=node_api_module_get_api_version_v1" 43 "-Wl,--export=emnapi_async_worker_create" 44 "-Wl,--export=emnapi_async_worker_init" 45 "-Wl,--import-memory,--shared-memory,--max-memory=2147483648,--import-undefined" 46 "-Wl,--export-dynamic,--export=malloc,--export=free,--export-table" 47 ) 48endif()
1# emscripten 2emcmake cmake -DCMAKE_BUILD_TYPE=Release \ 3 -DEMNAPI_FIND_NODE_ADDON_API=ON \ 4 -DEMNAPI_WORKER_POOL_SIZE=4 \ 5 -G Ninja -H. -Bbuild 6 7# wasi-sdk with thread support 8cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk-pthread.cmake \ 9 -DWASI_SDK_PREFIX=$WASI_SDK_PATH \ 10 -DEMNAPI_FIND_NODE_ADDON_API=ON \ 11 -DCMAKE_BUILD_TYPE=Release \ 12 -G Ninja -H. -Bbuild 13 14cmake -DCMAKE_TOOLCHAIN_FILE=node_modules/emnapi/cmake/wasm32.cmake \ 15 -DLLVM_PREFIX=$WASI_SDK_PATH \ 16 -DCMAKE_BUILD_TYPE=Release \ 17 -G Ninja -H. -Bbuild 18 19cmake --build build
And additional work is required during instantiating wasm compiled with non-emscripten.
1// emnapi main thread (could be in a Worker) 2instantiateNapiModule(input, { 3 context: getDefaultContext(), 4 /** 5 * emscripten 6 * 0: no effect 7 * > 0: the same effect to UV_THREADPOOL_SIZE 8 * non-emscripten 9 * 0: single thread mock 10 * > 0 schedule async work in web worker 11 */ 12 asyncWorkPoolSize: 4, // 0: single thread mock, > 0: schedule async work in web worker 13 wasi: new WASI(/* ... */), 14 15 /** 16 * Setting this to `true` or a delay (ms) makes 17 * pthread_create() do not return until worker actually start. 18 * It will throw error if emnapi runs in browser main thread 19 * since browser disallow blocking the main thread (Atomics.wait). 20 * @defaultValue false 21 */ 22 waitThreadStart: isNode || (isBrowser && !isBrowserMainThread), 23 24 /** 25 * Reuse the thread worker after thread exit to avoid re-creatation 26 * @defaultValue false 27 */ 28 reuseWorker: { 29 /** 30 * @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size | PTHREAD_POOL_SIZE} 31 */ 32 size: 0, 33 34 /** 35 * @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size-strict | PTHREAD_POOL_SIZE_STRICT} 36 */ 37 strict: false 38 }, 39 40 onCreateWorker () { 41 return new Worker('./worker.js') 42 // Node.js 43 // const { Worker } = require('worker_threads') 44 // return new Worker(join(__dirname, './worker.js'), { 45 // env: process.env, 46 // execArgv: ['--experimental-wasi-unstable-preview1'] 47 // }) 48 }, 49 overwriteImports (importObject) { 50 importObject.env.memory = new WebAssembly.Memory({ 51 initial: 16777216 / 65536, 52 maximum: 2147483648 / 65536, 53 shared: true 54 }) 55 } 56})
1// worker.js 2(function () { 3 let fs, WASI, emnapiCore 4 5 const ENVIRONMENT_IS_NODE = 6 typeof process === 'object' && process !== null && 7 typeof process.versions === 'object' && process.versions !== null && 8 typeof process.versions.node === 'string' 9 10 if (ENVIRONMENT_IS_NODE) { 11 const nodeWorkerThreads = require('worker_threads') 12 13 const parentPort = nodeWorkerThreads.parentPort 14 15 parentPort.on('message', (data) => { 16 globalThis.onmessage({ data }) 17 }) 18 19 fs = require('fs') 20 21 Object.assign(globalThis, { 22 self: globalThis, 23 require, 24 Worker: nodeWorkerThreads.Worker, 25 importScripts: function (f) { 26 (0, eval)(fs.readFileSync(f, 'utf8') + '//# sourceURL=' + f) 27 }, 28 postMessage: function (msg) { 29 parentPort.postMessage(msg) 30 } 31 }) 32 33 WASI = require('wasi').WASI 34 emnapiCore = require('@emnapi/core') 35 } else { 36 importScripts('./node_modules/memfs-browser/dist/memfs.js') 37 importScripts('./node_modules/@tybys/wasm-util/dist/wasm-util.min.js') 38 importScripts('./node_modules/@emnapi/core/dist/emnapi-core.js') 39 emnapiCore = globalThis.emnapiCore 40 41 const { Volume, createFsFromVolume } = memfs 42 fs = createFsFromVolume(Volume.fromJSON({ 43 '/': null 44 })) 45 46 WASI = globalThis.wasmUtil.WASI 47 } 48 49 const { instantiateNapiModuleSync, MessageHandler } = emnapiCore 50 51 const handler = new MessageHandler({ 52 onLoad ({ wasmModule, wasmMemory }) { 53 const wasi = new WASI({ fs }) 54 55 return instantiateNapiModuleSync(wasmModule, { 56 childThread: true, 57 wasi, 58 overwriteImports (importObject) { 59 importObject.env.memory = wasmMemory 60 } 61 }) 62 } 63 }) 64 65 globalThis.onmessage = function (e) { 66 handler.handle(e) 67 // handle other messages 68 } 69})()
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:
1Module.init({
2 // ...
3 asyncWorkPoolSize: 2
4})
5
6// if asyncWorkPoolSize is not specified
7Module.preRun = Module.preRun || [];
8Module.preRun.push(function () {
9 if (typeof ENV !== 'undefined') {
10 ENV.UV_THREADPOOL_SIZE = '2';
11 }
12});
1// wasi 2instantiateNapiModule({ 3 // ... 4 asyncWorkPoolSize: 2 5}) 6// if asyncWorkPoolSize is not specified 7new WASI({ 8 env: { 9 UV_THREADPOOL_SIZE: '2' 10 } 11})
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
: UsesetImmediate()
(Node.js nativesetImmediate
or browserMessageChannel
andport.postMessage
)1
: UsePromise.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.
No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
11 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10
Reason
no binaries found in the repo
Reason
license file detected
Details
- Info: project has a license file: LICENSE:0
- Info: FSF or OSI recognized license: MIT License: LICENSE:0
Reason
0 existing vulnerabilities detected
Reason
packaging workflow detected
Details
- Info: Project packages its releases by way of GitHub Actions.: .github/workflows/main.yml:144
Reason
Found 0/30 approved changesets -- score normalized to 0
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/main.yml:1
- Info: no jobLevel write permissions found
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:151: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/main.yml:156: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:167: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/main.yml:190: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:208: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:209: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:43: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:51: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: third-party GitHubAction not pinned by hash: .github/workflows/main.yml:55: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:77: update your workflow using https://app.stepsecurity.io/secureworkflow/toyobayashi/emnapi/main.yml/main?enable=pin
- Warn: npmCommand not pinned by hash: .github/workflows/main.yml:87
- Warn: npmCommand not pinned by hash: .github/workflows/main.yml:88
- Warn: npmCommand not pinned by hash: .github/workflows/main.yml:177
- Warn: npmCommand not pinned by hash: .github/workflows/main.yml:178
- Warn: npmCommand not pinned by hash: .github/workflows/main.yml:219
- Warn: npmCommand not pinned by hash: .github/workflows/main.yml:220
- Info: 0 out of 7 GitHub-owned GitHubAction dependencies pinned
- Info: 0 out of 3 third-party GitHubAction dependencies pinned
- Info: 0 out of 6 npmCommand dependencies pinned
Reason
security policy file not detected
Details
- Warn: no security policy file detected
- Warn: no security file to analyze
- Warn: no security file to analyze
- Warn: no security file to analyze
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
branch protection not enabled on development/release branches
Details
- Warn: branch protection not enabled for branch 'main'
Reason
Project has not signed or included provenance with any releases.
Details
- Warn: release artifact v1.3.1 not signed: https://api.github.com/repos/toyobayashi/emnapi/releases/179634362
- Warn: release artifact v1.3.0 not signed: https://api.github.com/repos/toyobayashi/emnapi/releases/178361029
- Warn: release artifact v1.2.0 not signed: https://api.github.com/repos/toyobayashi/emnapi/releases/156406287
- Warn: release artifact v1.1.1 not signed: https://api.github.com/repos/toyobayashi/emnapi/releases/149041675
- Warn: release artifact v1.1.0 not signed: https://api.github.com/repos/toyobayashi/emnapi/releases/147404287
- Warn: release artifact v1.3.1 does not have provenance: https://api.github.com/repos/toyobayashi/emnapi/releases/179634362
- Warn: release artifact v1.3.0 does not have provenance: https://api.github.com/repos/toyobayashi/emnapi/releases/178361029
- Warn: release artifact v1.2.0 does not have provenance: https://api.github.com/repos/toyobayashi/emnapi/releases/156406287
- Warn: release artifact v1.1.1 does not have provenance: https://api.github.com/repos/toyobayashi/emnapi/releases/149041675
- Warn: release artifact v1.1.0 does not have provenance: https://api.github.com/repos/toyobayashi/emnapi/releases/147404287
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 12 are checked with a SAST tool
Score
4.3
/10
Last Scanned on 2024-11-18
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More