From cf5e056542e03b6d1e2f57d189736bef59253ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 13 Nov 2025 12:23:17 +0100 Subject: [PATCH 1/2] Remove incorrect comments --- packages/weak-node-api/scripts/generators/NodeApiHost.ts | 3 --- packages/weak-node-api/scripts/generators/weak-node-api.ts | 7 +------ packages/weak-node-api/src/node-api-functions.ts | 4 ---- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/weak-node-api/scripts/generators/NodeApiHost.ts b/packages/weak-node-api/scripts/generators/NodeApiHost.ts index 54b34a97..c273cfe7 100644 --- a/packages/weak-node-api/scripts/generators/NodeApiHost.ts +++ b/packages/weak-node-api/scripts/generators/NodeApiHost.ts @@ -8,9 +8,6 @@ export function generateFunctionDecl({ return `${returnType} (*${name})(${argumentTypes.join(", ")});`; } -/** - * Generates source code for a version script for the given Node API version. - */ export function generateHeader(functions: FunctionDecl[]) { return ` #pragma once diff --git a/packages/weak-node-api/scripts/generators/weak-node-api.ts b/packages/weak-node-api/scripts/generators/weak-node-api.ts index 2fb2c33f..a3442b9d 100644 --- a/packages/weak-node-api/scripts/generators/weak-node-api.ts +++ b/packages/weak-node-api/scripts/generators/weak-node-api.ts @@ -1,9 +1,7 @@ import type { FunctionDecl } from "../../src/node-api-functions.js"; import { generateFunction } from "./shared.js"; -/** - * Generates source code for a version script for the given Node API version. - */ + export function generateHeader() { return ` #pragma once @@ -36,9 +34,6 @@ function generateFunctionImpl(fn: FunctionDecl) { }); } -/** - * Generates source code for a version script for the given Node API version. - */ export function generateSource(functions: FunctionDecl[]) { return ` #include "weak_node_api.hpp" diff --git a/packages/weak-node-api/src/node-api-functions.ts b/packages/weak-node-api/src/node-api-functions.ts index 4290a471..f92bd956 100644 --- a/packages/weak-node-api/src/node-api-functions.ts +++ b/packages/weak-node-api/src/node-api-functions.ts @@ -49,10 +49,6 @@ const clangAstDump = z.object({ ), }); -/** - * Generates source code for a version script for the given Node API version. - * @param version - */ export function getNodeApiHeaderAST(version: NodeApiVersion) { const output = cp.execFileSync( "clang", From daf66860d1f5346266911273fcf7d81ea282bb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 9 Nov 2025 22:29:24 +0100 Subject: [PATCH 2/2] Implement WeakNodeApiMultiHost generator --- packages/weak-node-api/CMakeLists.txt | 2 + packages/weak-node-api/scripts/generate.ts | 21 ++ .../scripts/generators/NodeApiMultiHost.ts | 263 ++++++++++++++++++ packages/weak-node-api/tests/CMakeLists.txt | 1 + .../weak-node-api/tests/test_multi_host.cpp | 173 ++++++++++++ 5 files changed, 460 insertions(+) create mode 100644 packages/weak-node-api/scripts/generators/NodeApiMultiHost.ts create mode 100644 packages/weak-node-api/tests/test_multi_host.cpp diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt index 90525434..bea21bd7 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -13,10 +13,12 @@ set(GENERATED_SOURCE_DIR "generated") target_sources(${PROJECT_NAME} PUBLIC ${GENERATED_SOURCE_DIR}/weak_node_api.cpp + ${GENERATED_SOURCE_DIR}/NodeApiMultiHost.cpp PUBLIC FILE_SET HEADERS BASE_DIRS ${GENERATED_SOURCE_DIR} ${INCLUDE_DIR} FILES ${GENERATED_SOURCE_DIR}/weak_node_api.hpp ${GENERATED_SOURCE_DIR}/NodeApiHost.hpp + ${GENERATED_SOURCE_DIR}/NodeApiMultiHost.hpp ${INCLUDE_DIR}/js_native_api_types.h ${INCLUDE_DIR}/js_native_api.h ${INCLUDE_DIR}/node_api_types.h diff --git a/packages/weak-node-api/scripts/generate.ts b/packages/weak-node-api/scripts/generate.ts index fc21e485..3691e0b3 100644 --- a/packages/weak-node-api/scripts/generate.ts +++ b/packages/weak-node-api/scripts/generate.ts @@ -10,6 +10,7 @@ import { import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; import * as hostGenerator from "./generators/NodeApiHost.js"; +import * as multiHostGenerator from "./generators/NodeApiMultiHost.js"; export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); @@ -87,6 +88,26 @@ async function run() { Provides the implementation for deferring Node-API function calls from addons into a Node-API host. `, }); + await generateFile({ + functions, + fileName: "NodeApiMultiHost.hpp", + generator: multiHostGenerator.generateHeader, + headingComment: ` + @brief Weak Node-API multi-host injection interface. + + This header provides the struct for deferring Node-API function calls from addons into multiple Node-API host implementations. + `, + }); + await generateFile({ + functions, + fileName: "NodeApiMultiHost.cpp", + generator: multiHostGenerator.generateSource, + headingComment: ` + @brief Weak Node-API multi-host injection implementation. + + Provides the implementation for deferring Node-API function calls from addons into multiple Node-API host implementations. + `, + }); } run().catch((err) => { diff --git a/packages/weak-node-api/scripts/generators/NodeApiMultiHost.ts b/packages/weak-node-api/scripts/generators/NodeApiMultiHost.ts new file mode 100644 index 00000000..e0bef9da --- /dev/null +++ b/packages/weak-node-api/scripts/generators/NodeApiMultiHost.ts @@ -0,0 +1,263 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; +import { generateFunction } from "./shared.js"; + +const ARGUMENT_NAMES_PR_FUNCTION: Record = { + napi_create_threadsafe_function: [ + "env", + "func", + "async_resource", + "async_resource_name", + "max_queue_size", + "initial_thread_count", + "thread_finalize_data", + "thread_finalize_cb", + "context", + "call_js_cb", + "result", + ], + napi_add_async_cleanup_hook: ["env", "hook", "arg", "remove_handle"], + napi_fatal_error: ["location", "location_len", "message", "message_len"], +}; + +export function generateFunctionDecl(fn: FunctionDecl) { + return generateFunction({ ...fn, static: true }); +} + +export function generateHeader(functions: FunctionDecl[]) { + return ` + #pragma once + + #include + #include + + #include + + #include "NodeApiHost.hpp" + + struct NodeApiMultiHost : public NodeApiHost { + + struct WrappedEnv; + + struct WrappedThreadsafeFunction { + napi_threadsafe_function value; + WrappedEnv *env; + std::weak_ptr host; + }; + + struct WrappedAsyncCleanupHookHandle { + napi_async_cleanup_hook_handle value; + WrappedEnv *env; + std::weak_ptr host; + }; + + struct WrappedEnv { + WrappedEnv(napi_env &&value, std::weak_ptr &&host); + + napi_env value; + std::weak_ptr host; + + private: + std::vector> + threadsafe_functions; + std::vector> + async_cleanup_hook_handles; + + public: + napi_threadsafe_function wrap(napi_threadsafe_function original, + WrappedEnv *env, + std::weak_ptr); + napi_async_cleanup_hook_handle wrap(napi_async_cleanup_hook_handle original, + WrappedEnv *env, + std::weak_ptr); + }; + + napi_env wrap(napi_env original, std::weak_ptr); + + NodeApiMultiHost( + void napi_module_register(napi_module *), + void napi_fatal_error(const char *, size_t, const char *, size_t) + ); + + private: + std::vector> envs; + + public: + + ${functions.map(generateFunctionDecl).join("\n")} + }; + `; +} + +function generateFunctionImpl(fn: FunctionDecl) { + const { name, argumentTypes, returnType } = fn; + const [firstArgument] = argumentTypes; + const argumentNames = + ARGUMENT_NAMES_PR_FUNCTION[name] ?? + argumentTypes.map((_, index) => `arg${index}`); + if (name === "napi_fatal_error") { + // Providing a default implementation + return generateFunction({ + ...fn, + namespace: "NodeApiMultiHost", + argumentNames, + body: ` + if (location && location_len) { + fprintf(stderr, "Fatal Node-API error: %.*s %.*s", + static_cast(location_len), + location, + static_cast(message_len), + message + ); + } else { + fprintf(stderr, "Fatal Node-API error: %.*s", static_cast(message_len), message); + } + abort(); + `, + }); + } else if (name === "napi_module_register") { + // Providing a default implementation + return generateFunction({ + ...fn, + namespace: "NodeApiMultiHost", + argumentNames: [""], + body: ` + fprintf(stderr, "napi_module_register is not implemented for this NodeApiMultiHost"); + abort(); + `, + }); + } else if ( + [ + "napi_env", + "node_api_basic_env", + "napi_threadsafe_function", + "napi_async_cleanup_hook_handle", + ].includes(firstArgument) + ) { + const joinedArguments = argumentTypes + .map((_, index) => + index === 0 ? "wrapped->value" : argumentNames[index], + ) + .join(", "); + + function generateCall() { + if (name === "napi_create_threadsafe_function") { + return ` + auto status = host->${name}(${joinedArguments}); + if (status == napi_status::napi_ok) { + *${argumentNames[10]} = wrapped->wrap(*${argumentNames[10]}, wrapped, wrapped->host); + } + return status; + `; + } else if (name === "napi_add_async_cleanup_hook") { + return ` + auto status = host->${name}(${joinedArguments}); + if (status == napi_status::napi_ok) { + *${argumentNames[3]} = wrapped->wrap(*${argumentNames[3]}, wrapped, wrapped->host); + } + return status; + `; + } else { + return ` + ${returnType === "void" ? "" : "return"} host->${name}(${joinedArguments}); + `; + } + } + + function getWrappedType(nodeApiType: string) { + if (nodeApiType === "napi_env" || nodeApiType === "node_api_basic_env") { + return "WrappedEnv"; + } else if (nodeApiType === "napi_threadsafe_function") { + return "WrappedThreadsafeFunction"; + } else if (nodeApiType === "napi_async_cleanup_hook_handle") { + return "WrappedAsyncCleanupHookHandle"; + } else { + throw new Error(`Unexpected Node-API type '${nodeApiType}'`); + } + } + + return generateFunction({ + ...fn, + namespace: "NodeApiMultiHost", + argumentNames, + body: ` + auto wrapped = reinterpret_cast<${getWrappedType(firstArgument)}*>(${argumentNames[0]}); + if (auto host = wrapped->host.lock()) { + if (host->${name} == nullptr) { + fprintf(stderr, "Node-API function '${name}' called on a host which doesn't provide an implementation\\n"); + return napi_status::napi_generic_failure; + } + ${generateCall()} + } else { + fprintf(stderr, "Node-API function '${name}' called after host was destroyed.\\n"); + return napi_status::napi_generic_failure; + } + `, + }); + } else { + throw new Error(`Unexpected signature for '${name}' Node-API function`); + } +} + +export function generateSource(functions: FunctionDecl[]) { + return ` + #include "NodeApiMultiHost.hpp" + + NodeApiMultiHost::NodeApiMultiHost( + void napi_module_register(napi_module *), + void napi_fatal_error(const char *, size_t, const char *, size_t) + ) + : NodeApiHost({ + ${functions + .map(({ name }) => { + if ( + name === "napi_module_register" || + name === "napi_fatal_error" + ) { + // We take functions not taking a wrap-able argument via the constructor and call them directly + return `.${name} = ${name} == nullptr ? NodeApiMultiHost::${name} : ${name},`; + } else { + return `.${name} = NodeApiMultiHost::${name},`; + } + }) + .join("\n")} + }), envs{} {}; + + // TODO: Ensure the Node-API functions aren't throwing (using NOEXCEPT) + // TODO: Find a better way to delete these along the way + + NodeApiMultiHost::WrappedEnv::WrappedEnv(napi_env &&value, + std::weak_ptr &&host) + : value(value), host(host), threadsafe_functions{}, + async_cleanup_hook_handles{} {} + + napi_env NodeApiMultiHost::wrap(napi_env value, + std::weak_ptr host) { + auto ptr = std::make_unique(std::move(value), std::move(host)); + auto raw_ptr = ptr.get(); + envs.push_back(std::move(ptr)); + return reinterpret_cast(raw_ptr); + } + + napi_threadsafe_function + NodeApiMultiHost::WrappedEnv::wrap(napi_threadsafe_function original, + WrappedEnv *env, + std::weak_ptr weak_host) { + auto ptr = std::make_unique(original, env, weak_host); + auto raw_ptr = ptr.get(); + env->threadsafe_functions.push_back(std::move(ptr)); + return reinterpret_cast(raw_ptr); + } + + napi_async_cleanup_hook_handle + NodeApiMultiHost::WrappedEnv::wrap(napi_async_cleanup_hook_handle original, + WrappedEnv *env, + std::weak_ptr weak_host) { + auto ptr = std::make_unique(original, env, weak_host); + auto raw_ptr = ptr.get(); + env->async_cleanup_hook_handles.push_back(std::move(ptr)); + return reinterpret_cast(raw_ptr); + } + + ${functions.map(generateFunctionImpl).join("\n")} + `; +} diff --git a/packages/weak-node-api/tests/CMakeLists.txt b/packages/weak-node-api/tests/CMakeLists.txt index 89b19f84..4a4684ec 100644 --- a/packages/weak-node-api/tests/CMakeLists.txt +++ b/packages/weak-node-api/tests/CMakeLists.txt @@ -10,6 +10,7 @@ FetchContent_MakeAvailable(Catch2) add_executable(weak-node-api-tests test_inject.cpp + test_multi_host.cpp ) target_link_libraries(weak-node-api-tests PRIVATE diff --git a/packages/weak-node-api/tests/test_multi_host.cpp b/packages/weak-node-api/tests/test_multi_host.cpp new file mode 100644 index 00000000..e4409316 --- /dev/null +++ b/packages/weak-node-api/tests/test_multi_host.cpp @@ -0,0 +1,173 @@ +#include "js_native_api_types.h" +#include "node_api.h" +#include "node_api_types.h" +#include +#include +#include + +#include +#include +#include + +TEST_CASE("NodeApiMultiHost") { + SECTION("is injectable") { + NodeApiMultiHost host{nullptr, nullptr}; + inject_weak_node_api_host(host); + } + + SECTION("propagates calls to the right napi_create_object") { + // Setup two hosts + static size_t foo_calls = 0; + auto host_foo = std::shared_ptr(new NodeApiHost{ + .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(new NodeApiHost{ + .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 + NodeApiMultiHost 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); + } + + SECTION("handles multi-host resetting") { + // Setup two hosts + static size_t called = 0; + auto host = std::shared_ptr(new NodeApiHost{ + .napi_create_object = [](napi_env env, + napi_value *result) -> napi_status { + called++; + return napi_status::napi_ok; + }}); + + // Create and inject a multi host and wrap two envs + NodeApiMultiHost multi_host{nullptr, nullptr}; + inject_weak_node_api_host(multi_host); + + auto env = multi_host.wrap(napi_env{}, host); + + napi_value result; + REQUIRE(called == 0); + + REQUIRE(napi_create_object(env, &result) == napi_ok); + REQUIRE(called == 1); + + host.reset(); + REQUIRE(napi_create_object(env, &result) == napi_generic_failure); + REQUIRE(called == 1); + } + + SECTION("wraps threadsafe functions") { + // Setup two hosts + static size_t calls = 0; + auto host_foo = std::shared_ptr(new NodeApiHost{ + .napi_create_object = [](napi_env env, + napi_value *result) -> napi_status { + calls++; + return napi_status::napi_ok; + }, + .napi_create_threadsafe_function = + [](napi_env, napi_value, napi_value, napi_value, size_t, size_t, + void *, napi_finalize, void *, napi_threadsafe_function_call_js, + napi_threadsafe_function *out) -> napi_status { + calls++; + (*out) = {}; + return napi_status::napi_ok; + }, + .napi_release_threadsafe_function = + [](napi_threadsafe_function, + napi_threadsafe_function_release_mode) -> napi_status { + calls++; + return napi_status::napi_ok; + }}); + + // Create and inject a multi host and wrap two envs + NodeApiMultiHost multi_host{nullptr, nullptr}; + inject_weak_node_api_host(multi_host); + + auto foo_env = multi_host.wrap(napi_env{}, host_foo); + + { + napi_threadsafe_function result; + + REQUIRE(calls == 0); + + REQUIRE(napi_create_threadsafe_function( + foo_env, nullptr, nullptr, nullptr, 0, 0, nullptr, nullptr, + nullptr, nullptr, &result) == napi_ok); + REQUIRE(calls == 1); + + REQUIRE(napi_release_threadsafe_function( + result, + napi_threadsafe_function_release_mode::napi_tsfn_release) == + napi_ok); + REQUIRE(calls == 2); + } + } + + SECTION("wraps async cleanup hook handles") { + // Setup two hosts + static size_t calls = 0; + auto host_foo = std::shared_ptr(new NodeApiHost{ + .napi_create_object = [](napi_env env, + napi_value *result) -> napi_status { + calls++; + return napi_status::napi_ok; + }, + .napi_add_async_cleanup_hook = + [](node_api_basic_env env, napi_async_cleanup_hook hook, void *arg, + napi_async_cleanup_hook_handle *remove_handle) -> napi_status { + calls++; + (*remove_handle) = {}; + return napi_status::napi_ok; + }, + .napi_remove_async_cleanup_hook = + [](napi_async_cleanup_hook_handle remove_handle) -> napi_status { + calls++; + return napi_status::napi_ok; + }}); + + // Create and inject a multi host and wrap two envs + NodeApiMultiHost multi_host{nullptr, nullptr}; + inject_weak_node_api_host(multi_host); + + auto foo_env = multi_host.wrap(napi_env{}, host_foo); + + { + napi_async_cleanup_hook_handle result; + + REQUIRE(calls == 0); + + REQUIRE(napi_add_async_cleanup_hook(foo_env, nullptr, nullptr, &result) == + napi_ok); + REQUIRE(calls == 1); + + REQUIRE(napi_remove_async_cleanup_hook(result) == napi_ok); + REQUIRE(calls == 2); + } + } +}