diff --git a/.changeset/slimy-parts-admire.md b/.changeset/slimy-parts-admire.md new file mode 100644 index 00000000..de6b0ed2 --- /dev/null +++ b/.changeset/slimy-parts-admire.md @@ -0,0 +1,5 @@ +--- +"weak-node-api": minor +--- + +Renamed WeakNodeApiHost to NodeApiHost diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 17cfdac4..d2f613be 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -97,7 +97,7 @@ jobs: - run: npm ci - run: npm run build - name: Prepare weak-node-api - run: npm run prepare-weak-node-api --workspace weak-node-api + run: npm run prebuild:prepare --workspace weak-node-api - name: Build and run weak-node-api C++ tests run: | cmake -S . -B build -DBUILD_TESTS=ON @@ -213,7 +213,7 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: Build weak-node-api for all Android architectures - run: npm run build-weak-node-api:android --workspace weak-node-api + run: npm run prebuild:build:android --workspace weak-node-api - name: Build ferric-example for all architectures run: npm run build -- --android working-directory: packages/ferric-example @@ -268,8 +268,8 @@ jobs: - run: npm run build - name: Build weak-node-api for all Apple architectures run: | - npm run prepare-weak-node-api --workspace weak-node-api - npm run build-weak-node-api:apple --workspace weak-node-api + npm run prebuild:prepare --workspace weak-node-api + npm run prebuild:build:apple --workspace weak-node-api # Build Ferric example for all Apple architectures - run: npx ferric --apple working-directory: packages/ferric-example diff --git a/package.json b/package.json index 7c4400dc..72dab66e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "prettier:write": "prettier --experimental-cli --write .", "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", - "prerelease": "node --run build && npm run prerelease --workspaces --if-present", "changeset": "changeset", "release": "changeset publish", "init-macos-test-app": "node scripts/init-macos-test-app.ts" diff --git a/packages/host/package.json b/packages/host/package.json index 77b51b50..8bc1444e 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -42,11 +42,10 @@ ], "scripts": { "build": "tsc --build", - "generate-weak-node-api-injector": "node scripts/generate-weak-node-api-injector.mts", + "injector:generate": "node scripts/generate-injector.mts", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", - "bootstrap": "node --run generate-weak-node-api-injector", - "prerelease": "node --run generate-weak-node-api-injector" + "bootstrap": "node --run injector:generate" }, "keywords": [ "node-api", diff --git a/packages/host/scripts/generate-weak-node-api-injector.mts b/packages/host/scripts/generate-injector.mts similarity index 96% rename from packages/host/scripts/generate-weak-node-api-injector.mts rename to packages/host/scripts/generate-injector.mts index 71acfe86..de5ca275 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.mts +++ b/packages/host/scripts/generate-injector.mts @@ -59,8 +59,8 @@ export function generateSource(functions: FunctionDecl[]) { abort(); } - log_debug("Injecting WeakNodeApiHost"); - inject_weak_node_api_host(WeakNodeApiHost { + log_debug("Injecting NodeApiHost"); + inject_weak_node_api_host(NodeApiHost { ${functions .filter( ({ kind, name }) => 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..bea21bd7 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -13,9 +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/package.json b/packages/weak-node-api/package.json index 84a510f3..2d2d0f5c 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -26,18 +26,17 @@ "scripts": { "build": "tsc --build", "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", - "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", - "prepare-weak-node-api": "node --run copy-node-api-headers && node --run generate-weak-node-api", - "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", - "build-weak-node-api:android": "node --run build-weak-node-api -- --android", - "build-weak-node-api:apple": "node --run build-weak-node-api -- --apple", - "build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple", + "generate": "tsx scripts/generate.ts", + "prebuild:prepare": "node --run copy-node-api-headers && node --run generate", + "prebuild:build": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", + "prebuild:build:android": "node --run prebuild:build -- --android", + "prebuild:build:apple": "node --run prebuild:build -- --apple", + "prebuild:build:all": "node --run prebuild:build -- --android --apple", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:configure": "cmake -S . -B build-tests -DBUILD_TESTS=ON", "test:build": "cmake --build build-tests", "test:run": "ctest --test-dir build-tests --output-on-failure", - "bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api", - "prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all" + "bootstrap": "node --run prebuild:prepare && node --run prebuild:build" }, "keywords": [ "react-native", diff --git a/packages/weak-node-api/scripts/generate-weak-node-api.ts b/packages/weak-node-api/scripts/generate-weak-node-api.ts deleted file mode 100644 index 7b99d472..00000000 --- a/packages/weak-node-api/scripts/generate-weak-node-api.ts +++ /dev/null @@ -1,59 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; -import cp from "node:child_process"; - -import { - FunctionDecl, - getNodeApiFunctions, -} from "../src/node-api-functions.js"; - -import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; - -export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); - -type GenerateFileOptions = { - functions: FunctionDecl[]; - fileName: string; - generator: (functions: FunctionDecl[]) => string; -}; - -async function generateFile({ - functions, - fileName, - generator, -}: GenerateFileOptions) { - const generated = generator(functions); - const output = `// This file is generated - don't edit it directly\n\n${generated}`; - const outputPath = path.join(OUTPUT_PATH, fileName); - await fs.promises.writeFile(outputPath, output, "utf-8"); - const { status, stderr = "No error output" } = cp.spawnSync( - "clang-format", - ["-i", outputPath], - { - encoding: "utf8", - }, - ); - assert.equal(status, 0, `Failed to format ${fileName}: ${stderr}`); -} - -async function run() { - await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); - - const functions = getNodeApiFunctions(); - await generateFile({ - functions, - fileName: "weak_node_api.hpp", - generator: weakNodeApiGenerator.generateHeader, - }); - await generateFile({ - functions, - fileName: "weak_node_api.cpp", - generator: weakNodeApiGenerator.generateSource, - }); -} - -run().catch((err) => { - console.error(err); - process.exitCode = 1; -}); diff --git a/packages/weak-node-api/scripts/generate.ts b/packages/weak-node-api/scripts/generate.ts new file mode 100644 index 00000000..3691e0b3 --- /dev/null +++ b/packages/weak-node-api/scripts/generate.ts @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import cp from "node:child_process"; + +import { + FunctionDecl, + getNodeApiFunctions, +} from "../src/node-api-functions.js"; + +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"); + +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 = ` + /** + * @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.trim(), "utf-8"); + const { status, stderr = "No error output" } = cp.spawnSync( + "clang-format", + ["-i", outputPath], + { + encoding: "utf8", + }, + ); + assert.equal(status, 0, `Failed to format ${fileName}: ${stderr}`); +} + +async function run() { + await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); + + const functions = getNodeApiFunctions(); + await generateFile({ + functions, + fileName: "NodeApiHost.hpp", + generator: hostGenerator.generateHeader, + headingComment: ` + @brief NodeApiHost struct. + + This header provides a struct of Node-API functions implemented by a host to inject its implementations. + `, + }); + await generateFile({ + 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: "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) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/packages/weak-node-api/scripts/generators/NodeApiHost.ts b/packages/weak-node-api/scripts/generators/NodeApiHost.ts new file mode 100644 index 00000000..c273cfe7 --- /dev/null +++ b/packages/weak-node-api/scripts/generators/NodeApiHost.ts @@ -0,0 +1,32 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; + +export function generateFunctionDecl({ + returnType, + name, + argumentTypes, +}: FunctionDecl) { + return `${returnType} (*${name})(${argumentTypes.join(", ")});`; +} + +export function generateHeader(functions: FunctionDecl[]) { + return ` + #pragma once + + #include + + // Ideally we would have just used NAPI_NO_RETURN, but + // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct + // TODO: If we targeted C++23 we could use std::unreachable() + + #if defined(__GNUC__) + #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() + #else + #define WEAK_NODE_API_UNREACHABLE __assume(0) + #endif + + // Generate the struct of function pointers + struct NodeApiHost { + ${functions.map(generateFunctionDecl).join("\n")} + }; + `; +} 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/scripts/generators/weak-node-api.ts b/packages/weak-node-api/scripts/generators/weak-node-api.ts index 964b3d73..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,41 +1,19 @@ import type { FunctionDecl } from "../../src/node-api-functions.js"; import { generateFunction } from "./shared.js"; -export function generateFunctionDecl({ - returnType, - name, - argumentTypes, -}: FunctionDecl) { - return `${returnType} (*${name})(${argumentTypes.join(", ")});`; -} -/** - * Generates source code for a version script for the given Node API version. - */ -export function generateHeader(functions: FunctionDecl[]) { +export function generateHeader() { return ` #pragma once - #include // Node-API + #include #include // fprintf() #include // abort() - - // Ideally we would have just used NAPI_NO_RETURN, but - // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct - // TODO: If we targeted C++23 we could use std::unreachable() - - #if defined(__GNUC__) - #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() - #else - #define WEAK_NODE_API_UNREACHABLE __assume(0) - #endif - // Generate the struct of function pointers - struct WeakNodeApiHost { - ${functions.map(generateFunctionDecl).join("\n")} - }; - typedef void(*InjectHostFunction)(const WeakNodeApiHost&); - extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host); + #include "NodeApiHost.hpp" + + typedef void(*InjectHostFunction)(const NodeApiHost&); + extern "C" void inject_weak_node_api_host(const NodeApiHost& host); `; } @@ -56,15 +34,19 @@ 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" - WeakNodeApiHost g_host; - void inject_weak_node_api_host(const WeakNodeApiHost& host) { + /** + * @brief Global instance of the injected Node-API host. + * + * This variable holds the function table for Node-API calls. + * It is set via inject_weak_node_api_host() before any Node-API function is dispatched. + * All Node-API calls are routed through this host. + */ + NodeApiHost g_host; + void inject_weak_node_api_host(const NodeApiHost& host) { g_host = host; }; 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", 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_inject.cpp b/packages/weak-node-api/tests/test_inject.cpp index e2101c8c..5b35f15e 100644 --- a/packages/weak-node-api/tests/test_inject.cpp +++ b/packages/weak-node-api/tests/test_inject.cpp @@ -3,7 +3,7 @@ TEST_CASE("inject_weak_node_api_host") { SECTION("is callable") { - WeakNodeApiHost host{}; + NodeApiHost host{}; inject_weak_node_api_host(host); } @@ -14,7 +14,7 @@ TEST_CASE("inject_weak_node_api_host") { called = true; return napi_status::napi_ok; }; - WeakNodeApiHost host{.napi_create_object = my_create_object}; + NodeApiHost host{.napi_create_object = my_create_object}; inject_weak_node_api_host(host); napi_value result; 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); + } + } +}