Skip to content

Conversation

@kraenhansen
Copy link
Collaborator

@kraenhansen kraenhansen commented Nov 10, 2025

Stacked on #328.

This is my suggestion for adding "multi-host" support to weak-node-api, enabling multiple engines implementing Node-API to co-exist and share the Node-API function namespace. While not needed specifically for bringing Node-API to React Native adding this could make weak-node-api more applicable in other scenarios where multiple engines implementing Node-API share a single process.

I'm proposing adding mechanisms to "wrap" the opaque pointers of specific Node-API implementors with:

template <typename T> struct Wrapped {
  T value; // The opaque pointer being wrapped.
  std::weak_ptr<WeakNodeApiHost> host; // Holds the actual implementation of the Node-API functions.
  WeakNodeApiMultiHost *multi_host; // Needed to wrap `napi_threadsafe_function` and `napi_async_cleanup_hook_handle` values before "returning" to the caller.
};

Where T is constrained to one of:

  • napi_env
  • node_api_basic_env (an alias for napi_env with slightly altered semantics)
  • napi_threadsafe_function
  • napi_async_cleanup_hook_handle

These Wrapped objects can then be passed around like their respective opaque pointers and "unwrapped" in the internal implementation of the "multi host" implementation of Node-API functions.

Wrapped objects are created calling one of these instance methods on WeakNodeApiMultiHost:

napi_env wrap(napi_env value, std::weak_ptr<WeakNodeApiHost>);
napi_threadsafe_function wrap(napi_threadsafe_function value, std::weak_ptr<WeakNodeApiHost>);
napi_async_cleanup_hook_handle wrap(napi_async_cleanup_hook_handle value, std::weak_ptr<WeakNodeApiHost>);

These functions are called internally in napi_create_threadsafe_function and napi_add_async_cleanup_hook.

Usage

  • Create a WeakNodeApiMultiHost (providing functions to register modules and handling a fatal error)
  • Inject it as the global host
  • Get an env from the actual Node-API implementatin
  • Wrap the env with using the WeakNodeApiMultiHost (calling multi_host.wrap(original_env, host);)
  • Pass the wrapped env to Node-API functions to delegate as needed

static size_t foo_calls = 0;
auto host_foo = std::shared_ptr<WeakNodeApiHost>(new WeakNodeApiHost{
.napi_create_object = [](napi_env env,
napi_value *result) -> napi_status {
foo_calls++;
return napi_status::napi_ok;
}});
static size_t bar_calls = 0;
auto host_bar = std::shared_ptr<WeakNodeApiHost>(new WeakNodeApiHost{
.napi_create_object = [](napi_env env,
napi_value *result) -> napi_status {
bar_calls++;
return napi_status::napi_ok;
}});
// Create and inject a multi host and wrap two envs
WeakNodeApiMultiHost multi_host{nullptr, nullptr};
inject_weak_node_api_host(multi_host);
auto foo_env = multi_host.wrap(napi_env{}, host_foo);
auto bar_env = multi_host.wrap(napi_env{}, host_bar);
napi_value result;
REQUIRE(foo_calls == 0);
REQUIRE(bar_calls == 0);
REQUIRE(napi_create_object(foo_env, &result) == napi_ok);
REQUIRE(foo_calls == 1);
REQUIRE(bar_calls == 0);
REQUIRE(napi_create_object(bar_env, &result) == napi_ok);
REQUIRE(foo_calls == 1);
REQUIRE(bar_calls == 1);

Open questions

  1. Should we use std::function instead of raw function pointers for all (or some) of the WeakNodeApiHost members? This would allow capturing lambdas, making it much easier to provide a meaningful implementation of for example napi_module_register.
  2. I suggest the Wrapped objects be owned by the WeakNodeApiMultiHost object (at least for now), but could we make memory management more efficient to release the Wrapped<T> before the WeakNodeApiMultiHost deletion: In napi_remove_async_cleanup_hook or some of the napi_*_threadsafe_function functions?

Generated code

Below are samples from the generated code:

napi_create_object implementation

napi_status WeakNodeApiMultiHost::napi_create_object(napi_env arg0,
                                                     napi_value *arg1) {
  auto wrapped = reinterpret_cast<Wrapped<napi_env> *>(arg0);
  if (auto host = wrapped->host.lock()) {
    if (host->napi_create_object == nullptr) {
      fprintf(stderr, "Node-API function 'napi_create_object' called on a host "
                      "which doesn't provide an implementation\n");
      return napi_status::napi_generic_failure;
    }

    return host->napi_create_object(wrapped->value, arg1);

  } else {
    fprintf(stderr, "Node-API function 'napi_create_object' called after host "
                    "was destroyed.\n");
    return napi_status::napi_generic_failure;
  }
};

napi_create_threadsafe_function and napi_add_async_cleanup_hook implementations

Notice the calls to wrap, wrapping their opaque "out" pointers.

napi_status WeakNodeApiMultiHost::napi_create_threadsafe_function(
    napi_env env, napi_value func, napi_value async_resource,
    napi_value async_resource_name, size_t max_queue_size,
    size_t initial_thread_count, void *thread_finalize_data,
    napi_finalize thread_finalize_cb, void *context,
    napi_threadsafe_function_call_js call_js_cb,
    napi_threadsafe_function *result) {
  auto wrapped = reinterpret_cast<Wrapped<napi_env> *>(env);
  if (auto host = wrapped->host.lock()) {
    if (host->napi_create_threadsafe_function == nullptr) {
      fprintf(stderr,
              "Node-API function 'napi_create_threadsafe_function' called on a "
              "host which doesn't provide an implementation\n");
      return napi_status::napi_generic_failure;
    }

    auto status = host->napi_create_threadsafe_function(
        wrapped->value, func, async_resource, async_resource_name,
        max_queue_size, initial_thread_count, thread_finalize_data,
        thread_finalize_cb, context, call_js_cb, result);
    if (status == napi_status::napi_ok) {
      *result = wrapped->multi_host->wrap(*result, wrapped->host);
    }
    return status;

  } else {
    fprintf(stderr, "Node-API function 'napi_create_threadsafe_function' "
                    "called after host was destroyed.\n");
    return napi_status::napi_generic_failure;
  }
};

napi_status WeakNodeApiMultiHost::napi_add_async_cleanup_hook(
    node_api_basic_env env, napi_async_cleanup_hook hook, void *arg,
    napi_async_cleanup_hook_handle *remove_handle) {
  auto wrapped = reinterpret_cast<Wrapped<node_api_basic_env> *>(env);
  if (auto host = wrapped->host.lock()) {
    if (host->napi_add_async_cleanup_hook == nullptr) {
      fprintf(stderr, "Node-API function 'napi_add_async_cleanup_hook' called "
                      "on a host which doesn't provide an implementation\n");
      return napi_status::napi_generic_failure;
    }

    auto status = host->napi_add_async_cleanup_hook(wrapped->value, hook, arg,
                                                    remove_handle);
    if (status == napi_status::napi_ok) {
      *remove_handle = wrapped->multi_host->wrap(*remove_handle, wrapped->host);
    }
    return status;

  } else {
    fprintf(stderr, "Node-API function 'napi_add_async_cleanup_hook' called "
                    "after host was destroyed.\n");
    return napi_status::napi_generic_failure;
  }
};

@kraenhansen kraenhansen force-pushed the kh/weak-node-api-multi-host branch from 6f9356d to f79a30b Compare November 10, 2025 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants