diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f24d9c04..ecff59bd 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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 + - 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) diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore index 30ea0716..5cc9e939 100644 --- a/packages/weak-node-api/.gitignore +++ b/packages/weak-node-api/.gitignore @@ -9,3 +9,6 @@ # Copied from node-api-headers by scripts/copy-node-api-headers.ts /include/ + +# Clang cache +/.cache/ diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt index e53c71cd..d61630f2 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -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 $<$:/W4 /WX> $<$>:-Wall -Wextra -Werror> ) + +option(BUILD_TESTS "Build the tests" OFF) +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() 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 b9a8736c..b47aed27 100644 --- a/packages/weak-node-api/scripts/generate-weak-node-api.ts +++ b/packages/weak-node-api/scripts/generate-weak-node-api.ts @@ -18,13 +18,24 @@ export function generateHeader(functions: FunctionDecl[]) { "#include ", // Node-API "#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( - ({ 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&);", @@ -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" : "", "};", - ]; + ].join(" "); }), ].join("\n"); } diff --git a/packages/weak-node-api/tests/CMakeLists.txt b/packages/weak-node-api/tests/CMakeLists.txt new file mode 100644 index 00000000..89b19f84 --- /dev/null +++ b/packages/weak-node-api/tests/CMakeLists.txt @@ -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) diff --git a/packages/weak-node-api/tests/test_inject.cpp b/packages/weak-node-api/tests/test_inject.cpp new file mode 100644 index 00000000..e2101c8c --- /dev/null +++ b/packages/weak-node-api/tests/test_inject.cpp @@ -0,0 +1,25 @@ +#include +#include + +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); + } +}