Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,32 @@ jobs:
- run: npm ci
- run: npm run bootstrap
- run: npm test
weak-node-api-tests:
if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'weak-node-api')
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- windows-latest
- macos-latest
runs-on: ${{ matrix.runner }}
name: Weak Node-API tests (${{ matrix.runner }})
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/jod
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'jod' to 'jodium'.

Copilot uses AI. Check for mistakes.
- run: npm ci
- run: npm run build
- name: Prepare weak-node-api
run: npm run prepare-weak-node-api --workspace weak-node-api
- name: Build and run weak-node-api C++ tests
run: |
cmake -S . -B build -DBUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure
working-directory: packages/weak-node-api
test-ios:
if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Apple 🍎')
name: Test app (iOS)
Expand Down
3 changes: 3 additions & 0 deletions packages/weak-node-api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@

# Copied from node-api-headers by scripts/copy-node-api-headers.ts
/include/

# Clang cache
/.cache/
9 changes: 8 additions & 1 deletion packages/weak-node-api/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,17 @@ if(APPLE)
)
endif()

target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
# C++20 is needed to use designated initializers
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20)
target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8)

target_compile_options(${PROJECT_NAME} PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Werror>
)

option(BUILD_TESTS "Build the tests" OFF)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
48 changes: 30 additions & 18 deletions packages/weak-node-api/scripts/generate-weak-node-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,24 @@ export function generateHeader(functions: FunctionDecl[]) {
"#include <node_api.h>", // Node-API
"#include <stdio.h>", // fprintf()
"#include <stdlib.h>", // 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(
({ returnType, noReturn, name, argumentTypes }) =>
`${returnType} ${
noReturn ? " __attribute__((noreturn))" : ""
}(*${name})(${argumentTypes.join(", ")});`,
...functions.map(({ returnType, name, argumentTypes }) =>
[
returnType,
// Signature
`(*${name})(${argumentTypes.join(", ")});`,
].join(" "),
),
"};",
"typedef void(*InjectHostFunction)(const WeakNodeApiHost&);",
Expand All @@ -46,25 +57,26 @@ export function generateSource(functions: FunctionDecl[]) {
"};",
``,
// Generate function calling into the host
...functions.flatMap(({ returnType, noReturn, name, argumentTypes }) => {
...functions.flatMap(({ returnType, name, argumentTypes, noReturn }) => {
return [
`extern "C" ${returnType} ${
noReturn ? " __attribute__((noreturn))" : ""
}${name}(${argumentTypes
.map((type, index) => `${type} arg${index}`)
.join(", ")}) {`,
'extern "C"',
returnType,
name,
"(",
argumentTypes.map((type, index) => `${type} arg${index}`).join(", "),
") {",
`if (g_host.${name} == nullptr) {`,
` fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");`,
" abort();",
"}",
(returnType === "void" ? "" : "return ") +
"g_host." +
name +
"(" +
argumentTypes.map((_, index) => `arg${index}`).join(", ") +
");",
returnType === "void" ? "" : "return ",
`g_host.${name}`,
"(",
argumentTypes.map((_, index) => `arg${index}`).join(", "),
");",
noReturn ? "WEAK_NODE_API_UNREACHABLE" : "",
"};",
Comment on lines +72 to 78
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated code will produce invalid C++ syntax. When noReturn is true, the unreachable macro appears after the semicolon and before the closing brace, creating ); WEAK_NODE_API_UNREACHABLE };. The macro should be placed before the semicolon or on its own line. Consider using ].filter(Boolean).join(" ") and including the semicolon with the return statement, or conditionally building the return line.

Copilot uses AI. Check for mistakes.
];
].join(" ");
}),
].join("\n");
}
Expand Down
27 changes: 27 additions & 0 deletions packages/weak-node-api/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Include(FetchContent)

FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.11.0
)

FetchContent_MakeAvailable(Catch2)

add_executable(weak-node-api-tests
test_inject.cpp
)
target_link_libraries(weak-node-api-tests
PRIVATE
weak-node-api
Catch2::Catch2WithMain
)

target_compile_features(weak-node-api-tests PRIVATE cxx_std_20)
target_compile_definitions(weak-node-api-tests PRIVATE NAPI_VERSION=8)

# As per https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#catchcmake-and-catchaddtestscmake
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
include(CTest)
include(Catch)
catch_discover_tests(weak-node-api-tests)
25 changes: 25 additions & 0 deletions packages/weak-node-api/tests/test_inject.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include <catch2/catch_test_macros.hpp>
#include <weak_node_api.hpp>

TEST_CASE("inject_weak_node_api_host") {
SECTION("is callable") {
WeakNodeApiHost host{};
inject_weak_node_api_host(host);
}

SECTION("propagates calls to napi_create_object") {
static bool called = false;
auto my_create_object = [](napi_env env,
napi_value *result) -> napi_status {
called = true;
return napi_status::napi_ok;
};
WeakNodeApiHost host{.napi_create_object = my_create_object};
inject_weak_node_api_host(host);

napi_value result;
napi_create_object({}, &result);

REQUIRE(called);
}
}
Loading