diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore index 652a5f16..83e62995 100644 --- a/packages/weak-node-api/.gitignore +++ b/packages/weak-node-api/.gitignore @@ -5,8 +5,7 @@ /build-tests/ /*.xcframework /*.android.node -/generated/weak_node_api.cpp -/generated/weak_node_api.hpp +/generated/ # Copied from node-api-headers by scripts/copy-node-api-headers.ts /include/ diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt index d61630f2..4449a299 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -13,9 +13,11 @@ set(GENERATED_SOURCE_DIR "generated") target_sources(${PROJECT_NAME} PUBLIC ${GENERATED_SOURCE_DIR}/weak_node_api.cpp + ${GENERATED_SOURCE_DIR}/weak_node_api_multi_host.cpp PUBLIC FILE_SET HEADERS BASE_DIRS ${GENERATED_SOURCE_DIR} ${INCLUDE_DIR} FILES ${GENERATED_SOURCE_DIR}/weak_node_api.hpp + ${GENERATED_SOURCE_DIR}/weak_node_api_multi_host.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-weak-node-api.ts b/packages/weak-node-api/scripts/generate-weak-node-api.ts index 7b99d472..e05c13e0 100644 --- a/packages/weak-node-api/scripts/generate-weak-node-api.ts +++ b/packages/weak-node-api/scripts/generate-weak-node-api.ts @@ -9,6 +9,7 @@ import { } from "../src/node-api-functions.js"; import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; +import * as multiHostGenerator from "./generators/multi-host.js"; export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); @@ -16,15 +17,30 @@ type GenerateFileOptions = { functions: FunctionDecl[]; fileName: string; generator: (functions: FunctionDecl[]) => string; + headingComment?: string; }; async function generateFile({ functions, fileName, generator, + headingComment = "", }: GenerateFileOptions) { const generated = generator(functions); - const output = `// This file is generated - don't edit it directly\n\n${generated}`; + const output = ` + /** + * @file ${fileName} + * ${headingComment + .trim() + .split("\n") + .map((l) => l.trim()) + .join("\n* ")} + * + * @note This file is generated - don't edit it directly + */ + + ${generated} + `; const outputPath = path.join(OUTPUT_PATH, fileName); await fs.promises.writeFile(outputPath, output, "utf-8"); const { status, stderr = "No error output" } = cp.spawnSync( @@ -45,11 +61,41 @@ async function run() { functions, fileName: "weak_node_api.hpp", generator: weakNodeApiGenerator.generateHeader, + headingComment: ` + @brief Weak Node-API host injection interface. + + This header provides the struct and injection function for deferring Node-API function calls from addons into a Node-API host. + `, }); await generateFile({ functions, fileName: "weak_node_api.cpp", generator: weakNodeApiGenerator.generateSource, + headingComment: ` + @brief Weak Node-API host injection implementation. + + Provides the implementation for deferring Node-API function calls from addons into a Node-API host. + `, + }); + await generateFile({ + functions, + fileName: "weak_node_api_multi_host.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: "weak_node_api_multi_host.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. + `, }); } diff --git a/packages/weak-node-api/scripts/generators/multi-host.ts b/packages/weak-node-api/scripts/generators/multi-host.ts new file mode 100644 index 00000000..f8ef0b46 --- /dev/null +++ b/packages/weak-node-api/scripts/generators/multi-host.ts @@ -0,0 +1,227 @@ +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 }); +} + +/** + * Generates source code for a version script for the given Node API version. + */ +export function generateHeader(functions: FunctionDecl[]) { + return ` + #pragma once + + #include + #include + #include + #include + + #include + #include "weak_node_api.hpp" + + struct WeakNodeApiMultiHost : WeakNodeApiHost { + template struct Wrapped { + static_assert(std::is_same::value || + std::is_same::value || + std::is_same::value, + "T must be either napi_env, node_api_basic_env, napi_threadsafe_function or napi_async_cleanup_hook_handle"); + T value; + std::weak_ptr host; + WeakNodeApiMultiHost *multi_host; + }; + + napi_env wrap(napi_env value, std::weak_ptr); + napi_threadsafe_function wrap(napi_threadsafe_function value, std::weak_ptr); + napi_async_cleanup_hook_handle wrap(napi_async_cleanup_hook_handle value, std::weak_ptr); + + WeakNodeApiMultiHost( + void napi_module_register(napi_module *), + void napi_fatal_error(const char *, size_t, const char *, size_t) + ); + + private: + std::vector>, std::unique_ptr>, std::unique_ptr>>> wrapped_values; + + 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: "WeakNodeApiMultiHost", + 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: "WeakNodeApiMultiHost", + argumentNames: [""], + body: ` + fprintf(stderr, "napi_module_register is not implemented for this WeakNodeApiMultiHost"); + abort(); + `, + }); + } else if ( + [ + "napi_env", + "node_api_basic_env", + // TODO: Wrap these on creation + "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->multi_host->wrap(*${argumentNames[10]}, 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->multi_host->wrap(*${argumentNames[3]}, wrapped->host); + } + return status; + `; + } else { + return ` + ${returnType === "void" ? "" : "return"} host->${name}(${joinedArguments}); + `; + } + } + + return generateFunction({ + ...fn, + namespace: "WeakNodeApiMultiHost", + argumentNames, + body: ` + auto wrapped = reinterpret_cast*>(${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`); + } +} + +/** + * Generates source code for a version script for the given Node API version. + */ +export function generateSource(functions: FunctionDecl[]) { + return ` + #include "weak_node_api_multi_host.hpp" + + WeakNodeApiMultiHost::WeakNodeApiMultiHost( + void napi_module_register(napi_module *), + void napi_fatal_error(const char *, size_t, const char *, size_t) + ) + : WeakNodeApiHost({ + ${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 ? WeakNodeApiMultiHost::${name} : ${name},`; + } else { + return `.${name} = WeakNodeApiMultiHost::${name},`; + } + }) + .join("\n")} + }), wrapped_values{} {}; + + // TODO: Find a better way to delete these along the way + + napi_env WeakNodeApiMultiHost::wrap(napi_env value, + std::weak_ptr host) { + auto ptr = std::make_unique>(value, host, this); + auto raw_ptr = ptr.get(); + wrapped_values.push_back(std::move(ptr)); + return reinterpret_cast(raw_ptr); + } + + napi_threadsafe_function + WeakNodeApiMultiHost::wrap(napi_threadsafe_function value, + std::weak_ptr host) { + auto ptr = std::make_unique>(value, host, this); + auto raw_ptr = ptr.get(); + wrapped_values.push_back(std::move(ptr)); + return reinterpret_cast(raw_ptr); + } + + napi_async_cleanup_hook_handle + WeakNodeApiMultiHost::wrap(napi_async_cleanup_hook_handle value, + std::weak_ptr host) { + auto ptr = std::make_unique>(value, host, this); + auto raw_ptr = ptr.get(); + wrapped_values.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..f660ba8f --- /dev/null +++ b/packages/weak-node-api/tests/test_multi_host.cpp @@ -0,0 +1,171 @@ +#include "js_native_api_types.h" +#include "node_api.h" +#include "node_api_types.h" +#include +#include +#include +#include +#include + +TEST_CASE("WeakNodeApiMultiHost") { + SECTION("is injectable") { + WeakNodeApiMultiHost 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 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(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); + } + + SECTION("handles multi-host resetting") { + // Setup two hosts + static size_t called = 0; + auto host = std::shared_ptr(new WeakNodeApiHost{ + .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 + WeakNodeApiMultiHost 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 WeakNodeApiHost{ + .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 + WeakNodeApiMultiHost 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 WeakNodeApiHost{ + .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 + WeakNodeApiMultiHost 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); + } + } +}