Skip to content

开始使用

本节将帮助你使用 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 的选择

验证环境:

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-api
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

TIP

每个包的版本必须对应一致。

编写源码

创建 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 \  -lemnapi
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 \  -lemnapi
bash
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
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
bash
# 你可以选择链接 `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 运行时,通过 createContextgetDefaultContext 获得 Context,每个 Context 都拥有独立的 Node-API 对象,例如 napi_envnapi_valuenapi_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.emnapiInitModule.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>