开始使用
本节将帮助你使用 emnapi 构建一个 Hello World 示例。
环境要求
你需要安装:
- Node.js
>= v16.15.0 - npm
>= v8 - Emscripten
>= v3.1.9/ wasi-sdk / LLVM clang with wasm support - (可选) CMake
>= v3.13 - (可选) ninja
- (可选) make
- (可选) node-addon-api
>= 6.1.0
TIP
设置 $EMSDK 环境变量为 emsdk 根目录。
把 $EMSDK/upstream/emscripten 添加到 $PATH 环境变量中。
TIP
Windows 用户有多种获取 make 的选择
- 安装 mingw-w64,然后使用
mingw32-make - 下载 make 的 MSVC 预构建二进制文件,添加到
%Path%后重命名为mingw32-make - 安装 Visual Studio 2022 C++ 桌面工作负载,在
Visual Studio Developer Command Prompt中使用nmake - 安装 Visual C++ Build Tools,在
Visual Studio Developer Command Prompt中使用nmake
验证环境:
bash
node -v npm -v emcc -v # clang -v # clang -print-targets # 确保支持 wasm32 cmake --version # 如果你使用 ninja ninja --version # 如果你使用 make make -v # 如果你在 Visual Studio Developer Command Prompt 使用 nmake nmake /?node -v npm -v emcc -v # clang -v # clang -print-targets # 确保支持 wasm32 cmake --version # 如果你使用 ninja ninja --version # 如果你使用 make make -v # 如果你在 Visual Studio Developer Command Prompt 使用 nmake nmake /?安装
bash
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
每个包的版本必须对应一致。
编写源码
创建 hello.c。
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; }C 代码等价于下面的 JavaScript:
js
module.exports = (function (exports) { const hello = function hello () { // js_hello 中的原生代码 const world = 'world' return world } exports.hello = hello return exports })(module.exports)module.exports = (function (exports) { const hello = function hello () { // js_hello 中的原生代码 const world = 'world' return world } exports.hello = hello return exports })(module.exports)构建源码
bash
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 \ -lemnapibash
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 \ -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 \ -lemnapibash
# 你可以选择链接 `libdlmalloc.a` 或者 `libemmalloc.a` # 以获得 `malloc` 和 `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# 你可以选择链接 `libdlmalloc.a` 或者 `libemmalloc.a` # 以获得 `malloc` 和 `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初始化
初始化 emnapi 需要先导入 emnapi 运行时,通过 createContext 或 getDefaultContext 获得 Context,每个 Context 都拥有独立的 Node-API 对象,例如 napi_env、napi_value、napi_ref。 如果你有多个 emnapi 模块,你应该在它们之间重用相同的 Context。
ts
declare namespace emnapi { // module '@emnapi/runtime' export class Context { /* ... */ } /** 创建一个新的 context */ export function createContext (): Context /** 懒惰创建 */ export function getDefaultContext (): Context // ... }declare namespace emnapi { // module '@emnapi/runtime' export class Context { /* ... */ } /** 创建一个新的 context */ export function createContext (): Context /** 懒惰创建 */ export function getDefaultContext (): Context // ... }emscripten
然后在 emscripten 运行时初始化后调用 Module.emnapiInit。 Module.emnapiInit 只初始化一次,初始化成功后总是返回相同的绑定导出。
ts
declare namespace Module { interface EmnapiInitOptions { context: emnapi.Context /** node_api_get_module_file_name */ filename?: string /** * 只有在 Node.js 运行时中支持以下 * 与 async_hooks 相关的 API * * napi_async_init, * napi_async_destroy, * napi_make_callback, * napi_create_async_work 和 napi_create_threadsafe_function * 的 async resource 参数 */ nodeBinding?: typeof import('@emnapi/node-binding') /** 查看多线程小节 */ asyncWorkPoolSize?: number } export function emnapiInit (options: EmnapiInitOptions): any }declare namespace Module { interface EmnapiInitOptions { context: emnapi.Context /** node_api_get_module_file_name */ filename?: string /** * 只有在 Node.js 运行时中支持以下 * 与 async_hooks 相关的 API * * napi_async_init, * napi_async_destroy, * napi_make_callback, * napi_create_async_work 和 napi_create_threadsafe_function * 的 async resource 参数 */ nodeBinding?: typeof import('@emnapi/node-binding') /** 查看多线程小节 */ asyncWorkPoolSize?: number } export function emnapiInit (options: EmnapiInitOptions): any }html
<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>js
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() }) })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) } // if -sMODULARIZE=1 Module({ /* Emscripten module init options */ }).then((Module) => { const binding = Module.emnapiInit({ context: emnapi.getDefaultContext() }) })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) } // if -sMODULARIZE=1 Module({ /* Emscripten module init options */ }).then((Module) => { const binding = Module.emnapiInit({ context: emnapi.getDefaultContext() }) })wasi-sdk or clang
对于非 emscripten,你可以使用 @emnapi/core,初始化流程与 emscripten 类似。
html
<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>js
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 // ... })js
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 // ... })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) { // 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 // ... })js
// 你可以在 [wasm-util](https://github.com/toyobayashi/wasm-util) 找到 WASI 的 polyfill, // 还有 [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 // ... })// 你可以在 [wasm-util](https://github.com/toyobayashi/wasm-util) 找到 WASI 的 polyfill, // 还有 [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 // ... })html
<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>