Skip to content

toyobayashi/emnapi

Repository files navigation

emnapi

emnapi logo

Sponsors

Build

This is 2.x branch, go stable 1.x branch here

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
  • npm >= v8

Tool chain choices:

  • Emscripten >= v4.1.7 with EMSDK environment variable set
  • wasi-sdk >= v26 with WASI_SDK_PATH environment variable set

Build system choices:

  • CMake >= v3.13
  • node-gyp >= v10.2.0
  • make / ninja

Optional:

There are several choices to get make for Windows user

Build from source

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 for emscripten
npm run rebuild -w packages/test
npm run test -w packages/test

# test for wasi-sdk
npm run rebuild:wt -w packages/test
npm run test:wt -w packages/test

See CONTRIBUTING for more details.

Quick Start

See full example

NPM Install

npm install -D emnapi
npm install @emnapi/runtime

# additionally, for wasi-sdk, install @emnapi/core
npm install @emnapi/core

# if you need node-addon-api
npm install node-addon-api

Each package should match the same version.

Using C

Create binding.c:

#include <node_api.h>

// ...

NAPI_MODULE_INIT() {
  napi_value fn;
  napi_create_function(env, "run", NAPI_AUTO_LENGTH, run, NULL, &fn);
  napi_set_named_property(env, exports, "run", fn);
  return exports;
}

Build with Emscripten:

emcc -O3 -pthread \
    -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 \
    -sWASM_BIGINT=1 \
    -sALLOW_MEMORY_GROWTH=1 \
    -sALLOW_TABLE_GROWTH=1 \
    -sEXPORTED_FUNCTIONS=$(node -p "JSON.stringify(require('emnapi').requiredConfig.emscripten.settings.EXPORTED_FUNCTIONS)") \
    -sEXPORTED_RUNTIME_METHODS=$(node -p "JSON.stringify(require('emnapi').requiredConfig.emscripten.settings.EXPORTED_RUNTIME_METHODS)") \
    -sEXPORT_ES6=1 \
    -sPTHREAD_POOL_SIZE=4 \
    -o out/binding.js \
    binding.c \
    -lemnapi-mt

Build with wasi-sdk:

"$WASI_SDK_PATH/bin/clang" --target=wasm32-wasip1-threads -O3 \
    -pthread -matomics -mbulk-memory \
    -DBUILDING_NODE_EXTENSION \
    -I./node_modules/emnapi/include/node \
    -L./node_modules/emnapi/lib/wasm32-wasip1-threads \
    --sysroot="$WASI_SDK_PATH/share/wasi-sysroot" \
    -mexec-model=reactor \
    -Wl,--import-memory -Wl,--shared-memory -Wl,--export-memory \
    -Wl,--export-table -Wl,--growable-table \
    -Wl,--export=malloc,--export=free \
    -Wl,--export=napi_register_wasm_v1 \
    -Wl,--export=emnapi_create_env,--export=emnapi_delete_env \
    -Wl,--export-if-defined=node_api_module_get_api_version_v1 \
    -Wl,--export-if-defined=uv_library_shutdown \
    -Wl,--import-undefined \
    -o out/binding.wasm \
    binding.c \
    -lemnapi-mt

Use in JavaScript (Emscripten):

import init from './out/binding.js'
import { createContext } from '@emnapi/runtime'

const emnapiCtx = createContext()
const Module = await init()
const binding = Module.emnapiInit({
  context: emnapiCtx,
  asyncWorkPoolSize: 4 // the same effect to UV_THREADPOOL_SIZE, must less than PTHREAD_POOL_SIZE
})
binding.run(/* ... */)

// cleanup
Module._uv_library_shutdown()
emnapiCtx.destroy()

For non-emscripten please reference full JavaScript example

Using C++ and node-addon-api

Note: C++ wrapper can only be used to target Node.js v14.6.0+ and modern browsers those support FinalizationRegistry and WeakRef (v8 engine v8.4+)!

Create binding.cpp:

#include <napi.h>

// ...

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set("run", Napi::Function::New(env, Run));
  return exports;
}

NODE_API_MODULE(binding, Init)

Build with Emscripten:

