diff --git a/thread_safe_function_counting_wrapped/node-addon-api/addon.cc b/thread_safe_function_counting_wrapped/node-addon-api/addon.cc new file mode 100644 index 00000000..d0674d9b --- /dev/null +++ b/thread_safe_function_counting_wrapped/node-addon-api/addon.cc @@ -0,0 +1,158 @@ +#include +#include +#include + +#include "napi.h" + +template +struct ExtractJSFunctionImpl; + +class NodeJSContext final { + private: + struct Impl final { + Napi::Env env_; + Napi::Promise::Deferred promise_; + std::vector threads_; + + explicit Impl(Napi::Env env) : env_(env), promise_(Napi::Promise::Deferred::New(env)) {} + + ~Impl() { + for (std::thread& t : threads_) { + t.join(); + } + // NOTE(dkorolev): This promise can be set to something other than `true`. + promise_.Resolve(Napi::Boolean::New(env_, true)); + } + }; + std::shared_ptr impl_; + + public: + explicit NodeJSContext(Napi::Env env) : impl_(std::make_shared(env)) {} + explicit NodeJSContext(const Napi::CallbackInfo& info) : NodeJSContext(info.Env()) {} + + template + typename ExtractJSFunctionImpl::retval_t ExtractJSFunction(T f) { + return ExtractJSFunctionImpl::DoIt(*this, f); + } + + void RunAsync(std::function f) { impl_->threads_.emplace_back(f); } + + Napi::Env GetEnv() const { return impl_->env_; } + Napi::Value GetPromise() const { return impl_->promise_.Promise(); } +}; + +template +struct ArgsPopulator final { + static void DoIt(Napi::Env env, const TUPLE& input, std::vector& output) { + PopulateArg(env, std::get(input), output[I]); + ArgsPopulator::DoIt(env, input, output); + } + static void PopulateArg(Napi::Env env, int input, napi_value& output) { output = Napi::Number::New(env, input); } + static void PopulateArg(Napi::Env env, const std::string& input, napi_value& output) { output = Napi::String::New(env, input); } + // NOTE(dkorolev): Add more type wrappers or find the right way to do it within Napi. +}; + +template +struct ArgsPopulator final { + static void DoIt(Napi::Env, const TUPLE&, std::vector&) {} +}; + +class NodeJSFunction final { + private: + struct Impl final { + // The `NodeJSContext` is captured into `std::shared_ptr`, to ensure proper cleanup order. + NodeJSContext context_; + Napi::ThreadSafeFunction function_; + + Impl(NodeJSContext context, Napi::Function jsf) + : context_(context), + function_(Napi::ThreadSafeFunction::New( + context_.GetEnv(), + jsf, + "dkorolev_cpp_callaback", + 0, // Max queue size (0 = unlimited). + 1, // Initial thread count. + static_cast(nullptr), + [context](Napi::Env, void*, void*) { + // NOTE(dkorolev): The *IMPORTANT* part is to capture `context` by value here. + // If this is not done, the reference counter for the very `NodeJSContext` would drop to zero, + // the functions will get called, but the cleanup would fail, crashing the application. + }, + reinterpret_cast(0))) {} + ~Impl() { + // NOTE(dkorolev): This `.Release()` would eventually call the finalization lambda, which, in its turn, + // would release the captured-by-copy `context`, ensuring the cleanup is happening as it should, + // first the captured functions, then by joining the async threads, and finally by setting the promise. + function_.Release(); + } + }; + std::shared_ptr impl_; + + public: + NodeJSFunction(NodeJSContext context, Napi::Function fun) : impl_(std::make_shared(context, fun)) {} + + template + void operator()(ARGS&&... args) const { + auto args_as_tuple_to_copy = std::make_tuple(std::forward(args)...); + if (impl_->function_.BlockingCall( + reinterpret_cast(0), [args_as_tuple_to_copy](Napi::Env env, Napi::Function jsf, int*) { + std::vector params; + using tuple_t = decltype(args_as_tuple_to_copy); + params.resize(std::tuple_size::value); + ArgsPopulator::value>::DoIt(env, args_as_tuple_to_copy, params); + jsf.Call(params); + // TODO(dkorolev): Process the return value as needed. + }) != napi_ok) { + Napi::Error::Fatal("NAPI", "`Napi::ThreadSafeNapi::Function.BlockingCall() != napi_ok`."); + } + } +}; + +template <> +struct ExtractJSFunctionImpl { + using retval_t = NodeJSFunction; + static NodeJSFunction DoIt(NodeJSContext self, Napi::Function js_function) { + return NodeJSFunction(self, js_function); + } +}; + +template <> +struct ExtractJSFunctionImpl { + using retval_t = NodeJSFunction; + static NodeJSFunction DoIt(NodeJSContext self, Napi::Value js_function) { + return NodeJSFunction(self, js_function.As()); + } +}; + +Napi::Value RunAsyncWork(const Napi::CallbackInfo& cbinfo) { + // Create the context that would manage the lifetime of the extracted JS functions, to `.Release()` them later. + NodeJSContext ctx(cbinfo); + + // Create the captured functions before starting the async thread, as the very `cbinfo` is a const reference. + NodeJSFunction f_even = ctx.ExtractJSFunction(cbinfo[0]); + NodeJSFunction f_odd = ctx.ExtractJSFunction(cbinfo[1]); + + // Run the C++ code asynchronously. + ctx.RunAsync([f_even, f_odd]() { + // NOTE(dkorolev): It is *IMPORTANT* to capture `f_{even,odd}` by value, so that their refcounts are incremented. + struct IntString final { int i; std::string s; }; + for (IntString& value : std::vector({{1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}, {5 ,"five"}})) { + ((value.i % 2 == 0) ? f_even : f_odd)(value.i, value.s); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + + // This `return` would return the promise immediately, as the "business logic" is run in a dedicated thread. + return ctx.GetPromise(); + + // The very `NodeJSContext ctx` would be released after the extracted functions are released, + // and the extracted functions will be released when they have no users left. + // The TL;DR is that as long as they are copied, not captured by reference, everything would work correctly. +} + +Napi::Object Init(Napi::Env env, Napi::Object exports) { + exports["runAsyncWork"] = Napi::Function::New(env, RunAsyncWork); + return exports; +} + +NODE_API_MODULE(addon, Init) diff --git a/thread_safe_function_counting_wrapped/node-addon-api/addon.js b/thread_safe_function_counting_wrapped/node-addon-api/addon.js new file mode 100644 index 00000000..fb725e81 --- /dev/null +++ b/thread_safe_function_counting_wrapped/node-addon-api/addon.js @@ -0,0 +1,9 @@ +const { runAsyncWork } = require('bindings')('addon'); + +console.log('RunAsyncWork(): calling the C++ function.'); +const promise = runAsyncWork( + (i, s) => { console.log(`Callback from C++: even ${s}=${i}.`); }, + (i, s) => { console.log(`Callback from C++: odd ${s}=${i}.`); } +); +console.log('RunAsyncWork(): the promise is returned from C++.'); +promise.then((value) => { console.log(`RunAsyncWork(): the promise resolved, from C++, to ${value}.`); }); diff --git a/thread_safe_function_counting_wrapped/node-addon-api/binding.gyp b/thread_safe_function_counting_wrapped/node-addon-api/binding.gyp new file mode 100644 index 00000000..492c7035 --- /dev/null +++ b/thread_safe_function_counting_wrapped/node-addon-api/binding.gyp @@ -0,0 +1,21 @@ +{ + 'targets': [{ + 'target_name': 'addon', + 'defines': ['V8_DEPRECATION_WARNINGS=1'], + 'sources': ['addon.cc'], + 'include_dirs': ["= 10.16.0" + } +}