Getting Started
This section will help you build a Hello World example by using emnapi.
Environment Requires
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
TIP
Set $EMSDK environment variable to the emsdk root path.
Add $EMSDK/upstream/emscripten to $PATH environment variable.
TIP
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 ninja ninja --version # if you use make make -v # if you use nmake in Visual Studio Developer Command Prompt nmake /?node -v npm -v emcc -v # clang -v # clang -print-targets # ensure wasm32 target exists cmake --version # if you use ninja ninja --version # if you use make make -v # if you use nmake in Visual Studio Developer Command Prompt nmake /?Installation
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-apinpm install -D emnapi npm install @emnapi/runtime # for non-emscripten npm install @emnapi/core # if you use node-addon-api npm install node-addon-apiTIP
Each package should match the same version.
Writing Source Code
Create hello.c
#include <node_api.h> #define NAPI_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"; NAPI_CALL(env, napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &world)); return world; } NAPI_MODULE_INIT() { napi_value hello; NAPI_CALL(env, napi_create_function(env, "hello", NAPI_AUTO_LENGTH, js_hello, NULL, &hello)); NAPI_CALL(env, napi_set_named_property(env, exports, "hello", hello)); return exports; }#include <node_api.h> #define NAPI_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"; NAPI_CALL(env, napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &world)); return world; } NAPI_MODULE_INIT() { napi_value hello; NAPI_CALL(env, napi_create_function(env, "hello", NAPI_AUTO_LENGTH, js_hello, NULL, &hello)); NAPI_CALL(env, napi_set_named_property(env, exports, "hello", hello)); return exports; }The C code is equivalant to the JavaScript below:
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)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)Buiding Source Code
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="['_napi_register_wasm_v1','_node_api_module_get_api_version_v1','_malloc','_free']" \ -o hello.js \ hello.c \ -lemnapiemcc -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="['_napi_register_wasm_v1','_node_api_module_get_api_version_v1','_malloc','_free']" \ -o hello.js \ hello.c \ -lemnapiclang -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 \ -lemnapiclang -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# 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# 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 # -lemmallocInitialzation
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 // ... }declare namespace emnapi { // module '@emnapi/runtime' export class Context { /* ... */ } /** Create a new context */ export function createContext (): Context /** Create or get */ 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 /** 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 }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.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); }; // if -sMODULARIZE=1 Module({ /* Emscripten module init options */ }).then(function (Module) { var binding = Module.emnapiInit({ context: emnapi.getDefaultContext() }); }); </script><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); }; // if -sMODULARIZE=1 Module({ /* Emscripten module init options */ }).then(function (Module) { var binding = Module.emnapiInit({ context: emnapi.getDefaultContext() }); }); </script>import { getDefaultContext } from '@emnapi/runtime' // emcc -sMODULARIZE=1 import * as init from './hello.js' init({ /* Emscripten module init options */ }).then((Module) => { const binding = Module.emnapiInit({ context: getDefaultContext() }) })import { getDefaultContext } from '@emnapi/runtime' // emcc -sMODULARIZE=1 import * as init from './hello.js' init({ /* Emscripten module init options */ }).then((Module) => { const binding = Module.emnapiInit({ context: getDefaultContext() }) })const emnapi = require('@emnapi/runtime') const Module = require('./hello.js') Module.onRuntimeInitialized = function () { let binding try { binding = Module.emnapiInit({ context: getDefaultContext() }) } catch (err) { console.error(err) return } const msg = `hello ${binding.hello()}` console.log(msg) } // if -sMODULARIZE=1 Module({ /* Emscripten module init options */ }).then((Module) => { const binding = Module.emnapiInit({ context: getDefaultContext() }) })const emnapi = require('@emnapi/runtime') const Module = require('./hello.js') Module.onRuntimeInitialized = function () { let binding try { binding = Module.emnapiInit({ context: getDefaultContext() }) } catch (err) { console.error(err) return } const msg = `hello ${binding.hello()}` console.log(msg) } // if -sMODULARIZE=1 Module({ /* Emscripten module init options */ }).then((Module) => { const binding = Module.emnapiInit({ context: getDefaultContext() }) })wasi-sdk or clang
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) { // Currently napi-rs imports all symbols from env module // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... }) </script><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) { // Currently napi-rs imports all symbols from env module // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... }) </script>import { instantiateNapiModule } from '@emnapi/core' import { getDefaultContext } from '@emnapi/runtime' import base64 from './hello.wasm' // configure load wasm as base64 instantiateNapiModule( fetch('data:application/wasm;base64,' + base64), { context: getDefaultContext(), overwriteImports (importObject) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } } ).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... })import { instantiateNapiModule } from '@emnapi/core' import { getDefaultContext } from '@emnapi/runtime' import base64 from './hello.wasm' // configure load wasm as base64 instantiateNapiModule( fetch('data:application/wasm;base64,' + base64), { context: getDefaultContext(), overwriteImports (importObject) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } } ).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... })const { instantiateNapiModule } = require('@emnapi/core') const { getDefaultContext } = require('@emnapi/runtime') const fs = require('fs') instantiateNapiModule(fs.promises.readFile('./hello.wasm'), { context: getDefaultContext(), overwriteImports (importObject) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... })const { instantiateNapiModule } = require('@emnapi/core') const { getDefaultContext } = require('@emnapi/runtime') const fs = require('fs') instantiateNapiModule(fs.promises.readFile('./hello.wasm'), { context: getDefaultContext(), overwriteImports (importObject) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... })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) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... })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) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... })// you can use WASI polyfill in [wasm-util](https://github.com/toyobayashi/wasm-util) // and [memfs-browser](https://github.com/toyobayashi/memfs-browser) import { instantiateNapiModule } from '@emnapi/core' import { getDefaultContext } from '@emnapi/runtime' import { WASI } from '@tybys/wasm-util' import { Volume, createFsFromVolume } from 'memfs-browser' import base64 from './hello.wasm' // configure load wasm as base64 const fs = createFsFromVolume(Volume.fromJSON({ /* ... */ })) instantiateNapiModule(fetch('data:application/wasm;base64,' + base64), { wasi: new WASI({ fs, /* ... */ }) context: getDefaultContext(), overwriteImports (importObject) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... })// you can use WASI polyfill in [wasm-util](https://github.com/toyobayashi/wasm-util) // and [memfs-browser](https://github.com/toyobayashi/memfs-browser) import { instantiateNapiModule } from '@emnapi/core' import { getDefaultContext } from '@emnapi/runtime' import { WASI } from '@tybys/wasm-util' import { Volume, createFsFromVolume } from 'memfs-browser' import base64 from './hello.wasm' // configure load wasm as base64 const fs = createFsFromVolume(Volume.fromJSON({ /* ... */ })) instantiateNapiModule(fetch('data:application/wasm;base64,' + base64), { wasi: new WASI({ fs, /* ... */ }) context: getDefaultContext(), overwriteImports (importObject) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... })<script src="node_modules/@emnapi/runtime/dist/emnapi.min.js"></script> <script src="node_modules/@emnapi/core/dist/emnapi-core.min.js"></script> <script src="node_modules/@tybys/wasm-util/dist/wasm-util.min.js"></script> <script src="node_modules/memfs-browser/dist/memfs.min.js"></script> <script> const fs = memfs.createFsFromVolume(memfs.Volume.fromJSON({ /* ... */ })) emnapiCore.instantiateNapiModule(fetch('./hello.wasm'), { wasi: new wasmUtil.WASI({ fs, /* ... */ }) context: emnapi.getDefaultContext(), overwriteImports (importObject) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... }) </script><script src="node_modules/@emnapi/runtime/dist/emnapi.min.js"></script> <script src="node_modules/@emnapi/core/dist/emnapi-core.min.js"></script> <script src="node_modules/@tybys/wasm-util/dist/wasm-util.min.js"></script> <script src="node_modules/memfs-browser/dist/memfs.min.js"></script> <script> const fs = memfs.createFsFromVolume(memfs.Volume.fromJSON({ /* ... */ })) emnapiCore.instantiateNapiModule(fetch('./hello.wasm'), { wasi: new wasmUtil.WASI({ fs, /* ... */ }) context: emnapi.getDefaultContext(), overwriteImports (importObject) { // importObject.env = { // ...importObject.env, // ...importObject.napi, // ...importObject.emnapi, // // ... // } } }).then(({ instance, module, napiModule }) => { const binding = napiModule.exports // ... }) </script>