em++ -O3 -pthread \
    -DBUILDING_NODE_EXTENSION \
    -DNAPI_DISABLE_CPP_EXCEPTIONS \
    "-DNAPI_EXTERN=__attribute__((__import_module__(\"env\")))" \
    -I./node_modules/emnapi/include/node \
    -I$(node -p "require('node-addon-api').include_dir") \
    -L./node_modules/emnapi/lib/wasm32-emscripten \
    --js-library=./node_modules/emnapi/dist/library_napi.js \
    -sWASM_BIGINT=1 \
    -sALLOW_MEMORY_GROWTH=1 \
    -sALLOW_TABLE_GROWTH=1 \
    -sEXPORTED_FUNCTIONS=$(node -p "JSON.stringify(require('emnapi').requiredConfig.emscripten.settings.EXPORTED_FUNCTIONS)") \
    -sEXPORTED_RUNTIME_METHODS=$(node -p "JSON.stringify(require('emnapi').requiredConfig.emscripten.settings.EXPORTED_RUNTIME_METHODS)") \
    -sEXPORT_ES6=1 \
    -sPTHREAD_POOL_SIZE=4 \
    -o out/binding-naa.js \
    binding.cpp \
    -lemnapi-mt

Build with wasi-sdk:

"$WASI_SDK_PATH/bin/clang++" --target=wasm32-wasip1-threads -O3 \
    -pthread -matomics -mbulk-memory -fno-exceptions \
    -DBUILDING_NODE_EXTENSION \
    -DNAPI_DISABLE_CPP_EXCEPTIONS \
    -I./node_modules/emnapi/include/node \
    -I$(node -p "require('node-addon-api').include_dir") \
    -L./node_modules/emnapi/lib/wasm32-wasip1-threads \
    --sysroot="$WASI_SDK_PATH/share/wasi-sysroot" \
    -mexec-model=reactor \
    -Wl,--import-memory -Wl,--shared-memory -Wl,--export-memory \
    -Wl,--export-table -Wl,--growable-table \
    -Wl,--export=malloc,--export=free \
    -Wl,--export=napi_register_wasm_v1 \
    -Wl,--export=emnapi_create_env,--export=emnapi_delete_env \
    -Wl,--export-if-defined=node_api_module_get_api_version_v1 \
    -Wl,--export-if-defined=uv_library_shutdown \
    -Wl,--import-undefined \
    -o out/binding-naa.wasm \
    binding.cpp \
    -lemnapi-mt

Using CMake

Create CMakeLists.txt:

cmake_minimum_required(VERSION 3.13)
project(myproject)

# optional: enable node-addon-api
# set(EMNAPI_FIND_NODE_ADDON_API ON)

add_subdirectory("node_modules/emnapi")

add_executable(binding binding.c)

target_link_libraries(binding PRIVATE emnapi-mt)

if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
  # ...
