Giter Site home page Giter Site logo

toyobayashi / emnapi Goto Github PK

View Code? Open in Web Editor NEW
118.0 2.0 5.0 1.93 MB

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

Home Page: https://emnapi-docs.vercel.app/

License: MIT License

JavaScript 18.92% C 44.93% TypeScript 24.90% C++ 7.38% HTML 0.92% CMake 1.63% Python 1.19% Rust 0.09% Assembly 0.04%
napi node-api emscripten webassembly wasm binding wasi wasi-sdk

emnapi's Introduction

emnapi

emnapi logo

Sponsors

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

Prerequests

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

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

emscripten
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']" \
     -o hello.js \
     hello.c \
     -lemnapi
wasi-sdk
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 wasm32

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

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
  // ...
}
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
}
<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>

If you are using Visual Studio Code and have Live Server extension installed, you can right click the HTML file in Visual Studio Code source tree and click Open With Live Server, then you can see the hello world alert!

Running on Node.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() })
})
wasi-sdk or clang wasm32

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) {
    // importObject.env = {
    //   ...importObject.env,
    //   ...importObject.napi,
    //   ...importObject.emnapi,
    //   // ...
    // }
  }
}).then(({ instance, module, napiModule }) => {
  const binding = napiModule.exports
  // ...
})
</script>

Using WASI on Node.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
  // ...
})

Using WASI on browser, you can use WASI polyfill in wasm-util, and memfs-browser

import { instantiateNapiModule } from '@emnapi/core'
import { getDefaultContext } from '@emnapi/runtime'
import { WASI } from '@tybys/wasm-util'
import { Volume, createFsFromVolume } from 'memfs-browser'

const fs = createFsFromVolume(Volume.fromJSON({ /* ... */ }))
instantiateNapiModule(fetch('./hello.wasm'), {
  wasi: new WASI({ fs, /* ... */ })
  context: getDefaultContext(),
  overwriteImports (importObject) {
    // importObject.env = {
    //   ...importObject.env,
    //   ...importObject.napi,
    //   ...importObject.emnapi,
    //   // ...
    // }
  }
}).then(({ instance, module, napiModule }) => {
  const binding = napiModule.exports
  // ...
})

Using C++ and node-addon-api

Require node-addon-api >= 6.1.0

npm install 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 hello.cpp.

#include <napi.h>

Napi::String Method(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  return Napi::String::New(env, "world");
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "hello"),
              Napi::Function::New(env, Method)).Check();
  return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)

Compile hello.cpp using em++. C++ exception is disabled by Emscripten default, and not supported by wasi-sdk, so predefine -DNAPI_DISABLE_CPP_EXCEPTIONS and -DNODE_ADDON_API_ENABLE_MAYBE here. If you would like to enable C++ exception, use -sDISABLE_EXCEPTION_CATCHING=0 instead and remove .Check() call. See official documentation here.

Building

emscripten
em++ -O3 \
     -DBUILDING_NODE_EXTENSION \
     "-DNAPI_EXTERN=__attribute__((__import_module__(\"env\")))" \
     -DNAPI_DISABLE_CPP_EXCEPTIONS \
     -DNODE_ADDON_API_ENABLE_MAYBE \
     -I./node_modules/emnapi/include/node \
     -I./node_modules/node-addon-api \
     -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']" \
     -o hello.js \
     hello.cpp \
     -lemnapi
wasi-sdk
clang++ -O3 \
        -DBUILDING_NODE_EXTENSION \
        -DNAPI_DISABLE_CPP_EXCEPTIONS \
        -DNODE_ADDON_API_ENABLE_MAYBE \
        -I./node_modules/emnapi/include/node \
        -I./node_modules/node-addon-api \
        -L./node_modules/emnapi/lib/wasm32-wasi \
        --target=wasm32-wasi \
        --sysroot=$WASI_SDK_PATH/share/wasi-sysroot \
        -fno-exceptions \
        -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.cpp \
        -lemnapi
clang wasm32

node-addon-api is using the C++ standard libraries, so you must use WASI if you are using node-addon-api.

You can still use wasm32-unknown-unknown target if you use Node-API C API only in C++.

clang++ -O3 \
        -DBUILDING_NODE_EXTENSION \
        -I./node_modules/emnapi/include/node \
        -L./node_modules/emnapi/lib/wasm32 \
        --target=wasm32 \
        -fno-exceptions \
        -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 node_api_c_api_only.wasm \
        node_api_c_api_only.cpp \
        -lemnapi \
        -ldlmalloc # -lemmalloc

operator new and operator delete.

#include <stddef.h>

extern "C" void* malloc(size_t size);
extern "C" void free(void* p);

void* operator new(size_t size) {
  return malloc(size);
}

void operator delete(void* p) noexcept {
  free(p);
}

Using CMake

Create CMakeLists.txt.

cmake_minimum_required(VERSION 3.13)

project(emnapiexample)

add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/node_modules/emnapi")

add_executable(hello hello.c)

target_link_libraries(hello emnapi)
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
  target_link_options(hello PRIVATE
    "-sEXPORTED_FUNCTIONS=['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']"
  )
elseif(CMAKE_SYSTEM_NAME STREQUAL "WASI")
  set_target_properties(hello PROPERTIES SUFFIX ".wasm")
  target_link_options(hello PRIVATE
    "-mexec-model=reactor"
    "-Wl,--export=napi_register_wasm_v1"
    "-Wl,--export-if-defined=node_api_module_get_api_version_v1"
    "-Wl,--initial-memory=16777216,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table"
  )
