From f5a78486fb06ff4b08ebb7aee999030eeb4d63c6 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:20:36 +0200 Subject: [PATCH 01/19] Added RuntimeNodeApiAsync class --- packages/host/android/CMakeLists.txt | 2 + packages/host/cpp/RuntimeNodeApiAsync.cpp | 183 ++++++++++++++++++++++ packages/host/cpp/RuntimeNodeApiAsync.hpp | 26 +++ 3 files changed, 211 insertions(+) create mode 100644 packages/host/cpp/RuntimeNodeApiAsync.cpp create mode 100644 packages/host/cpp/RuntimeNodeApiAsync.hpp diff --git a/packages/host/android/CMakeLists.txt b/packages/host/android/CMakeLists.txt index 9fd1d22c..d062762a 100644 --- a/packages/host/android/CMakeLists.txt +++ b/packages/host/android/CMakeLists.txt @@ -22,6 +22,8 @@ add_library(node-api-host SHARED ../cpp/WeakNodeApiInjector.cpp ../cpp/RuntimeNodeApi.cpp ../cpp/RuntimeNodeApi.hpp + ../cpp/RuntimeNodeApiAsync.cpp + ../cpp/RuntimeNodeApiAsync.hpp ) target_include_directories(node-api-host PRIVATE diff --git a/packages/host/cpp/RuntimeNodeApiAsync.cpp b/packages/host/cpp/RuntimeNodeApiAsync.cpp new file mode 100644 index 00000000..f4e0f47a --- /dev/null +++ b/packages/host/cpp/RuntimeNodeApiAsync.cpp @@ -0,0 +1,183 @@ +#include "RuntimeNodeApiAsync.hpp" +#include +#include "Logger.hpp" + +struct AsyncJob { + using IdType = uint64_t; + enum State { Created, Queued, Completed, Cancelled, Deleted }; + + IdType id{}; + State state{}; + napi_env env; + napi_value async_resource; + napi_value async_resource_name; + napi_async_execute_callback execute; + napi_async_complete_callback complete; + void* data{nullptr}; + + static AsyncJob* fromWork(napi_async_work work) { + return reinterpret_cast(work); + } + static napi_async_work toWork(AsyncJob* job) { + return reinterpret_cast(job); + } +}; + +class AsyncWorkRegistry { + public: + using IdType = AsyncJob::IdType; + + std::shared_ptr create(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data) { + const auto job = std::shared_ptr(new AsyncJob{ + .id = next_id(), + .state = AsyncJob::State::Created, + .env = env, + .async_resource = async_resource, + .async_resource_name = async_resource_name, + .execute = execute, + .complete = complete, + .data = data, + }); + + jobs_[job->id] = job; + return job; + } + + std::shared_ptr get(napi_async_work work) const { + const auto job = AsyncJob::fromWork(work); + if (!job) { + return {}; + } + if (const auto it = jobs_.find(job->id); it != jobs_.end()) { + return it->second; + } + return {}; + } + + bool release(IdType id) { + if (const auto it = jobs_.find(id); it != jobs_.end()) { + it->second->state = AsyncJob::State::Deleted; + jobs_.erase(it); + return true; + } + return false; + } + + private: + IdType next_id() { + if (current_id_ == std::numeric_limits::max()) [[unlikely]] { + current_id_ = 0; + } + return ++current_id_; + } + + IdType current_id_{0}; + std::unordered_map> jobs_; +}; + +static std::weak_ptr callInvoker; +static AsyncWorkRegistry asyncWorkRegistry; + +namespace callstack::nodeapihost { + +void setCallInvoker( + const std::shared_ptr& invoker) { + callInvoker = invoker; +} + +napi_status napi_create_async_work(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result) { + const auto job = asyncWorkRegistry.create( + env, async_resource, async_resource_name, execute, complete, data); + if (!job) { + log_debug("Error: Failed to create async work job"); + return napi_generic_failure; + } + + *result = AsyncJob::toWork(job.get()); + return napi_ok; +} + +napi_status napi_queue_async_work( + node_api_basic_env env, napi_async_work work) { + const auto job = asyncWorkRegistry.get(work); + if (!job) { + log_debug("Error: Received null job in napi_queue_async_work"); + return napi_invalid_arg; + } + + const auto invoker = callInvoker.lock(); + if (!invoker) { + log_debug("Error: No CallInvoker available for async work"); + return napi_invalid_arg; + } + + invoker->invokeAsync([env, weakJob = std::weak_ptr{job}]() { + const auto job = weakJob.lock(); + if (!job) { + log_debug("Error: Async job has been deleted before execution"); + return; + } + if (job->state == AsyncJob::State::Queued) { + job->execute(job->env, job->data); + } + + job->complete(env, + job->state == AsyncJob::State::Cancelled ? napi_cancelled : napi_ok, + job->data); + job->state = AsyncJob::State::Completed; + }); + + job->state = AsyncJob::State::Queued; + return napi_ok; +} + +napi_status napi_delete_async_work( + node_api_basic_env env, napi_async_work work) { + const auto job = asyncWorkRegistry.get(work); + if (!job) { + log_debug("Error: Received non-existent job in napi_delete_async_work"); + return napi_invalid_arg; + } + + if (!asyncWorkRegistry.release(job->id)) { + log_debug("Error: Failed to release async work job"); + return napi_generic_failure; + } + + return napi_ok; +} + +napi_status napi_cancel_async_work( + node_api_basic_env env, napi_async_work work) { + const auto job = asyncWorkRegistry.get(work); + if (!job) { + log_debug("Error: Received null job in napi_cancel_async_work"); + return napi_invalid_arg; + } + switch (job->state) { + case AsyncJob::State::Completed: + log_debug("Error: Cannot cancel async work that is already completed"); + return napi_generic_failure; + case AsyncJob::State::Deleted: + log_debug("Warning: Async work job is already deleted"); + return napi_generic_failure; + case AsyncJob::State::Cancelled: + log_debug("Warning: Async work job is already cancelled"); + return napi_ok; + } + + job->state = AsyncJob::State::Cancelled; + return napi_ok; +} +} // namespace callstack::nodeapihost diff --git a/packages/host/cpp/RuntimeNodeApiAsync.hpp b/packages/host/cpp/RuntimeNodeApiAsync.hpp new file mode 100644 index 00000000..11fe62ad --- /dev/null +++ b/packages/host/cpp/RuntimeNodeApiAsync.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include "node_api.h" + +namespace callstack::nodeapihost { +void setCallInvoker( + const std::shared_ptr& invoker); + +napi_status napi_create_async_work(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result); + +napi_status napi_queue_async_work(node_api_basic_env env, napi_async_work work); + +napi_status napi_delete_async_work( + node_api_basic_env env, napi_async_work work); + +napi_status napi_cancel_async_work( + node_api_basic_env env, napi_async_work work); +} // namespace callstack::nodeapihost From e510ada7bb07468ef6ca00a87203e4968f9664bc Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:20:50 +0200 Subject: [PATCH 02/19] Refactor + fixed externel buffer --- packages/host/cpp/RuntimeNodeApi.cpp | 34 +++++++++++++++++++--------- packages/host/cpp/RuntimeNodeApi.hpp | 3 ++- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/host/cpp/RuntimeNodeApi.cpp b/packages/host/cpp/RuntimeNodeApi.cpp index dc661833..e86c18cd 100644 --- a/packages/host/cpp/RuntimeNodeApi.cpp +++ b/packages/host/cpp/RuntimeNodeApi.cpp @@ -3,11 +3,13 @@ auto ArrayType = napi_uint8_array; -napi_status NAPI_CDECL callstack::nodeapihost::napi_create_buffer( +namespace callstack::nodeapihost { + +napi_status napi_create_buffer( napi_env env, size_t length, void** data, napi_value* result) { napi_value buffer; - const auto status = napi_create_arraybuffer(env, length, data, &buffer); - if (status != napi_ok) { + if (const auto status = napi_create_arraybuffer(env, length, data, &buffer); + status != napi_ok) { return status; } @@ -18,8 +20,7 @@ napi_status NAPI_CDECL callstack::nodeapihost::napi_create_buffer( return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } -napi_status NAPI_CDECL callstack::nodeapihost::napi_create_buffer_copy( - napi_env env, +napi_status napi_create_buffer_copy(napi_env env, size_t length, const void* data, void** result_data, @@ -38,8 +39,7 @@ napi_status NAPI_CDECL callstack::nodeapihost::napi_create_buffer_copy( return napi_ok; } -napi_status callstack::nodeapihost::napi_is_buffer( - napi_env env, napi_value value, bool* result) { +napi_status napi_is_buffer(napi_env env, napi_value value, bool* result) { if (!result) { return napi_invalid_arg; } @@ -74,7 +74,7 @@ napi_status callstack::nodeapihost::napi_is_buffer( return napi_ok; } -napi_status callstack::nodeapihost::napi_get_buffer_info( +napi_status napi_get_buffer_info( napi_env env, napi_value value, void** data, size_t* length) { if (!data || !length) { return napi_invalid_arg; @@ -101,12 +101,24 @@ napi_status callstack::nodeapihost::napi_get_buffer_info( return napi_ok; } -napi_status callstack::nodeapihost::napi_create_external_buffer(napi_env env, +napi_status napi_create_external_buffer(napi_env env, size_t length, void* data, node_api_basic_finalize basic_finalize_cb, void* finalize_hint, napi_value* result) { - return napi_create_external_arraybuffer( - env, data, length, basic_finalize_cb, finalize_hint, result); + napi_value buffer; + if (const auto status = napi_create_external_arraybuffer( + env, data, length, basic_finalize_cb, finalize_hint, &buffer); + status != napi_ok) { + return status; + } + + // Warning: The returned data structure does not fully align with the + // characteristics of a Buffer. + // @see + // https://github.com/callstackincubator/react-native-node-api/issues/171 + return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } + +} // namespace callstack::nodeapihost \ No newline at end of file diff --git a/packages/host/cpp/RuntimeNodeApi.hpp b/packages/host/cpp/RuntimeNodeApi.hpp index e64b03fa..5b7d9b86 100644 --- a/packages/host/cpp/RuntimeNodeApi.hpp +++ b/packages/host/cpp/RuntimeNodeApi.hpp @@ -1,3 +1,5 @@ +#pragma once + #include "node_api.h" namespace callstack::nodeapihost { @@ -21,5 +23,4 @@ napi_status napi_create_external_buffer(napi_env env, node_api_basic_finalize basic_finalize_cb, void* finalize_hint, napi_value* result); - } // namespace callstack::nodeapihost From 6f0147370ebd2b06d274dff5150c8210a4966d1c Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:21:19 +0200 Subject: [PATCH 03/19] Passing callInvoker down --- packages/host/ios/NodeApiHostModuleProvider.mm | 2 ++ packages/host/scripts/generate-weak-node-api-injector.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/host/ios/NodeApiHostModuleProvider.mm b/packages/host/ios/NodeApiHostModuleProvider.mm index d4ecd94f..303b9112 100644 --- a/packages/host/ios/NodeApiHostModuleProvider.mm +++ b/packages/host/ios/NodeApiHostModuleProvider.mm @@ -1,5 +1,6 @@ #import "CxxNodeApiHostModule.hpp" #import "WeakNodeApiInjector.hpp" +#include "RuntimeNodeApiAsync.hpp" #define USE_CXX_TURBO_MODULE_UTILS 0 #if defined(__has_include) @@ -27,6 +28,7 @@ + (void)load { facebook::react::registerCxxModuleToGlobalModuleMap( callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, [](std::shared_ptr jsInvoker) { + callstack::nodeapihost::setCallInvoker(jsInvoker); return std::make_shared( jsInvoker); }); diff --git a/packages/host/scripts/generate-weak-node-api-injector.ts b/packages/host/scripts/generate-weak-node-api-injector.ts index 71e66893..b9900c97 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.ts +++ b/packages/host/scripts/generate-weak-node-api-injector.ts @@ -13,6 +13,10 @@ const IMPLEMENTED_RUNTIME_FUNCTIONS = [ "napi_is_buffer", "napi_get_buffer_info", "napi_create_external_buffer", + "napi_create_async_work", + "napi_queue_async_work", + "napi_delete_async_work", + "napi_cancel_async_work", ]; /** @@ -25,6 +29,7 @@ export function generateSource(functions: FunctionDecl[]) { #include #include #include + #include #if defined(__APPLE__) #define WEAK_NODE_API_LIBRARY_NAME "@rpath/weak-node-api.framework/weak-node-api" @@ -61,6 +66,9 @@ export function generateSource(functions: FunctionDecl[]) { .join("\n")} }); } + void initialize(const std::shared_ptr& invoker) { + callstack::nodeapihost::initialize(invoker); + } } // namespace callstack::nodeapihost `; } From 9f078c8139b9a006adf16706c4cd8ec771744d58 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:21:43 +0200 Subject: [PATCH 04/19] Added async addon test --- apps/test-app/App.tsx | 7 +- packages/node-addon-examples/index.js | 1 + .../tests/RuntimeNodeApiTestsCommon.h | 108 ++++++++ .../tests/async/CMakeLists.txt | 15 + .../node-addon-examples/tests/async/addon.c | 260 ++++++++++++++++++ .../node-addon-examples/tests/async/addon.js | 52 ++++ .../tests/async/binding.gyp | 8 + .../tests/async/package.json | 14 + 8 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h create mode 100644 packages/node-addon-examples/tests/async/CMakeLists.txt create mode 100644 packages/node-addon-examples/tests/async/addon.c create mode 100644 packages/node-addon-examples/tests/async/addon.js create mode 100644 packages/node-addon-examples/tests/async/binding.gyp create mode 100644 packages/node-addon-examples/tests/async/package.json diff --git a/apps/test-app/App.tsx b/apps/test-app/App.tsx index 48a11861..3ca4ddc2 100644 --- a/apps/test-app/App.tsx +++ b/apps/test-app/App.tsx @@ -14,8 +14,11 @@ function loadTests() { for (const [suiteName, examples] of Object.entries(nodeAddonExamples)) { describe(suiteName, () => { for (const [exampleName, requireExample] of Object.entries(examples)) { - it(exampleName, () => { - requireExample(); + it(exampleName, async () => { + const test = requireExample(); + if (test instanceof Function) { + await test(); + } }); } }); diff --git a/packages/node-addon-examples/index.js b/packages/node-addon-examples/index.js index 8d5faecf..e5bf5eeb 100644 --- a/packages/node-addon-examples/index.js +++ b/packages/node-addon-examples/index.js @@ -17,5 +17,6 @@ module.exports = { }, "tests": { "buffers": () => require("./tests/buffers/addon.js"), + "async": () => require("./tests/async/addon.js"), }, }; diff --git a/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h b/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h new file mode 100644 index 00000000..2330a5dc --- /dev/null +++ b/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h @@ -0,0 +1,108 @@ +#pragma once + +#define NODE_API_RETVAL_NOTHING // Intentionally blank #define + +#define GET_AND_THROW_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info* error_info; \ + napi_get_last_error_info((env), &error_info); \ + bool is_pending; \ + const char* err_message = error_info->error_message; \ + napi_is_exception_pending((env), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + const char* error_message = \ + err_message != NULL ? err_message : "empty error message"; \ + napi_throw_error((env), NULL, error_message); \ + } \ + } while (0) + +// The basic version of GET_AND_THROW_LAST_ERROR. We cannot access any +// exceptions and we cannot fail by way of JS exception, so we abort. +#define FATALLY_FAIL_WITH_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info* error_info; \ + napi_get_last_error_info((env), &error_info); \ + const char* err_message = error_info->error_message; \ + const char* error_message = \ + err_message != NULL ? err_message : "empty error message"; \ + fprintf(stderr, "%s\n", error_message); \ + abort(); \ + } while (0) + +#define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + napi_throw_error( \ + (env), NULL, "assertion (" #assertion ") failed: " message); \ + return ret_val; \ + } \ + } while (0) + +#define NODE_API_BASIC_ASSERT_BASE(assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + fprintf(stderr, "assertion (" #assertion ") failed: " message); \ + abort(); \ + return ret_val; \ + } \ + } while (0) + +// Returns NULL on failed assertion. +// This is meant to be used inside napi_callback methods. +#define NODE_API_ASSERT(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, NULL) + +// Returns empty on failed assertion. +// This is meant to be used inside functions with void return type. +#define NODE_API_ASSERT_RETURN_VOID(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, NODE_API_RETVAL_NOTHING) + +#define NODE_API_BASIC_ASSERT_RETURN_VOID(assertion, message) \ + NODE_API_BASIC_ASSERT_BASE(assertion, message, NODE_API_RETVAL_NOTHING) + +#define NODE_API_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + GET_AND_THROW_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + +#define NODE_API_BASIC_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + FATALLY_FAIL_WITH_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + +// Returns NULL if the_call doesn't return napi_ok. +#define NODE_API_CALL(env, the_call) NODE_API_CALL_BASE(env, the_call, NULL) + +// Returns empty if the_call doesn't return napi_ok. +#define NODE_API_CALL_RETURN_VOID(env, the_call) \ + NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + +#define NODE_API_BASIC_CALL_RETURN_VOID(env, the_call) \ + NODE_API_BASIC_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + +#define NODE_API_CHECK_STATUS(the_call) \ + do { \ + napi_status status = (the_call); \ + if (status != napi_ok) { \ + return status; \ + } \ + } while (0) + +#define NODE_API_ASSERT_STATUS(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, napi_generic_failure) + +#define DECLARE_NODE_API_PROPERTY(name, func) \ + {(name), NULL, (func), NULL, NULL, NULL, napi_default, NULL} + +#define DECLARE_NODE_API_GETTER(name, func) \ + {(name), NULL, NULL, (func), NULL, NULL, napi_default, NULL} + +#define DECLARE_NODE_API_PROPERTY_VALUE(name, value) \ + {(name), NULL, NULL, NULL, NULL, (value), napi_default, NULL} \ No newline at end of file diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt new file mode 100644 index 00000000..31d513c0 --- /dev/null +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.15) +project(tests-async) + +add_compile_definitions(NAPI_VERSION=8) + +add_library(addon SHARED addon.c ${CMAKE_JS_SRC}) +set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") +target_include_directories(addon PRIVATE ${CMAKE_JS_INC}) +target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB}) +target_compile_features(addon PRIVATE cxx_std_17) + +if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) + # Generate node.lib + execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) +endif() \ No newline at end of file diff --git a/packages/node-addon-examples/tests/async/addon.c b/packages/node-addon-examples/tests/async/addon.c new file mode 100644 index 00000000..54c611c9 --- /dev/null +++ b/packages/node-addon-examples/tests/async/addon.c @@ -0,0 +1,260 @@ +#include +#include +#include +#include +#include +#include "../RuntimeNodeApiTestsCommon.h" + +#ifdef WIN32 +#include +#elif _POSIX_C_SOURCE >= 199309L +#include // for nanosleep +#else +#include // for usleep +#endif + +void sleep_ms(int milliseconds) { // cross-platform sleep function +#ifdef WIN32 + Sleep(milliseconds); +#elif _POSIX_C_SOURCE >= 199309L + struct timespec ts; + ts.tv_sec = milliseconds / 1000; + ts.tv_nsec = (milliseconds % 1000) * 1000000; + nanosleep(&ts, NULL); +#else + if (milliseconds >= 1000) sleep(milliseconds / 1000); + usleep((milliseconds % 1000) * 1000); +#endif +} +#define MAX_CANCEL_THREADS 6 + +typedef struct { + int32_t _input; + int32_t _output; + napi_ref _callback; + napi_async_work _request; +} carrier; + +static carrier the_carrier; +static carrier async_carrier[MAX_CANCEL_THREADS]; + +static carrier the_carrier; +static void Execute(napi_env env, void* data) { + sleep_ms(10); + carrier* c = (carrier*)(data); + + assert(c == &the_carrier); + + c->_output = c->_input * 2; +} + +static void Complete(napi_env env, napi_status status, void* data) { + carrier* c = (carrier*)(data); + + if (c != &the_carrier) { + napi_throw_type_error(env, NULL, "Wrong data parameter to Complete."); + return; + } + + if (status != napi_ok && status != napi_cancelled) { + napi_throw_type_error(env, NULL, "Execute callback failed."); + return; + } + + napi_value argv[2]; + + NODE_API_CALL_RETURN_VOID(env, napi_get_null(env, &argv[0])); + NODE_API_CALL_RETURN_VOID(env, napi_create_int32(env, c->_output, &argv[1])); + napi_value callback; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, c->_callback, &callback)); + napi_value global; + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &global)); + napi_value result; + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, global, callback, 2, argv, &result)); + + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); +} + +static napi_value Test(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value argv[3]; + napi_value _this; + napi_value resource_name; + void* data; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, &_this, &data)); + NODE_API_ASSERT(env, argc >= 3, "Not enough arguments, expected 3."); + + napi_valuetype t; + NODE_API_CALL(env, napi_typeof(env, argv[0], &t)); + NODE_API_ASSERT( + env, t == napi_number, "Wrong first argument, integer expected."); + NODE_API_CALL(env, napi_typeof(env, argv[1], &t)); + NODE_API_ASSERT( + env, t == napi_object, "Wrong second argument, object expected."); + NODE_API_CALL(env, napi_typeof(env, argv[2], &t)); + NODE_API_ASSERT( + env, t == napi_function, "Wrong third argument, function expected."); + + the_carrier._output = 0; + + NODE_API_CALL(env, napi_get_value_int32(env, argv[0], &the_carrier._input)); + NODE_API_CALL( + env, napi_create_reference(env, argv[2], 1, &the_carrier._callback)); + + NODE_API_CALL(env, + napi_create_string_utf8( + env, "TestResource", NAPI_AUTO_LENGTH, &resource_name)); + NODE_API_CALL(env, + napi_create_async_work(env, + argv[1], + resource_name, + Execute, + Complete, + &the_carrier, + &the_carrier._request)); + NODE_API_CALL(env, napi_queue_async_work(env, the_carrier._request)); + + return NULL; +} + +static void BusyCancelComplete(napi_env env, napi_status status, void* data) { + carrier* c = (carrier*)(data); + NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); +} + +static void CancelComplete(napi_env env, napi_status status, void* data) { + carrier* c = (carrier*)(data); + + if (status == napi_cancelled) { + // ok we got the status we expected so make the callback to + // indicate the cancel succeeded. + napi_value callback; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, c->_callback, &callback)); + napi_value global; + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &global)); + napi_value result; + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, global, callback, 0, NULL, &result)); + } + + NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback)); +} + +static void CancelExecute(napi_env env, void* data) { + sleep_ms(100); +} + +static napi_value TestCancel(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_value _this; + napi_value resource_name; + void* data; + + NODE_API_CALL(env, + napi_create_string_utf8( + env, "TestResource", NAPI_AUTO_LENGTH, &resource_name)); + + // make sure the work we are going to cancel will not be + // able to start by using all the threads in the pool + for (int i = 1; i < MAX_CANCEL_THREADS; i++) { + NODE_API_CALL(env, + napi_create_async_work(env, + NULL, + resource_name, + CancelExecute, + BusyCancelComplete, + &async_carrier[i], + &async_carrier[i]._request)); + NODE_API_CALL(env, napi_queue_async_work(env, async_carrier[i]._request)); + } + + // now queue the work we are going to cancel and then cancel it. + // cancel will fail if the work has already started, but + // we have prevented it from starting by consuming all of the + // workers above. + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, &_this, &data)); + NODE_API_CALL(env, + napi_create_async_work(env, + NULL, + resource_name, + CancelExecute, + CancelComplete, + &async_carrier[0], + &async_carrier[0]._request)); + NODE_API_CALL( + env, napi_create_reference(env, argv[0], 1, &async_carrier[0]._callback)); + NODE_API_CALL(env, napi_queue_async_work(env, async_carrier[0]._request)); + NODE_API_CALL(env, napi_cancel_async_work(env, async_carrier[0]._request)); + return NULL; +} + +struct { + napi_ref ref; + napi_async_work work; +} repeated_work_info = {NULL, NULL}; + +static void RepeatedWorkerThread(napi_env env, void* data) {} + +static void RepeatedWorkComplete(napi_env env, napi_status status, void* data) { + napi_value cb, js_status; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, repeated_work_info.ref, &cb)); + NODE_API_CALL_RETURN_VOID( + env, napi_delete_async_work(env, repeated_work_info.work)); + NODE_API_CALL_RETURN_VOID( + env, napi_delete_reference(env, repeated_work_info.ref)); + repeated_work_info.work = NULL; + repeated_work_info.ref = NULL; + NODE_API_CALL_RETURN_VOID( + env, napi_create_uint32(env, (uint32_t)status, &js_status)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, cb, cb, 1, &js_status, NULL)); +} + +static napi_value DoRepeatedWork(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value cb, name; + NODE_API_ASSERT(env, + repeated_work_info.ref == NULL, + "Reference left over from previous work"); + NODE_API_ASSERT(env, + repeated_work_info.work == NULL, + "Work pointer left over from previous work"); + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &cb, NULL, NULL)); + NODE_API_CALL( + env, napi_create_reference(env, cb, 1, &repeated_work_info.ref)); + NODE_API_CALL(env, + napi_create_string_utf8(env, "Repeated Work", NAPI_AUTO_LENGTH, &name)); + NODE_API_CALL(env, + napi_create_async_work(env, + NULL, + name, + RepeatedWorkerThread, + RepeatedWorkComplete, + &repeated_work_info, + &repeated_work_info.work)); + NODE_API_CALL(env, napi_queue_async_work(env, repeated_work_info.work)); + return NULL; +} + +static napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("Test", Test), + DECLARE_NODE_API_PROPERTY("TestCancel", TestCancel), + DECLARE_NODE_API_PROPERTY("DoRepeatedWork", DoRepeatedWork), + }; + + NODE_API_CALL(env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) \ No newline at end of file diff --git a/packages/node-addon-examples/tests/async/addon.js b/packages/node-addon-examples/tests/async/addon.js new file mode 100644 index 00000000..cdb2987e --- /dev/null +++ b/packages/node-addon-examples/tests/async/addon.js @@ -0,0 +1,52 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable no-undef */ +const test_async = require("bindings")("addon.node"); +const assert = require("assert"); + +module.exports = async () => { + return new Promise((resolve, reject) => { + const test = () => + new Promise((resolve, reject) => { + test_async.Test(5, {}, (err, val) => { + if (err) { + reject(err); + return; + } + try { + assert.strictEqual(err, null); + assert.strictEqual(val, 10); + } catch (e) { + reject(e); + } + resolve(); + }); + }); + + const testCancel = () => + new Promise((resolve) => { + test_async.TestCancel(() => resolve()); + }); + + const doRepeatedWork = (count = 0) => + new Promise((resolve, reject) => { + const iterations = 100; + const workDone = (status) => { + try { + assert.strictEqual(status, 0); + } catch (e) { + reject(e); + } + if (++count < iterations) { + test_async.DoRepeatedWork(workDone); + } else { + resolve(); + } + }; + test_async.DoRepeatedWork(workDone); + }); + + Promise.all([test(), testCancel(), doRepeatedWork()]) + .then(() => resolve()) + .catch(reject); + }); +}; diff --git a/packages/node-addon-examples/tests/async/binding.gyp b/packages/node-addon-examples/tests/async/binding.gyp new file mode 100644 index 00000000..80f9fa87 --- /dev/null +++ b/packages/node-addon-examples/tests/async/binding.gyp @@ -0,0 +1,8 @@ +{ + "targets": [ + { + "target_name": "addon", + "sources": [ "addon.c" ] + } + ] +} diff --git a/packages/node-addon-examples/tests/async/package.json b/packages/node-addon-examples/tests/async/package.json new file mode 100644 index 00000000..c8016da0 --- /dev/null +++ b/packages/node-addon-examples/tests/async/package.json @@ -0,0 +1,14 @@ +{ + "name": "async-test", + "version": "0.0.0", + "description": "Tests of runtime async functions", + "main": "addon.js", + "private": true, + "dependencies": { + "bindings": "~1.5.0" + }, + "scripts": { + "test": "node addon.js" + }, + "gypfile": true +} From a0f9789a346e2c7b0de7643564875136c6f53879 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:21:57 +0200 Subject: [PATCH 05/19] Added assertions to buffer tests --- .../node-addon-examples/tests/buffers/addon.c | 108 +----------------- .../tests/buffers/addon.js | 23 ++-- 2 files changed, 16 insertions(+), 115 deletions(-) diff --git a/packages/node-addon-examples/tests/buffers/addon.c b/packages/node-addon-examples/tests/buffers/addon.c index 2f83f681..024fc0d3 100644 --- a/packages/node-addon-examples/tests/buffers/addon.c +++ b/packages/node-addon-examples/tests/buffers/addon.c @@ -2,113 +2,7 @@ #include #include #include - -#define NODE_API_RETVAL_NOTHING // Intentionally blank #define - -#define GET_AND_THROW_LAST_ERROR(env) \ - do { \ - const napi_extended_error_info* error_info; \ - napi_get_last_error_info((env), &error_info); \ - bool is_pending; \ - const char* err_message = error_info->error_message; \ - napi_is_exception_pending((env), &is_pending); \ - /* If an exception is already pending, don't rethrow it */ \ - if (!is_pending) { \ - const char* error_message = \ - err_message != NULL ? err_message : "empty error message"; \ - napi_throw_error((env), NULL, error_message); \ - } \ - } while (0) - -// The basic version of GET_AND_THROW_LAST_ERROR. We cannot access any -// exceptions and we cannot fail by way of JS exception, so we abort. -#define FATALLY_FAIL_WITH_LAST_ERROR(env) \ - do { \ - const napi_extended_error_info* error_info; \ - napi_get_last_error_info((env), &error_info); \ - const char* err_message = error_info->error_message; \ - const char* error_message = \ - err_message != NULL ? err_message : "empty error message"; \ - fprintf(stderr, "%s\n", error_message); \ - abort(); \ - } while (0) - -#define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \ - do { \ - if (!(assertion)) { \ - napi_throw_error( \ - (env), NULL, "assertion (" #assertion ") failed: " message); \ - return ret_val; \ - } \ - } while (0) - -#define NODE_API_BASIC_ASSERT_BASE(assertion, message, ret_val) \ - do { \ - if (!(assertion)) { \ - fprintf(stderr, "assertion (" #assertion ") failed: " message); \ - abort(); \ - return ret_val; \ - } \ - } while (0) - -// Returns NULL on failed assertion. -// This is meant to be used inside napi_callback methods. -#define NODE_API_ASSERT(env, assertion, message) \ - NODE_API_ASSERT_BASE(env, assertion, message, NULL) - -// Returns empty on failed assertion. -// This is meant to be used inside functions with void return type. -#define NODE_API_ASSERT_RETURN_VOID(env, assertion, message) \ - NODE_API_ASSERT_BASE(env, assertion, message, NODE_API_RETVAL_NOTHING) - -#define NODE_API_BASIC_ASSERT_RETURN_VOID(assertion, message) \ - NODE_API_BASIC_ASSERT_BASE(assertion, message, NODE_API_RETVAL_NOTHING) - -#define NODE_API_CALL_BASE(env, the_call, ret_val) \ - do { \ - if ((the_call) != napi_ok) { \ - GET_AND_THROW_LAST_ERROR((env)); \ - return ret_val; \ - } \ - } while (0) - -#define NODE_API_BASIC_CALL_BASE(env, the_call, ret_val) \ - do { \ - if ((the_call) != napi_ok) { \ - FATALLY_FAIL_WITH_LAST_ERROR((env)); \ - return ret_val; \ - } \ - } while (0) - -// Returns NULL if the_call doesn't return napi_ok. -#define NODE_API_CALL(env, the_call) NODE_API_CALL_BASE(env, the_call, NULL) - -// Returns empty if the_call doesn't return napi_ok. -#define NODE_API_CALL_RETURN_VOID(env, the_call) \ - NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) - -#define NODE_API_BASIC_CALL_RETURN_VOID(env, the_call) \ - NODE_API_BASIC_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) - -#define NODE_API_CHECK_STATUS(the_call) \ - do { \ - napi_status status = (the_call); \ - if (status != napi_ok) { \ - return status; \ - } \ - } while (0) - -#define NODE_API_ASSERT_STATUS(env, assertion, message) \ - NODE_API_ASSERT_BASE(env, assertion, message, napi_generic_failure) - -#define DECLARE_NODE_API_PROPERTY(name, func) \ - {(name), NULL, (func), NULL, NULL, NULL, napi_default, NULL} - -#define DECLARE_NODE_API_GETTER(name, func) \ - {(name), NULL, NULL, (func), NULL, NULL, napi_default, NULL} - -#define DECLARE_NODE_API_PROPERTY_VALUE(name, value) \ - {(name), NULL, NULL, NULL, NULL, (value), napi_default, NULL} +#include "../RuntimeNodeApiTestsCommon.h" static inline void add_returned_status(napi_env env, const char* key, diff --git a/packages/node-addon-examples/tests/buffers/addon.js b/packages/node-addon-examples/tests/buffers/addon.js index 2e73501f..3f279fe8 100644 --- a/packages/node-addon-examples/tests/buffers/addon.js +++ b/packages/node-addon-examples/tests/buffers/addon.js @@ -1,19 +1,26 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ const addon = require("bindings")("addon.node"); +const assert = require("assert"); const toLocaleString = (text) => { return text - .toLocaleString() + .toString() .split(",") .map((code) => String.fromCharCode(parseInt(code, 10))) .join(""); }; -console.log(addon.newBuffer().toLocaleString(), addon.theText); -console.log(toLocaleString(addon.newExternalBuffer()), addon.theText); -console.log(addon.copyBuffer(), addon.theText); -let buffer = addon.staticBuffer(); -console.log(addon.bufferHasInstance(buffer), true); -console.log(addon.bufferInfo(buffer), true); -addon.invalidObjectAsBuffer({}); +module.exports = async () => { + assert.strictEqual(toLocaleString(addon.newBuffer()), addon.theText); + assert.strictEqual(toLocaleString(addon.newExternalBuffer()), addon.theText); + assert.strictEqual(toLocaleString(addon.copyBuffer()), addon.theText); + let buffer = addon.staticBuffer(); + assert.strictEqual(addon.bufferHasInstance(buffer), true); + assert.strictEqual(addon.bufferInfo(buffer), true); + addon.invalidObjectAsBuffer({}); + + // TODO: Add gc tests + // @see + // https://github.com/callstackincubator/react-native-node-api/issues/182 +}; From 7dec11cae53dad011b2d80dfddbc010bc8b38a4b Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:25:13 +0200 Subject: [PATCH 06/19] New line added after file generation --- packages/node-addon-examples/tests/buffers/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index 7de25130..8e8cc950 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -12,4 +12,4 @@ target_compile_features(addon PRIVATE cxx_std_17) if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) # Generate node.lib execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) -endif() +endif() \ No newline at end of file From 0582b7c468ddb926a42d6bced7ba057fb32ca529 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:32:05 +0200 Subject: [PATCH 07/19] Added changeset --- .changeset/rich-donuts-mix.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/rich-donuts-mix.md diff --git a/.changeset/rich-donuts-mix.md b/.changeset/rich-donuts-mix.md new file mode 100644 index 00000000..964a352e --- /dev/null +++ b/.changeset/rich-donuts-mix.md @@ -0,0 +1,6 @@ +--- +"react-native-node-api-test-app": patch +"react-native-node-api": patch +--- + +Added implementation of async work runtime functions From 6cf32e900a3e65e92dc2d5eaa798256062545f1c Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:47:36 +0200 Subject: [PATCH 08/19] Added Android OnLoad --- packages/host/android/src/main/cpp/OnLoad.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/host/android/src/main/cpp/OnLoad.cpp b/packages/host/android/src/main/cpp/OnLoad.cpp index 35e27128..b645e763 100644 --- a/packages/host/android/src/main/cpp/OnLoad.cpp +++ b/packages/host/android/src/main/cpp/OnLoad.cpp @@ -3,15 +3,17 @@ #include #include +#include #include // Called when the library is loaded -jint JNI_OnLoad(JavaVM *vm, void *reserved) { +jint JNI_OnLoad(JavaVM* vm, void* reserved) { callstack::nodeapihost::injectIntoWeakNodeApi(); // Register the C++ TurboModule facebook::react::registerCxxModuleToGlobalModuleMap( callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, [](std::shared_ptr jsInvoker) { + callstack::nodeapihost::setCallInvoker(jsInvoker); return std::make_shared( jsInvoker); }); From d5bd040f5ff22793226d0e16ad86ff134d42d582 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 11:52:56 +0200 Subject: [PATCH 09/19] Cleanup --- packages/host/cpp/RuntimeNodeApi.cpp | 2 +- packages/host/ios/NodeApiHostModuleProvider.mm | 2 +- packages/host/scripts/generate-weak-node-api-injector.ts | 3 --- packages/node-addon-examples/tests/async/addon.c | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/host/cpp/RuntimeNodeApi.cpp b/packages/host/cpp/RuntimeNodeApi.cpp index e86c18cd..1644240e 100644 --- a/packages/host/cpp/RuntimeNodeApi.cpp +++ b/packages/host/cpp/RuntimeNodeApi.cpp @@ -121,4 +121,4 @@ napi_status napi_create_external_buffer(napi_env env, return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } -} // namespace callstack::nodeapihost \ No newline at end of file +} // namespace callstack::nodeapihost diff --git a/packages/host/ios/NodeApiHostModuleProvider.mm b/packages/host/ios/NodeApiHostModuleProvider.mm index 303b9112..0ec7afcd 100644 --- a/packages/host/ios/NodeApiHostModuleProvider.mm +++ b/packages/host/ios/NodeApiHostModuleProvider.mm @@ -1,6 +1,6 @@ #import "CxxNodeApiHostModule.hpp" #import "WeakNodeApiInjector.hpp" -#include "RuntimeNodeApiAsync.hpp" +#import "RuntimeNodeApiAsync.hpp" #define USE_CXX_TURBO_MODULE_UTILS 0 #if defined(__has_include) diff --git a/packages/host/scripts/generate-weak-node-api-injector.ts b/packages/host/scripts/generate-weak-node-api-injector.ts index b9900c97..129d2fc0 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.ts +++ b/packages/host/scripts/generate-weak-node-api-injector.ts @@ -66,9 +66,6 @@ export function generateSource(functions: FunctionDecl[]) { .join("\n")} }); } - void initialize(const std::shared_ptr& invoker) { - callstack::nodeapihost::initialize(invoker); - } } // namespace callstack::nodeapihost `; } diff --git a/packages/node-addon-examples/tests/async/addon.c b/packages/node-addon-examples/tests/async/addon.c index 54c611c9..a5e4d979 100644 --- a/packages/node-addon-examples/tests/async/addon.c +++ b/packages/node-addon-examples/tests/async/addon.c @@ -257,4 +257,4 @@ static napi_value Init(napi_env env, napi_value exports) { return exports; } -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) \ No newline at end of file +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) From 9baf932272ae2673875d17b3a5cc9d73ecac2d6d Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 12:23:42 +0200 Subject: [PATCH 10/19] Removed duplicated line --- packages/node-addon-examples/tests/async/addon.c | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/node-addon-examples/tests/async/addon.c b/packages/node-addon-examples/tests/async/addon.c index a5e4d979..9444aacf 100644 --- a/packages/node-addon-examples/tests/async/addon.c +++ b/packages/node-addon-examples/tests/async/addon.c @@ -38,7 +38,6 @@ typedef struct { static carrier the_carrier; static carrier async_carrier[MAX_CANCEL_THREADS]; -static carrier the_carrier; static void Execute(napi_env env, void* data) { sleep_ms(10); carrier* c = (carrier*)(data); From 021a07fb2a5750ce5ee91988ab9de2c2ae763041 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 12:27:16 +0200 Subject: [PATCH 11/19] Applied formatting --- packages/host/android/src/main/cpp/OnLoad.cpp | 2 +- packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/host/android/src/main/cpp/OnLoad.cpp b/packages/host/android/src/main/cpp/OnLoad.cpp index b645e763..982d4a19 100644 --- a/packages/host/android/src/main/cpp/OnLoad.cpp +++ b/packages/host/android/src/main/cpp/OnLoad.cpp @@ -7,7 +7,7 @@ #include // Called when the library is loaded -jint JNI_OnLoad(JavaVM* vm, void* reserved) { +jint JNI_OnLoad(JavaVM *vm, void *reserved) { callstack::nodeapihost::injectIntoWeakNodeApi(); // Register the C++ TurboModule facebook::react::registerCxxModuleToGlobalModuleMap( diff --git a/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h b/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h index 2330a5dc..9db55d7f 100644 --- a/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h +++ b/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h @@ -105,4 +105,4 @@ {(name), NULL, NULL, (func), NULL, NULL, napi_default, NULL} #define DECLARE_NODE_API_PROPERTY_VALUE(name, value) \ - {(name), NULL, NULL, NULL, NULL, (value), napi_default, NULL} \ No newline at end of file + {(name), NULL, NULL, NULL, NULL, (value), napi_default, NULL} From 1a1745556aa6177cd16244a65615324f1940c45f Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 16:09:04 +0200 Subject: [PATCH 12/19] Moved setting CallInvoker to turbo module constructor --- packages/host/android/src/main/cpp/OnLoad.cpp | 2 -- packages/host/cpp/CxxNodeApiHostModule.cpp | 3 +++ packages/host/ios/NodeApiHostModuleProvider.mm | 2 -- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/host/android/src/main/cpp/OnLoad.cpp b/packages/host/android/src/main/cpp/OnLoad.cpp index 982d4a19..35e27128 100644 --- a/packages/host/android/src/main/cpp/OnLoad.cpp +++ b/packages/host/android/src/main/cpp/OnLoad.cpp @@ -3,7 +3,6 @@ #include #include -#include #include // Called when the library is loaded @@ -13,7 +12,6 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) { facebook::react::registerCxxModuleToGlobalModuleMap( callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, [](std::shared_ptr jsInvoker) { - callstack::nodeapihost::setCallInvoker(jsInvoker); return std::make_shared( jsInvoker); }); diff --git a/packages/host/cpp/CxxNodeApiHostModule.cpp b/packages/host/cpp/CxxNodeApiHostModule.cpp index 727241cc..5c4a884f 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.cpp +++ b/packages/host/cpp/CxxNodeApiHostModule.cpp @@ -1,5 +1,6 @@ #include "CxxNodeApiHostModule.hpp" #include "Logger.hpp" +#include "RuntimeNodeApiAsync.hpp" using namespace facebook; @@ -10,6 +11,8 @@ CxxNodeApiHostModule::CxxNodeApiHostModule( : TurboModule(CxxNodeApiHostModule::kModuleName, jsInvoker) { methodMap_["requireNodeAddon"] = MethodMetadata{1, &CxxNodeApiHostModule::requireNodeAddon}; + + callstack::nodeapihost::setCallInvoker(jsInvoker); } jsi::Value diff --git a/packages/host/ios/NodeApiHostModuleProvider.mm b/packages/host/ios/NodeApiHostModuleProvider.mm index 0ec7afcd..d4ecd94f 100644 --- a/packages/host/ios/NodeApiHostModuleProvider.mm +++ b/packages/host/ios/NodeApiHostModuleProvider.mm @@ -1,6 +1,5 @@ #import "CxxNodeApiHostModule.hpp" #import "WeakNodeApiInjector.hpp" -#import "RuntimeNodeApiAsync.hpp" #define USE_CXX_TURBO_MODULE_UTILS 0 #if defined(__has_include) @@ -28,7 +27,6 @@ + (void)load { facebook::react::registerCxxModuleToGlobalModuleMap( callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, [](std::shared_ptr jsInvoker) { - callstack::nodeapihost::setCallInvoker(jsInvoker); return std::make_shared( jsInvoker); }); From 9e6c1e5cfee806eba143e61f1732501459f63150 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 16:17:43 +0200 Subject: [PATCH 13/19] Added assert dependency --- packages/node-addon-examples/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index 8960d63d..3ebee6f5 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -22,5 +22,8 @@ "node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a", "gyp-to-cmake": "*", "read-pkg": "^9.0.1" + }, + "dependencies": { + "assert": "^2.1.0" } } From 04875811f40baf4781e365e05e9f42deecfff2be Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 16:18:11 +0200 Subject: [PATCH 14/19] Stored call invokers by napi_env --- packages/host/cpp/CxxNodeApiHostModule.cpp | 3 ++- packages/host/cpp/CxxNodeApiHostModule.hpp | 2 ++ packages/host/cpp/RuntimeNodeApiAsync.cpp | 15 +++++++++++---- packages/host/cpp/RuntimeNodeApiAsync.hpp | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/host/cpp/CxxNodeApiHostModule.cpp b/packages/host/cpp/CxxNodeApiHostModule.cpp index 5c4a884f..950c7af6 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.cpp +++ b/packages/host/cpp/CxxNodeApiHostModule.cpp @@ -12,7 +12,7 @@ CxxNodeApiHostModule::CxxNodeApiHostModule( methodMap_["requireNodeAddon"] = MethodMetadata{1, &CxxNodeApiHostModule::requireNodeAddon}; - callstack::nodeapihost::setCallInvoker(jsInvoker); + callInvoker_ = std::move(jsInvoker); } jsi::Value @@ -127,6 +127,7 @@ bool CxxNodeApiHostModule::initializeNodeModule(jsi::Runtime &rt, napi_set_named_property(env, global, addon.generatedName.data(), exports); assert(status == napi_ok); + callstack::nodeapihost::setCallInvoker(env, callInvoker_); return true; } diff --git a/packages/host/cpp/CxxNodeApiHostModule.hpp b/packages/host/cpp/CxxNodeApiHostModule.hpp index f77a89af..9445cdaf 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.hpp +++ b/packages/host/cpp/CxxNodeApiHostModule.hpp @@ -28,6 +28,8 @@ class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { std::string generatedName; }; std::unordered_map nodeAddons_; + std::shared_ptr callInvoker_; + using LoaderPolicy = PosixLoader; // FIXME: HACK: This is temporary workaround // for my lazyness (work on iOS and Android) diff --git a/packages/host/cpp/RuntimeNodeApiAsync.cpp b/packages/host/cpp/RuntimeNodeApiAsync.cpp index f4e0f47a..dd4c87c3 100644 --- a/packages/host/cpp/RuntimeNodeApiAsync.cpp +++ b/packages/host/cpp/RuntimeNodeApiAsync.cpp @@ -80,14 +80,21 @@ class AsyncWorkRegistry { std::unordered_map> jobs_; }; -static std::weak_ptr callInvoker; +static std::unordered_map> + callInvokers; static AsyncWorkRegistry asyncWorkRegistry; namespace callstack::nodeapihost { -void setCallInvoker( +void setCallInvoker(napi_env env, const std::shared_ptr& invoker) { - callInvoker = invoker; + callInvokers[env] = invoker; +} + +std::weak_ptr getCallInvoker(napi_env env) { + return callInvokers.contains(env) + ? callInvokers[env] + : std::weak_ptr{}; } napi_status napi_create_async_work(napi_env env, @@ -116,7 +123,7 @@ napi_status napi_queue_async_work( return napi_invalid_arg; } - const auto invoker = callInvoker.lock(); + const auto invoker = getCallInvoker(env).lock(); if (!invoker) { log_debug("Error: No CallInvoker available for async work"); return napi_invalid_arg; diff --git a/packages/host/cpp/RuntimeNodeApiAsync.hpp b/packages/host/cpp/RuntimeNodeApiAsync.hpp index 11fe62ad..f0108e6d 100644 --- a/packages/host/cpp/RuntimeNodeApiAsync.hpp +++ b/packages/host/cpp/RuntimeNodeApiAsync.hpp @@ -6,7 +6,7 @@ namespace callstack::nodeapihost { void setCallInvoker( - const std::shared_ptr& invoker); + napi_env env, const std::shared_ptr& invoker); napi_status napi_create_async_work(napi_env env, napi_value async_resource, From 9efc6b32f417559e22781c5b6e3e0bd49a10fe75 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 16 Jul 2025 16:28:45 +0200 Subject: [PATCH 15/19] Updated package-lock --- package-lock.json | 327 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9774e28a..1940bfc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6442,6 +6442,19 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -6470,6 +6483,21 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", @@ -6862,6 +6890,24 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -7580,6 +7626,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8394,6 +8474,21 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -8762,6 +8857,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -9019,12 +9126,40 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -9082,6 +9217,24 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -9103,6 +9256,22 @@ "node": ">=8" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9121,6 +9290,24 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -9146,6 +9333,21 @@ "node": ">=4" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -10728,6 +10930,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -11075,6 +11322,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11766,6 +12022,23 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11898,6 +12171,23 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -12773,6 +13063,19 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "license": "MIT" }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12877,6 +13180,27 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -14191,6 +14515,9 @@ }, "packages/node-addon-examples": { "name": "@react-native-node-api/node-addon-examples", + "dependencies": { + "assert": "^2.1.0" + }, "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", From 32208ffa287f342f7b33d2085219fbe3e4e99184 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 21 Jul 2025 09:10:39 +0200 Subject: [PATCH 16/19] refactor: Move 'bindings' require statement inside module exports for better scoping --- packages/node-addon-examples/tests/async/addon.js | 2 +- packages/node-addon-examples/tests/buffers/addon.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node-addon-examples/tests/async/addon.js b/packages/node-addon-examples/tests/async/addon.js index cdb2987e..93daacc5 100644 --- a/packages/node-addon-examples/tests/async/addon.js +++ b/packages/node-addon-examples/tests/async/addon.js @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ -const test_async = require("bindings")("addon.node"); const assert = require("assert"); module.exports = async () => { + const test_async = require("bindings")("addon.node"); return new Promise((resolve, reject) => { const test = () => new Promise((resolve, reject) => { diff --git a/packages/node-addon-examples/tests/buffers/addon.js b/packages/node-addon-examples/tests/buffers/addon.js index 3f279fe8..bf940473 100644 --- a/packages/node-addon-examples/tests/buffers/addon.js +++ b/packages/node-addon-examples/tests/buffers/addon.js @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ -const addon = require("bindings")("addon.node"); const assert = require("assert"); const toLocaleString = (text) => { @@ -12,6 +11,7 @@ const toLocaleString = (text) => { }; module.exports = async () => { + const addon = require("bindings")("addon.node"); assert.strictEqual(toLocaleString(addon.newBuffer()), addon.theText); assert.strictEqual(toLocaleString(addon.newExternalBuffer()), addon.theText); assert.strictEqual(toLocaleString(addon.copyBuffer()), addon.theText); From 1049aabd96e98a039026d613e56ff5ec121527a8 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 21 Jul 2025 12:59:46 +0200 Subject: [PATCH 17/19] fix: Removed the async from tests --- .../node-addon-examples/tests/async/addon.js | 82 +++++++++---------- .../tests/buffers/addon.js | 4 +- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/packages/node-addon-examples/tests/async/addon.js b/packages/node-addon-examples/tests/async/addon.js index 93daacc5..f30d87d2 100644 --- a/packages/node-addon-examples/tests/async/addon.js +++ b/packages/node-addon-examples/tests/async/addon.js @@ -1,52 +1,48 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ const assert = require("assert"); +const test_async = require("bindings")("addon.node"); -module.exports = async () => { - const test_async = require("bindings")("addon.node"); - return new Promise((resolve, reject) => { - const test = () => - new Promise((resolve, reject) => { - test_async.Test(5, {}, (err, val) => { - if (err) { - reject(err); - return; - } - try { - assert.strictEqual(err, null); - assert.strictEqual(val, 10); - } catch (e) { - reject(e); - } - resolve(); - }); - }); +const test = () => + new Promise((resolve, reject) => { + test_async.Test(5, {}, (err, val) => { + if (err) { + reject(err); + return; + } + try { + assert.strictEqual(err, null); + assert.strictEqual(val, 10); + } catch (e) { + reject(e); + } + resolve(); + }); + }); - const testCancel = () => - new Promise((resolve) => { - test_async.TestCancel(() => resolve()); - }); +const testCancel = () => + new Promise((resolve) => { + test_async.TestCancel(() => resolve()); + }); - const doRepeatedWork = (count = 0) => - new Promise((resolve, reject) => { - const iterations = 100; - const workDone = (status) => { - try { - assert.strictEqual(status, 0); - } catch (e) { - reject(e); - } - if (++count < iterations) { - test_async.DoRepeatedWork(workDone); - } else { - resolve(); - } - }; +const doRepeatedWork = (count = 0) => + new Promise((resolve, reject) => { + const iterations = 100; + const workDone = (status) => { + try { + assert.strictEqual(status, 0); + } catch (e) { + reject(e); + } + if (++count < iterations) { test_async.DoRepeatedWork(workDone); - }); - - Promise.all([test(), testCancel(), doRepeatedWork()]) - .then(() => resolve()) - .catch(reject); + } else { + resolve(); + } + }; + test_async.DoRepeatedWork(workDone); }); + +module.exports = () => { + return Promise.all([test(), testCancel(), doRepeatedWork()]); }; diff --git a/packages/node-addon-examples/tests/buffers/addon.js b/packages/node-addon-examples/tests/buffers/addon.js index bf940473..a5ccd75f 100644 --- a/packages/node-addon-examples/tests/buffers/addon.js +++ b/packages/node-addon-examples/tests/buffers/addon.js @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ const assert = require("assert"); +const addon = require("bindings")("addon.node"); const toLocaleString = (text) => { return text @@ -10,8 +11,7 @@ const toLocaleString = (text) => { .join(""); }; -module.exports = async () => { - const addon = require("bindings")("addon.node"); +module.exports = () => { assert.strictEqual(toLocaleString(addon.newBuffer()), addon.theText); assert.strictEqual(toLocaleString(addon.newExternalBuffer()), addon.theText); assert.strictEqual(toLocaleString(addon.copyBuffer()), addon.theText); From e1495a1b6c777dfeb0499ee04371de5d1099d909 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 21 Jul 2025 13:27:09 +0200 Subject: [PATCH 18/19] chore: revert to previous version to test pipeline stability --- .../tests/buffers/addon.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/node-addon-examples/tests/buffers/addon.js b/packages/node-addon-examples/tests/buffers/addon.js index a5ccd75f..212861fd 100644 --- a/packages/node-addon-examples/tests/buffers/addon.js +++ b/packages/node-addon-examples/tests/buffers/addon.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ -const assert = require("assert"); const addon = require("bindings")("addon.node"); +// const assert = require("assert"); const toLocaleString = (text) => { return text @@ -11,16 +11,16 @@ const toLocaleString = (text) => { .join(""); }; -module.exports = () => { - assert.strictEqual(toLocaleString(addon.newBuffer()), addon.theText); - assert.strictEqual(toLocaleString(addon.newExternalBuffer()), addon.theText); - assert.strictEqual(toLocaleString(addon.copyBuffer()), addon.theText); - let buffer = addon.staticBuffer(); - assert.strictEqual(addon.bufferHasInstance(buffer), true); - assert.strictEqual(addon.bufferInfo(buffer), true); - addon.invalidObjectAsBuffer({}); +// module.exports = async () => { +console.log(toLocaleString(addon.newBuffer()), addon.theText); +console.log(toLocaleString(addon.newExternalBuffer()), addon.theText); +console.log(toLocaleString(addon.copyBuffer()), addon.theText); +let buffer = addon.staticBuffer(); +console.log(addon.bufferHasInstance(buffer), true); +console.log(addon.bufferInfo(buffer), true); +addon.invalidObjectAsBuffer({}); - // TODO: Add gc tests - // @see - // https://github.com/callstackincubator/react-native-node-api/issues/182 -}; +// TODO: Add gc tests +// @see +// https://github.com/callstackincubator/react-native-node-api/issues/182 +// }; From 5ce6a0275475f509effc85de6f0786109dbe5e78 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 21 Jul 2025 14:00:03 +0200 Subject: [PATCH 19/19] fix: restore assert statements in tests + pulling device logs --- .github/workflows/check.yml | 11 ++++++++ .../tests/buffers/addon.js | 26 +++++++++---------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1abfe758..5c8c9199 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -152,7 +152,18 @@ jobs: adb reverse tcp:8090 tcp:8090 # Uninstall the app if already in the snapshot (unlikely but could result in a signature mismatch failure) adb uninstall com.microsoft.reacttestapp || true + # Start logcat in background and save logs + adb logcat > emulator-logcat.txt 2>&1 & + LOGCAT_PID=$! # Build, install and run the app npm run test:android -- --mode Release # Wait a bit for the sub-process to terminate, before terminating the emulator sleep 5 + # Stop logcat + kill $LOGCAT_PID || true + - name: Upload device logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: emulator-logcat + path: apps/test-app/emulator-logcat.txt diff --git a/packages/node-addon-examples/tests/buffers/addon.js b/packages/node-addon-examples/tests/buffers/addon.js index 212861fd..a5ccd75f 100644 --- a/packages/node-addon-examples/tests/buffers/addon.js +++ b/packages/node-addon-examples/tests/buffers/addon.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ +const assert = require("assert"); const addon = require("bindings")("addon.node"); -// const assert = require("assert"); const toLocaleString = (text) => { return text @@ -11,16 +11,16 @@ const toLocaleString = (text) => { .join(""); }; -// module.exports = async () => { -console.log(toLocaleString(addon.newBuffer()), addon.theText); -console.log(toLocaleString(addon.newExternalBuffer()), addon.theText); -console.log(toLocaleString(addon.copyBuffer()), addon.theText); -let buffer = addon.staticBuffer(); -console.log(addon.bufferHasInstance(buffer), true); -console.log(addon.bufferInfo(buffer), true); -addon.invalidObjectAsBuffer({}); +module.exports = () => { + assert.strictEqual(toLocaleString(addon.newBuffer()), addon.theText); + assert.strictEqual(toLocaleString(addon.newExternalBuffer()), addon.theText); + assert.strictEqual(toLocaleString(addon.copyBuffer()), addon.theText); + let buffer = addon.staticBuffer(); + assert.strictEqual(addon.bufferHasInstance(buffer), true); + assert.strictEqual(addon.bufferInfo(buffer), true); + addon.invalidObjectAsBuffer({}); -// TODO: Add gc tests -// @see -// https://github.com/callstackincubator/react-native-node-api/issues/182 -// }; + // TODO: Add gc tests + // @see + // https://github.com/callstackincubator/react-native-node-api/issues/182 +};