SkillAgentSearch skills...

Emnapi

Node-API implementation for Emscripten, wasi-sdk, clang wasm32 and napi-rs

Install / Use

/learn @toyobayashi/Emnapi
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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>

Build

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:

中文文档:

Full API List

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.0 for developing this repository on local machine, >= v16.15.0 for 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

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

View on GitHub
GitHub Stars226
CategoryDevelopment
Updated7d ago
Forks12

Languages

C

Security Score

100/100

Audited on Mar 23, 2026

No findings