elseif((CMAKE_C_COMPILER_TARGET STREQUAL "wasm32") OR (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-unknown-unknown"))
  set_target_properties(hello PROPERTIES SUFFIX ".wasm")
  target_link_options(hello PRIVATE
    "-nostdlib"
    "-Wl,--export=napi_register_wasm_v1"
    "-Wl,--export-if-defined=node_api_module_get_api_version_v1"
    "-Wl,--no-entry"
    "-Wl,--initial-memory=16777216,--export-dynamic,--export=malloc,--export=free,--import-undefined,--export-table"
  )
  target_link_libraries(hello dlmalloc)
  # target_link_libraries(hello emmalloc)
endif()

If you use node-addon-api, you can use -DEMNAPI_FIND_NODE_ADDON_API=ON or manually add node-addon-api directory to the include dir via include_directories() or target_include_directories().

mkdir build

# emscripten
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
              -DEMNAPI_FIND_NODE_ADDON_API=ON \
              -G Ninja -H. -Bbuild

# wasi-sdk
cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk.cmake \
      -DWASI_SDK_PREFIX=$WASI_SDK_PATH \
      -DEMNAPI_FIND_NODE_ADDON_API=ON \
      -DCMAKE_BUILD_TYPE=Release \
      -G Ninja -H. -Bbuild

# wasm32
cmake -DCMAKE_TOOLCHAIN_FILE=node_modules/emnapi/cmake/wasm32.cmake \
      -DLLVM_PREFIX=$WASI_SDK_PATH \
      -DCMAKE_BUILD_TYPE=Release \
      -G Ninja -H. -Bbuild

cmake --build build

Output code can run in recent version modern browsers and Node.js latest LTS. IE is not supported.

Using node-gyp (Experimental)

Currently node-gyp works on Linux only and don't support static library linking in cross-compiling. There are opened PRs to try to make node-gyp work fine.

If you experienced issues on Windows or macOS, please check the PRs for upstream changes detail and 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' | ''

/**
 * 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

          # Windows and Linux
          "cflags": [],
          "cflags_c": [],
          "cflags_cc": [],
          "ldflags": [],

          # macOS uses following config
          'xcode_settings': {
            "WARNING_CFLAGS": [], # cflags
            "OTHER_CFLAGS": [], # cflags_c
            "OTHER_CPLUSPLUSFLAGS": [], # cflags_cc
            "OTHER_LDFLAGS": [] # ldflags
          }
        }],
        ["OS == 'wasi'", {
          # ...
        }],
        ["OS == 'unknown' or OS == ''", {
          # ...
        }]
      ]
    }
  ]
}
  • 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-linux -DOS=emscripten # -Dwasm_threads=1

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

# bare wasm32
node-gyp rebuild \
  --arch=wasm32 \
  --nodedir=./node_modules/emnapi \
  -- -f make-linux -DOS=unknown # -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-linux -DOS=emscripten
call emmake.bat make -C %~dp0build

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

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

Using Rust

See napi-rs

Multithread

Related API:

They are available in emnapi, but you need to know more details before you start to use them. Now emnapi has 3 implementations of async work and 2 implementations of TSFN:

  • Async work
    • A. Libuv threadpool and pthread based implementation in C
    • B. Single thread mock in JavaScript
    • C. Web worker based implementation in C (stack allocation) and JavaScript
  • TSFN
    • D. Libuv and pthread based implementation in C
    • E. Web worker based implementation in JavaScript
Library to Link wasm32-emscripten wasm32 wasm32-wasi wasm32-wasi-threads
A libemnapi-mt.a
B libemnapi-basic(-mt).a
C libemnapi-basic-mt.a
D libemnapi-mt.a
E libemnapi-basic(-mt).a

There are some limitations on browser about wasi-libc's pthread implementation, for example pthread_mutex_lock may call __builtin_wasm_memory_atomic_wait32(memory.atomic.wait32) which is disallowed in browser JS main thread. While Emscripten's pthread implementation has considered usage in browser. If you need to run your addon with multithreaded features on browser, we recommend you use Emscripten A & D, or bare wasm32 C & E.

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

If you would like to avoid SharedArrayBuffer and cross-origin isolation, please use B & E (link against libemnapi-basic.a), see the following table for more details.

About Prebuilt Libraries

Prebuilt libraries can be found in the lib directory in emnapi npm package.

Library Description wasm32-emscripten wasm32 wasm32-wasi wasm32-wasi-threads
libemnapi.a no atomics feature.

no libuv port.

napi_*_async_work and napi_*_threadsafe_function always return napi_generic_failure.
libemnapi-mt.a atomics feature enabled.

napi_*_async_work and napi_*_threadsafe_function are based on pthread and libuv port.
libemnapi-basic.a no atomics feature.

no libuv port.

napi_*_async_work and napi_*_threadsafe_function are imported from JavaScript land.
libemnapi-basic-mt.a atomics feature enabled.

no libuv port.

napi_*_async_work and napi_*_threadsafe_function are imported from JavaScript land.

include emnapi_async_worker_create and emnapi_async_worker_init for WebWorker based async work implementation.
libdlmalloc.a no atomics feature, no thread safe garanteed.
libdlmalloc-mt.a atomics feature enabled, thread safe.
libemmalloc.a no atomics feature, no thread safe garanteed.
libemmalloc-mt.a atomics feature enabled, thread safe.

Usage

add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/node_modules/emnapi")

add_executable(hello hello.c)

if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
  target_link_libraries(hello emnapi-mt)
  target_compile_options(hello PRIVATE "-pthread")
  target_link_options(hello PRIVATE
    "-sALLOW_MEMORY_GROWTH=1"
    "-sEXPORTED_FUNCTIONS=['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']"
    "-pthread"
    "-sPTHREAD_POOL_SIZE=4"
    # try to specify stack size if you experience pthread errors
    "-sSTACK_SIZE=2MB"
    "-sDEFAULT_PTHREAD_STACK_SIZE=2MB"
  )
elseif(CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-wasi-threads")
  target_link_libraries(hello emnapi-mt)
  set_target_properties(hello PROPERTIES SUFFIX ".wasm")
  target_compile_options(hello PRIVATE "-fno-exceptions" "-pthread")
  target_link_options(hello PRIVATE
    "-pthread"
    "-mexec-model=reactor"
    "-Wl,--import-memory"
    "-Wl,--max-memory=2147483648"
    "-Wl,--export-dynamic"
    "-Wl,--export=napi_register_wasm_v1"
    "-Wl,--export-if-defined=node_api_module_get_api_version_v1"
    "-Wl,--export=malloc"
    "-Wl,--export=free"
    "-Wl,--import-undefined"
    "-Wl,--export-table"
  )
elseif((CMAKE_C_COMPILER_TARGET STREQUAL "wasm32") OR (CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-unknown-unknown"))
  target_link_libraries(hello emnapi-basic-mt)
  set_target_properties(hello PROPERTIES SUFFIX ".wasm")
  target_compile_options(hello PRIVATE "-fno-exceptions" "-matomics" "-mbulk-memory")
  target_link_options(hello PRIVATE
    "-nostdlib"
    "-Wl,--no-entry"
    "-Wl,--export=napi_register_wasm_v1"
    "-Wl,--export-if-defined=node_api_module_get_api_version_v1"
    "-Wl,--export=emnapi_async_worker_create"
    "-Wl,--export=emnapi_async_worker_init"
    "-Wl,--import-memory,--shared-memory,--max-memory=2147483648,--import-undefined"
    "-Wl,--export-dynamic,--export=malloc,--export=free,--export-table"
  )
endif()
# emscripten
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
              -DEMNAPI_FIND_NODE_ADDON_API=ON \
              -DEMNAPI_WORKER_POOL_SIZE=4 \
              -G Ninja -H. -Bbuild

# wasi-sdk with thread support
cmake -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk-pthread.cmake \
      -DWASI_SDK_PREFIX=$WASI_SDK_PATH \
      -DEMNAPI_FIND_NODE_ADDON_API=ON \
      -DCMAKE_BUILD_TYPE=Release \
      -G Ninja -H. -Bbuild

cmake -DCMAKE_TOOLCHAIN_FILE=node_modules/emnapi/cmake/wasm32.cmake \
      -DLLVM_PREFIX=$WASI_SDK_PATH \
      -DCMAKE_BUILD_TYPE=Release \
      -G Ninja -H. -Bbuild

cmake --build build

And additional work is required during instantiating wasm compiled with non-emscripten.

// emnapi main thread (could be in a Worker)
instantiateNapiModule(input, {
  context: getDefaultContext(),
  /**
   * emscripten
   *   0: no effect
   *   > 0: the same effect to UV_THREADPOOL_SIZE
   * non-emscripten
   *   0: single thread mock
   *   > 0 schedule async work in web worker
   */
  asyncWorkPoolSize: 4, // 0: single thread mock, > 0: schedule async work in web worker
  wasi: new WASI(/* ... */),
  // reuseWorker: true,
  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
    })
  }
})
// worker.js
(function () {
  let fs, WASI, emnapiCore

  const ENVIRONMENT_IS_NODE =
    typeof process === 'object' && process !== null &&
    typeof process.versions === 'object' && process.versions !== null &&
    typeof process.versions.node === 'string'

  if (ENVIRONMENT_IS_NODE) {
    const nodeWorkerThreads = require('worker_threads')

    const parentPort = nodeWorkerThreads.parentPort

    parentPort.on('message', (data) => {
      globalThis.onmessage({ data })
    })

    fs = require('fs')

    Object.assign(globalThis, {
      self: globalThis,
      require,
      Worker: nodeWorkerThreads.Worker,
      importScripts: function (f) {
        (0, eval)(fs.readFileSync(f, 'utf8') + '//# sourceURL=' + f)
      },
      postMessage: function (msg) {
        parentPort.postMessage(msg)
      }
    })

    WASI = require('./wasi').WASI
    emnapiCore = require('@emnapi/core')
  } else {
    importScripts('./node_modules/memfs-browser/dist/memfs.js')
    importScripts('./node_modules/@tybys/wasm-util/dist/wasm-util.min.js')
    importScripts('./node_modules/@emnapi/core/dist/emnapi-core.js')
    emnapiCore = globalThis.emnapiCore

    const { Volume, createFsFromVolume } = memfs
    fs = createFsFromVolume(Volume.fromJSON({
      '/': null
    }))

    WASI = globalThis.wasmUtil.WASI
  }

  const { instantiateNapiModuleSync, MessageHandler } = emnapiCore

  const handler = new MessageHandler({
    onLoad ({ wasmModule, wasmMemory }) {
      const wasi = new WASI({ fs })

      return instantiateNapiModuleSync(wasmModule, {
        childThread: true,
        wasi,
        overwriteImports (importObject) {
          importObject.env.memory = wasmMemory
        }
      })
    }
  })

  globalThis.onmessage = function (e) {
    handler.handle(e)
    // handle other messages
  }
})()

