Overview of emnapi
wasm32-unknown-unknown target. Its primary goal is to allow developers to port existing Node-API native addons to WebAssembly (Wasm) with minimal code changes, ensuring that runtime behavior closely matches native Node.js.website·Indexed 30 days ago
https://emnapi-docs.vercel.app/A library providing a nearly full implementation of Node-API for Emscripten, allowing Node-API native addons to run in the browser with minimal code changes. Documentation covers function and class binding, multithreaded asynchronous operations, C++ integration via node-addon-api, and build configurations using CMake and node-gyp.
wasm32-unknown-unknown target. Its primary goal is to allow developers to port existing Node-API native addons to WebAssembly (Wasm) with minimal code changes, ensuring that runtime behavior closely matches native Node.js.--arch flag with wasm32 or wasm64.node-gyp configure --arch=wasm32embind allows code to run in both the browser and Node.js, it often requires writing separate bindings if you want native performance in Node.js (via native addons) and Wasm performance in the browser. emnapi solves this by providing a Node-API implementation for Wasm, allowing the same Node-API binding code to be used across both environments. It is specifically recommended for projects targeting StackBlitz's WebContainer.When exposing C++ classes to JavaScript via emnapi, memory management depends on the JavaScript runtime's support for FinalizationRegistry and WeakRef.
dispose()) from the C++ side. The JavaScript consumer must call this method to delete the native instance pointer to prevent memory leaks.Module.onRuntimeInitialized = function () {
const MyClass = Module.emnapiExports.MyClass
const instance = new MyClass(10, "hello")
// Manual cleanup for runtimes without FinalizationRegistry
if (typeof FinalizationRegistry !== 'function') {
instance.dispose()
// After disposing, the instance must no longer be used
}
}node_api.h provide multithreading and thread-safe function capabilities, provided the pthread and environment requirements are met:napi_create_async_work
napi_delete_async_work
napi_queue_async_work
napi_cancel_async_work
napi_create_threadsafe_function
napi_get_threadsafe_function_context
napi_call_threadsafe_function
napi_acquire_threadsafe_function
napi_release_threadsafe_function
napi_unref_threadsafe_function
napi_ref_threadsafe_functionNODE_ADDON_API_ENABLE_MAYBE is predefined, functions that can fail return a Maybe<T> boxed value instead of throwing. Developers must check if the Maybe value is empty using .To() or .IsNothing(). If empty, a JavaScript exception is pending and should be managed via env.GetAndClearPendingException() before calling other Node-API functions.Napi::Env env = ...;
Napi::Function jsFunctionThatThrows = someValue.As<Napi::Function>();
// Propagating: Check if Maybe is empty
Maybe<Napi::Value> maybeResult = jsFunctionThatThrows({ arg1, arg2 });
Napi::Value result;
if (!maybeResult.To(&result)) {
return result; // Propagate pending exception
}
// Handling: Clear the pending exception
if (maybeResult.IsNothing()) {
Napi::Error e = env.GetAndClearPendingException();
std::cerr << "Caught JavaScript exception: " + e.Message();
}napi_unwrap to retrieve the C++ instance from the JavaScript this object.// Getter implementation
napi_value MyClass::JsGetX(napi_env env, napi_callback_info info) {
napi_value this_arg;
NAPI_CALL(env, napi_get_cb_info(env, info, nullptr, nullptr, &this_arg, nullptr));
MyClass* instance = nullptr;
NAPI_CALL(env, napi_unwrap(env, this_arg, reinterpret_cast<void**>(&instance)));
napi_value ret;
NAPI_CALL(env, napi_create_int32(env, instance->GetX(), &ret));
return ret;
}
// Setter implementation
napi_value MyClass::JsSetX(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
napi_value this_arg;
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, &this_arg, nullptr));
int x = 0;
NAPI_CALL(env, napi_get_value_int32(env, args[0], &x));
MyClass* instance = nullptr;
NAPI_CALL(env, napi_unwrap(env, this_arg, reinterpret_cast<void**>(&instance)));
instance->SetX(x);
napi_value undefined;
NAPI_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
}To perform a heavy operation in a child thread and notify the main thread upon completion, extend the Napi::AsyncWorker class.
Execute(): This method runs in the child thread. Perform the heavy computation here. Do not access JavaScript objects or the Napi::Env directly inside this method.OnOK(): This method runs in the main thread after Execute() completes successfully. Use it to call the JavaScript callback with the result.OnError(): This method runs in the main thread if an error occurs during execution.To start the worker, instantiate your class and call the Queue() method.
#include <napi.h>
class MonteCarloEstimatePiWorker : public Napi::AsyncWorker {
public:
MonteCarloEstimatePiWorker(int points, const Napi::Function& callback)
: Napi::AsyncWorker(callback), points_(points), result_(0) {}
void Execute() {
// Runs in child thread
result_ = monte_carlo_estimate_pi(points_);
}
void OnOK() {
// Runs in main thread
Callback().Call(Env().Undefined(), {
Env().Null(),
Napi::Number::New(Env(), result_)
});
}
void OnError(const Napi::Error& e) {
// Runs in main thread
Callback().Call(Env().Undefined(), { e.Value() });
}
private:
int points_;
double result_;
};
static Napi::Value JsEstimate(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
int points = info[0].As<Napi::Number>().Uint32Value();
Napi::Function callback = info[1].As<Napi::Function>();
auto* piWorker = new MonteCarloEstimatePiWorker(points, callback);
piWorker->Queue();
return env.Undefined();
}node-addon-api automatically converts JavaScript exceptions and internal API failures (like invalid arguments) into C++ exceptions of type Napi::Error. These can be caught using standard try/catch blocks. If a Napi::Error escapes a native method, it is automatically converted back into a JavaScript exception upon returning to the JS environment.// Throwing a C++ exception
Napi::Env env = ...;
throw Napi::Error::New(env, "Example exception");
// Handling a C++ exception
Napi::Function jsFunctionThatThrows = someValue.As<Napi::Function>();
Napi::Value result;
try {
result = jsFunctionThatThrows({ arg1, arg2 });
} catch (const Napi::Error& e) {
std::cerr << "Caught JavaScript exception: " + std::string(e.what());
}emnapi_sync_memory (or the exported runtime method emnapiSyncMemory) to synchronize data between the WASM environment and JavaScript. This is particularly necessary when using napi_create_external_arraybuffer or when memory has been modified on the WASM side after the JS object was created.#include <emnapi.h>
napi_status emnapi_sync_memory(napi_env env,
bool js_to_wasm,
napi_value* arraybuffer_or_view,
size_t byte_offset,
size_t length);
void finalizer(napi_env env, void* finalize_data, void* finalize_hint) {
free(finalize_data);
}
napi_value createExternalArraybuffer(napi_env env, napi_callback_info info) {
uint8_t* external_data = malloc(3);
external_data[0] = 0;
external_data[1] = 1;
external_data[2] = 2;
napi_value array_buffer;
napi_create_external_arraybuffer(env, external_data, 3, finalizer, NULL, &array_buffer);
external_data[0] = 3; // JS ArrayBuffer memory will not change automatically
emnapi_sync_memory(env, false, array_buffer, 0, NAPI_AUTO_LENGTH);
// after sync memory, new Uint8Array(array_buffer)[0] === 3
return array_buffer;
}// JavaScript/TypeScript usage
declare function emnapiSyncMemory (
jsToWasm: boolean,
arrayBufferOrView: ArrayBuffer | ArrayBufferView,
byteOffset?: number,
length?: number
): void
const array_buffer = Module.emnapiExports.createExternalArraybuffer()
new Uint8Array(array_buffer)[1] = 4
Module.emnapiSyncMemory(true, array_buffer)This example shows how to use the node-addon-api C++ wrapper to implement a function that concatenates two strings and passes the result to a JavaScript callback.
Constraint: node-addon-api requires the runtime to support FinalizationRegistry and WeakRef.
Napi::Value JsConcatString(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 3) {
Napi::TypeError e = Napi::TypeError::New(env, "Wrong number of arguments");
NAPI_THROW(e, Napi::Value());
}
if (!info[0].IsString() || !info[1].IsString() || !info[2].IsFunction()) {
Napi::TypeError e = Napi::TypeError::New(env, "Wrong arguments");
NAPI_THROW(e, Napi::Value());
}
Napi::Value undefined = env.Undefined();
std::string str1 = info[0].As<Napi::String>().Utf8Value();
std::string str2 = info[1].As<Napi::String>().Utf8Value();
std::string result = str1 + str2;
info[2].As<Napi::Function>().Call(undefined, { Napi::String::New(env, result) });
return undefined;
}