Emnapi
Node-API implementation for Emscripten, wasi-sdk, clang wasm32 and napi-rs
Install / Use
/learn @toyobayashi/EmnapiREADME
emnapi
<p align="center"> <img src="https://toyobayashi.github.io/emnapi-docs/emnapi.svg" alt="emnapi logo" width="256" /> </p>Sponsors
<p align="center"> <a href="https://cdn.jsdelivr.net/gh/toyobayashi/toyobayashi/sponsorkit/sponsors.svg"> <img src='https://cdn.jsdelivr.net/gh/toyobayashi/toyobayashi/sponsorkit/sponsors.svg'/> </a> </p>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
If you want to deep dive into WebAssembly, highly recommend you to visit learn-wasm.dev.
Prerequests
You will need to install:
- Node.js
>= v22.12.0for developing this repository on local machine,>= v16.15.0for user runtime. - 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
nmakeinVisual Studio Developer Command Prompt - Install Visual C++ Build Tools, use
nmakeinVisual Studio Developer Command Prompt
Verify your environment:
node -v
npm -v
emcc -v
# clang -v
# clang -print-targets # ensure wasm32 target exists
cmake --version
# if you use node-gyp
node-gyp --version
# if you use ninja
ninja --version
# if you use make
make -v
# if you use nmake in Visual Studio Developer Command Prompt
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 # output ./packages/*/dist
node ./script/release.js # output ./out
# test
npm run rebuild:test
npm test
See CONTRIBUTING for more details.
Quick Start
NPM Install
npm install -D emnapi
npm install @emnapi/runtime
# for non-emscripten
npm install @emnapi/core
# if you use node-addon-api
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 () {
// native code in js_hello
const world = 'world'
return world
}
exports.hello = hello
return exports
})(module.exports)
Building
<details> <summary>emscripten</summary><br />emcc -O3 \
-DBUILDING_NODE_EXTENSION \
"-DNAPI_EXTERN=__attribute__((__import_module__(\"env\")))" \
-I./node_modules/emnapi/include/node \
-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']" \
-sEXPORTED_RUNTIME_METHODS=['emnapiInit'] \
-o hello.js \
hello.c \
-lemnapi
</details>
<details>
<summary>wasi-sdk</summary><br />
clang -O3 \
-DBUILDING_NODE_EXTENSION \
-I./node_modules/emnapi/include/node \
-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
</details>
<details>
<summary>clang wasm32</summary><br />
Choose libdlmalloc.a or libemmalloc.a for malloc and free.
clang -O3 \
-DBUILDING_NODE_EXTENSION \
-I./node_modules/emnapi/include/node \
-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 # -lemmalloc
</details>
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 {
// module '@emnapi/runtime'
export class Context { /* ... */ }
/** Create a new context */
export function createContext (): Context
/** Create or get */
export function getDefaultContext (): Context
// ...
}
<details>
<summary>emscripten</summary><br />
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
/** node_api_get_module_file_name */
filename?: string
/**
* Support following async_hooks related things
* on Node.js runtime only
*
* napi_async_init,
* napi_async_destroy,
* napi_make_callback,
* async resource parameter of
* napi_create_async_work and napi_create_threadsafe_function
*/
nodeBinding?: typeof import('@emnapi/node-binding')
/** See Multithread part */
asyncWorkPoolSize?: number
}
export function emnapiInit (options: EmnapiInitOptions): any
}
<script src="./node_modules/@emnapi/runtime/dist/emnapi.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);
};
// if -sMODULARIZE=1
Module({ /* Emscripten module init options */ }).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 clic