Preprocess Macro Options

-DEMNAPI_WORKER_POOL_SIZE=4

This is UV_THREADPOOL_SIZE equivalent at compile time, if not predefined, emnapi will read asyncWorkPoolSize option or UV_THREADPOOL_SIZE from Emscripten environment variable at runtime:

Module.init({
  // ...
  asyncWorkPoolSize: 2
})

// if asyncWorkPoolSize is not specified
Module.preRun = Module.preRun || [];
Module.preRun.push(function () {
  if (typeof ENV !== 'undefined') {
    ENV.UV_THREADPOOL_SIZE = '2';
  }
});
// wasi
instantiateNapiModule({
  // ...
  asyncWorkPoolSize: 2
})
// if asyncWorkPoolSize is not specified
new WASI({
  env: {
    UV_THREADPOOL_SIZE: '2'
  }
})

It represent max of EMNAPI_WORKER_POOL_SIZE async work (napi_queue_async_work) can be executed in parallel. Default is not defined.

You can set both PTHREAD_POOL_SIZE and EMNAPI_WORKER_POOL_SIZE to number of CPU cores in general. If you use another library function which may create N child threads in async work, then you need to set PTHREAD_POOL_SIZE to EMNAPI_WORKER_POOL_SIZE * (N + 1).

This option only has effect if you use -pthread. Emnapi will create EMNAPI_WORKER_POOL_SIZE threads when initializing, it will throw error if PTHREAD_POOL_SIZE < EMNAPI_WORKER_POOL_SIZE && PTHREAD_POOL_SIZE_STRICT == 2.

See Issue #8 for more detail.

-DEMNAPI_NEXTTICK_TYPE=0

This option only has effect if you use -pthread, Default is 0. Tell emnapi how to delay async work in uv_async_send / uv__async_close.

  • 0: Use setImmediate() (Node.js native setImmediate or browser MessageChannel and port.postMessage)
  • 1: Use Promise.resolve().then()

-DEMNAPI_USE_PROXYING=1

This option only has effect if you use emscripten -pthread. Default is 1 if emscripten version >= 3.1.9, else 0.

  • 0

    Use JavaScript implementation to send async work from worker threads, runtime code will access the Emscripten internal PThread object to add custom worker message listener.

  • 1:

    Use Emscripten proxying API to send async work from worker threads in C. If you experience something wrong, you can switch set this to 0 and feel free to create an issue.

emnapi's People

Contributors

danzw1995 avatar mmomtchev avatar rreverser avatar toyobayashi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

emnapi's Issues

`js_native_api.c` contains some C++ statements

It cannot be compiled in strict C mode:

../../../../../../../../../../.nvm/versions/node/v18.19.0/lib/node_modules/emnapi/src/js_native_api.c:45:3: error: implicit declaration of function 'static_assert' [-Werror,-Wimplicit-function-declaration]
   45 |   static_assert((sizeof(emnapi_error_messages) / sizeof(const char*)) == napi_cannot_run_js + 1,
      |   ^
../../../../../../../../../../.nvm/versions/node/v18.19.0/lib/node_modules/emnapi/src/js_native_api.c:43:13: error: mixing declarations and code is a C99 extension [-Werror,-Wdeclaration-after-statement]
   43 |   const int last_status = napi_cannot_run_js;
      |             ^
2 errors generated.

npm registry reports erroneous install script for emnapi package v1.0.0+

Hi Toyo, thanks for all your work on this tool.

As of 1.0.0, the npm registry is reporting that the emnapi package has an install script:

$ npm view --json emnapi scripts
{
  "build": "node ./script/build.js",
  "version": "node ./script/version.js",
  "install": "node-gyp rebuild"
}

This causes a problem from npm v10.5.0 (Node.js 20.12.0) onwards as it starts to respect this field.

npm v10.2.4 works:

$ nvm use 20.11.1
Now using node v20.11.1 (npm v10.2.4)

$ npm i emnapi
added 1 package, and audited 2 packages in 1s

npm v10.5.0 does not work:

$ nvm use 20.12.0
Now using node v20.12.0 (npm v10.5.0)

$ npm i emnapi
npm ERR! code 1
npm ERR! command failed
npm ERR! command sh -c node-gyp rebuild
npm ERR! gyp info it worked if it ends with ok
npm ERR! gyp info using [email protected]
npm ERR! gyp info using [email protected] | linux | x64
...
npm ERR! gyp: binding.gyp not found (cwd: /tmp/node_modules/emnapi) while trying to load binding.gyp
npm ERR! gyp ERR! configure error 
...
npm ERR! gyp ERR! node -v v20.12.0
npm ERR! gyp ERR! node-gyp -v v10.0.1
npm ERR! gyp ERR! not ok

There is no mention of an install script in the source package.json file:

"scripts": {
"build": "node ./script/build.js",
"version": "node ./script/version.js"
},

My best guess of the cause is that there might have been an extra (i.e. not under version control) packages/emnapi/binding.gyp file at the point in time at which npm publish was run, which the npm CLI tooling interpreted as the package requiring an install script, thanks to the @npmcli/node-gyp logic.

Are you able to check to see if this might have been the case, and if so, publish a new version without the binding.gyp file to see if the npm registry will remove the install script? Alternatively, you might need to declare a no-op install script to avoid the default value.

(A similar problem affected the @parcel/watcher package - see parcel-bundler/watcher#156 (comment))

Error hadling

👋 @toyobayashi my library (that you already know) runs in worker. How can I catch the errors coming from it?
Can I setup worker.onerror?

Here is my returned error:

worker sent an error! http://localhost:8081/hnswlib.worker.js:1: Uncaught 18952616

Initialization order regression

We yesterday spent half of the day chasing down a weird new bug in a large project, which ended up narrowing down to a regression in emnapi. Git bisect ended up pointing to aa2af08.

Basically, that commit somewhat changed the ordering of init.

For background, any functions marked by __attribute__((constructor)) in C/C++ end up added to a special __wasm_call_ctors function and Emscripten adds them to the init array via addOnInit.

addOnInit runs init functions in the reverse order, so __wasm_call_ctors - which is added last - is always expected to be invoked first.

That was true with emnapi before, but after the change in that commit emnapi's initialization invokes addOnInit later in the process, which means - due to reverse order - the callback will now be actually invoked first, invoking the NAPI module init before __wasm_call_ctors, and so before the C/C++ is actually ready.

I've added a failing regression test in #14 to demonstrate the behaviour, but I'll leave the fix to you as I'm not as closely familiar with the codebase.

multiple emnapi modules binding

In the examples of loading emnapi modules in the browser, there is only one global "Module". When "Module.onRuntimeInitialized", it performs em​​napiInit. How to load multiple modules, such as A.js and B.js ? Is there any corresponding examples ?

Unused variables

Noticing this when building a project with emnapi. Obviously not a serious issue but would be nice to clean up:

../emnapi/packages/emnapi/src/emnapi.c:121:7: warning: unused variable 'r' [-Wunused-variable]
  int r = emscripten_resize_heap(old_size + (size_t) change_in_bytes);
      ^
1 warning generated.
../emnapi/packages/emnapi/src/uv/uv-common.c:23:10: warning: unused variable 'q' [-Wunused-variable]
  QUEUE* q;
         ^
1 warning generated.
../emnapi/packages/emnapi/src/uv/unix/thread.c:97:18: warning: unused variable 'attr_storage' [-Wunused-variable]
  pthread_attr_t attr_storage;
                 ^

[help] Docs do not reflect current lib

Hi!

I wanted to give a shot the library converting hnswlib-node without luck

System

My system is MacOS Ventura running in an M1.

node -v
v16.18.1

npm -v
8.19.2

emcc -v
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.34-git
clang version 17.0.0 (https://github.com/llvm/llvm-project.git a031f72187ce495b9faa4ccf99b1e901a3872f4b)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /opt/homebrew/Cellar/emscripten/3.1.34/libexec/llvm/bin

cmake --version
cmake version 3.25.1

Conversion step

yarn add @emnapi/runtime

I have slightly modified the C++ example

em++ -O3 \
     -DNAPI_DISABLE_CPP_EXCEPTIONS \
     -DNODE_ADDON_API_ENABLE_MAYBE \
     -I./node_modules/@emnapi/runtime/include \
     -L./node_modules/@emnapi/runtime/lib/wasm32-emscripten \
     --js-library=./node_modules/@emnapi/runtime/dist/emnapi.js \
     -sEXPORTED_FUNCTIONS="['_malloc','_free']" \
     -o hnswlib.js \
     ./src/addon.cc \
     -lemnapi
./node_modules/node-addon-api/napi.h:4:10: fatal error: 'node_api.h' file not found
#include <node_api.h>
         ^~~~~~~~~~~~
1 error generated.
em++: error: '/opt/homebrew/Cellar/emscripten/3.1.34/libexec/llvm/bin/clang++ -target wasm32-unknown-emscripten -fignore-exceptions -fvisibility=default -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -DEMSCRIPTEN --sysroot=/opt/homebrew/Cellar/emscripten/3.1.34/libexec/cache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat -O3 -DNAPI_DISABLE_CPP_EXCEPTIONS -DNODE_ADDON_API_ENABLE_MAYBE -I./node_modules/node-addon-api ./src/addon.cc -c -o /var/folders/7q/gtp1y2js5xng03nkg0mvd6xm0000gn/T/emscripten_temp__q2otjvq/addon_0.o' failed (returned 1)

however Im missing the include and wasm-emscripten folders. Please note also that Im uisng emnapi.js instead of library_napi.js that seems not to be either.

Am I going in the right direction?

Thanks 🙏

Not only for emscripten? wasi-sdk or napi-rs?

@tybys/emnapi-runtime is reusable, the emscripten js library part need to be rollup to a runtime bundle as well, and handle those compile time preprocess code somehow.

pthreads part is also a challenge.

don't know if it is possible.

node-gyp support?

It would be nice if node-gyp can work with emnapi on win/linux/mac.

Regression between 0.20.0..0.25.1

Sorry for the vague description, but not sure how to narrow it down further yet. I tried to upgrade the sharp PR to newer emnapi, and got weird test failures. In the end I could narrow it down to somewhere between 0.20.0 which I used in the PR and 0.25.1 which is the first version with WASM_ASYNC_COMPILATION-compatible fix for #15.

The failures I'm seeing seem to indicate some global state sharing isssue, either in Buffer allocation / copying, or maybe in error state handling. I can't think of any good explanation without digging much deeper.

Check out this commit for the repro: RReverser/sharp-fork@295d478

Example:

> npx --arch=wasm32 mocha -g 'Grayscale to RGBA, file'


  Image channel insertion
    ✔ Grayscale to RGBA, file
    1) Grayscale to RGBA, files, two arrays


  1 passing (410ms)
  1 failing

  1) Image channel insertion
       Grayscale to RGBA, files, two arrays:
     Uncaught Error: Input buffer contains unsupported image format
      at _napi_create_error (build/Release/sharp-emscripten-wasm32.node.js:5620:14)
      at Napi::Error::New(napi_env__*, std::__2::basic_string<char, std::__2::char_traits<char>, std::__2::allocator<char>> const&) (wasm://wasm/02b27b26:wasm-function[396]:0x212f1)
      at invoke_viii (build/Release/sharp-emscripten-wasm32.node.js:7691:27)
      at PipelineWorker::OnOK() (wasm://wasm/02b27b26:wasm-function[481]:0x50e86)
      at Napi::AsyncWorker::OnWorkComplete(Napi::Env, napi_status)::'lambda'()::operator()() const (wasm://wasm/02b27b26:wasm-function[380]:0x1ab29)
      ...

fails but running same tests individually works fine:

> npx --arch=wasm32 mocha -g 'Grayscale to RGBA, file$'


  Image channel insertion
    ✔ Grayscale to RGBA, file


  1 passing (327ms)

> npx --arch=wasm32 mocha -g 'Grayscale to RGBA, files, two arrays'


  Image channel insertion
    ✔ Grayscale to RGBA, files, two arrays


  1 passing (334ms)

Same as above happens in few other tests, and finally one test has a new timeout problem:

> npx --arch=wasm32 mocha -g 'Stream in, finish event fires before metadata is requested'


  Image metadata
    1) Stream in, finish event fires before metadata is requested


  0 passing (30s)
  1 failing

  1) Image metadata
       Stream in, finish event fires before metadata is requested:
     Error: Timeout of 30000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/rreverser/sharp/test/unit/metadata.js)
      at listOnTimeout (node:internal/timers:564:17)
      at process.processTimers (node:internal/timers:507:7)

None of these issues were reproducible with v0.20.0, but seem to be reproducible with every version from at least v0.25.1 to latest. Maybe I just refactored to emnapiInit incorrectly but can't seem to figure out what went wrong, and the errors seem suspicious so I'm inclined to think it's some change in the emnapi itself.

Current AsyncWorker scheduling conflict with threads used by other libraries

Currently, AsyncWorker uses the global pthread pool for scheduling its own tasks, taking threads as they become available. This works fine if AsyncWorker is the exclusive user of multithreading, but not when the addon is linked with other libraries that also rely on threads.

Here's the general problematic scenario:

  • addon A has async_method that spawns tasks onto an AsyncWorker
  • it links with library B and invokes B.some_method() that uses up to N threads itself
  • logically, user builds addon with PTHREAD_POOL_SIZE=N+1 (N threads for the library + 1 for the async op in the addon itself)

Now, if user invokes A.async_method(), the addon A will take one thread from the pool for the worker, N threads are left, the library takes those N threads, finishes the work, everything's fine.

But now another user has code that invokes async_method() on various inputs in parallel e.g. via Promise.all:

Promise.all([input1, input2]).map(input => A.async_method())

Now, the addon A will take 2 threads (one per each async_method() invocation) to create the AsyncWorkers, each of those workers invokes the B.some_method(), each of those parallel B.some_method() invocations tries to allocate N workers == 2*N total, but the pool only has (N+1)-2 == N-1 threads at this point! So each of them deadlocks.

User could increase the PTHREAD_POOL_SIZE to 2*(N+1), but then the same failure will happen if they happen to invoke 3 invocations of the library in parallel, and so on.

I actually observed this behaviour with deadlocks in a real-world addon I'm cross-compiling to Wasm with emnapi right now, and it took some time to figure out what's happening.

In my case I fixed this by monkey-patching the async methods to queue them one by one when compiled to Wasm, but, ideally, this should be handled at the emnapi level.

I think the only reliable solution here is for emnapi to have a separate, exclusive, thread pool, instead of taking as many threads as it wants from the global Emscripten pool. Then, it could either use the same JS logic or even some C++ implementation of work-stealing to execute max of EMNAPI_WORKER_POOL_SIZE in parallel, and no more. This would allow users to configure EMNAPI_WORKER_POOL_SIZE to the desired number of tasks, and PTHREAD_POOL_SIZE to EMNAPI_WORKER_POOL_SIZE * (N + 1) in advance, and avoid risk of deadlocks altogether.

And then, those users who know that they're not linking against any multithreaded libraries will be able to set both PTHREAD_POOL_SIZE and EMNAPI_WORKER_POOL to (number of CPU cores) and get max parallelisation as they can today.

undefined symbol: `napi_get_last_error_info`

Hello there. Thanks for making this project available. I'm using it to port a C++ library.

Currently, I'm using node-gyp to build the bindings for this library, and I'm almost done. The issue is that the napi_get_last_error_info symbol is not being exported from library_napi.js, while it is present in js_native_api.h. I'm not sure, but it might be because the corresponding function in error.ts starts with an underscore.

For reference, this is the error message from the node-gyp build command:

wasm-ld: error: ./Release/obj.target/binding/src/binding.o: undefined symbol: napi_get_last_error_info

Compilation errors when defining `NAPI_VERSION=7`

Details
CC(target) Release/obj.target/sharp-emscripten-wasm32/node_modules/emnapi/src/async_cleanup_hook.o
../node_modules/emnapi/src/async_cleanup_hook.c:18:3: error: unknown type name 'napi_async_cleanup_hook'; did you mean 'async_cleanup_hook'?
napi_async_cleanup_hook user_hook_;
^~~~~~~~~~~~~~~~~~~~~~~
async_cleanup_hook
../node_modules/emnapi/src/async_cleanup_hook.c:6:16: note: 'async_cleanup_hook' declared here
typedef void (*async_cleanup_hook)(void* arg, void(*)(void*), void*);
             ^
../node_modules/emnapi/src/async_cleanup_hook.c:25:3: error: use of undeclared identifier 'napi_async_cleanup_hook_handle'
napi_async_cleanup_hook_handle handle =
^
../node_modules/emnapi/src/async_cleanup_hook.c:27:3: error: use of undeclared identifier 'handle'
handle->done_cb_ = done_cb;
^
../node_modules/emnapi/src/async_cleanup_hook.c:28:3: error: use of undeclared identifier 'handle'
handle->done_data_ = done_data;
^
../node_modules/emnapi/src/async_cleanup_hook.c:29:3: error: use of undeclared identifier 'handle'
handle->user_hook_(handle, handle->user_data_);
^
../node_modules/emnapi/src/async_cleanup_hook.c:29:22: error: use of undeclared identifier 'handle'
handle->user_hook_(handle, handle->user_data_);
                   ^
../node_modules/emnapi/src/async_cleanup_hook.c:29:30: error: use of undeclared identifier 'handle'
handle->user_hook_(handle, handle->user_data_);
                           ^
../node_modules/emnapi/src/async_cleanup_hook.c:68:8: error: unknown type name 'napi_async_cleanup_hook_handle'
static napi_async_cleanup_hook_handle
     ^
../node_modules/emnapi/src/async_cleanup_hook.c:70:27: error: unknown type name 'napi_async_cleanup_hook'; did you mean 'async_cleanup_hook'?
                        napi_async_cleanup_hook user_hook,
                        ^~~~~~~~~~~~~~~~~~~~~~~
                        async_cleanup_hook
../node_modules/emnapi/src/async_cleanup_hook.c:6:16: note: 'async_cleanup_hook' declared here
typedef void (*async_cleanup_hook)(void* arg, void(*)(void*), void*);
             ^
../node_modules/emnapi/src/async_cleanup_hook.c:72:3: error: use of undeclared identifier 'napi_async_cleanup_hook_handle'
napi_async_cleanup_hook_handle handle =
^
../node_modules/emnapi/src/async_cleanup_hook.c:74:3: error: use of undeclared identifier 'handle'
handle->env_ = env;
^
../node_modules/emnapi/src/async_cleanup_hook.c:75:3: error: use of undeclared identifier 'handle'
handle->user_hook_ = user_hook;
^
../node_modules/emnapi/src/async_cleanup_hook.c:76:3: error: use of undeclared identifier 'handle'
handle->user_data_ = user_data;
^
../node_modules/emnapi/src/async_cleanup_hook.c:77:3: error: use of undeclared identifier 'handle'
handle->handle_ = _emnapi_add_async_environment_cleanup_hook(env, _emnapi_ach_handle_hook, handle);
^
../node_modules/emnapi/src/async_cleanup_hook.c:77:94: error: use of undeclared identifier 'handle'
handle->handle_ = _emnapi_add_async_environment_cleanup_hook(env, _emnapi_ach_handle_hook, handle);
                                                                                           ^
../node_modules/emnapi/src/async_cleanup_hook.c:80:10: error: use of undeclared identifier 'handle'
return handle;
       ^
../node_modules/emnapi/src/async_cleanup_hook.c:91:27: error: unknown type name 'napi_async_cleanup_hook_handle'
_emnapi_ach_handle_delete(napi_async_cleanup_hook_handle handle) {
                        ^
../node_modules/emnapi/src/async_cleanup_hook.c:103:29: error: unknown type name 'napi_async_cleanup_hook'; did you mean 'async_cleanup_hook'?
                          napi_async_cleanup_hook hook,
                          ^~~~~~~~~~~~~~~~~~~~~~~
                          async_cleanup_hook
../node_modules/emnapi/src/async_cleanup_hook.c:6:16: note: 'async_cleanup_hook' declared here
typedef void (*async_cleanup_hook)(void* arg, void(*)(void*), void*);
             ^
../node_modules/emnapi/src/async_cleanup_hook.c:105:29: error: unknown type name 'napi_async_cleanup_hook_handle'
                          napi_async_cleanup_hook_handle* remove_handle) {
                          ^
fatal error: too many errors emitted, stopping now [-ferror-limit=]
20 errors generated.
emcc: error: '/emsdk/upstream/bin/clang -target wasm32-unknown-emscripten -fignore-exceptions -fvisibility=default -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -D__EMSCRIPTEN_SHARED_MEMORY__=1 -DEMSCRIPTEN -Werror=implicit-function-declaration --sysroot=/src/build/emcache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat -Wall -Wextra -Wno-unused-parameter -pthread -g0 -O1 -DNODE_GYP_MODULE_NAME=sharp-emscripten-wasm32 -DUSING_UV_SHARED=1 -DUSING_V8_SHARED=1 -DV8_DEPRECATION_WARNINGS=1 -D__STDC_FORMAT_MACROS -DBUILDING_NODE_EXTENSION -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -DNAPI_VERSION=7 -DEMNAPI_WORKER_POOL_SIZE=1 -I../wasm-scripts/include/node -I../wasm-scripts/src -I../wasm-scripts/deps/openssl/config -I../wasm-scripts/deps/openssl/openssl/include -I../wasm-scripts/deps/uv/include -I../wasm-scripts/deps/zlib -I../wasm-scripts/deps/v8/include -I/sharp-src/node_modules/emnapi/include -I../node_modules/node-addon-api -I/src/build/target/include -I/src/build/target/include/glib-2.0 -I/src/build/target/lib/glib-2.0/include -I/src/build/target/include/webp -c -MMD -MF ./Release/.deps/Release/obj.target/sharp-emscripten-wasm32/node_modules/emnapi/src/async_cleanup_hook.o.d.raw -matomics -mbulk-memory ../node_modules/emnapi/src/async_cleanup_hook.c -o Release/obj.target/sharp-emscripten-wasm32/node_modules/emnapi/src/async_cleanup_hook.o' failed (returned 1)
make: Leaving directory '/sharp-src/build'
make: *** [sharp-emscripten-wasm32.target.mk:145: Release/obj.target/sharp-emscripten-wasm32/node_modules/emnapi/src/async_cleanup_hook.o] Error 1

It looks like these declarations are conditional guarded with NAPI_VERSION >= 8:

#if NAPI_VERSION >= 8
typedef struct napi_async_cleanup_hook_handle__* napi_async_cleanup_hook_handle;
typedef void(NAPI_CDECL* napi_async_cleanup_hook)(
napi_async_cleanup_hook_handle handle, void* data);
#endif // NAPI_VERSION >= 8

While the packages/emnapi/src/async_cleanup_hook.c file still makes use of these declarations for all Node-API versions. Perhaps that whole file should be guarded with NAPI_VERSION >= 8?

This was found while trying to update emnapi in lovell/sharp#3522.

Investigate using ESM in source code

No breaking changes should be introduced.

Before

  • source --> tsc "module": "none" & Node.js macro transformer --> regex replace emscripten directive and macro --> library_napi.js
  • source --> tsc "module": "none" & Node.js macro transformer and emscripten compile time macro transformer --> ifdef branch selection and wrap whole script --> @emnapi/core

Plan

  • source --> @rollup/plugin-typescript (output esm) & Node.js macro transformer --> transform esm to emscripten JS library format (make this to rollup plugin?) --> regex replace emscripten directive and macro --> library_napi.js
  • source --> @rollup/plugin-typescript (output iife?) & Node.js macro transformer and emscripten compile time macro transformer --> ifdef branch selection and wrap whole script --> @emnapi/core

ESM version of the runtime is broken

ESM version of the runtime (https://unpkg.com/@tybys/[email protected]/dist/emnapi.mjs) currently has this code, seems like a leftover from something that assumed it will be generated as CommonJS:

...

Object.defineProperty(exports, 'version', {
    configurable: true,
    enumerable: true,
    writable: false,
    value: "0.25.0"
});

export { CallbackInfo, CallbackInfoStack, ConstHandle, Context, Deferred, DeferredStore, EmnapiError, Env, EnvStore, Finalizer, Handle, HandleScope, HandleStore, NotSupportBigIntError, NotSupportWeakRefError, Ownership, Persistent, RefBase, RefStore, RefTracker, Reference, ScopeStore, Store, TryCatch, createContext, isReferenceType };

Trying to import it results in ReferenceError: exports is not defined.

Writing in `ArrayBuffer`

Currently when passing an ArrayBuffer argument _napi_get_arraybuffer_info() copies the data into the WASM heap.
(thinking... obviously WASM is never going to be very efficient - but hey, it works)

This means that it is not possible to have a function which writes into an ArrayBuffer? WASM does not have access to anything outside its (shared) heap? Not possible to sync at exit?

1.0 Release Plan

So far emnapi has already been used in sharp and napi-rs, and runs perfectly on StackBlitz. I think it's time to release 1.0

Issues with ArrayBuffer

👋 I need to access the file system so Im tried to compile this repo with sFORCE_FILESYSTEM :

em++ -O3 \
     -DNAPI_DISABLE_CPP_EXCEPTIONS \
     -I./node_modules/emnapi/include \
     -L./node_modules/emnapi/lib/wasm32-emscripten \
     --js-library=./node_modules/emnapi/dist/library_napi.js \
     -sEXPORTED_FUNCTIONS="['_malloc','_free']" \
     -sALLOW_MEMORY_GROWTH=1 \
     -sMODULARIZE=1 \
     -sEXPORT_NAME=hnswlib \
     -sFORCE_FILESYSTEM \
     -pthread \
     -o hnswlib.js \
     ./src/addon.cc \
     -lemnapi-basic

However when I try to use the lib:

RuntimeError: Aborted(FATAL ERROR: Error::New napi_get_last_error_info)
    at abort (/Users/davidgortega/Documents/projects/conjurors/hnswlib-node/hnswlib.js:8:18319)
    at _napi_fatal_error (/Users/davidgortega/Documents/projects/conjurors/hnswlib-node/hnswlib.js:8:122483)
    at wasm://wasm/0011b46a:wasm-function[126]:0x11438
    at wasm://wasm/0011b46a:wasm-function[87]:0x2933
    at wasm://wasm/0011b46a:wasm-function[157]:0x12b75
    at wasm://wasm/0011b46a:wasm-function[101]:0x91c8
    at wasm://wasm/0011b46a:wasm-function[85]:0x273c
    at /Users/davidgortega/Documents/projects/conjurors/hnswlib-node/hnswlib.js:8:112269
    at Env.callIntoModule (/Users/davidgortega/Documents/projects/conjurors/hnswlib-node/node_modules/@emnapi/runtime/dist/emnapi.cjs.js:636:19)
    at /Users/davidgortega/Documents/projects/conjurors/hnswlib-node/hnswlib.js:8:112199

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.