elseif((CMAKE_SYSTEM_NAME STREQUAL "WASI") AND (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-wasi-threads"))
  # ...
endif()

Build with Emscripten:

mkdir build && cd build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build .

Build with wasi-sdk:

mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_TOOLCHAIN_FILE="$WASI_SDK_PATH/share/cmake/wasi-sdk-pthread.cmake"
cmake --build .

Using node-gyp

Require node-gyp >= 10.2.0

See emnapi-node-gyp-test for examples.

  • Variables

Arch: node-gyp configure --arch=<wasm32 | wasm64>

// node-gyp configure -- -Dvariable_name=value

declare var OS: 'emscripten' | 'wasi' | 'unknown' | 'wasm' | ''

/**
 * Enable async work and threadsafe-functions
 * @default 0
 */
declare var wasm_threads: 0 | 1

/** @default 1048576 */
declare var stack_size: number

/** @default 16777216 */
declare var initial_memory: number

/** @default 2147483648 */
declare var max_memory: number

/** @default path.join(path.dirname(commonGypiPath,'./dist/library_napi.js')) */
declare var emnapi_js_library: string

/** @default 0 */
declare var emnapi_manual_linking: 0 | 1
  • Create binding.gyp
{
  "targets": [
    {
      "target_name": "hello",
      "sources": [
        "hello.c"
      ],
      "conditions": [
        ["OS == 'emscripten'", {
          "product_extension": "js", # required

          "cflags": [],
          "cflags_c": [],
          "cflags_cc": [],
          "ldflags": []
        }],
        ["OS == 'wasi'", {
          # ...
        }],
        ["OS in ' wasm unknown'", {
          # ...
        }]
      ]
    }
  ]
}
  • Add the following environment variables.
# Linux or macOS
export GYP_CROSSCOMPILE=1

# emscripten
export AR_target="$EMSDK/upstream/emscripten/emar"
export CC_target="$EMSDK/upstream/emscripten/emcc"
export CXX_target="$EMSDK/upstream/emscripten/em++"

# wasi-sdk
export AR_target="$WASI_SDK_PATH/bin/ar"
export CC_target="$WASI_SDK_PATH/bin/clang"
export CXX_target="$WASI_SDK_PATH/bin/clang++"
@REM Windows

set GYP_CROSSCOMPILE=1

@REM emscripten
call set AR_target=%%EMSDK:\=/%%/upstream/emscripten/emar.bat
call set CC_target=%%EMSDK:\=/%%/upstream/emscripten/emcc.bat
call set CXX_target=%%EMSDK:\=/%%/upstream/emscripten/em++.bat

@REM wasi-sdk
call set AR_target=%%WASI_SDK_PATH:\=/%%/bin/ar.exe
call set CC_target=%%WASI_SDK_PATH:\=/%%/bin/clang.exe
call set CXX_target=%%WASI_SDK_PATH:\=/%%/bin/clang++.exe
  • Build
# Linux or macOS

# emscripten
emmake node-gyp rebuild \
  --arch=wasm32 \
  --nodedir=./node_modules/emnapi \
  -- -f make-emscripten # -Dwasm_threads=1

# wasi
node-gyp rebuild \
  --arch=wasm32 \
  --nodedir=./node_modules/emnapi \
  -- -f make-wasi # -Dwasm_threads=1

# bare wasm32
node-gyp rebuild \
  --arch=wasm32 \
  --nodedir=./node_modules/emnapi \
  -- -f make-wasm # -Dwasm_threads=1
@REM Use make generator on Windows
@REM Run the bat file in POSIX-like environment (e.g. Cygwin)

@REM emscripten
call npx.cmd node-gyp configure --arch=wasm32 --nodedir=./node_modules/emnapi -- -f make-emscripten
call emmake.bat make -C %~dp0build

@REM wasi
call npx.cmd node-gyp configure --arch=wasm32 --nodedir=./node_modules/emnapi -- -f make-wasi
make -C %~dp0build

@REM bare wasm32
call npx.cmd node-gyp configure --arch=wasm32 --nodedir=./node_modules/emnapi -- -f make-wasm
make -C %~dp0build

Using Rust

See napi-rs

Reference for instantiateNapiModule input parameters

// emnapi main thread (could be in a Worker)
instantiateNapiModule(input, {
  context: getDefaultContext(),
  asyncWorkPoolSize: 4, // the same effect to UV_THREADPOOL_SIZE, must less than `reuseWorker.size`
  wasi: new WASI(/* ... */),

  /**
   * Setting this to `true` or a delay (ms) makes
   * pthread_create() do not return until worker actually start.
   * It will throw error if emnapi runs in browser main thread
   * since browser disallow blocking the main thread (Atomics.wait).
   * @defaultValue false
   */
  waitThreadStart: isNode || (isBrowser && !isBrowserMainThread),

  /**
   * Reuse the thread worker after thread exit to avoid re-creatation
   * @defaultValue false
   */
  reuseWorker: {
    /**
     * @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size | PTHREAD_POOL_SIZE}
     */
    size: 0,

    /**
     * @see {@link https://emscripten.org/docs/tools_reference/settings_reference.html#pthread-pool-size-strict | PTHREAD_POOL_SIZE_STRICT}
     */
    strict: false
  },

  onCreateWorker () {
    return new Worker('./worker.js')
    // Node.js
    // const { Worker } = require('worker_threads')
    // return new Worker(join(__dirname, './worker.js'), {
    //   env: process.env,
    //   execArgv: ['--experimental-wasi-unstable-preview1']
    // })
  },
  overwriteImports (importObject) {
    importObject.env.memory = new WebAssembly.Memory({
      initial: 16777216 / 65536,
      maximum: 2147483648 / 65536,
      shared: true
    })
  }
})

Note: For browsers, all the multithreaded features relying on Web Workers (Emscripten pthread also relying on Web Workers) require cross-origin isolation to enable SharedArrayBuffer. You can make a page cross-origin isolated by serving the page with these headers:

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

About

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

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors