From 78196e018b5cb34d2b82a4fb6880d48154a9d8e6 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 22 Mar 2020 19:33:46 +0100 Subject: [PATCH 01/39] tsfn: implement ThreadSafeFunctionEx --- napi-inl.h | 125 ++++++++++++++++++ napi.h | 74 +++++++++++ test/binding.cc | 2 + test/binding.gyp | 1 + test/index.js | 2 + .../threadsafe_function_ex.cc | 93 +++++++++++++ .../threadsafe_function_ex.js | 17 +++ 7 files changed, 314 insertions(+) create mode 100644 test/threadsafe_function/threadsafe_function_ex.cc create mode 100644 test/threadsafe_function/threadsafe_function_ex.js diff --git a/napi-inl.h b/napi-inl.h index 82846c257..4bd4845cc 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4256,6 +4256,131 @@ inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { } #if (NAPI_VERSION > 3) +//////////////////////////////////////////////////////////////////////////////// +// ThreadSafeFunctionEx class +//////////////////////////////////////////////////////////////////////////////// + +// static +template +template +inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_threadsafe_function_call_js call_js_cb) { + return New(env, callback, resource, resourceName, maxQueueSize, + initialThreadCount, context, finalizeCallback, data, + details::ThreadSafeFinalize::FinalizeFinalizeWrapperWithDataAndContext, + call_js_cb); +} + +template +inline ThreadSafeFunctionEx::ThreadSafeFunctionEx() + : _tsfn() { +} + +template +inline ThreadSafeFunctionEx::ThreadSafeFunctionEx( + napi_threadsafe_function tsfn) + : _tsfn(tsfn) { +} + +template +inline ThreadSafeFunctionEx::operator napi_threadsafe_function() const { + return _tsfn; +} + +template +template +inline napi_status ThreadSafeFunctionEx::BlockingCall( + DataType* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); +} + +template +template +inline napi_status ThreadSafeFunctionEx::NonBlockingCall( + DataType* data) const { + return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); +} + +template +inline void ThreadSafeFunctionEx::Ref(napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_ref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +template +inline void ThreadSafeFunctionEx::Unref(napi_env env) const { + if (_tsfn != nullptr) { + napi_status status = napi_unref_threadsafe_function(env, _tsfn); + NAPI_THROW_IF_FAILED_VOID(env, status); + } +} + +template +inline napi_status ThreadSafeFunctionEx::Acquire() const { + return napi_acquire_threadsafe_function(_tsfn); +} + +template +inline napi_status ThreadSafeFunctionEx::Release() { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); +} + +template +inline napi_status ThreadSafeFunctionEx::Abort() { + return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); +} + +template +inline ContextType* ThreadSafeFunctionEx::GetContext() const { + void* context; + napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); + NAPI_FATAL_IF_FAILED(status, "ThreadSafeFunctionEx::GetContext", "napi_get_threadsafe_function_context"); + return static_cast(context); +} + +// static +template +template +inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_finalize wrapper, + napi_threadsafe_function_call_js call_js_cb) { + static_assert(details::can_make_string::value + || std::is_convertible::value, + "Resource name should be convertible to the string type"); + + ThreadSafeFunctionEx tsfn; + auto* finalizeData = new details::ThreadSafeFinalize({ data, finalizeCallback }); + napi_status status = napi_create_threadsafe_function(env, callback, resource, + Value::From(env, resourceName), maxQueueSize, initialThreadCount, + finalizeData, wrapper, context, call_js_cb, &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, ThreadSafeFunctionEx()); + } + + return tsfn; +} + //////////////////////////////////////////////////////////////////////////////// // ThreadSafeFunction class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index e4b964f87..4973321e7 100644 --- a/napi.h +++ b/napi.h @@ -2036,6 +2036,80 @@ namespace Napi { }; #if (NAPI_VERSION > 3) + + template + class ThreadSafeFunctionEx { + public: + // This API may only be called from the main thread. + template + static ThreadSafeFunctionEx New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_threadsafe_function_call_js call_js_cb); + + ThreadSafeFunctionEx(); + ThreadSafeFunctionEx(napi_threadsafe_function tsFunctionValue); + + operator napi_threadsafe_function() const; + + // // This API may be called from any thread. + // napi_status BlockingCall() const; + + // This API may be called from any thread. + template + napi_status BlockingCall(DataType* data = nullptr) const; + + // // This API may be called from any thread. + // napi_status NonBlockingCall() const; + + // This API may be called from any thread. + template + napi_status NonBlockingCall(DataType* data = nullptr) const; + + // This API may only be called from the main thread. + void Ref(napi_env env) const; + + // This API may only be called from the main thread. + void Unref(napi_env env) const; + + // This API may be called from any thread. + napi_status Acquire() const; + + // This API may be called from any thread. + napi_status Release(); + + // This API may be called from any thread. + napi_status Abort(); + + // This API may be called from any thread. + ContextType* GetContext() const; + + private: + using CallbackWrapper = std::function; + + template + static ThreadSafeFunctionEx New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, + napi_finalize wrapper, + napi_threadsafe_function_call_js call_js_cb); + + napi_threadsafe_function _tsfn; + }; + class ThreadSafeFunction { public: // This API may only be called from the main thread. diff --git a/test/binding.cc b/test/binding.cc index 111bcce01..499d9435a 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -43,6 +43,7 @@ Object InitPromise(Env env); Object InitRunScript(Env env); #if (NAPI_VERSION > 3) Object InitThreadSafeFunctionCtx(Env env); +Object InitThreadSafeFunctionEx(Env env); Object InitThreadSafeFunctionExistingTsfn(Env env); Object InitThreadSafeFunctionPtr(Env env); Object InitThreadSafeFunctionSum(Env env); @@ -100,6 +101,7 @@ Object Init(Env env, Object exports) { exports.Set("run_script", InitRunScript(env)); #if (NAPI_VERSION > 3) exports.Set("threadsafe_function_ctx", InitThreadSafeFunctionCtx(env)); + exports.Set("threadsafe_function_ex", InitThreadSafeFunctionEx(env)); exports.Set("threadsafe_function_existing_tsfn", InitThreadSafeFunctionExistingTsfn(env)); exports.Set("threadsafe_function_ptr", InitThreadSafeFunctionPtr(env)); exports.Set("threadsafe_function_sum", InitThreadSafeFunctionSum(env)); diff --git a/test/binding.gyp b/test/binding.gyp index 2d6ac9549..790bef5fa 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -35,6 +35,7 @@ 'promise.cc', 'run_script.cc', 'threadsafe_function/threadsafe_function_ctx.cc', + 'threadsafe_function/threadsafe_function_ex.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', 'threadsafe_function/threadsafe_function_ptr.cc', 'threadsafe_function/threadsafe_function_sum.cc', diff --git a/test/index.js b/test/index.js index e96ac5bf8..d91b54581 100644 --- a/test/index.js +++ b/test/index.js @@ -42,6 +42,7 @@ let testModules = [ 'promise', 'run_script', 'threadsafe_function/threadsafe_function_ctx', + 'threadsafe_function/threadsafe_function_ex', 'threadsafe_function/threadsafe_function_existing_tsfn', 'threadsafe_function/threadsafe_function_ptr', 'threadsafe_function/threadsafe_function_sum', @@ -76,6 +77,7 @@ if (napiVersion < 4) { testModules.splice(testModules.indexOf('asyncprogressqueueworker'), 1); testModules.splice(testModules.indexOf('asyncprogressworker'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_ctx'), 1); + testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_ex'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_existing_tsfn'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_ptr'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_sum'), 1); diff --git a/test/threadsafe_function/threadsafe_function_ex.cc b/test/threadsafe_function/threadsafe_function_ex.cc new file mode 100644 index 000000000..84ce1b77a --- /dev/null +++ b/test/threadsafe_function/threadsafe_function_ex.cc @@ -0,0 +1,93 @@ +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +using TSFNContext = Reference; + +namespace { + +struct CallJsData { + CallJsData(Napi::Env env) : deferred(Promise::Deferred::New(env)) { }; + + void resolve(TSFNContext* context) { + deferred.Resolve(context->Value()); + }; + Promise::Deferred deferred; +}; + +class TSFNWrap : public ObjectWrap { +public: + static Object Init(Napi::Env env, Object exports); + TSFNWrap(const CallbackInfo &info); + + Napi::Value GetContextByCall(const CallbackInfo &info) { + Napi::Env env = info.Env(); + std::unique_ptr callData = std::make_unique(env); + auto& deferred = callData->deferred; + _tsfn.BlockingCall(callData.release()); + return deferred.Promise(); + }; + + Napi::Value GetContextFromTsfn(const CallbackInfo &info) { + return _tsfn.GetContext()->Value(); + }; + + Napi::Value Release(const CallbackInfo &info) { + _tsfn.Release(); + return _deferred.Promise(); + }; + +private: + ThreadSafeFunctionEx _tsfn; + Promise::Deferred _deferred; +}; + +Object TSFNWrap::Init(Napi::Env env, Object exports) { + Function func = DefineClass( + env, "TSFNWrap", + {InstanceMethod("getContextByCall", &TSFNWrap::GetContextByCall), + InstanceMethod("getContextFromTsfn", &TSFNWrap::GetContextFromTsfn), + InstanceMethod("release", &TSFNWrap::Release)}); + + exports.Set("TSFNWrap", func); + return exports; +} + +TSFNWrap::TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + Napi::Env env = info.Env(); + + TSFNContext *ctx = new Reference; + *ctx = Persistent(info[0]); + + _tsfn = ThreadSafeFunctionEx::New( + info.Env(), // napi_env env, + Function::New( + env, + [](const CallbackInfo & /*info*/) {}), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 1, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + ctx, // ContextType* context, + + [this](Napi::Env env, void*, TSFNContext *ctx) { // Finalizer finalizeCallback, + _deferred.Resolve(env.Undefined()); + delete ctx; + }, + static_cast(nullptr), // FinalizerDataType* data, + [](napi_env env, napi_value js_callback, void *context, void *data) { // call_js_cb + std::unique_ptr callData(static_cast(data)); + callData->resolve(static_cast(context)); + }); +} +} // namespace + +Object InitThreadSafeFunctionEx(Env env) { + return TSFNWrap::Init(env, Object::New(env)); +} + +#endif diff --git a/test/threadsafe_function/threadsafe_function_ex.js b/test/threadsafe_function/threadsafe_function_ex.js new file mode 100644 index 000000000..63fd7dc5c --- /dev/null +++ b/test/threadsafe_function/threadsafe_function_ex.js @@ -0,0 +1,17 @@ +'use strict'; + +const assert = require('assert'); +const buildType = process.config.target_defaults.default_configuration; + +module.exports = Promise.all([ + test(require(`../build/${buildType}/binding.node`)), + test(require(`../build/${buildType}/binding_noexcept.node`)) +]); + +async function test(binding) { + const ctx = { }; + const tsfn = new binding.threadsafe_function_ex.TSFNWrap(ctx); + assert(ctx === await tsfn.getContextByCall(),"getContextByCall context not equal"); + assert(ctx === tsfn.getContextFromTsfn(),"getContextFromTsfn context not equal"); + await tsfn.release(); +} From 90ebd80ae1f7d703b11529433c5cf3d72ee97491 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 22 Mar 2020 20:02:42 +0100 Subject: [PATCH 02/39] fix unused parameter errors --- test/threadsafe_function/threadsafe_function_ex.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/threadsafe_function/threadsafe_function_ex.cc b/test/threadsafe_function/threadsafe_function_ex.cc index 84ce1b77a..fa7efb993 100644 --- a/test/threadsafe_function/threadsafe_function_ex.cc +++ b/test/threadsafe_function/threadsafe_function_ex.cc @@ -30,11 +30,11 @@ class TSFNWrap : public ObjectWrap { return deferred.Promise(); }; - Napi::Value GetContextFromTsfn(const CallbackInfo &info) { + Napi::Value GetContextFromTsfn(const CallbackInfo &) { return _tsfn.GetContext()->Value(); }; - Napi::Value Release(const CallbackInfo &info) { + Napi::Value Release(const CallbackInfo &) { _tsfn.Release(); return _deferred.Promise(); }; @@ -79,7 +79,7 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) delete ctx; }, static_cast(nullptr), // FinalizerDataType* data, - [](napi_env env, napi_value js_callback, void *context, void *data) { // call_js_cb + [](napi_env, napi_value, void *context, void *data) { // call_js_cb std::unique_ptr callData(static_cast(data)); callData->resolve(static_cast(context)); }); From fc339c4c46ec2d618d66a715bee9d1a5e7157514 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 31 Mar 2020 14:57:20 +0200 Subject: [PATCH 03/39] wip --- napi-inl.h | 31 +++++++++++++++++-- napi.h | 22 +++++++++---- .../threadsafe_function_ex.cc | 4 +-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/napi-inl.h b/napi-inl.h index 4bd4845cc..41097e8f1 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4272,7 +4272,7 @@ inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New( ContextType* context, Finalizer finalizeCallback, FinalizerDataType* data, - napi_threadsafe_function_call_js call_js_cb) { + ThreadSafeFunctionCallJS call_js_cb) { return New(env, callback, resource, resourceName, maxQueueSize, initialThreadCount, context, finalizeCallback, data, details::ThreadSafeFinalize ThreadSafeFunctionEx::New( Finalizer finalizeCallback, FinalizerDataType* data, napi_finalize wrapper, - napi_threadsafe_function_call_js call_js_cb) { + ThreadSafeFunctionCallJS call_js_cb) { static_assert(details::can_make_string::value || std::is_convertible::value, "Resource name should be convertible to the string type"); @@ -4372,7 +4372,15 @@ inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New( FinalizerDataType>({ data, finalizeCallback }); napi_status status = napi_create_threadsafe_function(env, callback, resource, Value::From(env, resourceName), maxQueueSize, initialThreadCount, - finalizeData, wrapper, context, call_js_cb, &tsfn._tsfn); + finalizeData, wrapper, context, + // [=](napi_env env, napi_value jsCallback, void* context, void* data) { + // if (env == nullptr && jsCallback == nullptr) { + // return; + // } + // call_js_cb( Napi::Env(env), Function(env, jsCallback), static_cast(context), data); + // }, + CallJS, + &tsfn._tsfn); if (status != napi_ok) { delete finalizeData; NAPI_THROW_IF_FAILED(env, status, ThreadSafeFunctionEx()); @@ -4381,6 +4389,23 @@ inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New( return tsfn; } +template +inline void +ThreadSafeFunctionEx::CallJS(napi_env env, napi_value jsCallback, + void *context, void *data) { + if (env == nullptr && jsCallback == nullptr) { + return; + } + + if (data != nullptr) { + auto* callbackWrapper = static_cast(data); + (*callbackWrapper)(env, Function(env, jsCallback)); + delete callbackWrapper; + } else if (jsCallback != nullptr) { + Function(env, jsCallback).Call({}); + } +} + //////////////////////////////////////////////////////////////////////////////// // ThreadSafeFunction class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index 4973321e7..c4b1228a4 100644 --- a/napi.h +++ b/napi.h @@ -2037,9 +2037,13 @@ namespace Napi { #if (NAPI_VERSION > 3) - template + + template class ThreadSafeFunctionEx { public: + + using ThreadSafeFunctionCallJS = std::function; + // This API may only be called from the main thread. template static ThreadSafeFunctionEx New(napi_env env, @@ -2051,7 +2055,7 @@ namespace Napi { ContextType* context, Finalizer finalizeCallback, FinalizerDataType* data, - napi_threadsafe_function_call_js call_js_cb); + ThreadSafeFunctionCallJS call_js_cb); ThreadSafeFunctionEx(); ThreadSafeFunctionEx(napi_threadsafe_function tsFunctionValue); @@ -2062,14 +2066,12 @@ namespace Napi { // napi_status BlockingCall() const; // This API may be called from any thread. - template napi_status BlockingCall(DataType* data = nullptr) const; // // This API may be called from any thread. // napi_status NonBlockingCall() const; // This API may be called from any thread. - template napi_status NonBlockingCall(DataType* data = nullptr) const; // This API may only be called from the main thread. @@ -2091,7 +2093,7 @@ namespace Napi { ContextType* GetContext() const; private: - using CallbackWrapper = std::function; + // using CallbackWrapper = std::function; template @@ -2105,7 +2107,15 @@ namespace Napi { Finalizer finalizeCallback, FinalizerDataType* data, napi_finalize wrapper, - napi_threadsafe_function_call_js call_js_cb); + ThreadSafeFunctionCallJS call_js_cb); + + static void CallJS(napi_env env, + napi_value jsCallback, + void* context, + void* data); + + protected: + void CallJS napi_threadsafe_function _tsfn; }; diff --git a/test/threadsafe_function/threadsafe_function_ex.cc b/test/threadsafe_function/threadsafe_function_ex.cc index fa7efb993..fccf2401f 100644 --- a/test/threadsafe_function/threadsafe_function_ex.cc +++ b/test/threadsafe_function/threadsafe_function_ex.cc @@ -79,9 +79,9 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) delete ctx; }, static_cast(nullptr), // FinalizerDataType* data, - [](napi_env, napi_value, void *context, void *data) { // call_js_cb + [](Napi::Env, Napi::Value, TSFNContext *context, void *data) { // call_js_cb std::unique_ptr callData(static_cast(data)); - callData->resolve(static_cast(context)); + callData->resolve(context); }); } } // namespace From fd530b217e9f7dec68c391e53af51658537a40f0 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 7 Apr 2020 18:03:51 +0200 Subject: [PATCH 04/39] wip --- napi-inl.h | 107 ++++++++++-------- napi.h | 32 ++---- .../threadsafe_function_ex.cc | 37 +++--- .../threadsafe_function_ex.js | 7 +- 4 files changed, 97 insertions(+), 86 deletions(-) diff --git a/napi-inl.h b/napi-inl.h index 41097e8f1..0ee2910b3 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4261,9 +4261,9 @@ inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { //////////////////////////////////////////////////////////////////////////////// // static -template +template template -inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New(napi_env env, +inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New(napi_env env, const Function& callback, const Object& resource, ResourceString resourceName, @@ -4271,78 +4271,74 @@ inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New( size_t initialThreadCount, ContextType* context, Finalizer finalizeCallback, - FinalizerDataType* data, - ThreadSafeFunctionCallJS call_js_cb) { + FinalizerDataType* data) { return New(env, callback, resource, resourceName, maxQueueSize, initialThreadCount, context, finalizeCallback, data, details::ThreadSafeFinalize::FinalizeFinalizeWrapperWithDataAndContext, - call_js_cb); + FinalizerDataType>::FinalizeFinalizeWrapperWithDataAndContext); } -template -inline ThreadSafeFunctionEx::ThreadSafeFunctionEx() +template +inline ThreadSafeFunctionEx::ThreadSafeFunctionEx() : _tsfn() { } -template -inline ThreadSafeFunctionEx::ThreadSafeFunctionEx( +template +inline ThreadSafeFunctionEx::ThreadSafeFunctionEx( napi_threadsafe_function tsfn) : _tsfn(tsfn) { } -template -inline ThreadSafeFunctionEx::operator napi_threadsafe_function() const { +template +inline ThreadSafeFunctionEx::operator napi_threadsafe_function() const { return _tsfn; } -template -template -inline napi_status ThreadSafeFunctionEx::BlockingCall( +template +inline napi_status ThreadSafeFunctionEx::BlockingCall( DataType* data) const { return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); } -template -template -inline napi_status ThreadSafeFunctionEx::NonBlockingCall( +template +inline napi_status ThreadSafeFunctionEx::NonBlockingCall( DataType* data) const { return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); } -template -inline void ThreadSafeFunctionEx::Ref(napi_env env) const { +template +inline void ThreadSafeFunctionEx::Ref(napi_env env) const { if (_tsfn != nullptr) { napi_status status = napi_ref_threadsafe_function(env, _tsfn); NAPI_THROW_IF_FAILED_VOID(env, status); } } -template -inline void ThreadSafeFunctionEx::Unref(napi_env env) const { +template +inline void ThreadSafeFunctionEx::Unref(napi_env env) const { if (_tsfn != nullptr) { napi_status status = napi_unref_threadsafe_function(env, _tsfn); NAPI_THROW_IF_FAILED_VOID(env, status); } } -template -inline napi_status ThreadSafeFunctionEx::Acquire() const { +template +inline napi_status ThreadSafeFunctionEx::Acquire() const { return napi_acquire_threadsafe_function(_tsfn); } -template -inline napi_status ThreadSafeFunctionEx::Release() { +template +inline napi_status ThreadSafeFunctionEx::Release() { return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); } -template -inline napi_status ThreadSafeFunctionEx::Abort() { +template +inline napi_status ThreadSafeFunctionEx::Abort() { return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); } -template -inline ContextType* ThreadSafeFunctionEx::GetContext() const { +template +inline ContextType* ThreadSafeFunctionEx::GetContext() const { void* context; napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); NAPI_FATAL_IF_FAILED(status, "ThreadSafeFunctionEx::GetContext", "napi_get_threadsafe_function_context"); @@ -4350,9 +4346,9 @@ inline ContextType* ThreadSafeFunctionEx::GetContext() const { } // static -template +template template -inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New(napi_env env, +inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New(napi_env env, const Function& callback, const Object& resource, ResourceString resourceName, @@ -4361,13 +4357,14 @@ inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New( ContextType* context, Finalizer finalizeCallback, FinalizerDataType* data, - napi_finalize wrapper, - ThreadSafeFunctionCallJS call_js_cb) { + napi_finalize wrapper) { static_assert(details::can_make_string::value || std::is_convertible::value, "Resource name should be convertible to the string type"); - ThreadSafeFunctionEx tsfn; + ThreadSafeFunctionEx tsfn; + // details::ThreadSafeCallJs cb{call_js_cb}; + auto* finalizeData = new details::ThreadSafeFinalize({ data, finalizeCallback }); napi_status status = napi_create_threadsafe_function(env, callback, resource, @@ -4379,33 +4376,45 @@ inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New( // } // call_js_cb( Napi::Env(env), Function(env, jsCallback), static_cast(context), data); // }, - CallJS, + CallJsInternal, &tsfn._tsfn); if (status != napi_ok) { delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, ThreadSafeFunctionEx()); + NAPI_THROW_IF_FAILED(env, status, ThreadSafeFunctionEx()); } return tsfn; } -template -inline void -ThreadSafeFunctionEx::CallJS(napi_env env, napi_value jsCallback, +template +void ThreadSafeFunctionEx::CallJsInternal(napi_env env, napi_value jsCallback, void *context, void *data) { - if (env == nullptr && jsCallback == nullptr) { - return; - } - if (data != nullptr) { - auto* callbackWrapper = static_cast(data); - (*callbackWrapper)(env, Function(env, jsCallback)); - delete callbackWrapper; - } else if (jsCallback != nullptr) { - Function(env, jsCallback).Call({}); + if (CallJs == nullptr && jsCallback != nullptr) { + Function(env, jsCallback).Call(0, nullptr); + } else { + CallJs(env, Function(env, jsCallback), static_cast(context), static_cast(data)); } } + +// template +// inline void +// ThreadSafeFunctionEx::CallJS(napi_env env, napi_value jsCallback, +// void *context, void *data) { +// if (env == nullptr && jsCallback == nullptr) { +// return; +// } + +// if (data != nullptr) { +// auto* callbackWrapper = static_cast(data); +// (*callbackWrapper)(env, Function(env, jsCallback)); +// delete callbackWrapper; +// } else if (jsCallback != nullptr) { +// Function(env, jsCallback).Call({}); +// } +// } + //////////////////////////////////////////////////////////////////////////////// // ThreadSafeFunction class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index c4b1228a4..17dacc87b 100644 --- a/napi.h +++ b/napi.h @@ -2036,17 +2036,13 @@ namespace Napi { }; #if (NAPI_VERSION > 3) - - - template + template class ThreadSafeFunctionEx { public: - using ThreadSafeFunctionCallJS = std::function; - // This API may only be called from the main thread. template - static ThreadSafeFunctionEx New(napi_env env, + static ThreadSafeFunctionEx New(napi_env env, const Function& callback, const Object& resource, ResourceString resourceName, @@ -2054,11 +2050,10 @@ namespace Napi { size_t initialThreadCount, ContextType* context, Finalizer finalizeCallback, - FinalizerDataType* data, - ThreadSafeFunctionCallJS call_js_cb); + FinalizerDataType* data); - ThreadSafeFunctionEx(); - ThreadSafeFunctionEx(napi_threadsafe_function tsFunctionValue); + ThreadSafeFunctionEx(); + ThreadSafeFunctionEx(napi_threadsafe_function tsFunctionValue); operator napi_threadsafe_function() const; @@ -2093,11 +2088,10 @@ namespace Napi { ContextType* GetContext() const; private: - // using CallbackWrapper = std::function; template - static ThreadSafeFunctionEx New(napi_env env, + static ThreadSafeFunctionEx New(napi_env env, const Function& callback, const Object& resource, ResourceString resourceName, @@ -2106,17 +2100,13 @@ namespace Napi { ContextType* context, Finalizer finalizeCallback, FinalizerDataType* data, - napi_finalize wrapper, - ThreadSafeFunctionCallJS call_js_cb); - - static void CallJS(napi_env env, - napi_value jsCallback, - void* context, - void* data); + napi_finalize wrapper); + static void CallJsInternal(napi_env env, + napi_value jsCallback, + void* context, + void* data); protected: - void CallJS - napi_threadsafe_function _tsfn; }; diff --git a/test/threadsafe_function/threadsafe_function_ex.cc b/test/threadsafe_function/threadsafe_function_ex.cc index fccf2401f..b66148d1c 100644 --- a/test/threadsafe_function/threadsafe_function_ex.cc +++ b/test/threadsafe_function/threadsafe_function_ex.cc @@ -4,19 +4,28 @@ using namespace Napi; -using TSFNContext = Reference; - namespace { +// Context of our TSFN. +using TSFNContext = Reference; + +// Data passed to ThreadSafeFunctionEx::[Non]BlockingCall struct CallJsData { - CallJsData(Napi::Env env) : deferred(Promise::Deferred::New(env)) { }; + CallJsData(Napi::Env env) : deferred(Promise::Deferred::New(env)){}; - void resolve(TSFNContext* context) { - deferred.Resolve(context->Value()); - }; Promise::Deferred deferred; }; +// CallJs callback function +static void CallJs(Napi::Env /*env*/, Napi::Function /*jsCallback*/, + TSFNContext *context, CallJsData *data) { + data->deferred.Resolve(context->Value()); +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. class TSFNWrap : public ObjectWrap { public: static Object Init(Napi::Env env, Object exports); @@ -25,7 +34,7 @@ class TSFNWrap : public ObjectWrap { Napi::Value GetContextByCall(const CallbackInfo &info) { Napi::Env env = info.Env(); std::unique_ptr callData = std::make_unique(env); - auto& deferred = callData->deferred; + auto &deferred = callData->deferred; _tsfn.BlockingCall(callData.release()); return deferred.Promise(); }; @@ -40,7 +49,7 @@ class TSFNWrap : public ObjectWrap { }; private: - ThreadSafeFunctionEx _tsfn; + TSFN _tsfn; Promise::Deferred _deferred; }; @@ -63,7 +72,7 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) TSFNContext *ctx = new Reference; *ctx = Persistent(info[0]); - _tsfn = ThreadSafeFunctionEx::New( + _tsfn = ThreadSafeFunctionEx::New( info.Env(), // napi_env env, Function::New( env, @@ -74,15 +83,13 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) 1, // size_t initialThreadCount, ctx, // ContextType* context, - [this](Napi::Env env, void*, TSFNContext *ctx) { // Finalizer finalizeCallback, + [this](Napi::Env env, void *, + TSFNContext *ctx) { // Finalizer finalizeCallback, _deferred.Resolve(env.Undefined()); delete ctx; }, - static_cast(nullptr), // FinalizerDataType* data, - [](Napi::Env, Napi::Value, TSFNContext *context, void *data) { // call_js_cb - std::unique_ptr callData(static_cast(data)); - callData->resolve(context); - }); + static_cast(nullptr) // FinalizerDataType* data, + ); } } // namespace diff --git a/test/threadsafe_function/threadsafe_function_ex.js b/test/threadsafe_function/threadsafe_function_ex.js index 63fd7dc5c..8b6eb87aa 100644 --- a/test/threadsafe_function/threadsafe_function_ex.js +++ b/test/threadsafe_function/threadsafe_function_ex.js @@ -5,13 +5,18 @@ const buildType = process.config.target_defaults.default_configuration; module.exports = Promise.all([ test(require(`../build/${buildType}/binding.node`)), - test(require(`../build/${buildType}/binding_noexcept.node`)) + // test(require(`../build/${buildType}/binding_noexcept.node`)) ]); async function test(binding) { const ctx = { }; const tsfn = new binding.threadsafe_function_ex.TSFNWrap(ctx); + console.log("1"); assert(ctx === await tsfn.getContextByCall(),"getContextByCall context not equal"); + console.log("2"); assert(ctx === tsfn.getContextFromTsfn(),"getContextFromTsfn context not equal"); + console.log("3"); + console.log("releasing"); await tsfn.release(); + console.log("released!"); } From 561b9687ffe615fb1f41fecab7b18ff9710091b6 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 7 Apr 2020 19:27:22 +0200 Subject: [PATCH 05/39] make CallJS template parameter --- napi-inl.h | 209 +++++++++--------- napi.h | 6 - test/binding.cc | 6 +- test/binding.gyp | 3 +- test/index.js | 6 +- .../threadsafe_function_ex.js | 22 -- .../context.cc} | 31 ++- test/threadsafe_function_ex/context.js | 17 ++ test/threadsafe_function_ex/simple.cc | 66 ++++++ test/threadsafe_function_ex/simple.js | 15 ++ 10 files changed, 224 insertions(+), 157 deletions(-) delete mode 100644 test/threadsafe_function/threadsafe_function_ex.js rename test/{threadsafe_function/threadsafe_function_ex.cc => threadsafe_function_ex/context.cc} (72%) create mode 100644 test/threadsafe_function_ex/context.js create mode 100644 test/threadsafe_function_ex/simple.cc create mode 100644 test/threadsafe_function_ex/simple.js diff --git a/napi-inl.h b/napi-inl.h index 0ee2910b3..3e57acf2e 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4257,164 +4257,161 @@ inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { #if (NAPI_VERSION > 3) //////////////////////////////////////////////////////////////////////////////// -// ThreadSafeFunctionEx class +// ThreadSafeFunctionEx class //////////////////////////////////////////////////////////////////////////////// // static -template -template -inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New(napi_env env, - const Function& callback, - const Object& resource, - ResourceString resourceName, - size_t maxQueueSize, - size_t initialThreadCount, - ContextType* context, - Finalizer finalizeCallback, - FinalizerDataType* data) { - return New(env, callback, resource, resourceName, maxQueueSize, - initialThreadCount, context, finalizeCallback, data, - details::ThreadSafeFinalize::FinalizeFinalizeWrapperWithDataAndContext); -} - -template -inline ThreadSafeFunctionEx::ThreadSafeFunctionEx() - : _tsfn() { -} - -template -inline ThreadSafeFunctionEx::ThreadSafeFunctionEx( - napi_threadsafe_function tsfn) - : _tsfn(tsfn) { -} - -template -inline ThreadSafeFunctionEx::operator napi_threadsafe_function() const { +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, const Function &callback, const Object &resource, + ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, + ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data) { + return New( + env, callback, resource, resourceName, maxQueueSize, initialThreadCount, + context, finalizeCallback, data, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext); +} + +template +inline ThreadSafeFunctionEx::ThreadSafeFunctionEx() + : _tsfn() {} + +template +inline ThreadSafeFunctionEx:: + ThreadSafeFunctionEx(napi_threadsafe_function tsfn) + : _tsfn(tsfn) {} + +template +inline ThreadSafeFunctionEx:: +operator napi_threadsafe_function() const { return _tsfn; } -template -inline napi_status ThreadSafeFunctionEx::BlockingCall( - DataType* data) const { +template +inline napi_status +ThreadSafeFunctionEx::BlockingCall( + DataType *data) const { return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); } -template -inline napi_status ThreadSafeFunctionEx::NonBlockingCall( - DataType* data) const { +template +inline napi_status +ThreadSafeFunctionEx::NonBlockingCall( + DataType *data) const { return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); } -template -inline void ThreadSafeFunctionEx::Ref(napi_env env) const { +template +inline void +ThreadSafeFunctionEx::Ref(napi_env env) const { if (_tsfn != nullptr) { napi_status status = napi_ref_threadsafe_function(env, _tsfn); NAPI_THROW_IF_FAILED_VOID(env, status); } } -template -inline void ThreadSafeFunctionEx::Unref(napi_env env) const { +template +inline void +ThreadSafeFunctionEx::Unref(napi_env env) const { if (_tsfn != nullptr) { napi_status status = napi_unref_threadsafe_function(env, _tsfn); NAPI_THROW_IF_FAILED_VOID(env, status); } } -template -inline napi_status ThreadSafeFunctionEx::Acquire() const { +template +inline napi_status +ThreadSafeFunctionEx::Acquire() const { return napi_acquire_threadsafe_function(_tsfn); } -template -inline napi_status ThreadSafeFunctionEx::Release() { +template +inline napi_status +ThreadSafeFunctionEx::Release() { return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); } -template -inline napi_status ThreadSafeFunctionEx::Abort() { +template +inline napi_status +ThreadSafeFunctionEx::Abort() { return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); } -template -inline ContextType* ThreadSafeFunctionEx::GetContext() const { - void* context; +template +inline ContextType * +ThreadSafeFunctionEx::GetContext() const { + void *context; napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); - NAPI_FATAL_IF_FAILED(status, "ThreadSafeFunctionEx::GetContext", "napi_get_threadsafe_function_context"); - return static_cast(context); + NAPI_FATAL_IF_FAILED(status, "ThreadSafeFunctionEx::GetContext", + "napi_get_threadsafe_function_context"); + return static_cast(context); } // static -template -template -inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New(napi_env env, - const Function& callback, - const Object& resource, - ResourceString resourceName, - size_t maxQueueSize, - size_t initialThreadCount, - ContextType* context, - Finalizer finalizeCallback, - FinalizerDataType* data, - napi_finalize wrapper) { - static_assert(details::can_make_string::value - || std::is_convertible::value, - "Resource name should be convertible to the string type"); +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, const Function &callback, const Object &resource, + ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, + ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data, + napi_finalize wrapper) { + static_assert(details::can_make_string::value || + std::is_convertible::value, + "Resource name should be convertible to the string type"); ThreadSafeFunctionEx tsfn; - // details::ThreadSafeCallJs cb{call_js_cb}; - auto* finalizeData = new details::ThreadSafeFinalize({ data, finalizeCallback }); - napi_status status = napi_create_threadsafe_function(env, callback, resource, - Value::From(env, resourceName), maxQueueSize, initialThreadCount, - finalizeData, wrapper, context, - // [=](napi_env env, napi_value jsCallback, void* context, void* data) { - // if (env == nullptr && jsCallback == nullptr) { - // return; - // } - // call_js_cb( Napi::Env(env), Function(env, jsCallback), static_cast(context), data); - // }, - CallJsInternal, - &tsfn._tsfn); + auto *finalizeData = new details::ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, callback, resource, Value::From(env, resourceName), maxQueueSize, + initialThreadCount, finalizeData, wrapper, context, CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, ThreadSafeFunctionEx()); + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); } return tsfn; } -template -void ThreadSafeFunctionEx::CallJsInternal(napi_env env, napi_value jsCallback, - void *context, void *data) { +template +void ThreadSafeFunctionEx::CallJsInternal( + napi_env env, napi_value jsCallback, void *context, void *data) { - if (CallJs == nullptr && jsCallback != nullptr) { - Function(env, jsCallback).Call(0, nullptr); + if (CallJs == nullptr) { + if (jsCallback != nullptr) { + Function(env, jsCallback).Call(0, nullptr); + } } else { - CallJs(env, Function(env, jsCallback), static_cast(context), static_cast(data)); + CallJs(env, Function(env, jsCallback), static_cast(context), + static_cast(data)); } } - -// template -// inline void -// ThreadSafeFunctionEx::CallJS(napi_env env, napi_value jsCallback, -// void *context, void *data) { -// if (env == nullptr && jsCallback == nullptr) { -// return; -// } - -// if (data != nullptr) { -// auto* callbackWrapper = static_cast(data); -// (*callbackWrapper)(env, Function(env, jsCallback)); -// delete callbackWrapper; -// } else if (jsCallback != nullptr) { -// Function(env, jsCallback).Call({}); -// } -// } - //////////////////////////////////////////////////////////////////////////////// // ThreadSafeFunction class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index 17dacc87b..57bc183c2 100644 --- a/napi.h +++ b/napi.h @@ -2057,15 +2057,9 @@ namespace Napi { operator napi_threadsafe_function() const; - // // This API may be called from any thread. - // napi_status BlockingCall() const; - // This API may be called from any thread. napi_status BlockingCall(DataType* data = nullptr) const; - // // This API may be called from any thread. - // napi_status NonBlockingCall() const; - // This API may be called from any thread. napi_status NonBlockingCall(DataType* data = nullptr) const; diff --git a/test/binding.cc b/test/binding.cc index 499d9435a..09a8c754c 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -43,12 +43,13 @@ Object InitPromise(Env env); Object InitRunScript(Env env); #if (NAPI_VERSION > 3) Object InitThreadSafeFunctionCtx(Env env); -Object InitThreadSafeFunctionEx(Env env); Object InitThreadSafeFunctionExistingTsfn(Env env); Object InitThreadSafeFunctionPtr(Env env); Object InitThreadSafeFunctionSum(Env env); Object InitThreadSafeFunctionUnref(Env env); Object InitThreadSafeFunction(Env env); +Object InitThreadSafeFunctionExContext(Env env); +Object InitThreadSafeFunctionExSimple(Env env); #endif Object InitTypedArray(Env env); Object InitObjectWrap(Env env); @@ -101,12 +102,13 @@ Object Init(Env env, Object exports) { exports.Set("run_script", InitRunScript(env)); #if (NAPI_VERSION > 3) exports.Set("threadsafe_function_ctx", InitThreadSafeFunctionCtx(env)); - exports.Set("threadsafe_function_ex", InitThreadSafeFunctionEx(env)); exports.Set("threadsafe_function_existing_tsfn", InitThreadSafeFunctionExistingTsfn(env)); exports.Set("threadsafe_function_ptr", InitThreadSafeFunctionPtr(env)); exports.Set("threadsafe_function_sum", InitThreadSafeFunctionSum(env)); exports.Set("threadsafe_function_unref", InitThreadSafeFunctionUnref(env)); exports.Set("threadsafe_function", InitThreadSafeFunction(env)); + exports.Set("threadsafe_function_ex_context", InitThreadSafeFunctionExContext(env)); + exports.Set("threadsafe_function_ex_simple", InitThreadSafeFunctionExSimple(env)); #endif exports.Set("typedarray", InitTypedArray(env)); exports.Set("objectwrap", InitObjectWrap(env)); diff --git a/test/binding.gyp b/test/binding.gyp index 790bef5fa..dda3c7154 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -34,8 +34,9 @@ 'object/set_property.cc', 'promise.cc', 'run_script.cc', + 'threadsafe_function_ex/context.cc', + 'threadsafe_function_ex/simple.cc', 'threadsafe_function/threadsafe_function_ctx.cc', - 'threadsafe_function/threadsafe_function_ex.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', 'threadsafe_function/threadsafe_function_ptr.cc', 'threadsafe_function/threadsafe_function_sum.cc', diff --git a/test/index.js b/test/index.js index d91b54581..fd9340613 100644 --- a/test/index.js +++ b/test/index.js @@ -41,8 +41,9 @@ let testModules = [ 'object/set_property', 'promise', 'run_script', + 'threadsafe_function_ex/context', + 'threadsafe_function_ex/simple', 'threadsafe_function/threadsafe_function_ctx', - 'threadsafe_function/threadsafe_function_ex', 'threadsafe_function/threadsafe_function_existing_tsfn', 'threadsafe_function/threadsafe_function_ptr', 'threadsafe_function/threadsafe_function_sum', @@ -77,12 +78,13 @@ if (napiVersion < 4) { testModules.splice(testModules.indexOf('asyncprogressqueueworker'), 1); testModules.splice(testModules.indexOf('asyncprogressworker'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_ctx'), 1); - testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_ex'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_existing_tsfn'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_ptr'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_sum'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_unref'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function'), 1); + testModules.splice(testModules.indexOf('threadsafe_function_ex/context'), 1); + testModules.splice(testModules.indexOf('threadsafe_function_ex/simple'), 1); } if (napiVersion < 5) { diff --git a/test/threadsafe_function/threadsafe_function_ex.js b/test/threadsafe_function/threadsafe_function_ex.js deleted file mode 100644 index 8b6eb87aa..000000000 --- a/test/threadsafe_function/threadsafe_function_ex.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const buildType = process.config.target_defaults.default_configuration; - -module.exports = Promise.all([ - test(require(`../build/${buildType}/binding.node`)), - // test(require(`../build/${buildType}/binding_noexcept.node`)) -]); - -async function test(binding) { - const ctx = { }; - const tsfn = new binding.threadsafe_function_ex.TSFNWrap(ctx); - console.log("1"); - assert(ctx === await tsfn.getContextByCall(),"getContextByCall context not equal"); - console.log("2"); - assert(ctx === tsfn.getContextFromTsfn(),"getContextFromTsfn context not equal"); - console.log("3"); - console.log("releasing"); - await tsfn.release(); - console.log("released!"); -} diff --git a/test/threadsafe_function/threadsafe_function_ex.cc b/test/threadsafe_function_ex/context.cc similarity index 72% rename from test/threadsafe_function/threadsafe_function_ex.cc rename to test/threadsafe_function_ex/context.cc index b66148d1c..19df41882 100644 --- a/test/threadsafe_function/threadsafe_function_ex.cc +++ b/test/threadsafe_function_ex/context.cc @@ -9,21 +9,18 @@ namespace { // Context of our TSFN. using TSFNContext = Reference; -// Data passed to ThreadSafeFunctionEx::[Non]BlockingCall -struct CallJsData { - CallJsData(Napi::Env env) : deferred(Promise::Deferred::New(env)){}; - - Promise::Deferred deferred; -}; +// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall +using TSFNData = Promise::Deferred; // CallJs callback function static void CallJs(Napi::Env /*env*/, Napi::Function /*jsCallback*/, - TSFNContext *context, CallJsData *data) { - data->deferred.Resolve(context->Value()); + TSFNContext *context, TSFNData *data) { + data->Resolve(context->Value()); + delete data; } // Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; // A JS-accessible wrap that holds a TSFN. class TSFNWrap : public ObjectWrap { @@ -33,10 +30,9 @@ class TSFNWrap : public ObjectWrap { Napi::Value GetContextByCall(const CallbackInfo &info) { Napi::Env env = info.Env(); - std::unique_ptr callData = std::make_unique(env); - auto &deferred = callData->deferred; - _tsfn.BlockingCall(callData.release()); - return deferred.Promise(); + auto* callData = new TSFNData(env); + _tsfn.NonBlockingCall( callData ); + return callData->Promise(); }; Napi::Value GetContextFromTsfn(const CallbackInfo &) { @@ -69,10 +65,9 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) _deferred(Promise::Deferred::New(info.Env())) { Napi::Env env = info.Env(); - TSFNContext *ctx = new Reference; - *ctx = Persistent(info[0]); + TSFNContext *context = new TSFNContext(Persistent(info[0])); - _tsfn = ThreadSafeFunctionEx::New( + _tsfn = TSFN::New( info.Env(), // napi_env env, Function::New( env, @@ -81,7 +76,7 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) "Test", // ResourceString resourceName, 1, // size_t maxQueueSize, 1, // size_t initialThreadCount, - ctx, // ContextType* context, + context, // ContextType* context, [this](Napi::Env env, void *, TSFNContext *ctx) { // Finalizer finalizeCallback, @@ -93,7 +88,7 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) } } // namespace -Object InitThreadSafeFunctionEx(Env env) { +Object InitThreadSafeFunctionExContext(Env env) { return TSFNWrap::Init(env, Object::New(env)); } diff --git a/test/threadsafe_function_ex/context.js b/test/threadsafe_function_ex/context.js new file mode 100644 index 000000000..3e068cabd --- /dev/null +++ b/test/threadsafe_function_ex/context.js @@ -0,0 +1,17 @@ +'use strict'; + +const assert = require('assert'); +const buildType = process.config.target_defaults.default_configuration; + +module.exports = Promise.all([ + test(require(`../build/${buildType}/binding.node`)), + test(require(`../build/${buildType}/binding_noexcept.node`)) +]); + +async function test(binding) { + const ctx = {}; + const tsfn = new binding.threadsafe_function_ex_context.TSFNWrap(ctx); + assert(ctx === await tsfn.getContextByCall(), "getContextByCall context not equal"); + assert(ctx === tsfn.getContextFromTsfn(), "getContextFromTsfn context not equal"); + await tsfn.release(); +} diff --git a/test/threadsafe_function_ex/simple.cc b/test/threadsafe_function_ex/simple.cc new file mode 100644 index 000000000..178635be3 --- /dev/null +++ b/test/threadsafe_function_ex/simple.cc @@ -0,0 +1,66 @@ +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace { + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx<>; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { +public: + static Object Init(Napi::Env env, Object exports); + TSFNWrap(const CallbackInfo &info); + + Napi::Value Release(const CallbackInfo &) { + _tsfn.Release(); + return _deferred.Promise(); + }; + + Napi::Value Call(const CallbackInfo &info) { + _tsfn.NonBlockingCall(); + return info.Env().Undefined(); + }; + +private: + TSFN _tsfn; + Promise::Deferred _deferred; +}; + +Object TSFNWrap::Init(Napi::Env env, Object exports) { + Function func = DefineClass(env, "TSFNWrap", + {InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("release", &TSFNWrap::Release)}); + + exports.Set("TSFNWrap", func); + return exports; +} + +TSFNWrap::TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + + _tsfn = TSFN::New( + info.Env(), // napi_env env, + Function(), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 1, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + static_cast(nullptr), // ContextType* context, + [this](Napi::Env env, // Finalizer finalizeCallback, + void * /*data*/, + void * /*ctx*/) { _deferred.Resolve(env.Undefined()); }, + static_cast(nullptr) // FinalizerDataType* data, + ); +} +} // namespace + +Object InitThreadSafeFunctionExSimple(Env env) { + return TSFNWrap::Init(env, Object::New(env)); +} + +#endif diff --git a/test/threadsafe_function_ex/simple.js b/test/threadsafe_function_ex/simple.js new file mode 100644 index 000000000..ece0929e7 --- /dev/null +++ b/test/threadsafe_function_ex/simple.js @@ -0,0 +1,15 @@ +'use strict'; + +const buildType = process.config.target_defaults.default_configuration; + +module.exports = Promise.all([ + test(require(`../build/${buildType}/binding.node`)), + test(require(`../build/${buildType}/binding_noexcept.node`)) +]); + +async function test(binding) { + const ctx = {}; + const tsfn = new binding.threadsafe_function_ex_simple.TSFNWrap(ctx); + tsfn.call(); + await tsfn.release(); +} From 630d88bcea966ff4bce45aac49ffdd6cc3a12fe5 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Wed, 8 Apr 2020 18:04:24 +0200 Subject: [PATCH 06/39] statically check CallJs; add no-Finalizer overload --- napi-inl.h | 42 +++++++++++++++++++++------ napi.h | 14 +++++++-- test/threadsafe_function_ex/simple.cc | 18 ++++-------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/napi-inl.h b/napi-inl.h index 3e57acf2e..78540a437 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -196,6 +196,22 @@ struct ThreadSafeFinalize { FinalizerDataType* data; Finalizer callback; }; + +template +typename std::enable_if::type +static inline CallJsWrapper(napi_env env, napi_value jsCallback, void *context, void *data) { + call(env, Function(env, jsCallback), static_cast(context), + static_cast(data)); +} + +template +typename std::enable_if::type +static inline CallJsWrapper(napi_env env, napi_value jsCallback, void * /*context*/, + void * /*data*/) { + if (jsCallback != nullptr) { + Function(env, jsCallback).Call(0, nullptr); + } +} #endif template @@ -4260,6 +4276,20 @@ inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { // ThreadSafeFunctionEx class //////////////////////////////////////////////////////////////////////////////// +// static +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, const Function &callback, const Object &resource, + ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, + ContextType *context) { + return New( + env, callback, resource, resourceName, maxQueueSize, initialThreadCount, + context, [](Env, void *, ContextType *) {}, static_cast(nullptr)); +} + // static template @@ -4397,19 +4427,13 @@ ThreadSafeFunctionEx::New( return tsfn; } +// static template void ThreadSafeFunctionEx::CallJsInternal( napi_env env, napi_value jsCallback, void *context, void *data) { - - if (CallJs == nullptr) { - if (jsCallback != nullptr) { - Function(env, jsCallback).Call(0, nullptr); - } - } else { - CallJs(env, Function(env, jsCallback), static_cast(context), - static_cast(data)); - } + details::CallJsWrapper( + env, jsCallback, context, data); } //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index 57bc183c2..2eb8d3c73 100644 --- a/napi.h +++ b/napi.h @@ -2041,7 +2041,17 @@ namespace Napi { public: // This API may only be called from the main thread. - template + template + static ThreadSafeFunctionEx New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); + + // This API may only be called from the main thread. + template static ThreadSafeFunctionEx New(napi_env env, const Function& callback, const Object& resource, @@ -2050,7 +2060,7 @@ namespace Napi { size_t initialThreadCount, ContextType* context, Finalizer finalizeCallback, - FinalizerDataType* data); + FinalizerDataType* data = nullptr); ThreadSafeFunctionEx(); ThreadSafeFunctionEx(napi_threadsafe_function tsFunctionValue); diff --git a/test/threadsafe_function_ex/simple.cc b/test/threadsafe_function_ex/simple.cc index 178635be3..40cf20d3e 100644 --- a/test/threadsafe_function_ex/simple.cc +++ b/test/threadsafe_function_ex/simple.cc @@ -43,18 +43,12 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) : ObjectWrap(info), _deferred(Promise::Deferred::New(info.Env())) { - _tsfn = TSFN::New( - info.Env(), // napi_env env, - Function(), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 1, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - static_cast(nullptr), // ContextType* context, - [this](Napi::Env env, // Finalizer finalizeCallback, - void * /*data*/, - void * /*ctx*/) { _deferred.Resolve(env.Undefined()); }, - static_cast(nullptr) // FinalizerDataType* data, + _tsfn = TSFN::New(info.Env(), // napi_env env, + Function(), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 1, // size_t maxQueueSize, + 1 // size_t initialThreadCount ); } } // namespace From 9ff352c3f3ede093ec52f527ed05495271d8ff79 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Fri, 17 Apr 2020 14:32:34 +0200 Subject: [PATCH 07/39] add tsfn test with tsfn cb function call --- test/binding.cc | 2 + test/binding.gyp | 1 + test/index.js | 2 + test/threadsafe_function_ex/call.cc | 83 +++++++++++++++++++++++++++++ test/threadsafe_function_ex/call.js | 18 +++++++ 5 files changed, 106 insertions(+) create mode 100644 test/threadsafe_function_ex/call.cc create mode 100644 test/threadsafe_function_ex/call.js diff --git a/test/binding.cc b/test/binding.cc index 09a8c754c..1c98637cf 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -48,6 +48,7 @@ Object InitThreadSafeFunctionPtr(Env env); Object InitThreadSafeFunctionSum(Env env); Object InitThreadSafeFunctionUnref(Env env); Object InitThreadSafeFunction(Env env); +Object InitThreadSafeFunctionExCall(Env env); Object InitThreadSafeFunctionExContext(Env env); Object InitThreadSafeFunctionExSimple(Env env); #endif @@ -107,6 +108,7 @@ Object Init(Env env, Object exports) { exports.Set("threadsafe_function_sum", InitThreadSafeFunctionSum(env)); exports.Set("threadsafe_function_unref", InitThreadSafeFunctionUnref(env)); exports.Set("threadsafe_function", InitThreadSafeFunction(env)); + exports.Set("threadsafe_function_ex_call", InitThreadSafeFunctionExCall(env)); exports.Set("threadsafe_function_ex_context", InitThreadSafeFunctionExContext(env)); exports.Set("threadsafe_function_ex_simple", InitThreadSafeFunctionExSimple(env)); #endif diff --git a/test/binding.gyp b/test/binding.gyp index dda3c7154..15654cda1 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -34,6 +34,7 @@ 'object/set_property.cc', 'promise.cc', 'run_script.cc', + 'threadsafe_function_ex/call.cc', 'threadsafe_function_ex/context.cc', 'threadsafe_function_ex/simple.cc', 'threadsafe_function/threadsafe_function_ctx.cc', diff --git a/test/index.js b/test/index.js index fd9340613..bb54e29ba 100644 --- a/test/index.js +++ b/test/index.js @@ -41,6 +41,7 @@ let testModules = [ 'object/set_property', 'promise', 'run_script', + 'threadsafe_function_ex/call', 'threadsafe_function_ex/context', 'threadsafe_function_ex/simple', 'threadsafe_function/threadsafe_function_ctx', @@ -83,6 +84,7 @@ if (napiVersion < 4) { testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_sum'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_unref'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function'), 1); + testModules.splice(testModules.indexOf('threadsafe_function_ex/call'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/context'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/simple'), 1); } diff --git a/test/threadsafe_function_ex/call.cc b/test/threadsafe_function_ex/call.cc new file mode 100644 index 000000000..26951e45a --- /dev/null +++ b/test/threadsafe_function_ex/call.cc @@ -0,0 +1,83 @@ +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace { + +// Context of our TSFN. +using TSFNContext = void; + +// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall +struct TSFNData { + Reference data; + Promise::Deferred deferred; +}; + +// CallJs callback function +static void CallJs(Napi::Env env, Napi::Function jsCallback, + TSFNContext * /*context*/, TSFNData *data) { + jsCallback.Call(env.Undefined(), {data->data.Value()}); + data->deferred.Resolve(data->data.Value()); + delete data; +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { +public: + static Object Init(Napi::Env env, Object exports); + TSFNWrap(const CallbackInfo &info); + + Napi::Value DoCall(const CallbackInfo &info) { + Napi::Env env = info.Env(); + TSFNData *data = + new TSFNData{Napi::Reference(Persistent(info[0])), + Promise::Deferred::New(env)}; + _tsfn.NonBlockingCall(data); + return data->deferred.Promise(); + }; + + Napi::Value Release(const CallbackInfo &) { + _tsfn.Release(); + return _deferred.Promise(); + }; + +private: + TSFN _tsfn; + Promise::Deferred _deferred; +}; + +Object TSFNWrap::Init(Napi::Env env, Object exports) { + Function func = DefineClass(env, "TSFNWrap", + {InstanceMethod("doCall", &TSFNWrap::DoCall), + InstanceMethod("release", &TSFNWrap::Release)}); + + exports.Set("TSFNWrap", func); + return exports; +} + +TSFNWrap::TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + Napi::Env env = info.Env(); + Function callback = info[0].As(); + + _tsfn = TSFN::New(env, // napi_env env, + callback, // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1 // size_t initialThreadCount, + ); +} +} // namespace + +Object InitThreadSafeFunctionExCall(Env env) { + return TSFNWrap::Init(env, Object::New(env)); +} + +#endif diff --git a/test/threadsafe_function_ex/call.js b/test/threadsafe_function_ex/call.js new file mode 100644 index 000000000..152637548 --- /dev/null +++ b/test/threadsafe_function_ex/call.js @@ -0,0 +1,18 @@ +'use strict'; + +const assert = require('assert'); +const buildType = process.config.target_defaults.default_configuration; + +module.exports = Promise.all([ + test(require(`../build/${buildType}/binding.node`)), + test(require(`../build/${buildType}/binding_noexcept.node`)) +]); + +async function test(binding) { + const data = {}; + const tsfn = new binding.threadsafe_function_ex_call.TSFNWrap(tsfnData => { + assert(data === tsfnData, "Data in and out of tsfn call do not equal"); + }); + await tsfn.doCall(data); + await tsfn.release(); +} From 2da3023bf0d9a69dc993afd6f87565432dab8f33 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 8 Jun 2020 15:25:14 +0200 Subject: [PATCH 08/39] test: Initial commit of TSFNEx threadsafe test --- test/binding.cc | 2 + test/binding.gyp | 1 + test/index.js | 2 + test/threadsafe_function_ex/threadsafe.cc | 183 ++++++++++++++++++++++ test/threadsafe_function_ex/threadsafe.js | 170 ++++++++++++++++++++ 5 files changed, 358 insertions(+) create mode 100644 test/threadsafe_function_ex/threadsafe.cc create mode 100644 test/threadsafe_function_ex/threadsafe.js diff --git a/test/binding.cc b/test/binding.cc index 1c98637cf..3fb26726c 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -51,6 +51,7 @@ Object InitThreadSafeFunction(Env env); Object InitThreadSafeFunctionExCall(Env env); Object InitThreadSafeFunctionExContext(Env env); Object InitThreadSafeFunctionExSimple(Env env); +Object InitThreadSafeFunctionExThreadSafe(Env env); #endif Object InitTypedArray(Env env); Object InitObjectWrap(Env env); @@ -111,6 +112,7 @@ Object Init(Env env, Object exports) { exports.Set("threadsafe_function_ex_call", InitThreadSafeFunctionExCall(env)); exports.Set("threadsafe_function_ex_context", InitThreadSafeFunctionExContext(env)); exports.Set("threadsafe_function_ex_simple", InitThreadSafeFunctionExSimple(env)); + exports.Set("threadsafe_function_ex_threadsafe", InitThreadSafeFunctionExThreadSafe(env)); #endif exports.Set("typedarray", InitTypedArray(env)); exports.Set("objectwrap", InitObjectWrap(env)); diff --git a/test/binding.gyp b/test/binding.gyp index 15654cda1..fdd7bf263 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -37,6 +37,7 @@ 'threadsafe_function_ex/call.cc', 'threadsafe_function_ex/context.cc', 'threadsafe_function_ex/simple.cc', + 'threadsafe_function_ex/threadsafe.cc', 'threadsafe_function/threadsafe_function_ctx.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', 'threadsafe_function/threadsafe_function_ptr.cc', diff --git a/test/index.js b/test/index.js index bb54e29ba..772ef310d 100644 --- a/test/index.js +++ b/test/index.js @@ -44,6 +44,7 @@ let testModules = [ 'threadsafe_function_ex/call', 'threadsafe_function_ex/context', 'threadsafe_function_ex/simple', + 'threadsafe_function_ex/threadsafe', 'threadsafe_function/threadsafe_function_ctx', 'threadsafe_function/threadsafe_function_existing_tsfn', 'threadsafe_function/threadsafe_function_ptr', @@ -87,6 +88,7 @@ if (napiVersion < 4) { testModules.splice(testModules.indexOf('threadsafe_function_ex/call'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/context'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/simple'), 1); + testModules.splice(testModules.indexOf('threadsafe_function_ex/threadsafe'), 1); } if (napiVersion < 5) { diff --git a/test/threadsafe_function_ex/threadsafe.cc b/test/threadsafe_function_ex/threadsafe.cc new file mode 100644 index 000000000..370fb1ac1 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe.cc @@ -0,0 +1,183 @@ +#include +#include +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +constexpr size_t ARRAY_LENGTH = 10; +constexpr size_t MAX_QUEUE_SIZE = 2; + +static std::thread threadsEx[2]; +static ThreadSafeFunction tsfnEx; + +struct ThreadSafeFunctionInfo { + enum CallType { + DEFAULT, + BLOCKING, + NON_BLOCKING + } type; + bool abort; + bool startSecondary; + FunctionReference jsFinalizeCallback; + uint32_t maxQueueSize; +} tsfnInfoEx; + +// Thread data to transmit to JS +static int intsEx[ARRAY_LENGTH]; + +static void SecondaryThreadEx() { + if (tsfnEx.Release() != napi_ok) { + Error::Fatal("SecondaryThread", "ThreadSafeFunction.Release() failed"); + } +} + +// Source thread producing the data +static void DataSourceThreadEx() { + ThreadSafeFunctionInfo* info = tsfnEx.GetContext(); + + if (info->startSecondary) { + if (tsfnEx.Acquire() != napi_ok) { + Error::Fatal("DataSourceThread", "ThreadSafeFunction.Acquire() failed"); + } + + threadsEx[1] = std::thread(SecondaryThreadEx); + } + + bool queueWasFull = false; + bool queueWasClosing = false; + for (int index = ARRAY_LENGTH - 1; index > -1 && !queueWasClosing; index--) { + napi_status status = napi_generic_failure; + auto callback = [](Env env, Function jsCallback, int* data) { + jsCallback.Call({ Number::New(env, *data) }); + }; + + switch (info->type) { + case ThreadSafeFunctionInfo::DEFAULT: + status = tsfnEx.BlockingCall(); + break; + case ThreadSafeFunctionInfo::BLOCKING: + status = tsfnEx.BlockingCall(&intsEx[index], callback); + break; + case ThreadSafeFunctionInfo::NON_BLOCKING: + status = tsfnEx.NonBlockingCall(&intsEx[index], callback); + break; + } + + if (info->maxQueueSize == 0) { + // Let's make this thread really busy for 200 ms to give the main thread a + // chance to abort. + auto start = std::chrono::high_resolution_clock::now(); + constexpr auto MS_200 = std::chrono::milliseconds(200); + for (; std::chrono::high_resolution_clock::now() - start < MS_200;); + } + + switch (status) { + case napi_queue_full: + queueWasFull = true; + index++; + // fall through + + case napi_ok: + continue; + + case napi_closing: + queueWasClosing = true; + break; + + default: + Error::Fatal("DataSourceThread", "ThreadSafeFunction.*Call() failed"); + } + } + + if (info->type == ThreadSafeFunctionInfo::NON_BLOCKING && !queueWasFull) { + Error::Fatal("DataSourceThread", "Queue was never full"); + } + + if (info->abort && !queueWasClosing) { + Error::Fatal("DataSourceThread", "Queue was never closing"); + } + + if (!queueWasClosing && tsfnEx.Release() != napi_ok) { + Error::Fatal("DataSourceThread", "ThreadSafeFunction.Release() failed"); + } +} + +static Value StopThreadEx(const CallbackInfo& info) { + tsfnInfoEx.jsFinalizeCallback = Napi::Persistent(info[0].As()); + bool abort = info[1].As(); + if (abort) { + tsfnEx.Abort(); + } else { + tsfnEx.Release(); + } + return Value(); +} + +// Join the thread and inform JS that we're done. +static void JoinTheThreadsEx(Env /* env */, + std::thread* theThreads, + ThreadSafeFunctionInfo* info) { + theThreads[0].join(); + if (info->startSecondary) { + theThreads[1].join(); + } + + info->jsFinalizeCallback.Call({}); + info->jsFinalizeCallback.Reset(); +} + +static Value StartThreadInternalEx(const CallbackInfo& info, + ThreadSafeFunctionInfo::CallType type) { + tsfnInfoEx.type = type; + tsfnInfoEx.abort = info[1].As(); + tsfnInfoEx.startSecondary = info[2].As(); + tsfnInfoEx.maxQueueSize = info[3].As().Uint32Value(); + + tsfnEx = ThreadSafeFunction::New(info.Env(), info[0].As(), + "Test", tsfnInfoEx.maxQueueSize, 2, &tsfnInfoEx, JoinTheThreadsEx, threadsEx); + + threadsEx[0] = std::thread(DataSourceThreadEx); + + return Value(); +} + +static Value ReleaseEx(const CallbackInfo& /* info */) { + if (tsfnEx.Release() != napi_ok) { + Error::Fatal("Release", "ThreadSafeFunction.Release() failed"); + } + return Value(); +} + +static Value StartThreadEx(const CallbackInfo& info) { + return StartThreadInternalEx(info, ThreadSafeFunctionInfo::BLOCKING); +} + +static Value StartThreadNonblockingEx(const CallbackInfo& info) { + return StartThreadInternalEx(info, ThreadSafeFunctionInfo::NON_BLOCKING); +} + +static Value StartThreadNoNativeEx(const CallbackInfo& info) { + return StartThreadInternalEx(info, ThreadSafeFunctionInfo::DEFAULT); +} + +Object InitThreadSafeFunctionExThreadSafe(Env env) { + for (size_t index = 0; index < ARRAY_LENGTH; index++) { + intsEx[index] = index; + } + + Object exports = Object::New(env); + exports["ARRAY_LENGTH"] = Number::New(env, ARRAY_LENGTH); + exports["MAX_QUEUE_SIZE"] = Number::New(env, MAX_QUEUE_SIZE); + exports["startThread"] = Function::New(env, StartThreadEx); + exports["startThreadNoNative"] = Function::New(env, StartThreadNoNativeEx); + exports["startThreadNonblocking"] = + Function::New(env, StartThreadNonblockingEx); + exports["stopThread"] = Function::New(env, StopThreadEx); + exports["release"] = Function::New(env, ReleaseEx); + + return exports; +} + +#endif diff --git a/test/threadsafe_function_ex/threadsafe.js b/test/threadsafe_function_ex/threadsafe.js new file mode 100644 index 000000000..2649d580d --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe.js @@ -0,0 +1,170 @@ +'use strict'; + +const buildType = process.config.target_defaults.default_configuration; +const assert = require('assert'); +const common = require('../common'); + +test(require(`../build/${buildType}/binding.node`)); +test(require(`../build/${buildType}/binding_noexcept.node`)); + +function test(binding) { + const expectedArray = (function(arrayLength) { + const result = []; + for (let index = 0; index < arrayLength; index++) { + result.push(arrayLength - 1 - index); + } + return result; + })(binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH); + + function testWithJSMarshaller({ + threadStarter, + quitAfter, + abort, + maxQueueSize, + launchSecondary }) { + return new Promise((resolve) => { + const array = []; + binding.threadsafe_function_ex_threadsafe[threadStarter](function testCallback(value) { + array.push(value); + if (array.length === quitAfter) { + setImmediate(() => { + binding.threadsafe_function_ex_threadsafe.stopThread(common.mustCall(() => { + resolve(array); + }), !!abort); + }); + } + }, !!abort, !!launchSecondary, maxQueueSize); + if (threadStarter === 'startThreadNonblocking') { + // Let's make this thread really busy for a short while to ensure that + // the queue fills and the thread receives a napi_queue_full. + const start = Date.now(); + while (Date.now() - start < 200); + } + }); + } + + new Promise(function testWithoutJSMarshaller(resolve) { + let callCount = 0; + binding.threadsafe_function_ex_threadsafe.startThreadNoNative(function testCallback() { + callCount++; + + // The default call-into-JS implementation passes no arguments. + assert.strictEqual(arguments.length, 0); + if (callCount === binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH) { + setImmediate(() => { + binding.threadsafe_function_ex_threadsafe.stopThread(common.mustCall(() => { + resolve(); + }), false); + }); + } + }, false /* abort */, false /* launchSecondary */, + binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE); + }) + + // Start the thread in blocking mode, and assert that all values are passed. + // Quit after it's done. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThread', + maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, + quitAfter: binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH + })) + .then((result) => assert.deepStrictEqual(result, expectedArray)) + + // Start the thread in blocking mode with an infinite queue, and assert that + // all values are passed. Quit after it's done. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThread', + maxQueueSize: 0, + quitAfter: binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH + })) + .then((result) => assert.deepStrictEqual(result, expectedArray)) + + // Start the thread in non-blocking mode, and assert that all values are + // passed. Quit after it's done. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThreadNonblocking', + maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, + quitAfter: binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH + })) + .then((result) => assert.deepStrictEqual(result, expectedArray)) + + // Start the thread in blocking mode, and assert that all values are passed. + // Quit early, but let the thread finish. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThread', + maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, + quitAfter: 1 + })) + .then((result) => assert.deepStrictEqual(result, expectedArray)) + + // Start the thread in blocking mode with an infinite queue, and assert that + // all values are passed. Quit early, but let the thread finish. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThread', + maxQueueSize: 0, + quitAfter: 1 + })) + .then((result) => assert.deepStrictEqual(result, expectedArray)) + + + // Start the thread in non-blocking mode, and assert that all values are + // passed. Quit early, but let the thread finish. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThreadNonblocking', + maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, + quitAfter: 1 + })) + .then((result) => assert.deepStrictEqual(result, expectedArray)) + + // Start the thread in blocking mode, and assert that all values are passed. + // Quit early, but let the thread finish. Launch a secondary thread to test + // the reference counter incrementing functionality. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThread', + quitAfter: 1, + maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, + launchSecondary: true + })) + .then((result) => assert.deepStrictEqual(result, expectedArray)) + + // Start the thread in non-blocking mode, and assert that all values are + // passed. Quit early, but let the thread finish. Launch a secondary thread + // to test the reference counter incrementing functionality. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThreadNonblocking', + quitAfter: 1, + maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, + launchSecondary: true + })) + .then((result) => assert.deepStrictEqual(result, expectedArray)) + + // Start the thread in blocking mode, and assert that it could not finish. + // Quit early by aborting. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThread', + quitAfter: 1, + maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, + abort: true + })) + .then((result) => assert.strictEqual(result.indexOf(0), -1)) + + // Start the thread in blocking mode with an infinite queue, and assert that + // it could not finish. Quit early by aborting. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThread', + quitAfter: 1, + maxQueueSize: 0, + abort: true + })) + .then((result) => assert.strictEqual(result.indexOf(0), -1)) + + // Start the thread in non-blocking mode, and assert that it could not finish. + // Quit early and aborting. + .then(() => testWithJSMarshaller({ + threadStarter: 'startThreadNonblocking', + quitAfter: 1, + maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, + abort: true + })) + .then((result) => assert.strictEqual(result.indexOf(0), -1)) +} From 91e885948e5c99cd503eafa9d43211f3ff92a81f Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 8 Jun 2020 16:06:35 +0200 Subject: [PATCH 09/39] test: Modify TSFNEx threadsafe to use TSFNEx --- test/threadsafe_function_ex/threadsafe.cc | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/test/threadsafe_function_ex/threadsafe.cc b/test/threadsafe_function_ex/threadsafe.cc index 370fb1ac1..108191a22 100644 --- a/test/threadsafe_function_ex/threadsafe.cc +++ b/test/threadsafe_function_ex/threadsafe.cc @@ -10,7 +10,6 @@ constexpr size_t ARRAY_LENGTH = 10; constexpr size_t MAX_QUEUE_SIZE = 2; static std::thread threadsEx[2]; -static ThreadSafeFunction tsfnEx; struct ThreadSafeFunctionInfo { enum CallType { @@ -24,6 +23,19 @@ struct ThreadSafeFunctionInfo { uint32_t maxQueueSize; } tsfnInfoEx; +static void TSFNCallJS(Env env, Function jsCallback, + ThreadSafeFunctionInfo * /* context */, int *data) { + // If called with no data + if (data == nullptr) { + jsCallback.Call({}); + } else { + jsCallback.Call({Number::New(env, *data)}); + } +} + +using TSFN = ThreadSafeFunctionEx; +static TSFN tsfnEx; + // Thread data to transmit to JS static int intsEx[ARRAY_LENGTH]; @@ -49,19 +61,16 @@ static void DataSourceThreadEx() { bool queueWasClosing = false; for (int index = ARRAY_LENGTH - 1; index > -1 && !queueWasClosing; index--) { napi_status status = napi_generic_failure; - auto callback = [](Env env, Function jsCallback, int* data) { - jsCallback.Call({ Number::New(env, *data) }); - }; switch (info->type) { case ThreadSafeFunctionInfo::DEFAULT: status = tsfnEx.BlockingCall(); break; case ThreadSafeFunctionInfo::BLOCKING: - status = tsfnEx.BlockingCall(&intsEx[index], callback); + status = tsfnEx.BlockingCall(&intsEx[index]); break; case ThreadSafeFunctionInfo::NON_BLOCKING: - status = tsfnEx.NonBlockingCall(&intsEx[index], callback); + status = tsfnEx.NonBlockingCall(&intsEx[index]); break; } @@ -135,7 +144,7 @@ static Value StartThreadInternalEx(const CallbackInfo& info, tsfnInfoEx.startSecondary = info[2].As(); tsfnInfoEx.maxQueueSize = info[3].As().Uint32Value(); - tsfnEx = ThreadSafeFunction::New(info.Env(), info[0].As(), + tsfnEx = TSFN::New(info.Env(), info[0].As(), Object::New(info.Env()), "Test", tsfnInfoEx.maxQueueSize, 2, &tsfnInfoEx, JoinTheThreadsEx, threadsEx); threadsEx[0] = std::thread(DataSourceThreadEx); From 692fbe161371a597840e9cca61f51881bbacf894 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 8 Jun 2020 16:07:12 +0200 Subject: [PATCH 10/39] src,test: Add SIGTRAP to TSFNEx::CallJS --- napi-inl.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/napi-inl.h b/napi-inl.h index cd5294485..31a063507 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace Napi { @@ -4432,6 +4433,9 @@ template void ThreadSafeFunctionEx::CallJsInternal( napi_env env, napi_value jsCallback, void *context, void *data) { + if (env == nullptr) { + raise(SIGTRAP); + } details::CallJsWrapper( env, jsCallback, context, data); } From 89181da235299b39f9c22d5d8894f9b05de5db63 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Fri, 12 Jun 2020 21:16:16 +0200 Subject: [PATCH 11/39] test: fix TSFNEx tests --- napi-inl.h | 3 - test/threadsafe_function_ex/call.cc | 12 ++- test/threadsafe_function_ex/context.cc | 12 ++- test/threadsafe_function_ex/threadsafe.cc | 97 ++++++++++++----------- 4 files changed, 68 insertions(+), 56 deletions(-) diff --git a/napi-inl.h b/napi-inl.h index 261202565..e5c6399af 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4473,9 +4473,6 @@ template void ThreadSafeFunctionEx::CallJsInternal( napi_env env, napi_value jsCallback, void *context, void *data) { - if (env == nullptr) { - raise(SIGTRAP); - } details::CallJsWrapper( env, jsCallback, context, data); } diff --git a/test/threadsafe_function_ex/call.cc b/test/threadsafe_function_ex/call.cc index 26951e45a..2e41799b8 100644 --- a/test/threadsafe_function_ex/call.cc +++ b/test/threadsafe_function_ex/call.cc @@ -18,9 +18,15 @@ struct TSFNData { // CallJs callback function static void CallJs(Napi::Env env, Napi::Function jsCallback, TSFNContext * /*context*/, TSFNData *data) { - jsCallback.Call(env.Undefined(), {data->data.Value()}); - data->deferred.Resolve(data->data.Value()); - delete data; + if (!(env == nullptr || jsCallback == nullptr)) { + if (data != nullptr) { + jsCallback.Call(env.Undefined(), {data->data.Value()}); + data->deferred.Resolve(data->data.Value()); + } + } + if (data != nullptr) { + delete data; + } } // Full type of our ThreadSafeFunctionEx diff --git a/test/threadsafe_function_ex/context.cc b/test/threadsafe_function_ex/context.cc index 19df41882..c67ebaba2 100644 --- a/test/threadsafe_function_ex/context.cc +++ b/test/threadsafe_function_ex/context.cc @@ -13,10 +13,16 @@ using TSFNContext = Reference; using TSFNData = Promise::Deferred; // CallJs callback function -static void CallJs(Napi::Env /*env*/, Napi::Function /*jsCallback*/, +static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, TSFNContext *context, TSFNData *data) { - data->Resolve(context->Value()); - delete data; + if (env != nullptr) { + if (data != nullptr) { + data->Resolve(context->Value()); + } + } + if (data != nullptr) { + delete data; + } } // Full type of our ThreadSafeFunctionEx diff --git a/test/threadsafe_function_ex/threadsafe.cc b/test/threadsafe_function_ex/threadsafe.cc index 108191a22..cf3eddca8 100644 --- a/test/threadsafe_function_ex/threadsafe.cc +++ b/test/threadsafe_function_ex/threadsafe.cc @@ -9,9 +9,9 @@ using namespace Napi; constexpr size_t ARRAY_LENGTH = 10; constexpr size_t MAX_QUEUE_SIZE = 2; -static std::thread threadsEx[2]; +static std::thread threads[2]; -struct ThreadSafeFunctionInfo { +static struct ThreadSafeFunctionInfo { enum CallType { DEFAULT, BLOCKING, @@ -21,40 +21,43 @@ struct ThreadSafeFunctionInfo { bool startSecondary; FunctionReference jsFinalizeCallback; uint32_t maxQueueSize; -} tsfnInfoEx; +} tsfnInfo; static void TSFNCallJS(Env env, Function jsCallback, ThreadSafeFunctionInfo * /* context */, int *data) { - // If called with no data - if (data == nullptr) { - jsCallback.Call({}); - } else { - jsCallback.Call({Number::New(env, *data)}); + // A null environment signifies the threadsafe function has been finalized. + if (!(env == nullptr || jsCallback == nullptr)) { + // If called with no data + if (data == nullptr) { + jsCallback.Call({}); + } else { + jsCallback.Call({Number::New(env, *data)}); + } } } using TSFN = ThreadSafeFunctionEx; -static TSFN tsfnEx; +static TSFN tsfn; // Thread data to transmit to JS -static int intsEx[ARRAY_LENGTH]; +static int ints[ARRAY_LENGTH]; -static void SecondaryThreadEx() { - if (tsfnEx.Release() != napi_ok) { +static void SecondaryThread() { + if (tsfn.Release() != napi_ok) { Error::Fatal("SecondaryThread", "ThreadSafeFunction.Release() failed"); } } // Source thread producing the data -static void DataSourceThreadEx() { - ThreadSafeFunctionInfo* info = tsfnEx.GetContext(); +static void DataSourceThread() { + ThreadSafeFunctionInfo* info = tsfn.GetContext(); if (info->startSecondary) { - if (tsfnEx.Acquire() != napi_ok) { + if (tsfn.Acquire() != napi_ok) { Error::Fatal("DataSourceThread", "ThreadSafeFunction.Acquire() failed"); } - threadsEx[1] = std::thread(SecondaryThreadEx); + threads[1] = std::thread(SecondaryThread); } bool queueWasFull = false; @@ -64,13 +67,13 @@ static void DataSourceThreadEx() { switch (info->type) { case ThreadSafeFunctionInfo::DEFAULT: - status = tsfnEx.BlockingCall(); + status = tsfn.BlockingCall(); break; case ThreadSafeFunctionInfo::BLOCKING: - status = tsfnEx.BlockingCall(&intsEx[index]); + status = tsfn.BlockingCall(&ints[index]); break; case ThreadSafeFunctionInfo::NON_BLOCKING: - status = tsfnEx.NonBlockingCall(&intsEx[index]); + status = tsfn.NonBlockingCall(&ints[index]); break; } @@ -108,24 +111,24 @@ static void DataSourceThreadEx() { Error::Fatal("DataSourceThread", "Queue was never closing"); } - if (!queueWasClosing && tsfnEx.Release() != napi_ok) { + if (!queueWasClosing && tsfn.Release() != napi_ok) { Error::Fatal("DataSourceThread", "ThreadSafeFunction.Release() failed"); } } -static Value StopThreadEx(const CallbackInfo& info) { - tsfnInfoEx.jsFinalizeCallback = Napi::Persistent(info[0].As()); +static Value StopThread(const CallbackInfo& info) { + tsfnInfo.jsFinalizeCallback = Napi::Persistent(info[0].As()); bool abort = info[1].As(); if (abort) { - tsfnEx.Abort(); + tsfn.Abort(); } else { - tsfnEx.Release(); + tsfn.Release(); } return Value(); } // Join the thread and inform JS that we're done. -static void JoinTheThreadsEx(Env /* env */, +static void JoinTheThreads(Env /* env */, std::thread* theThreads, ThreadSafeFunctionInfo* info) { theThreads[0].join(); @@ -137,54 +140,54 @@ static void JoinTheThreadsEx(Env /* env */, info->jsFinalizeCallback.Reset(); } -static Value StartThreadInternalEx(const CallbackInfo& info, +static Value StartThreadInternal(const CallbackInfo& info, ThreadSafeFunctionInfo::CallType type) { - tsfnInfoEx.type = type; - tsfnInfoEx.abort = info[1].As(); - tsfnInfoEx.startSecondary = info[2].As(); - tsfnInfoEx.maxQueueSize = info[3].As().Uint32Value(); + tsfnInfo.type = type; + tsfnInfo.abort = info[1].As(); + tsfnInfo.startSecondary = info[2].As(); + tsfnInfo.maxQueueSize = info[3].As().Uint32Value(); - tsfnEx = TSFN::New(info.Env(), info[0].As(), Object::New(info.Env()), - "Test", tsfnInfoEx.maxQueueSize, 2, &tsfnInfoEx, JoinTheThreadsEx, threadsEx); + tsfn = TSFN::New(info.Env(), info[0].As(), Object::New(info.Env()), + "Test", tsfnInfo.maxQueueSize, 2, &tsfnInfo, JoinTheThreads, threads); - threadsEx[0] = std::thread(DataSourceThreadEx); + threads[0] = std::thread(DataSourceThread); return Value(); } -static Value ReleaseEx(const CallbackInfo& /* info */) { - if (tsfnEx.Release() != napi_ok) { +static Value Release(const CallbackInfo& /* info */) { + if (tsfn.Release() != napi_ok) { Error::Fatal("Release", "ThreadSafeFunction.Release() failed"); } return Value(); } -static Value StartThreadEx(const CallbackInfo& info) { - return StartThreadInternalEx(info, ThreadSafeFunctionInfo::BLOCKING); +static Value StartThread(const CallbackInfo& info) { + return StartThreadInternal(info, ThreadSafeFunctionInfo::BLOCKING); } -static Value StartThreadNonblockingEx(const CallbackInfo& info) { - return StartThreadInternalEx(info, ThreadSafeFunctionInfo::NON_BLOCKING); +static Value StartThreadNonblocking(const CallbackInfo& info) { + return StartThreadInternal(info, ThreadSafeFunctionInfo::NON_BLOCKING); } -static Value StartThreadNoNativeEx(const CallbackInfo& info) { - return StartThreadInternalEx(info, ThreadSafeFunctionInfo::DEFAULT); +static Value StartThreadNoNative(const CallbackInfo& info) { + return StartThreadInternal(info, ThreadSafeFunctionInfo::DEFAULT); } Object InitThreadSafeFunctionExThreadSafe(Env env) { for (size_t index = 0; index < ARRAY_LENGTH; index++) { - intsEx[index] = index; + ints[index] = index; } Object exports = Object::New(env); exports["ARRAY_LENGTH"] = Number::New(env, ARRAY_LENGTH); exports["MAX_QUEUE_SIZE"] = Number::New(env, MAX_QUEUE_SIZE); - exports["startThread"] = Function::New(env, StartThreadEx); - exports["startThreadNoNative"] = Function::New(env, StartThreadNoNativeEx); + exports["startThread"] = Function::New(env, StartThread); + exports["startThreadNoNative"] = Function::New(env, StartThreadNoNative); exports["startThreadNonblocking"] = - Function::New(env, StartThreadNonblockingEx); - exports["stopThread"] = Function::New(env, StopThreadEx); - exports["release"] = Function::New(env, ReleaseEx); + Function::New(env, StartThreadNonblocking); + exports["stopThread"] = Function::New(env, StopThread); + exports["release"] = Function::New(env, Release); return exports; } From 8fcdb4d72c44d0a3aba4990b1e46fc7abbec8e8d Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Fri, 12 Jun 2020 22:52:56 +0200 Subject: [PATCH 12/39] test: add barebones new tsfn test for use in docs --- test/binding.cc | 2 + test/binding.gyp | 1 + test/index.js | 2 + test/threadsafe_function_ex/example.cc | 103 +++++++++++++++++++++++++ test/threadsafe_function_ex/example.js | 20 +++++ 5 files changed, 128 insertions(+) create mode 100644 test/threadsafe_function_ex/example.cc create mode 100644 test/threadsafe_function_ex/example.js diff --git a/test/binding.cc b/test/binding.cc index 4b42f1c33..fd2ab85b0 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -50,6 +50,7 @@ Object InitThreadSafeFunctionUnref(Env env); Object InitThreadSafeFunction(Env env); Object InitThreadSafeFunctionExCall(Env env); Object InitThreadSafeFunctionExContext(Env env); +Object InitThreadSafeFunctionExExample(Env env); Object InitThreadSafeFunctionExSimple(Env env); Object InitThreadSafeFunctionExThreadSafe(Env env); #endif @@ -111,6 +112,7 @@ Object Init(Env env, Object exports) { exports.Set("threadsafe_function", InitThreadSafeFunction(env)); exports.Set("threadsafe_function_ex_call", InitThreadSafeFunctionExCall(env)); exports.Set("threadsafe_function_ex_context", InitThreadSafeFunctionExContext(env)); + exports.Set("threadsafe_function_ex_example", InitThreadSafeFunctionExExample(env)); exports.Set("threadsafe_function_ex_simple", InitThreadSafeFunctionExSimple(env)); exports.Set("threadsafe_function_ex_threadsafe", InitThreadSafeFunctionExThreadSafe(env)); #endif diff --git a/test/binding.gyp b/test/binding.gyp index 72f41fafa..288ad14d5 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -37,6 +37,7 @@ 'run_script.cc', 'threadsafe_function_ex/call.cc', 'threadsafe_function_ex/context.cc', + 'threadsafe_function_ex/example.cc', 'threadsafe_function_ex/simple.cc', 'threadsafe_function_ex/threadsafe.cc', 'threadsafe_function/threadsafe_function_ctx.cc', diff --git a/test/index.js b/test/index.js index 9f5cac225..bc8470a61 100644 --- a/test/index.js +++ b/test/index.js @@ -44,6 +44,7 @@ let testModules = [ 'run_script', 'threadsafe_function_ex/call', 'threadsafe_function_ex/context', + 'threadsafe_function_ex/example', 'threadsafe_function_ex/simple', 'threadsafe_function_ex/threadsafe', 'threadsafe_function/threadsafe_function_ctx', @@ -80,6 +81,7 @@ if (napiVersion < 4) { testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/call'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/context'), 1); + testModules.splice(testModules.indexOf('threadsafe_function_ex/example'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/simple'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/threadsafe'), 1); } diff --git a/test/threadsafe_function_ex/example.cc b/test/threadsafe_function_ex/example.cc new file mode 100644 index 000000000..1445ae688 --- /dev/null +++ b/test/threadsafe_function_ex/example.cc @@ -0,0 +1,103 @@ +/** + * This test is programmatically represents the example shown in + * `doc/threadsafe_function_ex.md` + */ + +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace { + +// Context of our TSFN. +struct Context {}; + +// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall +using DataType = int; + +// Callback function +static void Callback(Napi::Env env, Napi::Function jsCallback, Context *context, + DataType *data) { + // Check that the threadsafe function has not been finalized. Node calls this + // callback for items remaining on the queue once finalization has completed. + if (!(env == nullptr || jsCallback == nullptr)) { + } + if (data != nullptr) { + delete data; + } +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { + +public: + static Object Init(Napi::Env env, Object exports); + TSFNWrap(const CallbackInfo &info); + +private: + Napi::Value Start(const CallbackInfo &info); + Napi::Value Release(const CallbackInfo &info); +}; + +/** + * @brief Initialize `TSFNWrap` on the environment. + * + * @param env + * @param exports + * @return Object + */ +Object TSFNWrap::Init(Napi::Env env, Object exports) { + Function func = DefineClass(env, "TSFNWrap", + {InstanceMethod("start", &TSFNWrap::Start), + InstanceMethod("release", &TSFNWrap::Release)}); + + exports.Set("TSFNWrap", func); + return exports; +} + +/** + * @brief Construct a new TSFNWrap::TSFNWrap object + * + * @param info + */ +TSFNWrap::TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) {} +} // namespace + +/** + * @brief Instance method `TSFNWrap#start` + * + * @param info + * @return undefined + */ +Napi::Value TSFNWrap::Start(const CallbackInfo &info) { + Napi::Env env = info.Env(); + return env.Undefined(); +}; + +/** + * @brief Instance method `TSFNWrap#release` + * + * @param info + * @return undefined + */ +Napi::Value TSFNWrap::Release(const CallbackInfo &info) { + Napi::Env env = info.Env(); + return env.Undefined(); +}; + +/** + * @brief Module initialization function + * + * @param env + * @return Object + */ +Object InitThreadSafeFunctionExExample(Env env) { + return TSFNWrap::Init(env, Object::New(env)); +} + +#endif diff --git a/test/threadsafe_function_ex/example.js b/test/threadsafe_function_ex/example.js new file mode 100644 index 000000000..836120029 --- /dev/null +++ b/test/threadsafe_function_ex/example.js @@ -0,0 +1,20 @@ +'use strict'; + +/** + * This test is programmatically represents the example shown in + * `doc/threadsafe_function_ex.md` + */ + +const assert = require('assert'); +const buildType = process.config.target_defaults.default_configuration; + +module.exports = Promise.all([ + test(require(`../build/${buildType}/binding.node`)), + test(require(`../build/${buildType}/binding_noexcept.node`)) +]); + +async function test(binding) { + const tsfn = new binding.threadsafe_function_ex_example.TSFNWrap(); + await tsfn.start(); + await tsfn.release(); +} From cc8de121c0abb3994855352398c9a4647a5b1efa Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sat, 13 Jun 2020 06:09:22 +0200 Subject: [PATCH 13/39] implement optional function callback --- common.gypi | 2 +- napi-inl.h | 348 +++++++++++++++++++++++--- napi.h | 179 ++++++++++--- test/threadsafe_function_ex/call.cc | 1 - test/threadsafe_function_ex/simple.cc | 23 +- 5 files changed, 466 insertions(+), 87 deletions(-) diff --git a/common.gypi b/common.gypi index 088f961ea..9e35b384a 100644 --- a/common.gypi +++ b/common.gypi @@ -1,6 +1,6 @@ { 'variables': { - 'NAPI_VERSION%': " class //////////////////////////////////////////////////////////////////////////////// -// static +// Starting with NAPI 4, the JavaScript function `func` parameter of +// `napi_create_threadsafe_function` is optional. +#if NAPI_VERSION > 4 +// static, with Callback [missing] Resource [missing] Finalizer [missing] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context) { + ThreadSafeFunctionEx tsfn; + + napi_status status = napi_create_threadsafe_function( + env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, + initialThreadCount, nullptr, nullptr, context, + CallJsInternal, &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with Callback [nullptr] Resource [missing] Finalizer [missing] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, std::nullptr_t callback, ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context) { + ThreadSafeFunctionEx tsfn; + + napi_status status = napi_create_threadsafe_function( + env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, + initialThreadCount, nullptr, nullptr, context, + CallJsInternal, &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [passed] Finalizer [missing] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, const Object &resource, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { + ThreadSafeFunctionEx tsfn; + + napi_status status = napi_create_threadsafe_function( + env, nullptr, resource, String::From(env, resourceName), maxQueueSize, + initialThreadCount, nullptr, nullptr, context, CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with Callback [nullptr] Resource [passed] Finalizer [missing] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, std::nullptr_t callback, const Object &resource, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { + ThreadSafeFunctionEx tsfn; + + napi_status status = napi_create_threadsafe_function( + env, nullptr, resource, String::From(env, resourceName), maxQueueSize, + initialThreadCount, nullptr, nullptr, context, CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [missing] Finalizer [passed] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, + FinalizerDataType *data) { + ThreadSafeFunctionEx tsfn; + + auto *finalizeData = new details::ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, + initialThreadCount, finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, CallJsInternal, &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with Callback [nullptr] Resource [missing] Finalizer [passed] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, std::nullptr_t callback, ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, + FinalizerDataType *data) { + ThreadSafeFunctionEx tsfn; + + auto *finalizeData = new details::ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, + initialThreadCount, finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, CallJsInternal, &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with Callback [missing] Resource [passed] Finalizer [passed] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, const Object &resource, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data) { + ThreadSafeFunctionEx tsfn; + + auto *finalizeData = new details::ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, nullptr, resource, String::From(env, resourceName), maxQueueSize, + initialThreadCount, finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, CallJsInternal, &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with Callback [nullptr] Resource [passed] Finalizer [passed] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, std::nullptr_t callback, const Object &resource, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data) { + ThreadSafeFunctionEx tsfn; + + auto *finalizeData = new details::ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, nullptr, resource, String::From(env, resourceName), maxQueueSize, + initialThreadCount, finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, CallJsInternal, &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} +#endif + +// static, with Callback [passed] Resource [missing] Finalizer [missing] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, const Function &callback, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { + ThreadSafeFunctionEx tsfn; + + napi_status status = napi_create_threadsafe_function( + env, callback, nullptr, String::From(env, resourceName), maxQueueSize, + initialThreadCount, nullptr, nullptr, context, CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with Callback [x] Resource [x] Finalizer [missing] template template @@ -4326,12 +4560,51 @@ ThreadSafeFunctionEx::New( napi_env env, const Function &callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { - return New( - env, callback, resource, resourceName, maxQueueSize, initialThreadCount, - context, [](Env, void *, ContextType *) {}, static_cast(nullptr)); + ThreadSafeFunctionEx tsfn; + + napi_status status = napi_create_threadsafe_function( + env, callback, resource, String::From(env, resourceName), maxQueueSize, + initialThreadCount, nullptr, nullptr, context, CallJsInternal, + &tsfn._tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; } -// static +// static, with Callback [x] Resource [missing ] Finalizer [x] +template +template +inline ThreadSafeFunctionEx +ThreadSafeFunctionEx::New( + napi_env env, const Function &callback, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data) { + ThreadSafeFunctionEx tsfn; + + auto *finalizeData = new details::ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, callback, nullptr, String::From(env, resourceName), maxQueueSize, + initialThreadCount, finalizeData, + details::ThreadSafeFinalize:: + FinalizeFinalizeWrapperWithDataAndContext, + context, CallJsInternal, &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; +} + +// static, with: Callback [x] Resource [x] Finalizer [x] template template ::New( napi_env env, const Function &callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data) { - return New( - env, callback, resource, resourceName, maxQueueSize, initialThreadCount, - context, finalizeCallback, data, + ThreadSafeFunctionEx tsfn; + + auto *finalizeData = new details::ThreadSafeFinalize( + {data, finalizeCallback}); + napi_status status = napi_create_threadsafe_function( + env, callback, resource, String::From(env, resourceName), maxQueueSize, + initialThreadCount, finalizeData, details::ThreadSafeFinalize:: - FinalizeFinalizeWrapperWithDataAndContext); + FinalizeFinalizeWrapperWithDataAndContext, + context, CallJsInternal, &tsfn._tsfn); + if (status != napi_ok) { + delete finalizeData; + NAPI_THROW_IF_FAILED(env, status, + ThreadSafeFunctionEx()); + } + + return tsfn; } template ::GetContext() const { // static template -template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( - napi_env env, const Function &callback, const Object &resource, - ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, - ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data, - napi_finalize wrapper) { - static_assert(details::can_make_string::value || - std::is_convertible::value, - "Resource name should be convertible to the string type"); - - ThreadSafeFunctionEx tsfn; - - auto *finalizeData = new details::ThreadSafeFinalize( - {data, finalizeCallback}); - napi_status status = napi_create_threadsafe_function( - env, callback, resource, Value::From(env, resourceName), maxQueueSize, - initialThreadCount, finalizeData, wrapper, context, CallJsInternal, - &tsfn._tsfn); - if (status != napi_ok) { - delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); - } - - return tsfn; +void ThreadSafeFunctionEx::CallJsInternal( + napi_env env, napi_value jsCallback, void *context, void *data) { + details::CallJsWrapper( + env, jsCallback, context, data); } // static template -void ThreadSafeFunctionEx::CallJsInternal( - napi_env env, napi_value jsCallback, void *context, void *data) { - details::CallJsWrapper( - env, jsCallback, context, data); +typename ThreadSafeFunctionEx::DefaultFunctionType +ThreadSafeFunctionEx::DefaultFunctionFactory( + Napi::Env env) { +#if NAPI_VERSION > 4 + return nullptr; +#else + return Function::New(env, [](const CallbackInfo &cb) {}); +#endif } //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index eed310432..1fb7178f8 100644 --- a/napi.h +++ b/napi.h @@ -2043,42 +2043,151 @@ namespace Napi { }; #if (NAPI_VERSION > 3) - template + template class ThreadSafeFunctionEx { + private: +#if NAPI_VERSION > 4 + using DefaultFunctionType = std::nullptr_t; +#else + using DefaultFunctionType = const Napi::Function; +#endif + public: + // This API may only be called from the main thread. + // Helper function that returns nullptr if running N-API 5+, otherwise a + // non-empty, no-op Function. This provides the ability to specify at + // compile-time a callback parameter to `New` that safely does no action. + static DefaultFunctionType DefaultFunctionFactory(Napi::Env env); +#if NAPI_VERSION > 4 // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [missing] Finalizer [missing] template - static ThreadSafeFunctionEx New(napi_env env, - const Function& callback, - const Object& resource, - ResourceString resourceName, - size_t maxQueueSize, - size_t initialThreadCount, - ContextType* context = nullptr); + static ThreadSafeFunctionEx + New(napi_env env, ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context = nullptr); // This API may only be called from the main thread. - template - static ThreadSafeFunctionEx New(napi_env env, - const Function& callback, - const Object& resource, - ResourceString resourceName, - size_t maxQueueSize, - size_t initialThreadCount, - ContextType* context, - Finalizer finalizeCallback, - FinalizerDataType* data = nullptr); + // Callback [nullptr] Resource [missing] Finalizer [missing] + template + static ThreadSafeFunctionEx + New(napi_env env, std::nullptr_t callback, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, + ContextType *context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [passed] Finalizer [missing] + template + static ThreadSafeFunctionEx + New(napi_env env, const Object &resource, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, + ContextType *context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [nullptr] Resource [passed] Finalizer [missing] + template + static ThreadSafeFunctionEx + New(napi_env env, std::nullptr_t callback, const Object &resource, + ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [missing] Finalizer [passed] + template + static ThreadSafeFunctionEx + New(napi_env env, ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [nullptr] Resource [missing] Finalizer [passed] + template + static ThreadSafeFunctionEx + New(napi_env env, std::nullptr_t callback, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [missing] Resource [passed] Finalizer [passed] + template + static ThreadSafeFunctionEx + New(napi_env env, const Object &resource, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [nullptr] Resource [passed] Finalizer [passed] + template + static ThreadSafeFunctionEx + New(napi_env env, std::nullptr_t callback, const Object &resource, + ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data = nullptr); +#endif + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [missing] Finalizer [missing] + template + static ThreadSafeFunctionEx + New(napi_env env, const Function &callback, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, + ContextType *context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [passed] Finalizer [missing] + template + static ThreadSafeFunctionEx + New(napi_env env, const Function &callback, const Object &resource, + ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [missing] Finalizer [passed] + template + static ThreadSafeFunctionEx + New(napi_env env, const Function &callback, ResourceString resourceName, + size_t maxQueueSize, size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data = nullptr); + + // This API may only be called from the main thread. + // Creates a new threadsafe function with: + // Callback [passed] Resource [passed] Finalizer [passed] + template + static ThreadSafeFunctionEx + New(napi_env env, const Function &callback, const Object &resource, + ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data = nullptr); ThreadSafeFunctionEx(); - ThreadSafeFunctionEx(napi_threadsafe_function tsFunctionValue); + ThreadSafeFunctionEx( + napi_threadsafe_function tsFunctionValue); operator napi_threadsafe_function() const; // This API may be called from any thread. - napi_status BlockingCall(DataType* data = nullptr) const; + napi_status BlockingCall(DataType *data = nullptr) const; // This API may be called from any thread. - napi_status NonBlockingCall(DataType* data = nullptr) const; + napi_status NonBlockingCall(DataType *data = nullptr) const; // This API may only be called from the main thread. void Ref(napi_env env) const; @@ -2096,27 +2205,21 @@ namespace Napi { napi_status Abort(); // This API may be called from any thread. - ContextType* GetContext() const; + ContextType *GetContext() const; private: + template + static ThreadSafeFunctionEx + New(napi_env env, const Function &callback, const Object &resource, + ResourceString resourceName, size_t maxQueueSize, + size_t initialThreadCount, ContextType *context, + Finalizer finalizeCallback, FinalizerDataType *data, + napi_finalize wrapper); - template - static ThreadSafeFunctionEx New(napi_env env, - const Function& callback, - const Object& resource, - ResourceString resourceName, - size_t maxQueueSize, - size_t initialThreadCount, - ContextType* context, - Finalizer finalizeCallback, - FinalizerDataType* data, - napi_finalize wrapper); + static void CallJsInternal(napi_env env, napi_value jsCallback, + void *context, void *data); - static void CallJsInternal(napi_env env, - napi_value jsCallback, - void* context, - void* data); protected: napi_threadsafe_function _tsfn; }; diff --git a/test/threadsafe_function_ex/call.cc b/test/threadsafe_function_ex/call.cc index 2e41799b8..7d799a93d 100644 --- a/test/threadsafe_function_ex/call.cc +++ b/test/threadsafe_function_ex/call.cc @@ -74,7 +74,6 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) _tsfn = TSFN::New(env, // napi_env env, callback, // const Function& callback, - Value(), // const Object& resource, "Test", // ResourceString resourceName, 0, // size_t maxQueueSize, 1 // size_t initialThreadCount, diff --git a/test/threadsafe_function_ex/simple.cc b/test/threadsafe_function_ex/simple.cc index 40cf20d3e..961facf14 100644 --- a/test/threadsafe_function_ex/simple.cc +++ b/test/threadsafe_function_ex/simple.cc @@ -43,13 +43,24 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) : ObjectWrap(info), _deferred(Promise::Deferred::New(info.Env())) { - _tsfn = TSFN::New(info.Env(), // napi_env env, - Function(), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 1, // size_t maxQueueSize, - 1 // size_t initialThreadCount + auto env = info.Env(); +#if NAPI_VERSION == 4 + // A threadsafe function on N-API 4 still requires a callback function. + _tsfn = + TSFN::New(env, // napi_env env, + TSFN::DefaultFunctionFactory( + env), // N-API 5+: nullptr; else: const Function& callback, + "Test", // ResourceString resourceName, + 1, // size_t maxQueueSize, + 1 // size_t initialThreadCount + ); +#else + _tsfn = TSFN::New(env, // napi_env env, + "Test", // ResourceString resourceName, + 1, // size_t maxQueueSize, + 1 // size_t initialThreadCount ); +#endif } } // namespace From 6706f969023660c0e790bcf063be7385b3065bd3 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sat, 13 Jun 2020 12:26:01 +0200 Subject: [PATCH 14/39] clean up optional callback implementation --- test/threadsafe_function_ex/call.js | 9 + test/threadsafe_function_ex/context.cc | 4 +- test/threadsafe_function_ex/context.js | 10 ++ test/threadsafe_function_ex/simple.cc | 190 +++++++++++++++++++++- test/threadsafe_function_ex/simple.js | 52 +++++- test/threadsafe_function_ex/threadsafe.js | 3 + 6 files changed, 259 insertions(+), 9 deletions(-) diff --git a/test/threadsafe_function_ex/call.js b/test/threadsafe_function_ex/call.js index 152637548..344369370 100644 --- a/test/threadsafe_function_ex/call.js +++ b/test/threadsafe_function_ex/call.js @@ -8,6 +8,15 @@ module.exports = Promise.all([ test(require(`../build/${buildType}/binding_noexcept.node`)) ]); +/** + * This test ensures the data sent to the NonBlockingCall and the data received + * in the JavaScript callback are the same. + * - Creates a contexted threadsafe function with callback. + * - Makes one call, and waits for call to complete. + * - The callback forwards the item's data to the given JavaScript function in + * the test. + * - Asserts the data is the same. + */ async function test(binding) { const data = {}; const tsfn = new binding.threadsafe_function_ex_call.TSFNWrap(tsfnData => { diff --git a/test/threadsafe_function_ex/context.cc b/test/threadsafe_function_ex/context.cc index c67ebaba2..1170eb913 100644 --- a/test/threadsafe_function_ex/context.cc +++ b/test/threadsafe_function_ex/context.cc @@ -36,8 +36,8 @@ class TSFNWrap : public ObjectWrap { Napi::Value GetContextByCall(const CallbackInfo &info) { Napi::Env env = info.Env(); - auto* callData = new TSFNData(env); - _tsfn.NonBlockingCall( callData ); + auto *callData = new TSFNData(env); + _tsfn.NonBlockingCall(callData); return callData->Promise(); }; diff --git a/test/threadsafe_function_ex/context.js b/test/threadsafe_function_ex/context.js index 3e068cabd..95e3b8914 100644 --- a/test/threadsafe_function_ex/context.js +++ b/test/threadsafe_function_ex/context.js @@ -8,6 +8,16 @@ module.exports = Promise.all([ test(require(`../build/${buildType}/binding_noexcept.node`)) ]); +/** + * The context provided to the threadsafe function's constructor is accessible + * on both the threadsafe function's callback as well the threadsafe function + * itself. This test ensures the context across all three are the same. + * - Creates a contexted threadsafe function with callback. + * - The callback forwards the item's data to the given JavaScript function in + * the test. + * - Makes one call, and waits for call to complete. + * - Asserts the contexts are the same. + */ async function test(binding) { const ctx = {}; const tsfn = new binding.threadsafe_function_ex_context.TSFNWrap(ctx); diff --git a/test/threadsafe_function_ex/simple.cc b/test/threadsafe_function_ex/simple.cc index 961facf14..fb62f756b 100644 --- a/test/threadsafe_function_ex/simple.cc +++ b/test/threadsafe_function_ex/simple.cc @@ -4,7 +4,7 @@ using namespace Napi; -namespace { +namespace simple { // Full type of our ThreadSafeFunctionEx using TSFN = ThreadSafeFunctionEx<>; @@ -62,10 +62,194 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) ); #endif } -} // namespace +} // namespace simple +namespace existing { + +struct DataType { + Promise::Deferred deferred; + bool reject; +}; + +// CallJs callback function provided to `napi_create_threadsafe_function`. It is +// _NOT_ used by `Napi::ThreadSafeFunctionEx<>`. +static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, + void *data) { + DataType *casted = static_cast(data); + if (env != nullptr) { + if (data != nullptr) { + napi_value undefined; + napi_status status = napi_get_undefined(env, &undefined); + NAPI_THROW_IF_FAILED(env, status); + if (casted->reject) { + casted->deferred.Reject(undefined); + } else { + casted->deferred.Resolve(undefined); + } + } + } + if (casted != nullptr) { + delete casted; + } +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. +class ExistingTSFNWrap : public ObjectWrap { +public: + static Object Init(Napi::Env env, Object exports); + ExistingTSFNWrap(const CallbackInfo &info); + + Napi::Value Release(const CallbackInfo &) { + _tsfn.Release(); + return _deferred.Promise(); + }; + + Napi::Value Call(const CallbackInfo &info) { + auto *data = + new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + _tsfn.NonBlockingCall(data); + return data->deferred.Promise(); + }; + +private: + TSFN _tsfn; + Promise::Deferred _deferred; +}; + +Object ExistingTSFNWrap::Init(Napi::Env env, Object exports) { + Function func = + DefineClass(env, "ExistingTSFNWrap", + {InstanceMethod("call", &ExistingTSFNWrap::Call), + InstanceMethod("release", &ExistingTSFNWrap::Release)}); + + exports.Set("ExistingTSFNWrap", func); + return exports; +} + +ExistingTSFNWrap::ExistingTSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + + auto env = info.Env(); +#if NAPI_VERSION == 4 + napi_threadsafe_function napi_tsfn; + auto status = napi_create_threadsafe_function( + info.Env(), TSFN::DefaultFunctionFactory(env), nullptr, + String::From(info.Env(), "Test"), 0, 1, nullptr, nullptr, nullptr, CallJs, + &napi_tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status); + } + // A threadsafe function on N-API 4 still requires a callback function. + _tsfn = TSFN(napi_tsfn); +#else + napi_threadsafe_function napi_tsfn; + auto status = napi_create_threadsafe_function( + info.Env(), nullptr, nullptr, String::From(info.Env(), "Test"), 0, 1, + nullptr, nullptr, nullptr, CallJs, &napi_tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status); + } + _tsfn = TSFN(napi_tsfn); +#endif +} +} // namespace existing + +namespace empty { +#if NAPI_VERSION > 4 + +using Context = void; + +struct DataType { + Promise::Deferred deferred; + bool reject; +}; + +// CallJs callback function +static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/, + DataType *data) { + if (env != nullptr) { + if (data != nullptr) { + if (data->reject) { + data->deferred.Reject(env.Undefined()); + } else { + data->deferred.Resolve(env.Undefined()); + } + } + } + if (data != nullptr) { + delete data; + } +} + +// Full type of our ThreadSafeFunctionEx +using EmptyTSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. +class EmptyTSFNWrap : public ObjectWrap { +public: + static Object Init(Napi::Env env, Object exports); + EmptyTSFNWrap(const CallbackInfo &info); + + Napi::Value Release(const CallbackInfo &) { + _tsfn.Release(); + return _deferred.Promise(); + }; + + Napi::Value Call(const CallbackInfo &info) { + if (info.Length() == 0 || !info[0].IsBoolean()) { + NAPI_THROW( + Napi::TypeError::New(info.Env(), "Expected argument 0 to be boolean"), + Value()); + } + + auto *data = + new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + _tsfn.NonBlockingCall(data); + return data->deferred.Promise(); + }; + +private: + EmptyTSFN _tsfn; + Promise::Deferred _deferred; +}; + +Object EmptyTSFNWrap::Init(Napi::Env env, Object exports) { + Function func = + DefineClass(env, "EmptyTSFNWrap", + {InstanceMethod("call", &EmptyTSFNWrap::Call), + InstanceMethod("release", &EmptyTSFNWrap::Release)}); + + exports.Set("EmptyTSFNWrap", func); + return exports; +} + +EmptyTSFNWrap::EmptyTSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + + auto env = info.Env(); + _tsfn = EmptyTSFN::New(env, // napi_env env, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1 // size_t initialThreadCount + ); +} +#endif +} // namespace empty Object InitThreadSafeFunctionExSimple(Env env) { - return TSFNWrap::Init(env, Object::New(env)); + +#if NAPI_VERSION > 4 + return empty::EmptyTSFNWrap::Init( + env, existing::ExistingTSFNWrap::Init( + env, simple::TSFNWrap::Init(env, Object::New(env)))); +#else + return existing::ExistingTSFNWrap::Init( + env, simple::TSFNWrap::Init(env, Object::New(env))); +#endif } #endif diff --git a/test/threadsafe_function_ex/simple.js b/test/threadsafe_function_ex/simple.js index ece0929e7..b3a6b0cf5 100644 --- a/test/threadsafe_function_ex/simple.js +++ b/test/threadsafe_function_ex/simple.js @@ -1,15 +1,59 @@ 'use strict'; +const assert = require('assert'); const buildType = process.config.target_defaults.default_configuration; module.exports = Promise.all([ test(require(`../build/${buildType}/binding.node`)), test(require(`../build/${buildType}/binding_noexcept.node`)) -]); +].reduce((p, c) => p.concat(c)), []); -async function test(binding) { - const ctx = {}; - const tsfn = new binding.threadsafe_function_ex_simple.TSFNWrap(ctx); +function test(binding) { + return [ + // testSimple(binding), + testEmpty(binding) + ]; +} + +/** + * A simple, fire-and-forget test. + * - Creates a threadsafe function with no context or callback. + * - The node-addon-api 'no callback' feature is implemented by passing either a + * no-op `Function` on N-API 4 or `std::nullptr` on N-API 5+ to the underlying + * `napi_create_threadsafe_function` call. + * - Makes one call, releases, then waits for finalization. + * - Inherently ignores the state of the item once it has been added to the + * queue. Since there are no callbacks or context, it is impossible to capture + * the state. + */ +async function testSimple(binding) { + const tsfn = new binding.threadsafe_function_ex_simple.TSFNWrap(); tsfn.call(); await tsfn.release(); } + +/** + * **ONLY ON N-API 5+**. The optional JavaScript function callback feature is + * not available in N-API <= 4. + * - Creates a threadsafe function with no JavaScript context or callback. + * - Makes two calls, expecting the first to resolve and the second to reject. + * - Waits for Node to process the items on the queue prior releasing the + * threadsafe function. + */ +async function testEmpty(binding) { + const { EmptyTSFNWrap } = binding.threadsafe_function_ex_simple; + + if (typeof EmptyTSFNWrap === 'function') { + const tsfn = new binding.threadsafe_function_ex_simple.EmptyTSFNWrap(); + await tsfn.call(false /* reject */); + let caught = false; + try { + await tsfn.call(true /* reject */); + } catch (ex) { + caught = true; + } + + assert.ok(caught, 'The promise rejection was not caught'); + await tsfn.release(); + } +} diff --git a/test/threadsafe_function_ex/threadsafe.js b/test/threadsafe_function_ex/threadsafe.js index 2649d580d..f9789db58 100644 --- a/test/threadsafe_function_ex/threadsafe.js +++ b/test/threadsafe_function_ex/threadsafe.js @@ -7,6 +7,9 @@ const common = require('../common'); test(require(`../build/${buildType}/binding.node`)); test(require(`../build/${buildType}/binding_noexcept.node`)); +/** + * This spec replicates the non-`Ex` multi-threaded spec using the `Ex` API. + */ function test(binding) { const expectedArray = (function(arrayLength) { const result = []; From 89d2dea08eae53e8d130028fc7863d22f4d06148 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 14 Jun 2020 14:19:14 +0200 Subject: [PATCH 15/39] test: wip with tsfnex tests --- test/binding.cc | 8 +- test/binding.gyp | 4 +- test/index.js | 66 +-- test/threadsafe_function_ex/README.md | 5 + test/threadsafe_function_ex/call.cc | 88 ---- test/threadsafe_function_ex/call.js | 27 -- test/threadsafe_function_ex/context.cc | 101 ---- test/threadsafe_function_ex/context.js | 27 -- test/threadsafe_function_ex/example.cc | 103 ----- test/threadsafe_function_ex/example.js | 20 - test/threadsafe_function_ex/index.js | 5 + test/threadsafe_function_ex/simple.cc | 255 ----------- test/threadsafe_function_ex/simple.js | 59 --- test/threadsafe_function_ex/test/basic.cc | 432 ++++++++++++++++++ test/threadsafe_function_ex/test/basic.js | 188 ++++++++ test/threadsafe_function_ex/test/example.cc | 257 +++++++++++ test/threadsafe_function_ex/test/example.js | 44 ++ .../{ => test}/threadsafe.cc | 1 + .../{ => test}/threadsafe.js | 19 +- 19 files changed, 982 insertions(+), 727 deletions(-) create mode 100644 test/threadsafe_function_ex/README.md delete mode 100644 test/threadsafe_function_ex/call.cc delete mode 100644 test/threadsafe_function_ex/call.js delete mode 100644 test/threadsafe_function_ex/context.cc delete mode 100644 test/threadsafe_function_ex/context.js delete mode 100644 test/threadsafe_function_ex/example.cc delete mode 100644 test/threadsafe_function_ex/example.js create mode 100644 test/threadsafe_function_ex/index.js delete mode 100644 test/threadsafe_function_ex/simple.cc delete mode 100644 test/threadsafe_function_ex/simple.js create mode 100644 test/threadsafe_function_ex/test/basic.cc create mode 100644 test/threadsafe_function_ex/test/basic.js create mode 100644 test/threadsafe_function_ex/test/example.cc create mode 100644 test/threadsafe_function_ex/test/example.js rename test/threadsafe_function_ex/{ => test}/threadsafe.cc (99%) rename test/threadsafe_function_ex/{ => test}/threadsafe.js (92%) diff --git a/test/binding.cc b/test/binding.cc index fd2ab85b0..d471831d6 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -48,10 +48,8 @@ Object InitThreadSafeFunctionPtr(Env env); Object InitThreadSafeFunctionSum(Env env); Object InitThreadSafeFunctionUnref(Env env); Object InitThreadSafeFunction(Env env); -Object InitThreadSafeFunctionExCall(Env env); -Object InitThreadSafeFunctionExContext(Env env); +Object InitThreadSafeFunctionExBasic(Env env); Object InitThreadSafeFunctionExExample(Env env); -Object InitThreadSafeFunctionExSimple(Env env); Object InitThreadSafeFunctionExThreadSafe(Env env); #endif Object InitTypedArray(Env env); @@ -110,10 +108,8 @@ Object Init(Env env, Object exports) { exports.Set("threadsafe_function_sum", InitThreadSafeFunctionSum(env)); exports.Set("threadsafe_function_unref", InitThreadSafeFunctionUnref(env)); exports.Set("threadsafe_function", InitThreadSafeFunction(env)); - exports.Set("threadsafe_function_ex_call", InitThreadSafeFunctionExCall(env)); - exports.Set("threadsafe_function_ex_context", InitThreadSafeFunctionExContext(env)); + exports.Set("threadsafe_function_ex_basic", InitThreadSafeFunctionExBasic(env)); exports.Set("threadsafe_function_ex_example", InitThreadSafeFunctionExExample(env)); - exports.Set("threadsafe_function_ex_simple", InitThreadSafeFunctionExSimple(env)); exports.Set("threadsafe_function_ex_threadsafe", InitThreadSafeFunctionExThreadSafe(env)); #endif exports.Set("typedarray", InitTypedArray(env)); diff --git a/test/binding.gyp b/test/binding.gyp index 288ad14d5..923cb2f55 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -35,10 +35,8 @@ 'object/set_property.cc', 'promise.cc', 'run_script.cc', - 'threadsafe_function_ex/call.cc', - 'threadsafe_function_ex/context.cc', + 'threadsafe_function_ex/basic.cc', 'threadsafe_function_ex/example.cc', - 'threadsafe_function_ex/simple.cc', 'threadsafe_function_ex/threadsafe.cc', 'threadsafe_function/threadsafe_function_ctx.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', diff --git a/test/index.js b/test/index.js index bc8470a61..a719c8d31 100644 --- a/test/index.js +++ b/test/index.js @@ -42,11 +42,7 @@ let testModules = [ 'object/set_property', 'promise', 'run_script', - 'threadsafe_function_ex/call', - 'threadsafe_function_ex/context', - 'threadsafe_function_ex/example', - 'threadsafe_function_ex/simple', - 'threadsafe_function_ex/threadsafe', + 'threadsafe_function_ex', 'threadsafe_function/threadsafe_function_ctx', 'threadsafe_function/threadsafe_function_existing_tsfn', 'threadsafe_function/threadsafe_function_ptr', @@ -79,10 +75,8 @@ if (napiVersion < 4) { testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_sum'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_unref'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function'), 1); - testModules.splice(testModules.indexOf('threadsafe_function_ex/call'), 1); - testModules.splice(testModules.indexOf('threadsafe_function_ex/context'), 1); + testModules.splice(testModules.indexOf('threadsafe_function_ex/basic'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/example'), 1); - testModules.splice(testModules.indexOf('threadsafe_function_ex/simple'), 1); testModules.splice(testModules.indexOf('threadsafe_function_ex/threadsafe'), 1); } @@ -96,35 +90,41 @@ if (napiVersion < 6) { testModules.splice(testModules.indexOf('addon_data'), 1); } -if (typeof global.gc === 'function') { - console.log(`Testing with N-API Version '${napiVersion}'.`); +async function run() { + if (typeof global.gc === 'function') { + console.log(`Testing with N-API Version '${napiVersion}'.`); - console.log('Starting test suite\n'); + console.log('Starting test suite\n'); - // Requiring each module runs tests in the module. - testModules.forEach(name => { - console.log(`Running test '${name}'`); - require('./' + name); - }); + // Requiring each module runs tests in the module. + testModules.forEach(name => { + console.log(`Running test '${name}'`); + require('./' + name); + }); - console.log('\nAll tests passed!'); -} else { - // Construct the correct (version-dependent) command-line args. - let args = ['--expose-gc', '--no-concurrent-array-buffer-freeing']; - if (majorNodeVersion >= 14) { - args.push('--no-concurrent-array-buffer-sweeping'); - } - args.push(__filename); + console.log('\nAll tests passed!'); + } else { + // Construct the correct (version-dependent) command-line args. + let args = ['--expose-gc', '--no-concurrent-array-buffer-freeing']; + if (majorNodeVersion >= 14) { + args.push('--no-concurrent-array-buffer-sweeping'); + } + args.push(__filename); - const child = require('./napi_child').spawnSync(process.argv[0], args, { - stdio: 'inherit', - }); + const child = require('./napi_child').spawnSync(process.argv[0], args, { + stdio: 'inherit', + }); - if (child.signal) { - console.error(`Tests aborted with ${child.signal}`); - process.exitCode = 1; - } else { - process.exitCode = child.status; + if (child.signal) { + console.error(`Tests aborted with ${child.signal}`); + process.exitCode = 1; + } else { + process.exitCode = child.status; + } + process.exit(process.exitCode); } - process.exit(process.exitCode); } + +run() + .catch(e => (console.error(e), process.exit(1))); + diff --git a/test/threadsafe_function_ex/README.md b/test/threadsafe_function_ex/README.md new file mode 100644 index 000000000..139838ae5 --- /dev/null +++ b/test/threadsafe_function_ex/README.md @@ -0,0 +1,5 @@ +# Napi::ThreadSafeFunctionEx tests + +|Spec|Test|Native|Node|Description| +|----|---|---|---|---| +|call \ No newline at end of file diff --git a/test/threadsafe_function_ex/call.cc b/test/threadsafe_function_ex/call.cc deleted file mode 100644 index 7d799a93d..000000000 --- a/test/threadsafe_function_ex/call.cc +++ /dev/null @@ -1,88 +0,0 @@ -#include "napi.h" - -#if (NAPI_VERSION > 3) - -using namespace Napi; - -namespace { - -// Context of our TSFN. -using TSFNContext = void; - -// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall -struct TSFNData { - Reference data; - Promise::Deferred deferred; -}; - -// CallJs callback function -static void CallJs(Napi::Env env, Napi::Function jsCallback, - TSFNContext * /*context*/, TSFNData *data) { - if (!(env == nullptr || jsCallback == nullptr)) { - if (data != nullptr) { - jsCallback.Call(env.Undefined(), {data->data.Value()}); - data->deferred.Resolve(data->data.Value()); - } - } - if (data != nullptr) { - delete data; - } -} - -// Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; - -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { -public: - static Object Init(Napi::Env env, Object exports); - TSFNWrap(const CallbackInfo &info); - - Napi::Value DoCall(const CallbackInfo &info) { - Napi::Env env = info.Env(); - TSFNData *data = - new TSFNData{Napi::Reference(Persistent(info[0])), - Promise::Deferred::New(env)}; - _tsfn.NonBlockingCall(data); - return data->deferred.Promise(); - }; - - Napi::Value Release(const CallbackInfo &) { - _tsfn.Release(); - return _deferred.Promise(); - }; - -private: - TSFN _tsfn; - Promise::Deferred _deferred; -}; - -Object TSFNWrap::Init(Napi::Env env, Object exports) { - Function func = DefineClass(env, "TSFNWrap", - {InstanceMethod("doCall", &TSFNWrap::DoCall), - InstanceMethod("release", &TSFNWrap::Release)}); - - exports.Set("TSFNWrap", func); - return exports; -} - -TSFNWrap::TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { - Napi::Env env = info.Env(); - Function callback = info[0].As(); - - _tsfn = TSFN::New(env, // napi_env env, - callback, // const Function& callback, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1 // size_t initialThreadCount, - ); -} -} // namespace - -Object InitThreadSafeFunctionExCall(Env env) { - return TSFNWrap::Init(env, Object::New(env)); -} - -#endif diff --git a/test/threadsafe_function_ex/call.js b/test/threadsafe_function_ex/call.js deleted file mode 100644 index 344369370..000000000 --- a/test/threadsafe_function_ex/call.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const buildType = process.config.target_defaults.default_configuration; - -module.exports = Promise.all([ - test(require(`../build/${buildType}/binding.node`)), - test(require(`../build/${buildType}/binding_noexcept.node`)) -]); - -/** - * This test ensures the data sent to the NonBlockingCall and the data received - * in the JavaScript callback are the same. - * - Creates a contexted threadsafe function with callback. - * - Makes one call, and waits for call to complete. - * - The callback forwards the item's data to the given JavaScript function in - * the test. - * - Asserts the data is the same. - */ -async function test(binding) { - const data = {}; - const tsfn = new binding.threadsafe_function_ex_call.TSFNWrap(tsfnData => { - assert(data === tsfnData, "Data in and out of tsfn call do not equal"); - }); - await tsfn.doCall(data); - await tsfn.release(); -} diff --git a/test/threadsafe_function_ex/context.cc b/test/threadsafe_function_ex/context.cc deleted file mode 100644 index 1170eb913..000000000 --- a/test/threadsafe_function_ex/context.cc +++ /dev/null @@ -1,101 +0,0 @@ -#include "napi.h" - -#if (NAPI_VERSION > 3) - -using namespace Napi; - -namespace { - -// Context of our TSFN. -using TSFNContext = Reference; - -// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall -using TSFNData = Promise::Deferred; - -// CallJs callback function -static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, - TSFNContext *context, TSFNData *data) { - if (env != nullptr) { - if (data != nullptr) { - data->Resolve(context->Value()); - } - } - if (data != nullptr) { - delete data; - } -} - -// Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; - -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { -public: - static Object Init(Napi::Env env, Object exports); - TSFNWrap(const CallbackInfo &info); - - Napi::Value GetContextByCall(const CallbackInfo &info) { - Napi::Env env = info.Env(); - auto *callData = new TSFNData(env); - _tsfn.NonBlockingCall(callData); - return callData->Promise(); - }; - - Napi::Value GetContextFromTsfn(const CallbackInfo &) { - return _tsfn.GetContext()->Value(); - }; - - Napi::Value Release(const CallbackInfo &) { - _tsfn.Release(); - return _deferred.Promise(); - }; - -private: - TSFN _tsfn; - Promise::Deferred _deferred; -}; - -Object TSFNWrap::Init(Napi::Env env, Object exports) { - Function func = DefineClass( - env, "TSFNWrap", - {InstanceMethod("getContextByCall", &TSFNWrap::GetContextByCall), - InstanceMethod("getContextFromTsfn", &TSFNWrap::GetContextFromTsfn), - InstanceMethod("release", &TSFNWrap::Release)}); - - exports.Set("TSFNWrap", func); - return exports; -} - -TSFNWrap::TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { - Napi::Env env = info.Env(); - - TSFNContext *context = new TSFNContext(Persistent(info[0])); - - _tsfn = TSFN::New( - info.Env(), // napi_env env, - Function::New( - env, - [](const CallbackInfo & /*info*/) {}), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 1, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - context, // ContextType* context, - - [this](Napi::Env env, void *, - TSFNContext *ctx) { // Finalizer finalizeCallback, - _deferred.Resolve(env.Undefined()); - delete ctx; - }, - static_cast(nullptr) // FinalizerDataType* data, - ); -} -} // namespace - -Object InitThreadSafeFunctionExContext(Env env) { - return TSFNWrap::Init(env, Object::New(env)); -} - -#endif diff --git a/test/threadsafe_function_ex/context.js b/test/threadsafe_function_ex/context.js deleted file mode 100644 index 95e3b8914..000000000 --- a/test/threadsafe_function_ex/context.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const buildType = process.config.target_defaults.default_configuration; - -module.exports = Promise.all([ - test(require(`../build/${buildType}/binding.node`)), - test(require(`../build/${buildType}/binding_noexcept.node`)) -]); - -/** - * The context provided to the threadsafe function's constructor is accessible - * on both the threadsafe function's callback as well the threadsafe function - * itself. This test ensures the context across all three are the same. - * - Creates a contexted threadsafe function with callback. - * - The callback forwards the item's data to the given JavaScript function in - * the test. - * - Makes one call, and waits for call to complete. - * - Asserts the contexts are the same. - */ -async function test(binding) { - const ctx = {}; - const tsfn = new binding.threadsafe_function_ex_context.TSFNWrap(ctx); - assert(ctx === await tsfn.getContextByCall(), "getContextByCall context not equal"); - assert(ctx === tsfn.getContextFromTsfn(), "getContextFromTsfn context not equal"); - await tsfn.release(); -} diff --git a/test/threadsafe_function_ex/example.cc b/test/threadsafe_function_ex/example.cc deleted file mode 100644 index 1445ae688..000000000 --- a/test/threadsafe_function_ex/example.cc +++ /dev/null @@ -1,103 +0,0 @@ -/** - * This test is programmatically represents the example shown in - * `doc/threadsafe_function_ex.md` - */ - -#include "napi.h" - -#if (NAPI_VERSION > 3) - -using namespace Napi; - -namespace { - -// Context of our TSFN. -struct Context {}; - -// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall -using DataType = int; - -// Callback function -static void Callback(Napi::Env env, Napi::Function jsCallback, Context *context, - DataType *data) { - // Check that the threadsafe function has not been finalized. Node calls this - // callback for items remaining on the queue once finalization has completed. - if (!(env == nullptr || jsCallback == nullptr)) { - } - if (data != nullptr) { - delete data; - } -} - -// Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; - -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { - -public: - static Object Init(Napi::Env env, Object exports); - TSFNWrap(const CallbackInfo &info); - -private: - Napi::Value Start(const CallbackInfo &info); - Napi::Value Release(const CallbackInfo &info); -}; - -/** - * @brief Initialize `TSFNWrap` on the environment. - * - * @param env - * @param exports - * @return Object - */ -Object TSFNWrap::Init(Napi::Env env, Object exports) { - Function func = DefineClass(env, "TSFNWrap", - {InstanceMethod("start", &TSFNWrap::Start), - InstanceMethod("release", &TSFNWrap::Release)}); - - exports.Set("TSFNWrap", func); - return exports; -} - -/** - * @brief Construct a new TSFNWrap::TSFNWrap object - * - * @param info - */ -TSFNWrap::TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) {} -} // namespace - -/** - * @brief Instance method `TSFNWrap#start` - * - * @param info - * @return undefined - */ -Napi::Value TSFNWrap::Start(const CallbackInfo &info) { - Napi::Env env = info.Env(); - return env.Undefined(); -}; - -/** - * @brief Instance method `TSFNWrap#release` - * - * @param info - * @return undefined - */ -Napi::Value TSFNWrap::Release(const CallbackInfo &info) { - Napi::Env env = info.Env(); - return env.Undefined(); -}; - -/** - * @brief Module initialization function - * - * @param env - * @return Object - */ -Object InitThreadSafeFunctionExExample(Env env) { - return TSFNWrap::Init(env, Object::New(env)); -} - -#endif diff --git a/test/threadsafe_function_ex/example.js b/test/threadsafe_function_ex/example.js deleted file mode 100644 index 836120029..000000000 --- a/test/threadsafe_function_ex/example.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -/** - * This test is programmatically represents the example shown in - * `doc/threadsafe_function_ex.md` - */ - -const assert = require('assert'); -const buildType = process.config.target_defaults.default_configuration; - -module.exports = Promise.all([ - test(require(`../build/${buildType}/binding.node`)), - test(require(`../build/${buildType}/binding_noexcept.node`)) -]); - -async function test(binding) { - const tsfn = new binding.threadsafe_function_ex_example.TSFNWrap(); - await tsfn.start(); - await tsfn.release(); -} diff --git a/test/threadsafe_function_ex/index.js b/test/threadsafe_function_ex/index.js new file mode 100644 index 000000000..a3cfc5c87 --- /dev/null +++ b/test/threadsafe_function_ex/index.js @@ -0,0 +1,5 @@ +module.exports = (async () => { + await require('./test/threadsafe') + await require('./test/basic'); + await require('./test/example'); +})(); \ No newline at end of file diff --git a/test/threadsafe_function_ex/simple.cc b/test/threadsafe_function_ex/simple.cc deleted file mode 100644 index fb62f756b..000000000 --- a/test/threadsafe_function_ex/simple.cc +++ /dev/null @@ -1,255 +0,0 @@ -#include "napi.h" - -#if (NAPI_VERSION > 3) - -using namespace Napi; - -namespace simple { - -// Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx<>; - -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { -public: - static Object Init(Napi::Env env, Object exports); - TSFNWrap(const CallbackInfo &info); - - Napi::Value Release(const CallbackInfo &) { - _tsfn.Release(); - return _deferred.Promise(); - }; - - Napi::Value Call(const CallbackInfo &info) { - _tsfn.NonBlockingCall(); - return info.Env().Undefined(); - }; - -private: - TSFN _tsfn; - Promise::Deferred _deferred; -}; - -Object TSFNWrap::Init(Napi::Env env, Object exports) { - Function func = DefineClass(env, "TSFNWrap", - {InstanceMethod("call", &TSFNWrap::Call), - InstanceMethod("release", &TSFNWrap::Release)}); - - exports.Set("TSFNWrap", func); - return exports; -} - -TSFNWrap::TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { - - auto env = info.Env(); -#if NAPI_VERSION == 4 - // A threadsafe function on N-API 4 still requires a callback function. - _tsfn = - TSFN::New(env, // napi_env env, - TSFN::DefaultFunctionFactory( - env), // N-API 5+: nullptr; else: const Function& callback, - "Test", // ResourceString resourceName, - 1, // size_t maxQueueSize, - 1 // size_t initialThreadCount - ); -#else - _tsfn = TSFN::New(env, // napi_env env, - "Test", // ResourceString resourceName, - 1, // size_t maxQueueSize, - 1 // size_t initialThreadCount - ); -#endif -} -} // namespace simple - -namespace existing { - -struct DataType { - Promise::Deferred deferred; - bool reject; -}; - -// CallJs callback function provided to `napi_create_threadsafe_function`. It is -// _NOT_ used by `Napi::ThreadSafeFunctionEx<>`. -static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, - void *data) { - DataType *casted = static_cast(data); - if (env != nullptr) { - if (data != nullptr) { - napi_value undefined; - napi_status status = napi_get_undefined(env, &undefined); - NAPI_THROW_IF_FAILED(env, status); - if (casted->reject) { - casted->deferred.Reject(undefined); - } else { - casted->deferred.Resolve(undefined); - } - } - } - if (casted != nullptr) { - delete casted; - } -} - -// Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; - -// A JS-accessible wrap that holds a TSFN. -class ExistingTSFNWrap : public ObjectWrap { -public: - static Object Init(Napi::Env env, Object exports); - ExistingTSFNWrap(const CallbackInfo &info); - - Napi::Value Release(const CallbackInfo &) { - _tsfn.Release(); - return _deferred.Promise(); - }; - - Napi::Value Call(const CallbackInfo &info) { - auto *data = - new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; - _tsfn.NonBlockingCall(data); - return data->deferred.Promise(); - }; - -private: - TSFN _tsfn; - Promise::Deferred _deferred; -}; - -Object ExistingTSFNWrap::Init(Napi::Env env, Object exports) { - Function func = - DefineClass(env, "ExistingTSFNWrap", - {InstanceMethod("call", &ExistingTSFNWrap::Call), - InstanceMethod("release", &ExistingTSFNWrap::Release)}); - - exports.Set("ExistingTSFNWrap", func); - return exports; -} - -ExistingTSFNWrap::ExistingTSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { - - auto env = info.Env(); -#if NAPI_VERSION == 4 - napi_threadsafe_function napi_tsfn; - auto status = napi_create_threadsafe_function( - info.Env(), TSFN::DefaultFunctionFactory(env), nullptr, - String::From(info.Env(), "Test"), 0, 1, nullptr, nullptr, nullptr, CallJs, - &napi_tsfn); - if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status); - } - // A threadsafe function on N-API 4 still requires a callback function. - _tsfn = TSFN(napi_tsfn); -#else - napi_threadsafe_function napi_tsfn; - auto status = napi_create_threadsafe_function( - info.Env(), nullptr, nullptr, String::From(info.Env(), "Test"), 0, 1, - nullptr, nullptr, nullptr, CallJs, &napi_tsfn); - if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status); - } - _tsfn = TSFN(napi_tsfn); -#endif -} -} // namespace existing - -namespace empty { -#if NAPI_VERSION > 4 - -using Context = void; - -struct DataType { - Promise::Deferred deferred; - bool reject; -}; - -// CallJs callback function -static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/, - DataType *data) { - if (env != nullptr) { - if (data != nullptr) { - if (data->reject) { - data->deferred.Reject(env.Undefined()); - } else { - data->deferred.Resolve(env.Undefined()); - } - } - } - if (data != nullptr) { - delete data; - } -} - -// Full type of our ThreadSafeFunctionEx -using EmptyTSFN = ThreadSafeFunctionEx; - -// A JS-accessible wrap that holds a TSFN. -class EmptyTSFNWrap : public ObjectWrap { -public: - static Object Init(Napi::Env env, Object exports); - EmptyTSFNWrap(const CallbackInfo &info); - - Napi::Value Release(const CallbackInfo &) { - _tsfn.Release(); - return _deferred.Promise(); - }; - - Napi::Value Call(const CallbackInfo &info) { - if (info.Length() == 0 || !info[0].IsBoolean()) { - NAPI_THROW( - Napi::TypeError::New(info.Env(), "Expected argument 0 to be boolean"), - Value()); - } - - auto *data = - new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; - _tsfn.NonBlockingCall(data); - return data->deferred.Promise(); - }; - -private: - EmptyTSFN _tsfn; - Promise::Deferred _deferred; -}; - -Object EmptyTSFNWrap::Init(Napi::Env env, Object exports) { - Function func = - DefineClass(env, "EmptyTSFNWrap", - {InstanceMethod("call", &EmptyTSFNWrap::Call), - InstanceMethod("release", &EmptyTSFNWrap::Release)}); - - exports.Set("EmptyTSFNWrap", func); - return exports; -} - -EmptyTSFNWrap::EmptyTSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { - - auto env = info.Env(); - _tsfn = EmptyTSFN::New(env, // napi_env env, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1 // size_t initialThreadCount - ); -} -#endif -} // namespace empty -Object InitThreadSafeFunctionExSimple(Env env) { - -#if NAPI_VERSION > 4 - return empty::EmptyTSFNWrap::Init( - env, existing::ExistingTSFNWrap::Init( - env, simple::TSFNWrap::Init(env, Object::New(env)))); -#else - return existing::ExistingTSFNWrap::Init( - env, simple::TSFNWrap::Init(env, Object::New(env))); -#endif -} - -#endif diff --git a/test/threadsafe_function_ex/simple.js b/test/threadsafe_function_ex/simple.js deleted file mode 100644 index b3a6b0cf5..000000000 --- a/test/threadsafe_function_ex/simple.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const buildType = process.config.target_defaults.default_configuration; - -module.exports = Promise.all([ - test(require(`../build/${buildType}/binding.node`)), - test(require(`../build/${buildType}/binding_noexcept.node`)) -].reduce((p, c) => p.concat(c)), []); - -function test(binding) { - return [ - // testSimple(binding), - testEmpty(binding) - ]; -} - -/** - * A simple, fire-and-forget test. - * - Creates a threadsafe function with no context or callback. - * - The node-addon-api 'no callback' feature is implemented by passing either a - * no-op `Function` on N-API 4 or `std::nullptr` on N-API 5+ to the underlying - * `napi_create_threadsafe_function` call. - * - Makes one call, releases, then waits for finalization. - * - Inherently ignores the state of the item once it has been added to the - * queue. Since there are no callbacks or context, it is impossible to capture - * the state. - */ -async function testSimple(binding) { - const tsfn = new binding.threadsafe_function_ex_simple.TSFNWrap(); - tsfn.call(); - await tsfn.release(); -} - -/** - * **ONLY ON N-API 5+**. The optional JavaScript function callback feature is - * not available in N-API <= 4. - * - Creates a threadsafe function with no JavaScript context or callback. - * - Makes two calls, expecting the first to resolve and the second to reject. - * - Waits for Node to process the items on the queue prior releasing the - * threadsafe function. - */ -async function testEmpty(binding) { - const { EmptyTSFNWrap } = binding.threadsafe_function_ex_simple; - - if (typeof EmptyTSFNWrap === 'function') { - const tsfn = new binding.threadsafe_function_ex_simple.EmptyTSFNWrap(); - await tsfn.call(false /* reject */); - let caught = false; - try { - await tsfn.call(true /* reject */); - } catch (ex) { - caught = true; - } - - assert.ok(caught, 'The promise rejection was not caught'); - await tsfn.release(); - } -} diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc new file mode 100644 index 000000000..49c9cd0ac --- /dev/null +++ b/test/threadsafe_function_ex/test/basic.cc @@ -0,0 +1,432 @@ +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace call { + +// Context of our TSFN. +using Context = void; + +// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall +struct DataType { + Reference data; + Promise::Deferred deferred; +}; + +// CallJs callback function +static void CallJs(Napi::Env env, Napi::Function jsCallback, + Context * /*context*/, DataType *data) { + if (!(env == nullptr || jsCallback == nullptr)) { + if (data != nullptr) { + jsCallback.Call(env.Undefined(), {data->data.Value()}); + data->deferred.Resolve(data->data.Value()); + } + } + if (data != nullptr) { + delete data; + } +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { +public: + static void Init(Napi::Env env, Object exports, const std::string &ns) { + Function func = + DefineClass(env, "TSFNCall", + {InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("release", &TSFNWrap::Release)}); + + auto locals(Object::New(env)); + exports.Set(ns, locals); + locals.Set("TSFNWrap", func); + } + + TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + Napi::Env env = info.Env(); + Function callback = info[0].As(); + + _tsfn = TSFN::New(env, // napi_env env, + callback, // const Function& callback, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1 // size_t initialThreadCount, + ); + } + Napi::Value Call(const CallbackInfo &info) { + Napi::Env env = info.Env(); + DataType *data = + new DataType{Napi::Reference(Persistent(info[0])), + Promise::Deferred::New(env)}; + _tsfn.NonBlockingCall(data); + return data->deferred.Promise(); + }; + + Napi::Value Release(const CallbackInfo &) { + _tsfn.Release(); + return _deferred.Promise(); + }; + +private: + TSFN _tsfn; + Promise::Deferred _deferred; +}; + +} // namespace call + +namespace context { + +// Context of our TSFN. +using Context = Reference; + +// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall +using DataType = Promise::Deferred; + +// CallJs callback function +static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, + Context *context, DataType *data) { + if (env != nullptr) { + if (data != nullptr) { + data->Resolve(context->Value()); + } + } + if (data != nullptr) { + delete data; + } +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { +public: + static void Init(Napi::Env env, Object exports, const char *ns) { + Function func = DefineClass( + env, "TSFNWrap", + {InstanceMethod("getContextByCall", &TSFNWrap::GetContextByCall), + InstanceMethod("getContextFromTsfn", &TSFNWrap::GetContextFromTsfn), + InstanceMethod("release", &TSFNWrap::Release)}); + + auto locals(Object::New(env)); + exports.Set(ns, locals); + locals.Set("TSFNWrap", func); + } + + TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + Napi::Env env = info.Env(); + + Context *context = new Context(Persistent(info[0])); + + _tsfn = TSFN::New( + info.Env(), // napi_env env, + Function::New( + env, + [](const CallbackInfo & /*info*/) {}), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 1, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + context, // ContextType* context, + + [this](Napi::Env env, void *, + Context *ctx) { // Finalizer finalizeCallback, + _deferred.Resolve(env.Undefined()); + delete ctx; + }, + static_cast(nullptr) // FinalizerDataType* data, + ); + } + + Napi::Value GetContextByCall(const CallbackInfo &info) { + Napi::Env env = info.Env(); + auto *callData = new DataType(env); + _tsfn.NonBlockingCall(callData); + return callData->Promise(); + }; + + Napi::Value GetContextFromTsfn(const CallbackInfo &) { + return _tsfn.GetContext()->Value(); + }; + + Napi::Value Release(const CallbackInfo &) { + _tsfn.Release(); + return _deferred.Promise(); + }; + +private: + TSFN _tsfn; + Promise::Deferred _deferred; +}; +} // namespace context + +namespace empty { +#if NAPI_VERSION > 4 + +using Context = void; + +struct DataType { + Promise::Deferred deferred; + bool reject; +}; + +// CallJs callback function +static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/, + DataType *data) { + if (env != nullptr) { + if (data != nullptr) { + if (data->reject) { + data->deferred.Reject(env.Undefined()); + } else { + data->deferred.Resolve(env.Undefined()); + } + } + } + if (data != nullptr) { + delete data; + } +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { +public: + static void Init(Napi::Env env, Object exports, const std::string &ns) { + Function func = + DefineClass(env, "TSFNWrap", + {InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("release", &TSFNWrap::Release)}); + + auto locals(Object::New(env)); + exports.Set(ns, locals); + locals.Set("TSFNWrap", func); + } + + TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + + auto env = info.Env(); + _tsfn = TSFN::New(env, // napi_env env, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1 // size_t initialThreadCount + ); + } + Napi::Value Release(const CallbackInfo &) { + _tsfn.Release(); + return _deferred.Promise(); + }; + + Napi::Value Call(const CallbackInfo &info) { + if (info.Length() == 0 || !info[0].IsBoolean()) { + NAPI_THROW( + Napi::TypeError::New(info.Env(), "Expected argument 0 to be boolean"), + Value()); + } + + auto *data = + new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + _tsfn.NonBlockingCall(data); + return data->deferred.Promise(); + }; + +private: + TSFN _tsfn; + Promise::Deferred _deferred; +}; + +#endif +} // namespace empty + +namespace existing { + +struct DataType { + Promise::Deferred deferred; + bool reject; +}; + +// CallJs callback function provided to `napi_create_threadsafe_function`. It is +// _NOT_ used by `Napi::ThreadSafeFunctionEx<>`. +static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, + void *data) { + DataType *casted = static_cast(data); + if (env != nullptr) { + if (data != nullptr) { + napi_value undefined; + napi_status status = napi_get_undefined(env, &undefined); + NAPI_THROW_IF_FAILED(env, status); + if (casted->reject) { + casted->deferred.Reject(undefined); + } else { + casted->deferred.Resolve(undefined); + } + } + } + if (casted != nullptr) { + delete casted; + } +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { +public: + static void Init(Napi::Env env, Object exports, const std::string &ns) { + Function func = + DefineClass(env, "TSFNWrap", + {InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("release", &TSFNWrap::Release)}); + auto locals(Object::New(env)); + exports.Set(ns, locals); + locals.Set("TSFNWrap", func); + } + + TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + + auto env = info.Env(); +#if NAPI_VERSION == 4 + napi_threadsafe_function napi_tsfn; + auto status = napi_create_threadsafe_function( + info.Env(), TSFN::DefaultFunctionFactory(env), nullptr, + String::From(info.Env(), "Test"), 0, 1, nullptr, nullptr, nullptr, + CallJs, &napi_tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status); + } + // A threadsafe function on N-API 4 still requires a callback function. + _tsfn = TSFN(napi_tsfn); +#else + napi_threadsafe_function napi_tsfn; + auto status = napi_create_threadsafe_function( + info.Env(), nullptr, nullptr, String::From(info.Env(), "Test"), 0, 1, + nullptr, nullptr, nullptr, CallJs, &napi_tsfn); + if (status != napi_ok) { + NAPI_THROW_IF_FAILED(env, status); + } + _tsfn = TSFN(napi_tsfn); +#endif + } + + Napi::Value Release(const CallbackInfo &) { + _tsfn.Release(); + return _deferred.Promise(); + }; + + Napi::Value Call(const CallbackInfo &info) { + auto *data = + new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + _tsfn.NonBlockingCall(data); + return data->deferred.Promise(); + }; + +private: + TSFN _tsfn; + Promise::Deferred _deferred; +}; + +} // namespace existing +namespace simple { + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx<>; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { +public: + static void Init(Napi::Env env, Object exports, const std::string &ns) { + Function func = + DefineClass(env, "TSFNSimple", + {InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("release", &TSFNWrap::Release)}); + + auto locals(Object::New(env)); + exports.Set(ns, locals); + locals.Set("TSFNWrap", func); + } + + TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info) { + + auto env = info.Env(); +#if NAPI_VERSION == 4 + // A threadsafe function on N-API 4 still requires a callback function. + _tsfn = TSFN::New( + env, // napi_env env, + TSFN::DefaultFunctionFactory( + env), // N-API 5+: nullptr; else: const Function& callback, + "Test", // ResourceString resourceName, + 1, // size_t maxQueueSize, + 1 // size_t initialThreadCount + ); +#else + _tsfn = TSFN::New(env, // napi_env env, + "Test", // ResourceString resourceName, + 1, // size_t maxQueueSize, + 1 // size_t initialThreadCount + ); +#endif + } + + // Since this test spec has no CALLBACK, CONTEXT, or FINALIZER. We have no way + // to know when the underlying ThreadSafeFunction has been finalized. + Napi::Value Release(const CallbackInfo &info) { + _tsfn.Release(); + return info.Env().Undefined(); + }; + + Napi::Value Call(const CallbackInfo &info) { + _tsfn.NonBlockingCall(); + return info.Env().Undefined(); + }; + +private: + TSFN _tsfn; +}; + +} // namespace simple + +Object InitThreadSafeFunctionExBasic(Env env) { + +// A list of v4+ enables spec namespaces. +#define V4_EXPORTS(V) \ + V(simple) \ + V(existing) \ + V(call) \ + V(context) + +// A list of v5+ enables spec namespaces. +#define V5_EXPORTS(V) V(empty) + +#if NAPI_VERSION == 4 +#define EXPORTS(V) V4_EXPORTS(V) +#else +#define EXPORTS(V) \ + V4_EXPORTS(V) \ + V5_EXPORTS(V) +#endif + + Object exports(Object::New(env)); + +#define V(modname) modname::TSFNWrap::Init(env, exports, #modname); + EXPORTS(V) +#undef V + + return exports; +} + +#endif diff --git a/test/threadsafe_function_ex/test/basic.js b/test/threadsafe_function_ex/test/basic.js new file mode 100644 index 000000000..a3f9769e4 --- /dev/null +++ b/test/threadsafe_function_ex/test/basic.js @@ -0,0 +1,188 @@ +// @ts-check +'use strict'; +const assert = require('assert'); +const buildType = process.config.target_defaults.default_configuration; + +// If `true`, this module will re-throw any error caught, allowing the caller to +// handle. +const SHOW_OUTPUT = true; + +const print = (isError, newLine, ...what) => { + if (SHOW_OUTPUT) { + let target, method; + target = newLine ? console : process[isError ? 'stderr' : 'stdout']; + method = target === console ? (isError ? 'error' : 'log') : 'write'; + if (isError) { + method + + } + return target[method].apply(target, what); + } +} + +/** @returns {void} */ +const log = (...what) => print(false, true, ...what); + +/** @returns {void} */ +const error = (...what) => print(true, true, ...what); + +/** @returns {Promise} */ +const write = (...what) => print(false, false, ...what); + +/** @returns {Promise} */ +// const rewind = () => print(false, false, `\x1b[K`); +const rewind = () => print(false, false, `\x1b[1A`); + +const pad = (what, targetLength = 20, padString = ' ', padLeft) => { + const padder = (pad, str) => { + if (typeof str === 'undefined') + return pad; + if (padLeft) { + return (pad + str).slice(-pad.length); + } else { + return (str + pad).substring(0, pad.length); + } + }; + return padder(padString.repeat(targetLength), String(what)); +} + +/** + * Test runner helper class. Each static method's name corresponds to the + * namespace the test as defined in the native addon. Each test specifics are + * documented on the individual method. The async test handler runs + * synchronously in the series of all tests so the test **MUST** wait on the + * finalizer. Otherwise, the test runner will assume the test completed. + */ +class TestRunner { + + static async run(isNoExcept) { + const binding = require(`../../build/${buildType}/binding${isNoExcept ? '_noexcept' : ''}.node`); + const runner = new this(); + // Errors thrown are caught by caller, and re-thrown if `BUBBLE_ERRORS` is + // `true. + const cmdlineTests = process.argv.length > 2 ? process.argv.slice(2) : null; + for (const nsName of Object.getOwnPropertyNames(this.prototype)) { + if (nsName !== 'constructor') { + const ns = binding.threadsafe_function_ex_basic[nsName]; + let state; + const setState = (...newState) => { state = newState }; + const toLine = (state) => { + const [label, time, isNoExcept, nsName] = state; + const except = () => pad(isNoExcept ? '[noexcept]' : '', 12); + const timeStr = () => time == null ? '...' : `${time}${typeof time === 'number' ? 'ms' : ''}`; + return `${pad(nsName, 10)} ${except()}| ${pad(timeStr(), 8)}| ${pad(label, 15)}`; + }; + const stateLine = () => toLine(state); + if (ns && (cmdlineTests == null || cmdlineTests.indexOf(nsName) > -1)) { + setState('Running test', null, isNoExcept, nsName); + log(stateLine()); + + const start = Date.now(); + await runner[nsName](ns); + await new Promise(resolve => setTimeout(resolve, 50)); + rewind(); + setState('Finished test', Date.now() - start, isNoExcept, nsName); + log(stateLine()); + } else { + setState('Skipping test', '-', isNoExcept, nsName); + debugger; + log(stateLine()); + } + } + } + } + /** + * This test ensures the data sent to the NonBlockingCall and the data + * received in the JavaScript callback are the same. + * - Creates a contexted threadsafe function with callback. + * - Makes one call, and waits for call to complete. + * - The callback forwards the item's data to the given JavaScript function in + * the test. + * - Asserts the data is the same. + */ + async call({ TSFNWrap }) { + const data = {}; + const tsfn = new TSFNWrap(tsfnData => { + assert(data === tsfnData, "Data in and out of tsfn call do not equal"); + }); + await tsfn.call(data); + await tsfn.release(); + } + + /** + * The context provided to the threadsafe function's constructor is accessible + * on both the threadsafe function's callback as well the threadsafe function + * itself. This test ensures the context across all three are the same. + * - Creates a contexted threadsafe function with callback. + * - The callback forwards the item's data to the given JavaScript function in + * the test. + * - Makes one call, and waits for call to complete. + * - Asserts the contexts are the same. + */ + async context({ TSFNWrap }) { + const ctx = {}; + const tsfn = new TSFNWrap(ctx); + assert(ctx === await tsfn.getContextByCall(), "getContextByCall context not equal"); + assert(ctx === tsfn.getContextFromTsfn(), "getContextFromTsfn context not equal"); + await tsfn.release(); + } + + /** + * **ONLY ON N-API 5+**. The optional JavaScript function callback feature is + * not available in N-API <= 4. + * - Creates a threadsafe function with no JavaScript context or callback. + * - Makes two calls, waiting for each, and expecting the first to resolve + * and the second to reject. + * - Waits for Node to process the items on the queue prior releasing the + * threadsafe function. + */ + async empty({ TSFNWrap }) { + debugger; + if (typeof TSFNWrap === 'function') { + const tsfn = new TSFNWrap(); + await tsfn.call(false /* reject */); + let caught = false; + try { + await tsfn.call(true /* reject */); + } catch (ex) { + caught = true; + } + + assert.ok(caught, 'The promise rejection was not caught'); + await tsfn.release(); + } + } + + /** + * A `ThreadSafeFunctionEx<>` can be constructed with default type arguments. + * - Creates a threadsafe function with no context or callback. + * - The node-addon-api 'no callback' feature is implemented by passing either + * a no-op `Function` on N-API 4 or `std::nullptr` on N-API 5+ to the + * underlying `napi_create_threadsafe_function` call. + * - Makes one call, releases, then waits for finalization. + * - Inherently ignores the state of the item once it has been added to the + * queue. Since there are no callbacks or context, it is impossible to + * capture the state. + */ + async simple({ TSFNWrap }) { + const tsfn = new TSFNWrap(); + tsfn.call(); + await tsfn.release(); + } + +} + +async function run() { + await TestRunner.run(false); + await TestRunner.run(true); +} + + +module.exports = run() + .then(() => { log(`Finished executing tests in .${__filename.replace(process.cwd(), '')}`); }) + .catch((e) => { + // if (require.main !== module) { throw e; } + console.error(`Test failed!`, e); + process.exit(1); + }); + diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc new file mode 100644 index 000000000..5d336b13e --- /dev/null +++ b/test/threadsafe_function_ex/test/example.cc @@ -0,0 +1,257 @@ +#undef NAPI_CPP_EXCEPTIONS +#define NAPI_DISABLE_CPP_EXCEPTIONS + +/** + * This test is programmatically represents the example shown in + * `doc/threadsafe_function_ex.md` + */ + +#if (NAPI_VERSION > 3) + +#include "napi.h" +#include +#include +static constexpr size_t DEFAULT_THREAD_COUNT = 10; +static constexpr int32_t DEFAULT_CALL_COUNT = 2; + +/** + * @brief Macro used specifically to support the dual CI test / documentation + * example setup. Exceptions are always thrown as JavaScript exceptions when + * running in example mode. + * + */ +#define TSFN_THROW(tsfnWrap, e, ...) \ + if (tsfnWrap->cppExceptions) { \ + do { \ + (e).ThrowAsJavaScriptException(); \ + return __VA_ARGS__; \ + } while (0); \ + } else { \ + NAPI_THROW(e, __VA_ARGS__); \ + } + +using namespace Napi; + +namespace { + +// Context of our TSFN. +struct Context { + int32_t threadId; +}; + +// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall +using DataType = int; + +// Callback function +static void Callback(Napi::Env env, Napi::Function jsCallback, Context *context, + DataType *data) { + // Check that the threadsafe function has not been finalized. Node calls this + // callback for items remaining on the queue once finalization has completed. + if (!(env == nullptr || jsCallback == nullptr)) { + } + if (data != nullptr) { + delete data; + } +} + +// Full type of our ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; + +struct FinalizerDataType { + std::vector threads; +}; + +// A JS-accessible wrap that holds a TSFN. +class TSFNWrap : public ObjectWrap { + +public: + static Object Init(Napi::Env env, Object exports); + TSFNWrap(const CallbackInfo &info); + + // When running as an example, we want exceptions to always go to JavaScript, + // allowing the user to try/catch errors from the addon. + bool cppExceptions; + +private: + Napi::Value Start(const CallbackInfo &info); + Napi::Value Release(const CallbackInfo &info); + + // Instantiated by `Start`; resolved on finalize of tsfn. + Promise::Deferred _deferred; + + // Reference to our TSFN + TSFN _tsfn; + + // Object.prototype.toString reference for use with error messages + FunctionReference _toString; +}; + +/** + * @brief Initialize `TSFNWrap` on the environment. + * + * @param env + * @param exports + * @return Object + */ +Object TSFNWrap::Init(Napi::Env env, Object exports) { + Function func = DefineClass(env, "TSFNWrap", + {InstanceMethod("start", &TSFNWrap::Start), + InstanceMethod("release", &TSFNWrap::Release)}); + + exports.Set("TSFNWrap", func); + return exports; +} + +static void threadEntry(size_t threadId, TSFN tsfn, int32_t callCount) { + using namespace std::chrono_literals; + for (int32_t i = 0; i < callCount; ++i) { + tsfn.NonBlockingCall(new int); + std::this_thread::sleep_for(50ms * threadId); + } + tsfn.Release(); +} + +/** + * @brief Construct a new TSFNWrap object on the main thread. If any arguments + * are passed, exceptions in the addon will always be thrown JavaScript + * exceptions, allowing the user to try/catch errors from the addon. + * + * @param info + */ +TSFNWrap::TSFNWrap(const CallbackInfo &info) + : ObjectWrap(info), + _deferred(Promise::Deferred::New(info.Env())) { + auto env = info.Env(); + _toString = Napi::Persistent(env.Global() + .Get("Object") + .ToObject() + .Get("prototype") + .ToObject() + .Get("toString") + .As()); + cppExceptions = true; + info.Length() > 0; +} +} // namespace + +/** + * @brief Instance method `TSFNWrap#start` + * + * @param info + * @return undefined + */ +Napi::Value TSFNWrap::Start(const CallbackInfo &info) { + Napi::Env env = info.Env(); + + // Creates a list to hold how many times each thread should make a call. + std::vector callCounts; + + // The JS-provided callback to execute for each call (if provided) + Function callback; + + if (info.Length() > 0 && info[0].IsObject()) { + auto arg0 = info[0].ToObject(); + if (arg0.Has("threads")) { + Napi::Value threads = arg0.Get("threads"); + if (threads.IsArray()) { + Napi::Array threadsArray = threads.As(); + for (auto i = 0U; i < threadsArray.Length(); ++i) { + Napi::Value elem = threadsArray.Get(i); + if (elem.IsNumber()) { + callCounts.push_back(elem.As().Int32Value()); + } else { + // TSFN_THROW(this, + // Napi::TypeError::New(Env(), + // "Invalid arguments"), + // Object()); + + // ThrowAsJavaScriptException + Napi::TypeError::New(Env(), "Invalid arguments") + .ThrowAsJavaScriptException(); + return env.Undefined(); + + // if (this->cppExceptions) { + // do { + // (Napi::TypeError::New(Env(), "Invalid arguments")) + // .ThrowAsJavaScriptException(); + // return Object(); + // } while (0); + // } else { + // NAPI_THROW(Napi::TypeError::New(Env(), "Invalid arguments"), + // Object()); + // } + } + } + } else if (threads.IsNumber()) { + auto threadCount = threads.As().Int32Value(); + for (int32_t i = 0; i < threadCount; ++i) { + callCounts.push_back(DEFAULT_CALL_COUNT); + } + } else { + TSFN_THROW(this, Napi::TypeError::New(Env(), "Invalid arguments"), + Number()); + } + } + + if (arg0.Has("callback")) { + auto cb = arg0.Get("callback"); + if (cb.IsFunction()) { + callback = cb.As(); + } else { + TSFN_THROW(this, + Napi::TypeError::New(Env(), "Callback is not a function"), + Number()); + } + } + } + + // Apply default arguments + if (callCounts.size() == 0) { + for (size_t i = 0; i < DEFAULT_THREAD_COUNT; ++i) { + callCounts.push_back(DEFAULT_CALL_COUNT); + } + } + + // const auto threadCount = callCounts.size(); + // FinalizerDataType *finalizerData = new FinalizerDataType(); + // // TSFN::New(info.Env(), info[0].As(), Object::New(info.Env()), + // // "Test", tsfnInfo.maxQueueSize, 2, &tsfnInfo, JoinTheThreads, + + // threads); _tsfn = TSFN::New(env, // napi_env env, + // callback, // const Function& callback, + // "Test", // ResourceString resourceName, + // 0, // size_t maxQueueSize, + // threadCount // size_t initialThreadCount, + // ); + // for (int32_t threadId = 0; threadId < threadCount; ++threadId) { + // // static void threadEntry(size_t threadId, TSFN tsfn, int32_t callCount) + // { + + // finalizerData->threads.push_back( + // std::thread(threadEntry, threadId, _tsfn, callCounts[threadId])); + // } + return env.Undefined(); +}; + +/** + * @brief Instance method `TSFNWrap#release` + * + * @param info + * @return undefined + */ +Napi::Value TSFNWrap::Release(const CallbackInfo &info) { + Napi::Env env = info.Env(); + return env.Undefined(); +}; + +/** + * @brief Module initialization function + * + * @param env + * @return Object + */ +Object InitThreadSafeFunctionExExample(Env env) { + return TSFNWrap::Init(env, Object::New(env)); +} + +#endif diff --git a/test/threadsafe_function_ex/test/example.js b/test/threadsafe_function_ex/test/example.js new file mode 100644 index 000000000..3aaf0c736 --- /dev/null +++ b/test/threadsafe_function_ex/test/example.js @@ -0,0 +1,44 @@ +'use strict'; + +/** + * This test is programmatically represents the example shown in + * `doc/threadsafe_function_ex.md` + */ + +const assert = require('assert'); +const buildType = 'Debug'; process.config.target_defaults.default_configuration; + +const isCI = require.main !== module; +const print = (isError, ...what) => isCI ? () => {} : console[isError ? 'error' : 'log'].apply(console.log, what); +const log = (...what) => print(false, ...what); +const error = (...what) => print(true, ...what); + +module.exports = Promise.all([ + // test(require(`../build/${buildType}/binding_noexcept.node`)), + isCI ? undefined : test(require(`../build/${buildType}/binding_noexcept.node`)) +]).catch(e => { + console.error('Error', e); +}); + +async function test(binding) { + try { + const tsfn = new binding.threadsafe_function_ex_example.TSFNWrap(true); + await tsfn.start({ + threads: ['f',5,5,5], + callback: ()=>{} + }); + await tsfn.release(); +} catch (e) { + error(e); +} +} + + +if (!isCI) { + console.log(module.exports); + module.exports.then(() => { + log('tea'); + }).catch((e) => { + error('Error!', e); + }); +} diff --git a/test/threadsafe_function_ex/threadsafe.cc b/test/threadsafe_function_ex/test/threadsafe.cc similarity index 99% rename from test/threadsafe_function_ex/threadsafe.cc rename to test/threadsafe_function_ex/test/threadsafe.cc index cf3eddca8..f12e7e49b 100644 --- a/test/threadsafe_function_ex/threadsafe.cc +++ b/test/threadsafe_function_ex/test/threadsafe.cc @@ -1,6 +1,7 @@ #include #include #include "napi.h" +#include #if (NAPI_VERSION > 3) diff --git a/test/threadsafe_function_ex/threadsafe.js b/test/threadsafe_function_ex/test/threadsafe.js similarity index 92% rename from test/threadsafe_function_ex/threadsafe.js rename to test/threadsafe_function_ex/test/threadsafe.js index f9789db58..6ceceb92c 100644 --- a/test/threadsafe_function_ex/threadsafe.js +++ b/test/threadsafe_function_ex/test/threadsafe.js @@ -2,10 +2,19 @@ const buildType = process.config.target_defaults.default_configuration; const assert = require('assert'); -const common = require('../common'); - -test(require(`../build/${buildType}/binding.node`)); -test(require(`../build/${buildType}/binding_noexcept.node`)); +const common = require('../../common'); + +module.exports = run() + .then(() => { console.log(`Finished executing tests in .${__filename.replace(process.cwd(),'')}`); }) + .catch((e) => { + console.error(`Test failed!`, e); + process.exit(1); + }); + +async function run() { + await test(require(`../../build/${buildType}/binding.node`)); + await test(require(`../../build/${buildType}/binding_noexcept.node`)); +} /** * This spec replicates the non-`Ex` multi-threaded spec using the `Ex` API. @@ -46,7 +55,7 @@ function test(binding) { }); } - new Promise(function testWithoutJSMarshaller(resolve) { + return new Promise(function testWithoutJSMarshaller(resolve) { let callCount = 0; binding.threadsafe_function_ex_threadsafe.startThreadNoNative(function testCallback() { callCount++; From d2bcc03d67fc6587b2547d95018f10e1e61b54a2 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 14 Jun 2020 17:41:57 +0200 Subject: [PATCH 16/39] test: napi v4,v5 all tests pass --- napi-inl.h | 6 +- napi.h | 7 +- test/threadsafe_function_ex/index.js | 16 +- test/threadsafe_function_ex/test/basic.cc | 190 +++++++++++------- test/threadsafe_function_ex/test/basic.js | 135 ++----------- test/threadsafe_function_ex/test/example.cc | 3 - .../threadsafe_function_ex/test/threadsafe.js | 2 +- .../threadsafe_function_ex/util/TestRunner.js | 166 +++++++++++++++ 8 files changed, 328 insertions(+), 197 deletions(-) create mode 100644 test/threadsafe_function_ex/util/TestRunner.js diff --git a/napi-inl.h b/napi-inl.h index 05e61d2e5..c5e262555 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4551,7 +4551,7 @@ ThreadSafeFunctionEx::New( return tsfn; } -// static, with Callback [x] Resource [x] Finalizer [missing] +// static, with Callback [passed] Resource [passed] Finalizer [missing] template template @@ -4574,7 +4574,7 @@ ThreadSafeFunctionEx::New( return tsfn; } -// static, with Callback [x] Resource [missing ] Finalizer [x] +// static, with Callback [passed] Resource [missing] Finalizer [passed] template template ::New( return tsfn; } -// static, with: Callback [x] Resource [x] Finalizer [x] +// static, with: Callback [passed] Resource [passed] Finalizer [passed] template template 3) - template class ThreadSafeFunctionEx { @@ -2058,7 +2060,8 @@ namespace Napi { // This API may only be called from the main thread. // Helper function that returns nullptr if running N-API 5+, otherwise a // non-empty, no-op Function. This provides the ability to specify at - // compile-time a callback parameter to `New` that safely does no action. + // compile-time a callback parameter to `New` that safely does no action + // when targeting _any_ N-API version. static DefaultFunctionType DefaultFunctionFactory(Napi::Env env); #if NAPI_VERSION > 4 diff --git a/test/threadsafe_function_ex/index.js b/test/threadsafe_function_ex/index.js index a3cfc5c87..9e7778d8f 100644 --- a/test/threadsafe_function_ex/index.js +++ b/test/threadsafe_function_ex/index.js @@ -1,5 +1,13 @@ +const tests = [ + // 'threadsafe', + 'basic', + 'example' +]; + +// Threadsafe tests must run synchronously. If two threaded-tests are running +// and one fails, Node may exit while `std::thread`s are running. module.exports = (async () => { - await require('./test/threadsafe') - await require('./test/basic'); - await require('./test/example'); -})(); \ No newline at end of file + for (const test of tests) { + await require(`./test/${test}`); + } +})(); diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc index 49c9cd0ac..94594dfe7 100644 --- a/test/threadsafe_function_ex/test/basic.cc +++ b/test/threadsafe_function_ex/test/basic.cc @@ -7,17 +7,17 @@ using namespace Napi; namespace call { // Context of our TSFN. -using Context = void; +using Context = std::nullptr_t; // Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall -struct DataType { +struct Data { Reference data; Promise::Deferred deferred; }; // CallJs callback function static void CallJs(Napi::Env env, Napi::Function jsCallback, - Context * /*context*/, DataType *data) { + Context * /*context*/, Data *data) { if (!(env == nullptr || jsCallback == nullptr)) { if (data != nullptr) { jsCallback.Call(env.Undefined(), {data->data.Value()}); @@ -30,7 +30,7 @@ static void CallJs(Napi::Env env, Napi::Function jsCallback, } // Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; // A JS-accessible wrap that holds a TSFN. class TSFNWrap : public ObjectWrap { @@ -46,36 +46,46 @@ class TSFNWrap : public ObjectWrap { locals.Set("TSFNWrap", func); } - TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { + TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { Napi::Env env = info.Env(); Function callback = info[0].As(); - _tsfn = TSFN::New(env, // napi_env env, callback, // const Function& callback, "Test", // ResourceString resourceName, 0, // size_t maxQueueSize, - 1 // size_t initialThreadCount, - ); + 1, // size_t initialThreadCount, + nullptr, + [this](Napi::Env env, void *, + Context *ctx) { // Finalizer finalizeCallback, + if (_deferred) { + _deferred->Resolve(Boolean::New(env, true)); + _deferred.release(); + } + }); } + Napi::Value Call(const CallbackInfo &info) { Napi::Env env = info.Env(); - DataType *data = - new DataType{Napi::Reference(Persistent(info[0])), - Promise::Deferred::New(env)}; + Data *data = new Data{Napi::Reference(Persistent(info[0])), + Promise::Deferred::New(env)}; _tsfn.NonBlockingCall(data); return data->deferred.Promise(); }; - Napi::Value Release(const CallbackInfo &) { + Napi::Value Release(const CallbackInfo &info) { + if (_deferred) { + return _deferred->Promise(); + } + + auto env = info.Env(); + _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); _tsfn.Release(); - return _deferred.Promise(); + return _deferred->Promise(); }; private: TSFN _tsfn; - Promise::Deferred _deferred; + std::unique_ptr _deferred; }; } // namespace call @@ -86,11 +96,11 @@ namespace context { using Context = Reference; // Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall -using DataType = Promise::Deferred; +using Data = Promise::Deferred; // CallJs callback function static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, - Context *context, DataType *data) { + Context *context, Data *data) { if (env != nullptr) { if (data != nullptr) { data->Resolve(context->Value()); @@ -102,7 +112,7 @@ static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, } // Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; // A JS-accessible wrap that holds a TSFN. class TSFNWrap : public ObjectWrap { @@ -119,36 +129,32 @@ class TSFNWrap : public ObjectWrap { locals.Set("TSFNWrap", func); } - TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { + TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { Napi::Env env = info.Env(); Context *context = new Context(Persistent(info[0])); - _tsfn = TSFN::New( - info.Env(), // napi_env env, - Function::New( - env, - [](const CallbackInfo & /*info*/) {}), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 1, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - context, // ContextType* context, - - [this](Napi::Env env, void *, - Context *ctx) { // Finalizer finalizeCallback, - _deferred.Resolve(env.Undefined()); - delete ctx; - }, - static_cast(nullptr) // FinalizerDataType* data, - ); + _tsfn = TSFN::New(info.Env(), // napi_env env, + Function::New(env, + [](const CallbackInfo & /*info*/) { + }), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + context, // Context* context, + [this](Napi::Env env, void *, + Context *ctx) { // Finalizer finalizeCallback, + if (_deferred) { + _deferred->Resolve(Boolean::New(env, true)); + _deferred.release(); + } + }); } Napi::Value GetContextByCall(const CallbackInfo &info) { Napi::Env env = info.Env(); - auto *callData = new DataType(env); + auto *callData = new Data(env); _tsfn.NonBlockingCall(callData); return callData->Promise(); }; @@ -157,14 +163,19 @@ class TSFNWrap : public ObjectWrap { return _tsfn.GetContext()->Value(); }; - Napi::Value Release(const CallbackInfo &) { + Napi::Value Release(const CallbackInfo &info) { + if (_deferred) { + return _deferred->Promise(); + } + auto env = info.Env(); + _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); _tsfn.Release(); - return _deferred.Promise(); + return _deferred->Promise(); }; private: TSFN _tsfn; - Promise::Deferred _deferred; + std::unique_ptr _deferred; }; } // namespace context @@ -173,14 +184,14 @@ namespace empty { using Context = void; -struct DataType { +struct Data { Promise::Deferred deferred; bool reject; }; // CallJs callback function static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/, - DataType *data) { + Data *data) { if (env != nullptr) { if (data != nullptr) { if (data->reject) { @@ -196,7 +207,7 @@ static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/, } // Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; // A JS-accessible wrap that holds a TSFN. class TSFNWrap : public ObjectWrap { @@ -212,20 +223,34 @@ class TSFNWrap : public ObjectWrap { locals.Set("TSFNWrap", func); } - TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { + TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { auto env = info.Env(); _tsfn = TSFN::New(env, // napi_env env, "Test", // ResourceString resourceName, 0, // size_t maxQueueSize, - 1 // size_t initialThreadCount - ); + 1, // size_t initialThreadCount, + nullptr, + [this](Napi::Env env, void *, + Context *ctx) { // Finalizer finalizeCallback, + if (_deferred) { + _deferred->Resolve(Boolean::New(env, true)); + _deferred.release(); + } + }); } - Napi::Value Release(const CallbackInfo &) { + + Napi::Value Release(const CallbackInfo &info) { + // Since this is actually a SINGLE-THREADED test, we don't have to worry + // about race conditions on accessing `_deferred`. + if (_deferred) { + return _deferred->Promise(); + } + + auto env = info.Env(); + _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); _tsfn.Release(); - return _deferred.Promise(); + return _deferred->Promise(); }; Napi::Value Call(const CallbackInfo &info) { @@ -236,14 +261,14 @@ class TSFNWrap : public ObjectWrap { } auto *data = - new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + new Data{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; _tsfn.NonBlockingCall(data); return data->deferred.Promise(); }; private: TSFN _tsfn; - Promise::Deferred _deferred; + std::unique_ptr _deferred; }; #endif @@ -251,7 +276,7 @@ class TSFNWrap : public ObjectWrap { namespace existing { -struct DataType { +struct Data { Promise::Deferred deferred; bool reject; }; @@ -260,7 +285,7 @@ struct DataType { // _NOT_ used by `Napi::ThreadSafeFunctionEx<>`. static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, void *data) { - DataType *casted = static_cast(data); + Data *casted = static_cast(data); if (env != nullptr) { if (data != nullptr) { napi_value undefined; @@ -278,8 +303,16 @@ static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, } } +// This test creates a native napi_threadsafe_function itself, whose `context` +// parameter is the `TSFNWrap` object itself. We forward-declare, so we can use +// it as an argument inside `ThreadSafeFunctionEx<>`. This also allows us to +// statically get the correct type when using `tsfn.GetContext()`. The converse +// is true: if the Context type does _not_ match that provided to the underlying +// napi_create_threadsafe_function, then the static type will be incorrect. +class TSFNWrap; + // Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; // A JS-accessible wrap that holds a TSFN. class TSFNWrap : public ObjectWrap { @@ -294,9 +327,7 @@ class TSFNWrap : public ObjectWrap { locals.Set("TSFNWrap", func); } - TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { + TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { auto env = info.Env(); #if NAPI_VERSION == 4 @@ -314,7 +345,7 @@ class TSFNWrap : public ObjectWrap { napi_threadsafe_function napi_tsfn; auto status = napi_create_threadsafe_function( info.Env(), nullptr, nullptr, String::From(info.Env(), "Test"), 0, 1, - nullptr, nullptr, nullptr, CallJs, &napi_tsfn); + nullptr, Finalizer, this, CallJs, &napi_tsfn); if (status != napi_ok) { NAPI_THROW_IF_FAILED(env, status); } @@ -322,21 +353,39 @@ class TSFNWrap : public ObjectWrap { #endif } - Napi::Value Release(const CallbackInfo &) { + Napi::Value Release(const CallbackInfo &info) { + if (_deferred) { + return _deferred->Promise(); + } + auto env = info.Env(); + _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); _tsfn.Release(); - return _deferred.Promise(); + return _deferred->Promise(); }; Napi::Value Call(const CallbackInfo &info) { auto *data = - new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + new Data{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; _tsfn.NonBlockingCall(data); return data->deferred.Promise(); }; private: TSFN _tsfn; - Promise::Deferred _deferred; + std::unique_ptr _deferred; + + static void Finalizer(napi_env env, void * /*data*/, void *ctx) { + TSFNWrap *tsfn = static_cast(ctx); + tsfn->Finalizer(env); + } + + void Finalizer(napi_env e) { + if (_deferred) { + _deferred->Resolve(Boolean::New(e, true)); + _deferred.release(); + } + // Finalizer finalizeCallback, + } }; } // namespace existing @@ -359,8 +408,7 @@ class TSFNWrap : public ObjectWrap { locals.Set("TSFNWrap", func); } - TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info) { + TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { auto env = info.Env(); #if NAPI_VERSION == 4 @@ -386,7 +434,7 @@ class TSFNWrap : public ObjectWrap { // to know when the underlying ThreadSafeFunction has been finalized. Napi::Value Release(const CallbackInfo &info) { _tsfn.Release(); - return info.Env().Undefined(); + return String::New(info.Env(), "TSFN may not have finalized."); }; Napi::Value Call(const CallbackInfo &info) { @@ -404,9 +452,9 @@ Object InitThreadSafeFunctionExBasic(Env env) { // A list of v4+ enables spec namespaces. #define V4_EXPORTS(V) \ + V(call) \ V(simple) \ V(existing) \ - V(call) \ V(context) // A list of v5+ enables spec namespaces. diff --git a/test/threadsafe_function_ex/test/basic.js b/test/threadsafe_function_ex/test/basic.js index a3f9769e4..598246b30 100644 --- a/test/threadsafe_function_ex/test/basic.js +++ b/test/threadsafe_function_ex/test/basic.js @@ -1,96 +1,14 @@ // @ts-check 'use strict'; const assert = require('assert'); -const buildType = process.config.target_defaults.default_configuration; -// If `true`, this module will re-throw any error caught, allowing the caller to -// handle. -const SHOW_OUTPUT = true; - -const print = (isError, newLine, ...what) => { - if (SHOW_OUTPUT) { - let target, method; - target = newLine ? console : process[isError ? 'stderr' : 'stdout']; - method = target === console ? (isError ? 'error' : 'log') : 'write'; - if (isError) { - method - - } - return target[method].apply(target, what); - } -} - -/** @returns {void} */ -const log = (...what) => print(false, true, ...what); - -/** @returns {void} */ -const error = (...what) => print(true, true, ...what); - -/** @returns {Promise} */ -const write = (...what) => print(false, false, ...what); - -/** @returns {Promise} */ -// const rewind = () => print(false, false, `\x1b[K`); -const rewind = () => print(false, false, `\x1b[1A`); - -const pad = (what, targetLength = 20, padString = ' ', padLeft) => { - const padder = (pad, str) => { - if (typeof str === 'undefined') - return pad; - if (padLeft) { - return (pad + str).slice(-pad.length); - } else { - return (str + pad).substring(0, pad.length); - } - }; - return padder(padString.repeat(targetLength), String(what)); -} +const { TestRunner } = require('../util/TestRunner'); /** - * Test runner helper class. Each static method's name corresponds to the - * namespace the test as defined in the native addon. Each test specifics are - * documented on the individual method. The async test handler runs - * synchronously in the series of all tests so the test **MUST** wait on the - * finalizer. Otherwise, the test runner will assume the test completed. + * A "basic" test spec. This spec does NOT use threads, and is primarily used to + * verify the API. */ -class TestRunner { - - static async run(isNoExcept) { - const binding = require(`../../build/${buildType}/binding${isNoExcept ? '_noexcept' : ''}.node`); - const runner = new this(); - // Errors thrown are caught by caller, and re-thrown if `BUBBLE_ERRORS` is - // `true. - const cmdlineTests = process.argv.length > 2 ? process.argv.slice(2) : null; - for (const nsName of Object.getOwnPropertyNames(this.prototype)) { - if (nsName !== 'constructor') { - const ns = binding.threadsafe_function_ex_basic[nsName]; - let state; - const setState = (...newState) => { state = newState }; - const toLine = (state) => { - const [label, time, isNoExcept, nsName] = state; - const except = () => pad(isNoExcept ? '[noexcept]' : '', 12); - const timeStr = () => time == null ? '...' : `${time}${typeof time === 'number' ? 'ms' : ''}`; - return `${pad(nsName, 10)} ${except()}| ${pad(timeStr(), 8)}| ${pad(label, 15)}`; - }; - const stateLine = () => toLine(state); - if (ns && (cmdlineTests == null || cmdlineTests.indexOf(nsName) > -1)) { - setState('Running test', null, isNoExcept, nsName); - log(stateLine()); - - const start = Date.now(); - await runner[nsName](ns); - await new Promise(resolve => setTimeout(resolve, 50)); - rewind(); - setState('Finished test', Date.now() - start, isNoExcept, nsName); - log(stateLine()); - } else { - setState('Skipping test', '-', isNoExcept, nsName); - debugger; - log(stateLine()); - } - } - } - } +class BasicTest extends TestRunner { /** * This test ensures the data sent to the NonBlockingCall and the data * received in the JavaScript callback are the same. @@ -106,35 +24,38 @@ class TestRunner { assert(data === tsfnData, "Data in and out of tsfn call do not equal"); }); await tsfn.call(data); - await tsfn.release(); + return await tsfn.release(); } /** * The context provided to the threadsafe function's constructor is accessible - * on both the threadsafe function's callback as well the threadsafe function - * itself. This test ensures the context across all three are the same. + * on both (A) the threadsafe function's callback as well as (B) the + * threadsafe function itself. This test ensures the context across all three + * are the same. * - Creates a contexted threadsafe function with callback. * - The callback forwards the item's data to the given JavaScript function in * the test. - * - Makes one call, and waits for call to complete. - * - Asserts the contexts are the same. + * - Asserts the contexts are the same as the context passed during threadsafe + * function construction in two places: + * - (A) Makes one call, and waits for call to complete. + * - (B) Asserts that the context returns from the API's `GetContext()` */ async context({ TSFNWrap }) { const ctx = {}; const tsfn = new TSFNWrap(ctx); assert(ctx === await tsfn.getContextByCall(), "getContextByCall context not equal"); assert(ctx === tsfn.getContextFromTsfn(), "getContextFromTsfn context not equal"); - await tsfn.release(); + return await tsfn.release(); } /** * **ONLY ON N-API 5+**. The optional JavaScript function callback feature is - * not available in N-API <= 4. + * not available in N-API <= 4. This test creates uses a threadsafe function + * that handles all of its JavaScript processing on the callJs instead of the + * callback. * - Creates a threadsafe function with no JavaScript context or callback. * - Makes two calls, waiting for each, and expecting the first to resolve * and the second to reject. - * - Waits for Node to process the items on the queue prior releasing the - * threadsafe function. */ async empty({ TSFNWrap }) { debugger; @@ -149,13 +70,14 @@ class TestRunner { } assert.ok(caught, 'The promise rejection was not caught'); - await tsfn.release(); + return await tsfn.release(); } + return true; } /** - * A `ThreadSafeFunctionEx<>` can be constructed with default type arguments. - * - Creates a threadsafe function with no context or callback. + * A `ThreadSafeFunctionEx<>` can be constructed with no type arguments. + * - Creates a threadsafe function with no context or callback or callJs. * - The node-addon-api 'no callback' feature is implemented by passing either * a no-op `Function` on N-API 4 or `std::nullptr` on N-API 5+ to the * underlying `napi_create_threadsafe_function` call. @@ -167,22 +89,9 @@ class TestRunner { async simple({ TSFNWrap }) { const tsfn = new TSFNWrap(); tsfn.call(); - await tsfn.release(); + return await tsfn.release(); } } -async function run() { - await TestRunner.run(false); - await TestRunner.run(true); -} - - -module.exports = run() - .then(() => { log(`Finished executing tests in .${__filename.replace(process.cwd(), '')}`); }) - .catch((e) => { - // if (require.main !== module) { throw e; } - console.error(`Test failed!`, e); - process.exit(1); - }); - +module.exports = new BasicTest('threadsafe_function_ex_basic', __filename).start(); diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc index 5d336b13e..2da92104a 100644 --- a/test/threadsafe_function_ex/test/example.cc +++ b/test/threadsafe_function_ex/test/example.cc @@ -1,6 +1,3 @@ -#undef NAPI_CPP_EXCEPTIONS -#define NAPI_DISABLE_CPP_EXCEPTIONS - /** * This test is programmatically represents the example shown in * `doc/threadsafe_function_ex.md` diff --git a/test/threadsafe_function_ex/test/threadsafe.js b/test/threadsafe_function_ex/test/threadsafe.js index 6ceceb92c..2d807b040 100644 --- a/test/threadsafe_function_ex/test/threadsafe.js +++ b/test/threadsafe_function_ex/test/threadsafe.js @@ -5,13 +5,13 @@ const assert = require('assert'); const common = require('../../common'); module.exports = run() - .then(() => { console.log(`Finished executing tests in .${__filename.replace(process.cwd(),'')}`); }) .catch((e) => { console.error(`Test failed!`, e); process.exit(1); }); async function run() { + console.log(`Running tests in .${__filename.replace(process.cwd(),'')}`); await test(require(`../../build/${buildType}/binding.node`)); await test(require(`../../build/${buildType}/binding_noexcept.node`)); } diff --git a/test/threadsafe_function_ex/util/TestRunner.js b/test/threadsafe_function_ex/util/TestRunner.js new file mode 100644 index 000000000..b7e99c1b4 --- /dev/null +++ b/test/threadsafe_function_ex/util/TestRunner.js @@ -0,0 +1,166 @@ +// @ts-check +'use strict'; +const assert = require('assert'); +const { basename, extname } = require('path'); +const buildType = process.config.target_defaults.default_configuration; + +// If you pass certain test names as argv, run those only. +const cmdlineTests = process.argv.length > 2 ? process.argv.slice(2) : null; + +const pad = (what, targetLength = 20, padString = ' ', padLeft) => { + const padder = (pad, str) => { + if (typeof str === 'undefined') + return pad; + if (padLeft) { + return (pad + str).slice(-pad.length); + } else { + return (str + pad).substring(0, pad.length); + } + }; + return padder(padString.repeat(targetLength), String(what)); +} + +/** + * Test runner helper class. Each static method's name corresponds to the + * namespace the test as defined in the native addon. Each test specifics are + * documented on the individual method. The async test handler runs + * synchronously in the series of all tests so the test **MUST** wait on the + * finalizer. Otherwise, the test runner will assume the test completed. + */ +class TestRunner { + + /** + * If `true`, always show results as interactive. See constructor for more + * information. + */ + static SHOW_OUTPUT = false; + + /** + * @param {string} bindingKey The key to use when accessing the binding. + * @param {string} filename Name of file that the current TestRunner instance + * is being constructed. This determines how to log to console: + * - When the test is running as the current module, output is shown on both + * start and stop of test in an 'interactive' styling. + * - Otherwise, the output is more of a CI-like styling. + */ + constructor(bindingKey, filename) { + this.bindingKey = bindingKey; + this.filename = filename; + this.interactive = TestRunner.SHOW_OUTPUT || filename === require.main.filename; + this.specName = `${this.bindingKey}/${basename(this.filename, extname(this.filename))}`; + } + + async start() { + try { + this.log(`Running tests in .${this.filename.replace(process.cwd(), '')}:\n`); + // Run tests in both except and noexcept + await this.run(false); + await this.run(true); + } catch (ex) { + console.error(`Test failed!`, ex); + process.exit(1); + } + } + + /** + * @param {boolean} isNoExcept If true, use the 'noexcept' binding. + */ + async run(isNoExcept) { + const binding = require(`../../build/${buildType}/binding${isNoExcept ? '_noexcept' : ''}.node`); + const { bindingKey } = this; + const spec = binding[bindingKey]; + const runner = this; + + // If we can't find the key in the binding, error. + if (!spec) { + throw new Error(`Could not find '${bindingKey}' in binding.`); + } + + // A 'test' is defined as any function on the prototype of this object. + for (const nsName of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) { + if (nsName !== 'constructor') { + const ns = spec[nsName]; + + // Interactive mode prints start and end messages + if (this.interactive) { + + + + /** @typedef {[string, string | null | number, boolean, string, any]} State [label, time, isNoExcept, nsName, returnValue] */ + + /** @type {State} */ + let state = [undefined, undefined, undefined, undefined, undefined] + + const stateLine = () => { + const [label, time, isNoExcept, nsName, returnValue] = state; + const except = () => pad(isNoExcept ? '[noexcept]' : '', 12); + const timeStr = () => time == null ? '...' : `${time}${typeof time === 'number' ? 'ms' : ''}`; + return `${pad(nsName, 10)} ${except()}| ${pad(timeStr(), 8)}| ${pad(label, 15)}${returnValue === undefined ? '' : `(return: ${returnValue})`}`; + }; + + /** + * @param {string} label + * @param {string | number} time + * @param {boolean} isNoExcept + * @param {string} nsName + * @param {any} returnValue + */ + const setState = (label, time, isNoExcept, nsName, returnValue) => { + if (state[1] === null) { + // Move to last line + this.print(false, `\x1b[1A`); + } + state = [label, time, isNoExcept, nsName, returnValue]; + this.log(stateLine()); + }; + + const runTest = (cmdlineTests == null || cmdlineTests.indexOf(nsName) > -1); + + if (ns && typeof runner[nsName] === 'function' && runTest) { + setState('Running test', null, isNoExcept, nsName, undefined); + const start = Date.now(); + const returnValue = await runner[nsName](ns); + await this.dummy(); + setState('Finished test', Date.now() - start, isNoExcept, nsName, returnValue); + } else { + setState('Skipping test', '-', isNoExcept, nsName, undefined); + } + } else { + console.log(`Running test '${this.specName}/${nsName}' ${isNoExcept ? '[noexcept]' : ''}`); + await runner[nsName](ns); + await this.dummy(); + } + } + } + } + + dummy() { return new Promise(resolve => setTimeout(resolve, 50)); } + + /** + * Print to console only when using interactive mode. + * + * @param {boolean} newLine If true, end with a new line. + * @param {any[]} what What to print + */ + print(newLine, ...what) { + if (this.interactive) { + let target, method; + target = newLine ? console : process.stdout; + method = target === console ? 'log' : 'write'; + return target[method].apply(target, what); + } + } + + /** + * Log to console only when using interactive mode. + * @param {string[]} what + */ + log(...what) { + this.print(true, ...what); + } + +} + +module.exports = { + TestRunner +}; From 6b7a7d05f2a70d3f168eee5313292b1b31984acb Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 14 Jun 2020 20:27:44 +0200 Subject: [PATCH 17/39] test: consolidate duplicated code --- test/binding.gyp | 6 +- test/threadsafe_function_ex/index.js | 2 +- test/threadsafe_function_ex/test/basic.cc | 304 +++++++++------------- test/threadsafe_function_ex/test/basic.js | 4 +- test/threadsafe_function_ex/util/util.h | 62 +++++ 5 files changed, 184 insertions(+), 194 deletions(-) create mode 100644 test/threadsafe_function_ex/util/util.h diff --git a/test/binding.gyp b/test/binding.gyp index 923cb2f55..8bfa59e3e 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -35,9 +35,9 @@ 'object/set_property.cc', 'promise.cc', 'run_script.cc', - 'threadsafe_function_ex/basic.cc', - 'threadsafe_function_ex/example.cc', - 'threadsafe_function_ex/threadsafe.cc', + 'threadsafe_function_ex/test/basic.cc', + 'threadsafe_function_ex/test/example.cc', + 'threadsafe_function_ex/test/threadsafe.cc', 'threadsafe_function/threadsafe_function_ctx.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', 'threadsafe_function/threadsafe_function_ptr.cc', diff --git a/test/threadsafe_function_ex/index.js b/test/threadsafe_function_ex/index.js index 9e7778d8f..917d1b060 100644 --- a/test/threadsafe_function_ex/index.js +++ b/test/threadsafe_function_ex/index.js @@ -1,5 +1,5 @@ const tests = [ - // 'threadsafe', + 'threadsafe', 'basic', 'example' ]; diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc index 94594dfe7..a0c1fd178 100644 --- a/test/threadsafe_function_ex/test/basic.cc +++ b/test/threadsafe_function_ex/test/basic.cc @@ -1,4 +1,6 @@ +#include #include "napi.h" +#include "../util/util.h" #if (NAPI_VERSION > 3) @@ -6,10 +8,10 @@ using namespace Napi; namespace call { -// Context of our TSFN. +// Context of the TSFN. using Context = std::nullptr_t; -// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall +// Data passed (as pointer) to [Non]BlockingCall struct Data { Reference data; Promise::Deferred deferred; @@ -29,39 +31,31 @@ static void CallJs(Napi::Env env, Napi::Function jsCallback, } } -// Full type of our ThreadSafeFunctionEx +// Full type of the ThreadSafeFunctionEx using TSFN = ThreadSafeFunctionEx; -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { +class TSFNWrap; +using base = tsfnutil::TSFNWrapBase; + +// A JS-accessible wrap that holds the TSFN. +class TSFNWrap : public base { public: - static void Init(Napi::Env env, Object exports, const std::string &ns) { - Function func = - DefineClass(env, "TSFNCall", - {InstanceMethod("call", &TSFNWrap::Call), - InstanceMethod("release", &TSFNWrap::Release)}); - - auto locals(Object::New(env)); - exports.Set(ns, locals); - locals.Set("TSFNWrap", func); + TSFNWrap(const CallbackInfo &info) : base(info) { + Napi::Env env = info.Env(); + _tsfn = TSFN::New(env, // napi_env env, + info[0].As(), // const Function& callback, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + nullptr, // ContextType* context + base::Finalizer, // Finalizer finalizer + &_deferred // FinalizerDataType data + ); } - TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { - Napi::Env env = info.Env(); - Function callback = info[0].As(); - _tsfn = TSFN::New(env, // napi_env env, - callback, // const Function& callback, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - nullptr, - [this](Napi::Env env, void *, - Context *ctx) { // Finalizer finalizeCallback, - if (_deferred) { - _deferred->Resolve(Boolean::New(env, true)); - _deferred.release(); - } - }); + static std::array, 2> InstanceMethods() { + return {InstanceMethod("release", &TSFNWrap::Release), + InstanceMethod("call", &TSFNWrap::Call)}; } Napi::Value Call(const CallbackInfo &info) { @@ -72,30 +66,16 @@ class TSFNWrap : public ObjectWrap { return data->deferred.Promise(); }; - Napi::Value Release(const CallbackInfo &info) { - if (_deferred) { - return _deferred->Promise(); - } - - auto env = info.Env(); - _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); - _tsfn.Release(); - return _deferred->Promise(); - }; - -private: - TSFN _tsfn; - std::unique_ptr _deferred; }; } // namespace call namespace context { -// Context of our TSFN. +// Context of the TSFN. using Context = Reference; -// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall +// Data passed (as pointer) to [Non]BlockingCall using Data = Promise::Deferred; // CallJs callback function @@ -111,79 +91,58 @@ static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, } } -// Full type of our ThreadSafeFunctionEx +// Full type of the ThreadSafeFunctionEx using TSFN = ThreadSafeFunctionEx; -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { -public: - static void Init(Napi::Env env, Object exports, const char *ns) { - Function func = DefineClass( - env, "TSFNWrap", - {InstanceMethod("getContextByCall", &TSFNWrap::GetContextByCall), - InstanceMethod("getContextFromTsfn", &TSFNWrap::GetContextFromTsfn), - InstanceMethod("release", &TSFNWrap::Release)}); - - auto locals(Object::New(env)); - exports.Set(ns, locals); - locals.Set("TSFNWrap", func); - } +class TSFNWrap; +using base = tsfnutil::TSFNWrapBase; - TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { +// A JS-accessible wrap that holds the TSFN. +class TSFNWrap : public base { +public: + TSFNWrap(const CallbackInfo &info) : base(info) { Napi::Env env = info.Env(); Context *context = new Context(Persistent(info[0])); - _tsfn = TSFN::New(info.Env(), // napi_env env, - Function::New(env, - [](const CallbackInfo & /*info*/) { - }), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - context, // Context* context, - [this](Napi::Env env, void *, - Context *ctx) { // Finalizer finalizeCallback, - if (_deferred) { - _deferred->Resolve(Boolean::New(env, true)); - _deferred.release(); - } - }); + _tsfn = TSFN::New( + env, // napi_env env, + TSFN::DefaultFunctionFactory(env), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + context, // Context* context, + base::Finalizer, // Finalizer finalizer + &_deferred // FinalizerDataType data + ); + } + + static std::array, 3> InstanceMethods() { + return {InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("getContext", &TSFNWrap::GetContext), + InstanceMethod("release", &TSFNWrap::Release)}; } - Napi::Value GetContextByCall(const CallbackInfo &info) { - Napi::Env env = info.Env(); - auto *callData = new Data(env); + Napi::Value Call(const CallbackInfo &info) { + auto *callData = new Data(info.Env()); _tsfn.NonBlockingCall(callData); return callData->Promise(); }; - Napi::Value GetContextFromTsfn(const CallbackInfo &) { + Napi::Value GetContext(const CallbackInfo &) { return _tsfn.GetContext()->Value(); }; - - Napi::Value Release(const CallbackInfo &info) { - if (_deferred) { - return _deferred->Promise(); - } - auto env = info.Env(); - _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); - _tsfn.Release(); - return _deferred->Promise(); - }; - -private: - TSFN _tsfn; - std::unique_ptr _deferred; }; } // namespace context namespace empty { #if NAPI_VERSION > 4 -using Context = void; +// Context of the TSFN. +using Context = std::nullptr_t; +// Data passed (as pointer) to [Non]BlockingCall struct Data { Promise::Deferred deferred; bool reject; @@ -206,52 +165,32 @@ static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/, } } -// Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +// Full type of the ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { -public: - static void Init(Napi::Env env, Object exports, const std::string &ns) { - Function func = - DefineClass(env, "TSFNWrap", - {InstanceMethod("call", &TSFNWrap::Call), - InstanceMethod("release", &TSFNWrap::Release)}); - - auto locals(Object::New(env)); - exports.Set(ns, locals); - locals.Set("TSFNWrap", func); - } +class TSFNWrap; +using base = tsfnutil::TSFNWrapBase; - TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { +// A JS-accessible wrap that holds the TSFN. +class TSFNWrap : public base { +public: + TSFNWrap(const CallbackInfo &info) : base(info) { auto env = info.Env(); - _tsfn = TSFN::New(env, // napi_env env, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - nullptr, - [this](Napi::Env env, void *, - Context *ctx) { // Finalizer finalizeCallback, - if (_deferred) { - _deferred->Resolve(Boolean::New(env, true)); - _deferred.release(); - } - }); + _tsfn = TSFN::New(env, // napi_env env, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + nullptr, // ContextType* context + base::Finalizer, // Finalizer finalizer + &_deferred // FinalizerDataType data + ); } - Napi::Value Release(const CallbackInfo &info) { - // Since this is actually a SINGLE-THREADED test, we don't have to worry - // about race conditions on accessing `_deferred`. - if (_deferred) { - return _deferred->Promise(); - } - - auto env = info.Env(); - _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); - _tsfn.Release(); - return _deferred->Promise(); - }; + static std::array, 2> InstanceMethods() { + return {InstanceMethod("release", &TSFNWrap::Release), + InstanceMethod("call", &TSFNWrap::Call)}; + } Napi::Value Call(const CallbackInfo &info) { if (info.Length() == 0 || !info[0].IsBoolean()) { @@ -265,10 +204,6 @@ class TSFNWrap : public ObjectWrap { _tsfn.NonBlockingCall(data); return data->deferred.Promise(); }; - -private: - TSFN _tsfn; - std::unique_ptr _deferred; }; #endif @@ -276,13 +211,15 @@ class TSFNWrap : public ObjectWrap { namespace existing { +// Data passed (as pointer) to [Non]BlockingCall struct Data { Promise::Deferred deferred; bool reject; }; // CallJs callback function provided to `napi_create_threadsafe_function`. It is -// _NOT_ used by `Napi::ThreadSafeFunctionEx<>`. +// _NOT_ used by `Napi::ThreadSafeFunctionEx<>`, which is why these arguments +// are napi_*. static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, void *data) { Data *casted = static_cast(data); @@ -304,34 +241,32 @@ static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, } // This test creates a native napi_threadsafe_function itself, whose `context` -// parameter is the `TSFNWrap` object itself. We forward-declare, so we can use +// parameter is the `TSFNWrap` object. We forward-declare, so we can use // it as an argument inside `ThreadSafeFunctionEx<>`. This also allows us to // statically get the correct type when using `tsfn.GetContext()`. The converse // is true: if the Context type does _not_ match that provided to the underlying // napi_create_threadsafe_function, then the static type will be incorrect. class TSFNWrap; +// Context of the TSFN. +using Context = TSFNWrap; + // Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; +using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { +class TSFNWrap : public base { public: - static void Init(Napi::Env env, Object exports, const std::string &ns) { - Function func = - DefineClass(env, "TSFNWrap", - {InstanceMethod("call", &TSFNWrap::Call), - InstanceMethod("release", &TSFNWrap::Release)}); - auto locals(Object::New(env)); - exports.Set(ns, locals); - locals.Set("TSFNWrap", func); - } - - TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { + TSFNWrap(const CallbackInfo &info) : base(info) { auto env = info.Env(); #if NAPI_VERSION == 4 napi_threadsafe_function napi_tsfn; + + // A threadsafe function on N-API 4 still requires a callback function, so + // this uses the `DefaultFunctionFactory` helper method to return a no-op + // Function. auto status = napi_create_threadsafe_function( info.Env(), TSFN::DefaultFunctionFactory(env), nullptr, String::From(info.Env(), "Test"), 0, 1, nullptr, nullptr, nullptr, @@ -339,10 +274,12 @@ class TSFNWrap : public ObjectWrap { if (status != napi_ok) { NAPI_THROW_IF_FAILED(env, status); } - // A threadsafe function on N-API 4 still requires a callback function. _tsfn = TSFN(napi_tsfn); #else napi_threadsafe_function napi_tsfn; + + // A threadsafe function may be `nullptr` on N-API 5+ as long as a `CallJS` + // is present. auto status = napi_create_threadsafe_function( info.Env(), nullptr, nullptr, String::From(info.Env(), "Test"), 0, 1, nullptr, Finalizer, this, CallJs, &napi_tsfn); @@ -353,15 +290,10 @@ class TSFNWrap : public ObjectWrap { #endif } - Napi::Value Release(const CallbackInfo &info) { - if (_deferred) { - return _deferred->Promise(); - } - auto env = info.Env(); - _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); - _tsfn.Release(); - return _deferred->Promise(); - }; + static std::array, 2> InstanceMethods() { + return {InstanceMethod("release", &TSFNWrap::Release), + InstanceMethod("call", &TSFNWrap::Call)}; + } Napi::Value Call(const CallbackInfo &info) { auto *data = @@ -371,44 +303,38 @@ class TSFNWrap : public ObjectWrap { }; private: - TSFN _tsfn; - std::unique_ptr _deferred; - + // This test uses a custom napi (NOT node-addon-api) TSFN finalizer. static void Finalizer(napi_env env, void * /*data*/, void *ctx) { TSFNWrap *tsfn = static_cast(ctx); tsfn->Finalizer(env); } + // Clean up the TSFNWrap by resolving the promise. void Finalizer(napi_env e) { if (_deferred) { _deferred->Resolve(Boolean::New(e, true)); _deferred.release(); } - // Finalizer finalizeCallback, } }; } // namespace existing namespace simple { -// Full type of our ThreadSafeFunctionEx +using Context = std::nullptr_t; + +// Full type of our ThreadSafeFunctionEx. We don't specify the `Context` here +// (even though the _default_ for the type argument is `std::nullptr_t`) to +// demonstrate construction with no type arguments. using TSFN = ThreadSafeFunctionEx<>; +class TSFNWrap; +using base = tsfnutil::TSFNWrapBase; + // A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { +class TSFNWrap : public base { public: - static void Init(Napi::Env env, Object exports, const std::string &ns) { - Function func = - DefineClass(env, "TSFNSimple", - {InstanceMethod("call", &TSFNWrap::Call), - InstanceMethod("release", &TSFNWrap::Release)}); - - auto locals(Object::New(env)); - exports.Set(ns, locals); - locals.Set("TSFNWrap", func); - } - - TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { + TSFNWrap(const CallbackInfo &info) : base(info) { auto env = info.Env(); #if NAPI_VERSION == 4 @@ -430,6 +356,11 @@ class TSFNWrap : public ObjectWrap { #endif } + static std::array, 2> InstanceMethods() { + return {InstanceMethod("release", &TSFNWrap::Release), + InstanceMethod("call", &TSFNWrap::Call)}; + } + // Since this test spec has no CALLBACK, CONTEXT, or FINALIZER. We have no way // to know when the underlying ThreadSafeFunction has been finalized. Napi::Value Release(const CallbackInfo &info) { @@ -441,16 +372,13 @@ class TSFNWrap : public ObjectWrap { _tsfn.NonBlockingCall(); return info.Env().Undefined(); }; - -private: - TSFN _tsfn; }; } // namespace simple Object InitThreadSafeFunctionExBasic(Env env) { -// A list of v4+ enables spec namespaces. +// A list of v4+ enabled spec namespaces. #define V4_EXPORTS(V) \ V(call) \ V(simple) \ diff --git a/test/threadsafe_function_ex/test/basic.js b/test/threadsafe_function_ex/test/basic.js index 598246b30..bcb1cdc8a 100644 --- a/test/threadsafe_function_ex/test/basic.js +++ b/test/threadsafe_function_ex/test/basic.js @@ -43,8 +43,8 @@ class BasicTest extends TestRunner { async context({ TSFNWrap }) { const ctx = {}; const tsfn = new TSFNWrap(ctx); - assert(ctx === await tsfn.getContextByCall(), "getContextByCall context not equal"); - assert(ctx === tsfn.getContextFromTsfn(), "getContextFromTsfn context not equal"); + assert(ctx === await tsfn.call(), "getContextByCall context not equal"); + assert(ctx === tsfn.getContext(), "getContextFromTsfn context not equal"); return await tsfn.release(); } diff --git a/test/threadsafe_function_ex/util/util.h b/test/threadsafe_function_ex/util/util.h new file mode 100644 index 000000000..36472d81a --- /dev/null +++ b/test/threadsafe_function_ex/util/util.h @@ -0,0 +1,62 @@ +#include +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace tsfnutil { +template +class TSFNWrapBase : public ObjectWrap { +public: + + + static void Init(Napi::Env env, Object exports, const std::string &ns) { + // Get methods defined by child + auto methods(TSFNWrapImpl::InstanceMethods()); + + // Create a vector, since DefineClass doesn't accept arrays. + std::vector> methodsVec(methods.begin(), methods.end()); + + auto locals(Object::New(env)); + locals.Set("TSFNWrap", ObjectWrap::DefineClass(env, "TSFNWrap", methodsVec)); + exports.Set(ns, locals); + } + + // Release the TSFN. Returns a Promise that is resolved in the TSFN's + // finalizer. + // NOTE: the 'simple' test overrides this method, because it has no finalizer. + Napi::Value Release(const CallbackInfo &info) { + if (_deferred) { + return _deferred->Promise(); + } + + auto env = info.Env(); + _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); + + _tsfn.Release(); + return _deferred->Promise(); + }; + + // TSFN finalizer. Resolves the Promise returned by `Release()` above. + static void Finalizer(Napi::Env env, + std::unique_ptr *deferred, + Context *ctx) { + if (deferred->get()) { + (*deferred)->Resolve(Boolean::New(env, true)); + deferred->release(); + } + } + + + TSFNWrapBase(const CallbackInfo &callbackInfo) + : ObjectWrap(callbackInfo) {} + +protected: + TSFN _tsfn; + std::unique_ptr _deferred; +}; + +} // namespace tsfnutil + +#endif From 7467a3f26f3ef2100af81cac28e58b1c3d9b2e3a Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 14 Jun 2020 21:00:00 +0200 Subject: [PATCH 18/39] test: basic example, standardize identifier names --- test/threadsafe_function_ex/test/basic.cc | 68 ++--- test/threadsafe_function_ex/test/example.cc | 279 ++++---------------- test/threadsafe_function_ex/test/example.js | 48 +--- 3 files changed, 100 insertions(+), 295 deletions(-) diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc index a0c1fd178..c26ca0738 100644 --- a/test/threadsafe_function_ex/test/basic.cc +++ b/test/threadsafe_function_ex/test/basic.cc @@ -9,17 +9,17 @@ using namespace Napi; namespace call { // Context of the TSFN. -using Context = std::nullptr_t; +using ContextType = std::nullptr_t; // Data passed (as pointer) to [Non]BlockingCall -struct Data { +struct DataType { Reference data; Promise::Deferred deferred; }; // CallJs callback function static void CallJs(Napi::Env env, Napi::Function jsCallback, - Context * /*context*/, Data *data) { + ContextType * /*context*/, DataType *data) { if (!(env == nullptr || jsCallback == nullptr)) { if (data != nullptr) { jsCallback.Call(env.Undefined(), {data->data.Value()}); @@ -32,10 +32,10 @@ static void CallJs(Napi::Env env, Napi::Function jsCallback, } // Full type of the ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; class TSFNWrap; -using base = tsfnutil::TSFNWrapBase; +using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds the TSFN. class TSFNWrap : public base { @@ -49,7 +49,7 @@ class TSFNWrap : public base { 1, // size_t initialThreadCount, nullptr, // ContextType* context base::Finalizer, // Finalizer finalizer - &_deferred // FinalizerDataType data + &_deferred // FinalizerDataType* data ); } @@ -60,7 +60,7 @@ class TSFNWrap : public base { Napi::Value Call(const CallbackInfo &info) { Napi::Env env = info.Env(); - Data *data = new Data{Napi::Reference(Persistent(info[0])), + DataType *data = new DataType{Napi::Reference(Persistent(info[0])), Promise::Deferred::New(env)}; _tsfn.NonBlockingCall(data); return data->deferred.Promise(); @@ -73,14 +73,14 @@ class TSFNWrap : public base { namespace context { // Context of the TSFN. -using Context = Reference; +using ContextType = Reference; // Data passed (as pointer) to [Non]BlockingCall -using Data = Promise::Deferred; +using DataType = Promise::Deferred; // CallJs callback function static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, - Context *context, Data *data) { + ContextType *context, DataType *data) { if (env != nullptr) { if (data != nullptr) { data->Resolve(context->Value()); @@ -92,10 +92,10 @@ static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, } // Full type of the ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; class TSFNWrap; -using base = tsfnutil::TSFNWrapBase; +using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds the TSFN. class TSFNWrap : public base { @@ -103,7 +103,7 @@ class TSFNWrap : public base { TSFNWrap(const CallbackInfo &info) : base(info) { Napi::Env env = info.Env(); - Context *context = new Context(Persistent(info[0])); + ContextType *context = new ContextType(Persistent(info[0])); _tsfn = TSFN::New( env, // napi_env env, @@ -112,9 +112,9 @@ class TSFNWrap : public base { "Test", // ResourceString resourceName, 0, // size_t maxQueueSize, 1, // size_t initialThreadCount, - context, // Context* context, + context, // ContextType* context, base::Finalizer, // Finalizer finalizer - &_deferred // FinalizerDataType data + &_deferred // FinalizerDataType* data ); } @@ -125,7 +125,7 @@ class TSFNWrap : public base { } Napi::Value Call(const CallbackInfo &info) { - auto *callData = new Data(info.Env()); + auto *callData = new DataType(info.Env()); _tsfn.NonBlockingCall(callData); return callData->Promise(); }; @@ -140,17 +140,17 @@ namespace empty { #if NAPI_VERSION > 4 // Context of the TSFN. -using Context = std::nullptr_t; +using ContextType = std::nullptr_t; // Data passed (as pointer) to [Non]BlockingCall -struct Data { +struct DataType { Promise::Deferred deferred; bool reject; }; // CallJs callback function -static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/, - Data *data) { +static void CallJs(Napi::Env env, Function jsCallback, ContextType * /*context*/, + DataType *data) { if (env != nullptr) { if (data != nullptr) { if (data->reject) { @@ -166,10 +166,10 @@ static void CallJs(Napi::Env env, Function jsCallback, Context * /*context*/, } // Full type of the ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; +using TSFN = ThreadSafeFunctionEx; class TSFNWrap; -using base = tsfnutil::TSFNWrapBase; +using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds the TSFN. class TSFNWrap : public base { @@ -183,7 +183,7 @@ class TSFNWrap : public base { 1, // size_t initialThreadCount, nullptr, // ContextType* context base::Finalizer, // Finalizer finalizer - &_deferred // FinalizerDataType data + &_deferred // FinalizerDataType* data ); } @@ -200,7 +200,7 @@ class TSFNWrap : public base { } auto *data = - new Data{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; _tsfn.NonBlockingCall(data); return data->deferred.Promise(); }; @@ -212,7 +212,7 @@ class TSFNWrap : public base { namespace existing { // Data passed (as pointer) to [Non]BlockingCall -struct Data { +struct DataType { Promise::Deferred deferred; bool reject; }; @@ -222,7 +222,7 @@ struct Data { // are napi_*. static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, void *data) { - Data *casted = static_cast(data); + DataType *casted = static_cast(data); if (env != nullptr) { if (data != nullptr) { napi_value undefined; @@ -244,16 +244,16 @@ static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, // parameter is the `TSFNWrap` object. We forward-declare, so we can use // it as an argument inside `ThreadSafeFunctionEx<>`. This also allows us to // statically get the correct type when using `tsfn.GetContext()`. The converse -// is true: if the Context type does _not_ match that provided to the underlying +// is true: if the ContextType does _not_ match that provided to the underlying // napi_create_threadsafe_function, then the static type will be incorrect. class TSFNWrap; // Context of the TSFN. -using Context = TSFNWrap; +using ContextType = TSFNWrap; // Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; -using base = tsfnutil::TSFNWrapBase; +using TSFN = ThreadSafeFunctionEx; +using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds a TSFN. class TSFNWrap : public base { @@ -297,7 +297,7 @@ class TSFNWrap : public base { Napi::Value Call(const CallbackInfo &info) { auto *data = - new Data{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; _tsfn.NonBlockingCall(data); return data->deferred.Promise(); }; @@ -321,15 +321,15 @@ class TSFNWrap : public base { } // namespace existing namespace simple { -using Context = std::nullptr_t; +using ContextType = std::nullptr_t; -// Full type of our ThreadSafeFunctionEx. We don't specify the `Context` here +// Full type of our ThreadSafeFunctionEx. We don't specify the `ContextType` here // (even though the _default_ for the type argument is `std::nullptr_t`) to // demonstrate construction with no type arguments. using TSFN = ThreadSafeFunctionEx<>; class TSFNWrap; -using base = tsfnutil::TSFNWrapBase; +using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds a TSFN. class TSFNWrap : public base { diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc index 2da92104a..a20d9ecae 100644 --- a/test/threadsafe_function_ex/test/example.cc +++ b/test/threadsafe_function_ex/test/example.cc @@ -1,254 +1,83 @@ -/** - * This test is programmatically represents the example shown in - * `doc/threadsafe_function_ex.md` - */ +#include +#include "napi.h" +#include "../util/util.h" #if (NAPI_VERSION > 3) -#include "napi.h" -#include -#include -static constexpr size_t DEFAULT_THREAD_COUNT = 10; -static constexpr int32_t DEFAULT_CALL_COUNT = 2; - -/** - * @brief Macro used specifically to support the dual CI test / documentation - * example setup. Exceptions are always thrown as JavaScript exceptions when - * running in example mode. - * - */ -#define TSFN_THROW(tsfnWrap, e, ...) \ - if (tsfnWrap->cppExceptions) { \ - do { \ - (e).ThrowAsJavaScriptException(); \ - return __VA_ARGS__; \ - } while (0); \ - } else { \ - NAPI_THROW(e, __VA_ARGS__); \ - } - using namespace Napi; -namespace { +namespace example { -// Context of our TSFN. -struct Context { - int32_t threadId; -}; +// Context of the TSFN. +using Context = Reference; -// Data passed (as pointer) to ThreadSafeFunctionEx::[Non]BlockingCall -using DataType = int; +// Data passed (as pointer) to [Non]BlockingCall +using DataType = Promise::Deferred; -// Callback function -static void Callback(Napi::Env env, Napi::Function jsCallback, Context *context, - DataType *data) { - // Check that the threadsafe function has not been finalized. Node calls this - // callback for items remaining on the queue once finalization has completed. - if (!(env == nullptr || jsCallback == nullptr)) { +// CallJs callback function +static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, + Context *context, DataType *data) { + if (env != nullptr) { + if (data != nullptr) { + data->Resolve(context->Value()); + } } if (data != nullptr) { delete data; } } -// Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; - -struct FinalizerDataType { - std::vector threads; -}; +// Full type of the ThreadSafeFunctionEx +using TSFN = ThreadSafeFunctionEx; -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public ObjectWrap { +class TSFNWrap; +using base = tsfnutil::TSFNWrapBase; +// A JS-accessible wrap that holds the TSFN. +class TSFNWrap : public base { public: - static Object Init(Napi::Env env, Object exports); - TSFNWrap(const CallbackInfo &info); - - // When running as an example, we want exceptions to always go to JavaScript, - // allowing the user to try/catch errors from the addon. - bool cppExceptions; - -private: - Napi::Value Start(const CallbackInfo &info); - Napi::Value Release(const CallbackInfo &info); - - // Instantiated by `Start`; resolved on finalize of tsfn. - Promise::Deferred _deferred; - - // Reference to our TSFN - TSFN _tsfn; - - // Object.prototype.toString reference for use with error messages - FunctionReference _toString; -}; - -/** - * @brief Initialize `TSFNWrap` on the environment. - * - * @param env - * @param exports - * @return Object - */ -Object TSFNWrap::Init(Napi::Env env, Object exports) { - Function func = DefineClass(env, "TSFNWrap", - {InstanceMethod("start", &TSFNWrap::Start), - InstanceMethod("release", &TSFNWrap::Release)}); - - exports.Set("TSFNWrap", func); - return exports; -} - -static void threadEntry(size_t threadId, TSFN tsfn, int32_t callCount) { - using namespace std::chrono_literals; - for (int32_t i = 0; i < callCount; ++i) { - tsfn.NonBlockingCall(new int); - std::this_thread::sleep_for(50ms * threadId); + TSFNWrap(const CallbackInfo &info) : base(info) { + Napi::Env env = info.Env(); + + Context *context = new Context(Persistent(info[0])); + + _tsfn = TSFN::New( + env, // napi_env env, + TSFN::DefaultFunctionFactory(env), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + context, // Context* context, + base::Finalizer, // Finalizer finalizer + &_deferred // FinalizerDataType data + ); } - tsfn.Release(); -} - -/** - * @brief Construct a new TSFNWrap object on the main thread. If any arguments - * are passed, exceptions in the addon will always be thrown JavaScript - * exceptions, allowing the user to try/catch errors from the addon. - * - * @param info - */ -TSFNWrap::TSFNWrap(const CallbackInfo &info) - : ObjectWrap(info), - _deferred(Promise::Deferred::New(info.Env())) { - auto env = info.Env(); - _toString = Napi::Persistent(env.Global() - .Get("Object") - .ToObject() - .Get("prototype") - .ToObject() - .Get("toString") - .As()); - cppExceptions = true; - info.Length() > 0; -} -} // namespace - -/** - * @brief Instance method `TSFNWrap#start` - * - * @param info - * @return undefined - */ -Napi::Value TSFNWrap::Start(const CallbackInfo &info) { - Napi::Env env = info.Env(); - - // Creates a list to hold how many times each thread should make a call. - std::vector callCounts; - - // The JS-provided callback to execute for each call (if provided) - Function callback; - - if (info.Length() > 0 && info[0].IsObject()) { - auto arg0 = info[0].ToObject(); - if (arg0.Has("threads")) { - Napi::Value threads = arg0.Get("threads"); - if (threads.IsArray()) { - Napi::Array threadsArray = threads.As(); - for (auto i = 0U; i < threadsArray.Length(); ++i) { - Napi::Value elem = threadsArray.Get(i); - if (elem.IsNumber()) { - callCounts.push_back(elem.As().Int32Value()); - } else { - // TSFN_THROW(this, - // Napi::TypeError::New(Env(), - // "Invalid arguments"), - // Object()); - - // ThrowAsJavaScriptException - Napi::TypeError::New(Env(), "Invalid arguments") - .ThrowAsJavaScriptException(); - return env.Undefined(); - - // if (this->cppExceptions) { - // do { - // (Napi::TypeError::New(Env(), "Invalid arguments")) - // .ThrowAsJavaScriptException(); - // return Object(); - // } while (0); - // } else { - // NAPI_THROW(Napi::TypeError::New(Env(), "Invalid arguments"), - // Object()); - // } - } - } - } else if (threads.IsNumber()) { - auto threadCount = threads.As().Int32Value(); - for (int32_t i = 0; i < threadCount; ++i) { - callCounts.push_back(DEFAULT_CALL_COUNT); - } - } else { - TSFN_THROW(this, Napi::TypeError::New(Env(), "Invalid arguments"), - Number()); - } - } - if (arg0.Has("callback")) { - auto cb = arg0.Get("callback"); - if (cb.IsFunction()) { - callback = cb.As(); - } else { - TSFN_THROW(this, - Napi::TypeError::New(Env(), "Callback is not a function"), - Number()); - } - } + static std::array, 3> InstanceMethods() { + return {InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("getContext", &TSFNWrap::GetContext), + InstanceMethod("release", &TSFNWrap::Release)}; } - // Apply default arguments - if (callCounts.size() == 0) { - for (size_t i = 0; i < DEFAULT_THREAD_COUNT; ++i) { - callCounts.push_back(DEFAULT_CALL_COUNT); - } - } + Napi::Value Call(const CallbackInfo &info) { + auto *callData = new DataType(info.Env()); + _tsfn.NonBlockingCall(callData); + return callData->Promise(); + }; - // const auto threadCount = callCounts.size(); - // FinalizerDataType *finalizerData = new FinalizerDataType(); - // // TSFN::New(info.Env(), info[0].As(), Object::New(info.Env()), - // // "Test", tsfnInfo.maxQueueSize, 2, &tsfnInfo, JoinTheThreads, - - // threads); _tsfn = TSFN::New(env, // napi_env env, - // callback, // const Function& callback, - // "Test", // ResourceString resourceName, - // 0, // size_t maxQueueSize, - // threadCount // size_t initialThreadCount, - // ); - // for (int32_t threadId = 0; threadId < threadCount; ++threadId) { - // // static void threadEntry(size_t threadId, TSFN tsfn, int32_t callCount) - // { - - // finalizerData->threads.push_back( - // std::thread(threadEntry, threadId, _tsfn, callCounts[threadId])); - // } - return env.Undefined(); + Napi::Value GetContext(const CallbackInfo &) { + return _tsfn.GetContext()->Value(); + }; }; +} // namespace context -/** - * @brief Instance method `TSFNWrap#release` - * - * @param info - * @return undefined - */ -Napi::Value TSFNWrap::Release(const CallbackInfo &info) { - Napi::Env env = info.Env(); - return env.Undefined(); -}; -/** - * @brief Module initialization function - * - * @param env - * @return Object - */ Object InitThreadSafeFunctionExExample(Env env) { - return TSFNWrap::Init(env, Object::New(env)); + auto exports(Object::New(env)); + example::TSFNWrap::Init(env, exports, "example"); + return exports; } + #endif diff --git a/test/threadsafe_function_ex/test/example.js b/test/threadsafe_function_ex/test/example.js index 3aaf0c736..323777a7c 100644 --- a/test/threadsafe_function_ex/test/example.js +++ b/test/threadsafe_function_ex/test/example.js @@ -1,44 +1,20 @@ +// @ts-check 'use strict'; - -/** - * This test is programmatically represents the example shown in - * `doc/threadsafe_function_ex.md` - */ - const assert = require('assert'); -const buildType = 'Debug'; process.config.target_defaults.default_configuration; -const isCI = require.main !== module; -const print = (isError, ...what) => isCI ? () => {} : console[isError ? 'error' : 'log'].apply(console.log, what); -const log = (...what) => print(false, ...what); -const error = (...what) => print(true, ...what); +const { TestRunner } = require('../util/TestRunner'); -module.exports = Promise.all([ - // test(require(`../build/${buildType}/binding_noexcept.node`)), - isCI ? undefined : test(require(`../build/${buildType}/binding_noexcept.node`)) -]).catch(e => { - console.error('Error', e); -}); -async function test(binding) { - try { - const tsfn = new binding.threadsafe_function_ex_example.TSFNWrap(true); - await tsfn.start({ - threads: ['f',5,5,5], - callback: ()=>{} - }); - await tsfn.release(); -} catch (e) { - error(e); -} -} +class ExampleTest extends TestRunner { + async example({ TSFNWrap }) { + const ctx = {}; + const tsfn = new TSFNWrap(ctx); + assert(ctx === await tsfn.call(), "getContextByCall context not equal"); + assert(ctx === tsfn.getContext(), "getContextFromTsfn context not equal"); + return await tsfn.release(); + } -if (!isCI) { - console.log(module.exports); - module.exports.then(() => { - log('tea'); - }).catch((e) => { - error('Error!', e); - }); } + +module.exports = new ExampleTest('threadsafe_function_ex_example', __filename).start(); From a01c3c865276b2bee298141892a1f16d5c0bad19 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 14 Jun 2020 21:27:17 +0200 Subject: [PATCH 19/39] test: refactor the the 'empty' tsfnex test It should actually check for empty jsCallback --- test/threadsafe_function_ex/test/basic.cc | 34 +++++++++-------------- test/threadsafe_function_ex/test/basic.js | 13 ++------- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc index c26ca0738..aa471dc91 100644 --- a/test/threadsafe_function_ex/test/basic.cc +++ b/test/threadsafe_function_ex/test/basic.cc @@ -1,6 +1,6 @@ -#include -#include "napi.h" #include "../util/util.h" +#include "napi.h" +#include #if (NAPI_VERSION > 3) @@ -60,12 +60,12 @@ class TSFNWrap : public base { Napi::Value Call(const CallbackInfo &info) { Napi::Env env = info.Env(); - DataType *data = new DataType{Napi::Reference(Persistent(info[0])), - Promise::Deferred::New(env)}; + DataType *data = + new DataType{Napi::Reference(Persistent(info[0])), + Promise::Deferred::New(env)}; _tsfn.NonBlockingCall(data); return data->deferred.Promise(); }; - }; } // namespace call @@ -145,18 +145,17 @@ using ContextType = std::nullptr_t; // Data passed (as pointer) to [Non]BlockingCall struct DataType { Promise::Deferred deferred; - bool reject; }; // CallJs callback function -static void CallJs(Napi::Env env, Function jsCallback, ContextType * /*context*/, - DataType *data) { +static void CallJs(Napi::Env env, Function jsCallback, + ContextType * /*context*/, DataType *data) { if (env != nullptr) { if (data != nullptr) { - if (data->reject) { - data->deferred.Reject(env.Undefined()); + if (jsCallback.IsEmpty()) { + data->deferred.Resolve(Boolean::New(env, true)); } else { - data->deferred.Resolve(env.Undefined()); + data->deferred.Reject(String::New(env, "jsCallback is not empty")); } } } @@ -193,14 +192,7 @@ class TSFNWrap : public base { } Napi::Value Call(const CallbackInfo &info) { - if (info.Length() == 0 || !info[0].IsBoolean()) { - NAPI_THROW( - Napi::TypeError::New(info.Env(), "Expected argument 0 to be boolean"), - Value()); - } - - auto *data = - new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; + auto data = new DataType{Promise::Deferred::New(info.Env())}; _tsfn.NonBlockingCall(data); return data->deferred.Promise(); }; @@ -323,8 +315,8 @@ namespace simple { using ContextType = std::nullptr_t; -// Full type of our ThreadSafeFunctionEx. We don't specify the `ContextType` here -// (even though the _default_ for the type argument is `std::nullptr_t`) to +// Full type of our ThreadSafeFunctionEx. We don't specify the `ContextType` +// here (even though the _default_ for the type argument is `std::nullptr_t`) to // demonstrate construction with no type arguments. using TSFN = ThreadSafeFunctionEx<>; diff --git a/test/threadsafe_function_ex/test/basic.js b/test/threadsafe_function_ex/test/basic.js index bcb1cdc8a..432856ddf 100644 --- a/test/threadsafe_function_ex/test/basic.js +++ b/test/threadsafe_function_ex/test/basic.js @@ -54,22 +54,13 @@ class BasicTest extends TestRunner { * that handles all of its JavaScript processing on the callJs instead of the * callback. * - Creates a threadsafe function with no JavaScript context or callback. - * - Makes two calls, waiting for each, and expecting the first to resolve - * and the second to reject. + * - Makes one call, waiting for completion. The internal `CallJs` resolves the call if jsCallback is empty, otherwise rejects. */ async empty({ TSFNWrap }) { debugger; if (typeof TSFNWrap === 'function') { const tsfn = new TSFNWrap(); - await tsfn.call(false /* reject */); - let caught = false; - try { - await tsfn.call(true /* reject */); - } catch (ex) { - caught = true; - } - - assert.ok(caught, 'The promise rejection was not caught'); + await tsfn.call(); return await tsfn.release(); } return true; From b0e7817f28bec975ea435937ba50d79a16037f5a Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 14 Jun 2020 22:46:12 +0200 Subject: [PATCH 20/39] basic multi-threading --- test/threadsafe_function_ex/test/example.cc | 270 ++++++++++++++++++-- test/threadsafe_function_ex/test/example.js | 6 +- 2 files changed, 252 insertions(+), 24 deletions(-) diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc index a20d9ecae..b97f0e826 100644 --- a/test/threadsafe_function_ex/test/example.cc +++ b/test/threadsafe_function_ex/test/example.cc @@ -1,6 +1,37 @@ -#include -#include "napi.h" #include "../util/util.h" +#include "napi.h" +#include +#include +#include +#include +#include + +static constexpr auto DEFAULT_THREAD_COUNT = 10U; +static constexpr auto DEFAULT_CALL_COUNT = 2; + + + static struct { + bool logCall = true; // Uses JS console.log to output when the TSFN is + // processing the NonBlockingCall(). + bool logThread = false; // Uses native std::cout to output when the thread's + // NonBlockingCall() request has finished. + } DefaultOptions; // Options from Start() + +/** + * @brief Macro used specifically to support the dual CI test / documentation + * example setup. Exceptions are always thrown as JavaScript exceptions when + * running in example mode. + * + */ +#define TSFN_THROW(tsfnWrap, e, ...) \ + if (tsfnWrap->cppExceptions) { \ + do { \ + (e).ThrowAsJavaScriptException(); \ + return __VA_ARGS__; \ + } while (0); \ + } else { \ + NAPI_THROW(e, __VA_ARGS__); \ + } #if (NAPI_VERSION > 3) @@ -8,18 +39,26 @@ using namespace Napi; namespace example { +class TSFNWrap; + // Context of the TSFN. -using Context = Reference; +using Context = TSFNWrap; + +using CompletionHandler = std::function; // Data passed (as pointer) to [Non]BlockingCall -using DataType = Promise::Deferred; +struct DataType { + // Promise::Deferred; + // CompletionHandler handler; + std::future deferred; +}; // CallJs callback function static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, Context *context, DataType *data) { if (env != nullptr) { if (data != nullptr) { - data->Resolve(context->Value()); + // data->Resolve(context->Value()); } } if (data != nullptr) { @@ -30,16 +69,168 @@ static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, // Full type of the ThreadSafeFunctionEx using TSFN = ThreadSafeFunctionEx; -class TSFNWrap; +struct FinalizerDataType { + std::vector threads; + std::unique_ptr deferred; + // struct { + // bool logCall = true; // Uses JS console.log to output when the TSFN is + // // processing the NonBlockingCall(). + // bool logThread = false; // Uses native std::cout to output when the thread's + // // NonBlockingCall() request has finished. + // } options; // Options from Start() +}; + +static void threadEntry(size_t threadId, TSFN tsfn, uint32_t callCount, + bool logThread) { + using namespace std::chrono_literals; + for (auto i = 0U; i < callCount; ++i) { + // auto callData = new DataType(); + // tsfn.NonBlockingCall(callData); + // auto result = callData->deferred.get(); + // if (logThread) { + // std::cout << "Thread " << threadId << " got result " << result << "\n"; + // } + + // std::this_thread::sleep_for(50ms * threadId); + } + std::cout << "Thread " << threadId << "finished\n"; + tsfn.Release(); +} + using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds the TSFN. class TSFNWrap : public base { public: TSFNWrap(const CallbackInfo &info) : base(info) { + if (info.Length() > 0 && info[0].IsObject()) { + auto arg0 = info[0].ToObject(); + if (arg0.Has("cppExceptions")) { + auto cppExceptions = arg0.Get("cppExceptions"); + if (cppExceptions.IsBoolean()) { + cppExceptions = cppExceptions.As(); + } else { + // We explicitly use the addon's except/noexcept settings here, since + // we don't have a valid setting. + Napi::TypeError::New(Env(), "cppExceptions is not a boolean") + .ThrowAsJavaScriptException(); + } + } + } + } + ~TSFNWrap() { + for (auto& thread : finalizerData->threads) { + if (thread.joinable()) { + thread.join(); + } + } + } + + static std::array, 3> InstanceMethods() { + return {InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("start", &TSFNWrap::Start), + InstanceMethod("release", &TSFNWrap::Release)}; + } + + bool cppExceptions = false; + std::shared_ptr finalizerData; + + Napi::Value Start(const CallbackInfo &info) { Napi::Env env = info.Env(); - Context *context = new Context(Persistent(info[0])); + if (_tsfn) { + TSFN_THROW(this, Napi::Error::New(Env(), "TSFN already exists."), + Value()); + } + + // Creates a list to hold how many times each thread should make a call. + std::vector callCounts; + + // The JS-provided callback to execute for each call (if provided) + Function callback; + + // std::unique_ptr finalizerData = + // std::make_unique(); + + // finalizerData = std::shared_ptr(FinalizerDataType{ std::vector() , Promise::Deferred::New(env) }); + + finalizerData = std::make_shared(); + + + bool logThread = DefaultOptions.logThread; + bool logCall = DefaultOptions.logCall; + + if (info.Length() > 0 && info[0].IsObject()) { + auto arg0 = info[0].ToObject(); + if (arg0.Has("threads")) { + Napi::Value threads = arg0.Get("threads"); + if (threads.IsArray()) { + Napi::Array threadsArray = threads.As(); + for (auto i = 0U; i < threadsArray.Length(); ++i) { + Napi::Value elem = threadsArray.Get(i); + if (elem.IsNumber()) { + callCounts.push_back(elem.As().Int32Value()); + } else { + TSFN_THROW(this, Napi::TypeError::New(Env(), "Invalid arguments"), + Value()); + } + } + } else if (threads.IsNumber()) { + auto threadCount = threads.As().Int32Value(); + for (auto i = 0; i < threadCount; ++i) { + callCounts.push_back(DEFAULT_CALL_COUNT); + } + } else { + TSFN_THROW(this, Napi::TypeError::New(Env(), "Invalid arguments"), + Value()); + } + } + + if (arg0.Has("callback")) { + auto cb = arg0.Get("callback"); + if (cb.IsFunction()) { + callback = cb.As(); + } else { + TSFN_THROW(this, + Napi::TypeError::New(Env(), "Callback is not a function"), + Value()); + } + } + + if (arg0.Has("logCall")) { + auto logCallOption = arg0.Get("logCall"); + if (logCallOption.IsBoolean()) { + logCall = logCallOption.As(); + } else { + TSFN_THROW(this, + Napi::TypeError::New(Env(), "logCall is not a boolean"), + Value()); + } + } + + if (arg0.Has("logThread")) { + auto logThreadOption = arg0.Get("logThread"); + if (logThreadOption.IsBoolean()) { + logThread = logThreadOption.As(); + } else { + TSFN_THROW(this, + Napi::TypeError::New(Env(), "logThread is not a boolean"), + Value()); + } + } + } + + + // Apply default arguments + if (callCounts.size() == 0) { + for (auto i = 0U; i < DEFAULT_THREAD_COUNT; ++i) { + callCounts.push_back(DEFAULT_CALL_COUNT); + } + } + + const auto threadCount = callCounts.size(); + + auto *finalizerDataPtr = new std::shared_ptr(finalizerData); _tsfn = TSFN::New( env, // napi_env env, @@ -47,31 +238,67 @@ class TSFNWrap : public base { Value(), // const Object& resource, "Test", // ResourceString resourceName, 0, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - context, // Context* context, - base::Finalizer, // Finalizer finalizer - &_deferred // FinalizerDataType data + threadCount + 1, // size_t initialThreadCount, +1 for Node thread + this, // Context* context, + Finalizer, // Finalizer finalizer + finalizerDataPtr // FinalizerDataType* data ); - } - static std::array, 3> InstanceMethods() { - return {InstanceMethod("call", &TSFNWrap::Call), - InstanceMethod("getContext", &TSFNWrap::GetContext), - InstanceMethod("release", &TSFNWrap::Release)}; + for (auto threadId = 0U; threadId < threadCount; ++threadId) { + finalizerData->threads.push_back( + std::thread(threadEntry, threadId, _tsfn, callCounts[threadId], + logThread)); + } + + + return String::New(env, "started"); + }; + + // TSFN finalizer. Resolves the Promise returned by `Release()` above. + static void Finalizer(Napi::Env env, std::shared_ptr *finalizeData, + Context *ctx) { + // for (auto thread : finalizeData->threads) { + + for (auto &thread : (*finalizeData)->threads) { + std::cout << "Finalizer joining thread\n"; + if (thread.joinable()) { + thread.join(); + } + } + + delete finalizeData; + + // } + // if (deferred->get()) { + // (*deferred)->Resolve(Boolean::New(env, true)); + // deferred->release(); + + // } } + Napi::Value Release(const CallbackInfo &info) { + if (finalizerData->deferred) { + return finalizerData->deferred->Promise(); + } + // return finalizerData->deferred.Promise(); + auto env = info.Env(); + finalizerData->deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); + _tsfn.Release(); + return finalizerData->deferred->Promise(); + }; + Napi::Value Call(const CallbackInfo &info) { - auto *callData = new DataType(info.Env()); - _tsfn.NonBlockingCall(callData); - return callData->Promise(); + // auto *callData = new DataType(info.Env()); + // _tsfn.NonBlockingCall(callData); + // return callData->Promise(); + return info.Env().Undefined(); }; Napi::Value GetContext(const CallbackInfo &) { return _tsfn.GetContext()->Value(); }; }; -} // namespace context - +} // namespace example Object InitThreadSafeFunctionExExample(Env env) { auto exports(Object::New(env)); @@ -79,5 +306,4 @@ Object InitThreadSafeFunctionExExample(Env env) { return exports; } - #endif diff --git a/test/threadsafe_function_ex/test/example.js b/test/threadsafe_function_ex/test/example.js index 323777a7c..3328371be 100644 --- a/test/threadsafe_function_ex/test/example.js +++ b/test/threadsafe_function_ex/test/example.js @@ -9,9 +9,11 @@ class ExampleTest extends TestRunner { async example({ TSFNWrap }) { const ctx = {}; + console.log("starting"); const tsfn = new TSFNWrap(ctx); - assert(ctx === await tsfn.call(), "getContextByCall context not equal"); - assert(ctx === tsfn.getContext(), "getContextFromTsfn context not equal"); + console.log("tsfn is", tsfn); + console.log("start is", tsfn.start()); + console.log(); return await tsfn.release(); } From 44adeea1bd66ce4ea8876506d34d3602b4eed7e3 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 15 Jun 2020 00:02:09 +0200 Subject: [PATCH 21/39] test: wip with example --- test/threadsafe_function_ex/test/example.cc | 146 ++++++++---------- test/threadsafe_function_ex/test/example.js | 15 +- .../threadsafe_function_ex/util/TestRunner.js | 3 +- 3 files changed, 79 insertions(+), 85 deletions(-) diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc index b97f0e826..a7c401aaf 100644 --- a/test/threadsafe_function_ex/test/example.cc +++ b/test/threadsafe_function_ex/test/example.cc @@ -9,13 +9,12 @@ static constexpr auto DEFAULT_THREAD_COUNT = 10U; static constexpr auto DEFAULT_CALL_COUNT = 2; - - static struct { - bool logCall = true; // Uses JS console.log to output when the TSFN is - // processing the NonBlockingCall(). - bool logThread = false; // Uses native std::cout to output when the thread's - // NonBlockingCall() request has finished. - } DefaultOptions; // Options from Start() +static struct { + bool logCall = true; // Uses JS console.log to output when the TSFN is + // processing the NonBlockingCall(). + bool logThread = false; // Uses native std::cout to output when the thread's + // NonBlockingCall() request has finished. +} DefaultOptions; // Options from Start() /** * @brief Macro used specifically to support the dual CI test / documentation @@ -44,59 +43,26 @@ class TSFNWrap; // Context of the TSFN. using Context = TSFNWrap; -using CompletionHandler = std::function; - // Data passed (as pointer) to [Non]BlockingCall -struct DataType { - // Promise::Deferred; - // CompletionHandler handler; - std::future deferred; -}; +using DataType = std::unique_ptr>; // CallJs callback function static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, Context *context, DataType *data) { - if (env != nullptr) { - if (data != nullptr) { - // data->Resolve(context->Value()); - } - } if (data != nullptr) { - delete data; + if (env != nullptr) { + (*data)->set_value(clock()); + } else { + (*data)->set_exception(std::make_exception_ptr( + std::runtime_error("TSFN has been finalized."))); + } } + // We do NOT delete data as it is a unique_ptr held by the calling thread. } // Full type of the ThreadSafeFunctionEx using TSFN = ThreadSafeFunctionEx; -struct FinalizerDataType { - std::vector threads; - std::unique_ptr deferred; - // struct { - // bool logCall = true; // Uses JS console.log to output when the TSFN is - // // processing the NonBlockingCall(). - // bool logThread = false; // Uses native std::cout to output when the thread's - // // NonBlockingCall() request has finished. - // } options; // Options from Start() -}; - -static void threadEntry(size_t threadId, TSFN tsfn, uint32_t callCount, - bool logThread) { - using namespace std::chrono_literals; - for (auto i = 0U; i < callCount; ++i) { - // auto callData = new DataType(); - // tsfn.NonBlockingCall(callData); - // auto result = callData->deferred.get(); - // if (logThread) { - // std::cout << "Thread " << threadId << " got result " << result << "\n"; - // } - - // std::this_thread::sleep_for(50ms * threadId); - } - std::cout << "Thread " << threadId << "finished\n"; - tsfn.Release(); -} - using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds the TSFN. @@ -119,13 +85,40 @@ class TSFNWrap : public base { } } ~TSFNWrap() { - for (auto& thread : finalizerData->threads) { + for (auto &thread : finalizerData->threads) { if (thread.joinable()) { thread.join(); } } } + struct FinalizerDataType { + std::vector threads; + std::unique_ptr deferred; + }; + + // The finalizer data is shared, because we want to join the threads if our + // TSFNWrap object gets garbage-collected and there are still active threads. + using SharedFinalizerDataType = std::shared_ptr; + + static void threadEntry(size_t threadId, TSFN tsfn, uint32_t callCount, + bool logThread) { + using namespace std::chrono_literals; + for (auto i = 0U; i < callCount; ++i) { + auto promise = std::make_unique>(); + tsfn.NonBlockingCall(&promise); + auto future = promise->get_future(); + auto result = future.get(); + if (logThread) { + std::cout << "Thread " << threadId << " got result " << result << "\n"; + } + } + if (logThread) { + std::cout << "Thread " << threadId << "finished\n"; + } + tsfn.Release(); + } + static std::array, 3> InstanceMethods() { return {InstanceMethod("call", &TSFNWrap::Call), InstanceMethod("start", &TSFNWrap::Start), @@ -133,6 +126,7 @@ class TSFNWrap : public base { } bool cppExceptions = false; + bool logThread; std::shared_ptr finalizerData; Napi::Value Start(const CallbackInfo &info) { @@ -149,15 +143,9 @@ class TSFNWrap : public base { // The JS-provided callback to execute for each call (if provided) Function callback; - // std::unique_ptr finalizerData = - // std::make_unique(); - - // finalizerData = std::shared_ptr(FinalizerDataType{ std::vector() , Promise::Deferred::New(env) }); - finalizerData = std::make_shared(); - - bool logThread = DefaultOptions.logThread; + logThread = DefaultOptions.logThread; bool logCall = DefaultOptions.logCall; if (info.Length() > 0 && info[0].IsObject()) { @@ -220,7 +208,6 @@ class TSFNWrap : public base { } } - // Apply default arguments if (callCounts.size() == 0) { for (auto i = 0U; i < DEFAULT_THREAD_COUNT; ++i) { @@ -230,7 +217,7 @@ class TSFNWrap : public base { const auto threadCount = callCounts.size(); - auto *finalizerDataPtr = new std::shared_ptr(finalizerData); + auto *finalizerDataPtr = new SharedFinalizerDataType(finalizerData); _tsfn = TSFN::New( env, // napi_env env, @@ -238,42 +225,40 @@ class TSFNWrap : public base { Value(), // const Object& resource, "Test", // ResourceString resourceName, 0, // size_t maxQueueSize, - threadCount + 1, // size_t initialThreadCount, +1 for Node thread - this, // Context* context, - Finalizer, // Finalizer finalizer + threadCount + 1, // size_t initialThreadCount, +1 for Node thread + this, // Context* context, + Finalizer, // Finalizer finalizer finalizerDataPtr // FinalizerDataType* data ); for (auto threadId = 0U; threadId < threadCount; ++threadId) { - finalizerData->threads.push_back( - std::thread(threadEntry, threadId, _tsfn, callCounts[threadId], - logThread)); + finalizerData->threads.push_back(std::thread( + threadEntry, threadId, _tsfn, callCounts[threadId], logThread)); } - return String::New(env, "started"); }; - // TSFN finalizer. Resolves the Promise returned by `Release()` above. - static void Finalizer(Napi::Env env, std::shared_ptr *finalizeData, + // TSFN finalizer. Joins the threads and resolves the Promise returned by + // `Release()` above. + static void Finalizer(Napi::Env env, SharedFinalizerDataType *finalizeData, Context *ctx) { - // for (auto thread : finalizeData->threads) { + if (ctx->logThread) { + std::cout << "Finalizer joining threads\n"; + } for (auto &thread : (*finalizeData)->threads) { - std::cout << "Finalizer joining thread\n"; if (thread.joinable()) { thread.join(); } } + ctx->clearTSFN(); + if (ctx->logThread) { + std::cout << "Finished\n"; + } + (*finalizeData)->deferred->Resolve(Boolean::New(env, true)); delete finalizeData; - - // } - // if (deferred->get()) { - // (*deferred)->Resolve(Boolean::New(env, true)); - // deferred->release(); - - // } } Napi::Value Release(const CallbackInfo &info) { @@ -282,21 +267,24 @@ class TSFNWrap : public base { } // return finalizerData->deferred.Promise(); auto env = info.Env(); - finalizerData->deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); + finalizerData->deferred.reset( + new Promise::Deferred(Promise::Deferred::New(env))); _tsfn.Release(); return finalizerData->deferred->Promise(); }; Napi::Value Call(const CallbackInfo &info) { // auto *callData = new DataType(info.Env()); - // _tsfn.NonBlockingCall(callData); - // return callData->Promise(); + // _tsfn.NonBlockingCall(callData); return callData->Promise(); return info.Env().Undefined(); }; Napi::Value GetContext(const CallbackInfo &) { return _tsfn.GetContext()->Value(); }; + + // This does not run on the node thread. + void clearTSFN() { _tsfn = TSFN(); } }; } // namespace example diff --git a/test/threadsafe_function_ex/test/example.js b/test/threadsafe_function_ex/test/example.js index 3328371be..eadea4f9e 100644 --- a/test/threadsafe_function_ex/test/example.js +++ b/test/threadsafe_function_ex/test/example.js @@ -9,12 +9,17 @@ class ExampleTest extends TestRunner { async example({ TSFNWrap }) { const ctx = {}; - console.log("starting"); const tsfn = new TSFNWrap(ctx); - console.log("tsfn is", tsfn); - console.log("start is", tsfn.start()); - console.log(); - return await tsfn.release(); + const run = async (i) => { + const result = tsfn.start({threads:[1]}); + await result; + return await tsfn.release(); + }; + const results = [ await run(1) ]; + results.push( await run(2) ); + return results; + // return await Promise.all( [ run(1), run(2) ] ); + // await run(2); } } diff --git a/test/threadsafe_function_ex/util/TestRunner.js b/test/threadsafe_function_ex/util/TestRunner.js index b7e99c1b4..6a7957a5c 100644 --- a/test/threadsafe_function_ex/util/TestRunner.js +++ b/test/threadsafe_function_ex/util/TestRunner.js @@ -56,6 +56,7 @@ class TestRunner { // Run tests in both except and noexcept await this.run(false); await this.run(true); + console.log("ALL DONE"); } catch (ex) { console.error(`Test failed!`, ex); process.exit(1); @@ -95,7 +96,7 @@ class TestRunner { const [label, time, isNoExcept, nsName, returnValue] = state; const except = () => pad(isNoExcept ? '[noexcept]' : '', 12); const timeStr = () => time == null ? '...' : `${time}${typeof time === 'number' ? 'ms' : ''}`; - return `${pad(nsName, 10)} ${except()}| ${pad(timeStr(), 8)}| ${pad(label, 15)}${returnValue === undefined ? '' : `(return: ${returnValue})`}`; + return `${pad(nsName, 10)} ${except()}| ${pad(timeStr(), 8)}| ${pad(label, 15)}${returnValue === undefined ? '' : `(return: ${JSON.stringify(returnValue)})`}`; }; /** From c20685be7a7678d9092f82bb7a058f3b5fb398b7 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 15 Jun 2020 01:22:24 +0200 Subject: [PATCH 22/39] test: wip with example test --- test/threadsafe_function_ex/test/example.cc | 96 ++++++++++++------- test/threadsafe_function_ex/test/example.js | 37 ++++--- .../threadsafe_function_ex/util/TestRunner.js | 1 - 3 files changed, 86 insertions(+), 48 deletions(-) diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc index a7c401aaf..b47d684ad 100644 --- a/test/threadsafe_function_ex/test/example.cc +++ b/test/threadsafe_function_ex/test/example.cc @@ -43,17 +43,22 @@ class TSFNWrap; // Context of the TSFN. using Context = TSFNWrap; -// Data passed (as pointer) to [Non]BlockingCall -using DataType = std::unique_ptr>; +struct Data { + // Data passed (as pointer) to [Non]BlockingCall + std::promise promise; + uint32_t base; +}; +using DataType = std::unique_ptr; // CallJs callback function static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, - Context *context, DataType *data) { - if (data != nullptr) { + Context *context, DataType *dataPtr) { + if (dataPtr != nullptr) { + auto &data = *dataPtr; if (env != nullptr) { - (*data)->set_value(clock()); + data->promise.set_value(data->base * data->base); } else { - (*data)->set_exception(std::make_exception_ptr( + data->promise.set_exception(std::make_exception_ptr( std::runtime_error("TSFN has been finalized."))); } } @@ -92,42 +97,54 @@ class TSFNWrap : public base { } } - struct FinalizerDataType { + struct FinalizerData { std::vector threads; std::unique_ptr deferred; }; // The finalizer data is shared, because we want to join the threads if our // TSFNWrap object gets garbage-collected and there are still active threads. - using SharedFinalizerDataType = std::shared_ptr; + using FinalizerDataType = std::shared_ptr; +#define THREADLOG(X) if (context->logThread) {\ +std::cout << X;\ +} static void threadEntry(size_t threadId, TSFN tsfn, uint32_t callCount, - bool logThread) { + Context *context) { using namespace std::chrono_literals; + + THREADLOG("Thread " << threadId << " starting...\n") + for (auto i = 0U; i < callCount; ++i) { - auto promise = std::make_unique>(); - tsfn.NonBlockingCall(&promise); - auto future = promise->get_future(); + auto data = std::make_unique(); + data->base = threadId + 1; + THREADLOG("Thread " << threadId << " making call, base = " << data->base << "\n") + + tsfn.NonBlockingCall(&data); + auto future = data->promise.get_future(); auto result = future.get(); - if (logThread) { - std::cout << "Thread " << threadId << " got result " << result << "\n"; - } - } - if (logThread) { - std::cout << "Thread " << threadId << "finished\n"; + context->callSucceeded(result); + THREADLOG("Thread " << threadId << " got result: " << result << "\n") } + + THREADLOG("Thread " << threadId << " finished.\n\n") tsfn.Release(); } +#undef THREADLOG - static std::array, 3> InstanceMethods() { - return {InstanceMethod("call", &TSFNWrap::Call), + static std::array, 4> InstanceMethods() { + return {InstanceMethod("getContext", &TSFNWrap::GetContext), InstanceMethod("start", &TSFNWrap::Start), + InstanceMethod("callCount", &TSFNWrap::CallCount), InstanceMethod("release", &TSFNWrap::Release)}; } bool cppExceptions = false; bool logThread; - std::shared_ptr finalizerData; + std::atomic_uint succeededCalls; + std::atomic_int aggregate; + + FinalizerDataType finalizerData; Napi::Value Start(const CallbackInfo &info) { Napi::Env env = info.Env(); @@ -143,7 +160,7 @@ class TSFNWrap : public base { // The JS-provided callback to execute for each call (if provided) Function callback; - finalizerData = std::make_shared(); + finalizerData = std::make_shared(); logThread = DefaultOptions.logThread; bool logCall = DefaultOptions.logCall; @@ -217,8 +234,10 @@ class TSFNWrap : public base { const auto threadCount = callCounts.size(); - auto *finalizerDataPtr = new SharedFinalizerDataType(finalizerData); + auto *finalizerDataPtr = new FinalizerDataType(finalizerData); + succeededCalls = 0; + aggregate = 0; _tsfn = TSFN::New( env, // napi_env env, TSFN::DefaultFunctionFactory(env), // const Function& callback, @@ -232,8 +251,8 @@ class TSFNWrap : public base { ); for (auto threadId = 0U; threadId < threadCount; ++threadId) { - finalizerData->threads.push_back(std::thread( - threadEntry, threadId, _tsfn, callCounts[threadId], logThread)); + finalizerData->threads.push_back(std::thread(threadEntry, threadId, _tsfn, + callCounts[threadId], this)); } return String::New(env, "started"); @@ -241,7 +260,7 @@ class TSFNWrap : public base { // TSFN finalizer. Joins the threads and resolves the Promise returned by // `Release()` above. - static void Finalizer(Napi::Env env, SharedFinalizerDataType *finalizeData, + static void Finalizer(Napi::Env env, FinalizerDataType *finalizeData, Context *ctx) { if (ctx->logThread) { @@ -254,7 +273,7 @@ class TSFNWrap : public base { } ctx->clearTSFN(); if (ctx->logThread) { - std::cout << "Finished\n"; + std::cout << "Finished finalizing threads.\n"; } (*finalizeData)->deferred->Resolve(Boolean::New(env, true)); @@ -265,26 +284,33 @@ class TSFNWrap : public base { if (finalizerData->deferred) { return finalizerData->deferred->Promise(); } - // return finalizerData->deferred.Promise(); - auto env = info.Env(); finalizerData->deferred.reset( - new Promise::Deferred(Promise::Deferred::New(env))); + new Promise::Deferred(Promise::Deferred::New(info.Env()))); _tsfn.Release(); return finalizerData->deferred->Promise(); }; - Napi::Value Call(const CallbackInfo &info) { - // auto *callData = new DataType(info.Env()); - // _tsfn.NonBlockingCall(callData); return callData->Promise(); - return info.Env().Undefined(); + Napi::Value CallCount(const CallbackInfo &info) { + Napi::Env env(info.Env()); + + auto results = Array::New(env, 2); + results.Set("0", Number::New(env, succeededCalls)); + results.Set("1", Number::New(env, aggregate)); + return results; }; Napi::Value GetContext(const CallbackInfo &) { return _tsfn.GetContext()->Value(); }; - // This does not run on the node thread. + // This method does not run on the Node thread. void clearTSFN() { _tsfn = TSFN(); } + + // This method does not run on the Node thread. + void callSucceeded(int result) { + succeededCalls++; + aggregate += result; + } }; } // namespace example diff --git a/test/threadsafe_function_ex/test/example.js b/test/threadsafe_function_ex/test/example.js index eadea4f9e..53b6783cf 100644 --- a/test/threadsafe_function_ex/test/example.js +++ b/test/threadsafe_function_ex/test/example.js @@ -8,18 +8,31 @@ const { TestRunner } = require('../util/TestRunner'); class ExampleTest extends TestRunner { async example({ TSFNWrap }) { - const ctx = {}; - const tsfn = new TSFNWrap(ctx); - const run = async (i) => { - const result = tsfn.start({threads:[1]}); - await result; - return await tsfn.release(); - }; - const results = [ await run(1) ]; - results.push( await run(2) ); - return results; - // return await Promise.all( [ run(1), run(2) ] ); - // await run(2); + const tsfn = new TSFNWrap(); + + const threads = [1]; //, 2, 2, 5, 12]; + const started = await tsfn.start({ threads, logThread: true }); + + /** + * Calculate the expected results. + */ + const expected = threads.reduce((p, threadCallCount, threadId) => ( + ++threadId, + p[0] += threadCallCount, + p[1] += threadCallCount * threadId ** 2, + p + ), [0, 0]); + + if (started) { + const released = await tsfn.release(); + const [callCountActual, aggregateActual] = tsfn.callCount(); + const [callCountExpected, aggregateExpected] = expected; + assert(callCountActual == callCountExpected, `The number of calls do not match: actual = ${callCountActual}, expected = ${callCountExpected}`); + assert(aggregateActual == aggregateExpected, `The aggregate of calls do not match: actual = ${aggregateActual}, expected = ${aggregateExpected}`); + return expected; + } else { + throw new Error('The TSFN failed to start'); + } } } diff --git a/test/threadsafe_function_ex/util/TestRunner.js b/test/threadsafe_function_ex/util/TestRunner.js index 6a7957a5c..e01b42480 100644 --- a/test/threadsafe_function_ex/util/TestRunner.js +++ b/test/threadsafe_function_ex/util/TestRunner.js @@ -56,7 +56,6 @@ class TestRunner { // Run tests in both except and noexcept await this.run(false); await this.run(true); - console.log("ALL DONE"); } catch (ex) { console.error(`Test failed!`, ex); process.exit(1); From 3b24e7431a53b8091c9b597f24a222392f2a8728 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 15 Jun 2020 03:21:30 +0200 Subject: [PATCH 23/39] src: consolidate duplicated tsfnex code --- napi-inl.h | 119 ++++++----------------------------------------------- napi.h | 50 +++------------------- 2 files changed, 18 insertions(+), 151 deletions(-) diff --git a/napi-inl.h b/napi-inl.h index c5e262555..959cffa36 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -213,6 +213,14 @@ static inline CallJsWrapper(napi_env env, napi_value jsCallback, void * /*contex Function(env, jsCallback).Call(0, nullptr); } } + +template +typename ThreadSafeFunctionEx<>::DefaultFunctionType +DefaultCallbackWrapper( + napi_env env, CallbackType cb) { + return ThreadSafeFunctionEx<>::DefaultFunctionFactory(env); +} + #endif template @@ -4342,28 +4350,6 @@ ThreadSafeFunctionEx::New( return tsfn; } -// static, with Callback [nullptr] Resource [missing] Finalizer [missing] -template -template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( - napi_env env, std::nullptr_t callback, ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context) { - ThreadSafeFunctionEx tsfn; - - napi_status status = napi_create_threadsafe_function( - env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, - initialThreadCount, nullptr, nullptr, context, - CallJsInternal, &tsfn._tsfn); - if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); - } - - return tsfn; -} - // static, with Callback [missing] Resource [passed] Finalizer [missing] template @@ -4386,28 +4372,6 @@ ThreadSafeFunctionEx::New( return tsfn; } -// static, with Callback [nullptr] Resource [passed] Finalizer [missing] -template -template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( - napi_env env, std::nullptr_t callback, const Object &resource, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { - ThreadSafeFunctionEx tsfn; - - napi_status status = napi_create_threadsafe_function( - env, nullptr, resource, String::From(env, resourceName), maxQueueSize, - initialThreadCount, nullptr, nullptr, context, CallJsInternal, - &tsfn._tsfn); - if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); - } - - return tsfn; -} - // static, with Callback [missing] Resource [missing] Finalizer [passed] template @@ -4438,36 +4402,6 @@ ThreadSafeFunctionEx::New( return tsfn; } -// static, with Callback [nullptr] Resource [missing] Finalizer [passed] -template -template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( - napi_env env, std::nullptr_t callback, ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, - FinalizerDataType *data) { - ThreadSafeFunctionEx tsfn; - - auto *finalizeData = new details::ThreadSafeFinalize( - {data, finalizeCallback}); - napi_status status = napi_create_threadsafe_function( - env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, - initialThreadCount, finalizeData, - details::ThreadSafeFinalize:: - FinalizeFinalizeWrapperWithDataAndContext, - context, CallJsInternal, &tsfn._tsfn); - if (status != napi_ok) { - delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); - } - - return tsfn; -} - // static, with Callback [missing] Resource [passed] Finalizer [passed] template @@ -4497,36 +4431,6 @@ ThreadSafeFunctionEx::New( return tsfn; } - -// static, with Callback [nullptr] Resource [passed] Finalizer [passed] -template -template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( - napi_env env, std::nullptr_t callback, const Object &resource, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data) { - ThreadSafeFunctionEx tsfn; - - auto *finalizeData = new details::ThreadSafeFinalize( - {data, finalizeCallback}); - napi_status status = napi_create_threadsafe_function( - env, nullptr, resource, String::From(env, resourceName), maxQueueSize, - initialThreadCount, finalizeData, - details::ThreadSafeFinalize:: - FinalizeFinalizeWrapperWithDataAndContext, - context, CallJsInternal, &tsfn._tsfn); - if (status != napi_ok) { - delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); - } - - return tsfn; -} #endif // static, with Callback [passed] Resource [missing] Finalizer [missing] @@ -4607,11 +4511,11 @@ ThreadSafeFunctionEx::New( // static, with: Callback [passed] Resource [passed] Finalizer [passed] template -template inline ThreadSafeFunctionEx ThreadSafeFunctionEx::New( - napi_env env, const Function &callback, const Object &resource, + napi_env env, CallbackType callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data) { ThreadSafeFunctionEx tsfn; @@ -4620,7 +4524,7 @@ ThreadSafeFunctionEx::New( FinalizerDataType>( {data, finalizeCallback}); napi_status status = napi_create_threadsafe_function( - env, callback, resource, String::From(env, resourceName), maxQueueSize, + env, details::DefaultCallbackWrapper(env, callback), resource, String::From(env, resourceName), maxQueueSize, initialThreadCount, finalizeData, details::ThreadSafeFinalize:: FinalizeFinalizeWrapperWithDataAndContext, @@ -4743,6 +4647,7 @@ ThreadSafeFunctionEx::DefaultFunctionFactory( #endif } + //////////////////////////////////////////////////////////////////////////////// // ThreadSafeFunction class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index 40e0d9359..1fc40bdda 100644 --- a/napi.h +++ b/napi.h @@ -2049,14 +2049,13 @@ namespace Napi { void (*CallJs)(Napi::Env, Napi::Function, ContextType *, DataType *) = nullptr> class ThreadSafeFunctionEx { - private: + + public: #if NAPI_VERSION > 4 using DefaultFunctionType = std::nullptr_t; #else using DefaultFunctionType = const Napi::Function; #endif - - public: // This API may only be called from the main thread. // Helper function that returns nullptr if running N-API 5+, otherwise a // non-empty, no-op Function. This provides the ability to specify at @@ -2064,6 +2063,7 @@ namespace Napi { // when targeting _any_ N-API version. static DefaultFunctionType DefaultFunctionFactory(Napi::Env env); + #if NAPI_VERSION > 4 // This API may only be called from the main thread. // Creates a new threadsafe function with: @@ -2073,14 +2073,6 @@ namespace Napi { New(napi_env env, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context = nullptr); - // This API may only be called from the main thread. - // Callback [nullptr] Resource [missing] Finalizer [missing] - template - static ThreadSafeFunctionEx - New(napi_env env, std::nullptr_t callback, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, - ContextType *context = nullptr); - // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [missing] Resource [passed] Finalizer [missing] @@ -2090,15 +2082,6 @@ namespace Napi { size_t maxQueueSize, size_t initialThreadCount, ContextType *context = nullptr); - // This API may only be called from the main thread. - // Creates a new threadsafe function with: - // Callback [nullptr] Resource [passed] Finalizer [missing] - template - static ThreadSafeFunctionEx - New(napi_env env, std::nullptr_t callback, const Object &resource, - ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context = nullptr); - // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [missing] Resource [missing] Finalizer [passed] @@ -2109,16 +2092,6 @@ namespace Napi { size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data = nullptr); - // This API may only be called from the main thread. - // Creates a new threadsafe function with: - // Callback [nullptr] Resource [missing] Finalizer [passed] - template - static ThreadSafeFunctionEx - New(napi_env env, std::nullptr_t callback, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data = nullptr); - // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [missing] Resource [passed] Finalizer [passed] @@ -2128,17 +2101,6 @@ namespace Napi { New(napi_env env, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data = nullptr); - - // This API may only be called from the main thread. - // Creates a new threadsafe function with: - // Callback [nullptr] Resource [passed] Finalizer [passed] - template - static ThreadSafeFunctionEx - New(napi_env env, std::nullptr_t callback, const Object &resource, - ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data = nullptr); #endif // This API may only be called from the main thread. @@ -2172,10 +2134,10 @@ namespace Napi { // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [passed] Resource [passed] Finalizer [passed] - template + template static ThreadSafeFunctionEx - New(napi_env env, const Function &callback, const Object &resource, + New(napi_env env, CallbackType callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data = nullptr); From a7a535284e4d2f067faa2d50ce99cc5924fb3323 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 15 Jun 2020 07:47:17 +0200 Subject: [PATCH 24/39] test: v4,v5+ tests pass --- napi-inl.h | 70 +++++++++++++++---- napi.h | 15 ++-- test/threadsafe_function_ex/test/basic.cc | 62 ++++++++-------- test/threadsafe_function_ex/test/example.cc | 62 +++++++++++----- test/threadsafe_function_ex/test/example.js | 32 ++++++--- .../threadsafe_function_ex/test/threadsafe.cc | 1 - test/threadsafe_function_ex/util/util.h | 2 +- 7 files changed, 165 insertions(+), 79 deletions(-) diff --git a/napi-inl.h b/napi-inl.h index 959cffa36..c200f971e 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -214,13 +214,27 @@ static inline CallJsWrapper(napi_env env, napi_value jsCallback, void * /*contex } } -template -typename ThreadSafeFunctionEx<>::DefaultFunctionType -DefaultCallbackWrapper( - napi_env env, CallbackType cb) { - return ThreadSafeFunctionEx<>::DefaultFunctionFactory(env); +#if NAPI_VERSION > 4 + +template +napi_value DefaultCallbackWrapper(napi_env /*env*/, std::nullptr_t /*cb*/) { + return nullptr; +} + +template +napi_value DefaultCallbackWrapper(napi_env /*env*/, Napi::Function cb) { + return cb; } +#else +template +napi_value DefaultCallbackWrapper(napi_env env, Napi::Function cb) { + if (cb.IsEmpty()) { + return TSFN::EmptyFunctionFactory(env); + } + return cb; +} +#endif #endif template @@ -4524,8 +4538,9 @@ ThreadSafeFunctionEx::New( FinalizerDataType>( {data, finalizeCallback}); napi_status status = napi_create_threadsafe_function( - env, details::DefaultCallbackWrapper(env, callback), resource, String::From(env, resourceName), maxQueueSize, - initialThreadCount, finalizeData, + env, details::DefaultCallbackWrapper>(env, callback), resource, + String::From(env, resourceName), maxQueueSize, initialThreadCount, + finalizeData, details::ThreadSafeFinalize:: FinalizeFinalizeWrapperWithDataAndContext, context, CallJsInternal, &tsfn._tsfn); @@ -4634,19 +4649,48 @@ void ThreadSafeFunctionEx::CallJsInternal( env, jsCallback, context, data); } +#if NAPI_VERSION == 4 // static template -typename ThreadSafeFunctionEx::DefaultFunctionType -ThreadSafeFunctionEx::DefaultFunctionFactory( +Napi::Function +ThreadSafeFunctionEx::EmptyFunctionFactory( Napi::Env env) { -#if NAPI_VERSION > 4 - return nullptr; + return Napi::Function::New(env, [](const CallbackInfo &cb) {}); +} + +// static +template +Napi::Function +ThreadSafeFunctionEx::FunctionOrEmpty( + Napi::Env env, Napi::Function &callback) { + if (callback.IsEmpty()) { + return EmptyFunctionFactory(env); + } + return callback; +} + #else - return Function::New(env, [](const CallbackInfo &cb) {}); -#endif +// static +template +std::nullptr_t +ThreadSafeFunctionEx::EmptyFunctionFactory( + Napi::Env /*env*/) { + return nullptr; } +// static +template +Napi::Function +ThreadSafeFunctionEx::FunctionOrEmpty( + Napi::Env /*env*/, Napi::Function &callback) { + return callback; +} + +#endif //////////////////////////////////////////////////////////////////////////////// // ThreadSafeFunction class diff --git a/napi.h b/napi.h index 1fc40bdda..1644632f6 100644 --- a/napi.h +++ b/napi.h @@ -2051,17 +2051,20 @@ namespace Napi { class ThreadSafeFunctionEx { public: -#if NAPI_VERSION > 4 - using DefaultFunctionType = std::nullptr_t; -#else - using DefaultFunctionType = const Napi::Function; -#endif + // This API may only be called from the main thread. // Helper function that returns nullptr if running N-API 5+, otherwise a // non-empty, no-op Function. This provides the ability to specify at // compile-time a callback parameter to `New` that safely does no action // when targeting _any_ N-API version. - static DefaultFunctionType DefaultFunctionFactory(Napi::Env env); +#if NAPI_VERSION > 4 + static std::nullptr_t EmptyFunctionFactory(Napi::Env env); +#else + static Napi::Function EmptyFunctionFactory(Napi::Env env); +#endif + static Napi::Function FunctionOrEmpty(Napi::Env env, Napi::Function& callback); + + #if NAPI_VERSION > 4 diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc index aa471dc91..e39b1f7b7 100644 --- a/test/threadsafe_function_ex/test/basic.cc +++ b/test/threadsafe_function_ex/test/basic.cc @@ -54,8 +54,8 @@ class TSFNWrap : public base { } static std::array, 2> InstanceMethods() { - return {InstanceMethod("release", &TSFNWrap::Release), - InstanceMethod("call", &TSFNWrap::Call)}; + return {{InstanceMethod("release", &TSFNWrap::Release), + InstanceMethod("call", &TSFNWrap::Call)}}; } Napi::Value Call(const CallbackInfo &info) { @@ -105,23 +105,23 @@ class TSFNWrap : public base { ContextType *context = new ContextType(Persistent(info[0])); - _tsfn = TSFN::New( - env, // napi_env env, - TSFN::DefaultFunctionFactory(env), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - context, // ContextType* context, - base::Finalizer, // Finalizer finalizer - &_deferred // FinalizerDataType* data - ); + _tsfn = + TSFN::New(env, // napi_env env, + TSFN::EmptyFunctionFactory(env), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, + 1, // size_t initialThreadCount, + context, // ContextType* context, + base::Finalizer, // Finalizer finalizer + &_deferred // FinalizerDataType* data + ); } static std::array, 3> InstanceMethods() { - return {InstanceMethod("call", &TSFNWrap::Call), - InstanceMethod("getContext", &TSFNWrap::GetContext), - InstanceMethod("release", &TSFNWrap::Release)}; + return {{InstanceMethod("call", &TSFNWrap::Call), + InstanceMethod("getContext", &TSFNWrap::GetContext), + InstanceMethod("release", &TSFNWrap::Release)}}; } Napi::Value Call(const CallbackInfo &info) { @@ -187,8 +187,8 @@ class TSFNWrap : public base { } static std::array, 2> InstanceMethods() { - return {InstanceMethod("release", &TSFNWrap::Release), - InstanceMethod("call", &TSFNWrap::Call)}; + return {{InstanceMethod("release", &TSFNWrap::Release), + InstanceMethod("call", &TSFNWrap::Call)}}; } Napi::Value Call(const CallbackInfo &info) { @@ -212,14 +212,17 @@ struct DataType { // CallJs callback function provided to `napi_create_threadsafe_function`. It is // _NOT_ used by `Napi::ThreadSafeFunctionEx<>`, which is why these arguments // are napi_*. -static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, +static void CallJs(napi_env env, napi_value /*jsCallback*/, void * /*context*/, void *data) { DataType *casted = static_cast(data); if (env != nullptr) { if (data != nullptr) { napi_value undefined; napi_status status = napi_get_undefined(env, &undefined); - NAPI_THROW_IF_FAILED(env, status); + if (status != napi_ok) { + NAPI_THROW_VOID( + Error::New(env, "Could not get undefined from environment")); + } if (casted->reject) { casted->deferred.Reject(undefined); } else { @@ -257,14 +260,14 @@ class TSFNWrap : public base { napi_threadsafe_function napi_tsfn; // A threadsafe function on N-API 4 still requires a callback function, so - // this uses the `DefaultFunctionFactory` helper method to return a no-op + // this uses the `EmptyFunctionFactory` helper method to return a no-op // Function. auto status = napi_create_threadsafe_function( - info.Env(), TSFN::DefaultFunctionFactory(env), nullptr, + info.Env(), TSFN::EmptyFunctionFactory(env), nullptr, String::From(info.Env(), "Test"), 0, 1, nullptr, nullptr, nullptr, CallJs, &napi_tsfn); if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status); + NAPI_THROW_VOID(Error::New(env, "Could not create TSFN.")); } _tsfn = TSFN(napi_tsfn); #else @@ -276,15 +279,16 @@ class TSFNWrap : public base { info.Env(), nullptr, nullptr, String::From(info.Env(), "Test"), 0, 1, nullptr, Finalizer, this, CallJs, &napi_tsfn); if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status); + NAPI_THROW_VOID( + Error::New(env, "Could not get undefined from environment")); } _tsfn = TSFN(napi_tsfn); #endif } static std::array, 2> InstanceMethods() { - return {InstanceMethod("release", &TSFNWrap::Release), - InstanceMethod("call", &TSFNWrap::Call)}; + return {{InstanceMethod("release", &TSFNWrap::Release), + InstanceMethod("call", &TSFNWrap::Call)}}; } Napi::Value Call(const CallbackInfo &info) { @@ -333,7 +337,7 @@ class TSFNWrap : public base { // A threadsafe function on N-API 4 still requires a callback function. _tsfn = TSFN::New( env, // napi_env env, - TSFN::DefaultFunctionFactory( + TSFN::EmptyFunctionFactory( env), // N-API 5+: nullptr; else: const Function& callback, "Test", // ResourceString resourceName, 1, // size_t maxQueueSize, @@ -349,8 +353,8 @@ class TSFNWrap : public base { } static std::array, 2> InstanceMethods() { - return {InstanceMethod("release", &TSFNWrap::Release), - InstanceMethod("call", &TSFNWrap::Call)}; + return {{InstanceMethod("release", &TSFNWrap::Release), + InstanceMethod("call", &TSFNWrap::Call)}}; } // Since this test spec has no CALLBACK, CONTEXT, or FINALIZER. We have no way diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc index b47d684ad..83293667c 100644 --- a/test/threadsafe_function_ex/test/example.cc +++ b/test/threadsafe_function_ex/test/example.cc @@ -43,20 +43,40 @@ class TSFNWrap; // Context of the TSFN. using Context = TSFNWrap; +// Data passed (as pointer) to [Non]BlockingCall struct Data { - // Data passed (as pointer) to [Non]BlockingCall std::promise promise; + uint32_t threadId; + bool logCall; uint32_t base; }; using DataType = std::unique_ptr; // CallJs callback function -static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, - Context *context, DataType *dataPtr) { +static void CallJs(Napi::Env env, Napi::Function jsCallback, + Context * /*context*/, DataType *dataPtr) { if (dataPtr != nullptr) { auto &data = *dataPtr; if (env != nullptr) { - data->promise.set_value(data->base * data->base); + auto calculated = data->base * data->base; + if (!jsCallback.IsEmpty()) { + auto value = jsCallback.Call({Number::New(env, data->threadId), Number::New(env, calculated)}); + if (env.IsExceptionPending()) { + const auto &error = env.GetAndClearPendingException(); + data->promise.set_exception( + std::make_exception_ptr(std::runtime_error(error.Message()))); + } else if (value.IsNumber()) { + calculated = value.ToNumber(); + } + } + if (data->logCall) { + std::string message("Thread " + std::to_string(data->threadId) + + " CallJs resolving std::promise"); + auto console = env.Global().Get("console").As(); + console.Get("log").As().Call(console, + {String::New(env, message)}); + } + data->promise.set_value(calculated); } else { data->promise.set_exception(std::make_exception_ptr( std::runtime_error("TSFN has been finalized."))); @@ -106,9 +126,10 @@ class TSFNWrap : public base { // TSFNWrap object gets garbage-collected and there are still active threads. using FinalizerDataType = std::shared_ptr; -#define THREADLOG(X) if (context->logThread) {\ -std::cout << X;\ -} +#define THREADLOG(X) \ + if (context->logThread) { \ + std::cout << X; \ + } static void threadEntry(size_t threadId, TSFN tsfn, uint32_t callCount, Context *context) { using namespace std::chrono_literals; @@ -118,7 +139,10 @@ std::cout << X;\ for (auto i = 0U; i < callCount; ++i) { auto data = std::make_unique(); data->base = threadId + 1; - THREADLOG("Thread " << threadId << " making call, base = " << data->base << "\n") + data->threadId = threadId; + data->logCall = context->logCall; + THREADLOG("Thread " << threadId << " making call, base = " << data->base + << "\n") tsfn.NonBlockingCall(&data); auto future = data->promise.get_future(); @@ -133,14 +157,15 @@ std::cout << X;\ #undef THREADLOG static std::array, 4> InstanceMethods() { - return {InstanceMethod("getContext", &TSFNWrap::GetContext), - InstanceMethod("start", &TSFNWrap::Start), - InstanceMethod("callCount", &TSFNWrap::CallCount), - InstanceMethod("release", &TSFNWrap::Release)}; + return {{InstanceMethod("getContext", &TSFNWrap::GetContext), + InstanceMethod("start", &TSFNWrap::Start), + InstanceMethod("callCount", &TSFNWrap::CallCount), + InstanceMethod("release", &TSFNWrap::Release)}}; } bool cppExceptions = false; bool logThread; + bool logCall; std::atomic_uint succeededCalls; std::atomic_int aggregate; @@ -163,7 +188,6 @@ std::cout << X;\ finalizerData = std::make_shared(); logThread = DefaultOptions.logThread; - bool logCall = DefaultOptions.logCall; if (info.Length() > 0 && info[0].IsObject()) { auto arg0 = info[0].ToObject(); @@ -239,11 +263,11 @@ std::cout << X;\ succeededCalls = 0; aggregate = 0; _tsfn = TSFN::New( - env, // napi_env env, - TSFN::DefaultFunctionFactory(env), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, + env, // napi_env env, + TSFN::FunctionOrEmpty(env, callback), // const Function& callback, + Value(), // const Object& resource, + "Test", // ResourceString resourceName, + 0, // size_t maxQueueSize, threadCount + 1, // size_t initialThreadCount, +1 for Node thread this, // Context* context, Finalizer, // Finalizer finalizer @@ -255,7 +279,7 @@ std::cout << X;\ callCounts[threadId], this)); } - return String::New(env, "started"); + return Number::New(env, threadCount); }; // TSFN finalizer. Joins the threads and resolves the Promise returned by diff --git a/test/threadsafe_function_ex/test/example.js b/test/threadsafe_function_ex/test/example.js index 53b6783cf..d47cba236 100644 --- a/test/threadsafe_function_ex/test/example.js +++ b/test/threadsafe_function_ex/test/example.js @@ -10,26 +10,38 @@ class ExampleTest extends TestRunner { async example({ TSFNWrap }) { const tsfn = new TSFNWrap(); - const threads = [1]; //, 2, 2, 5, 12]; - const started = await tsfn.start({ threads, logThread: true }); + const threads = [1, 2, 3, 4, 5]; + let callAggregate = 0; + const startedActual = await tsfn.start({ + threads, + logThread: false, + logCall: false, + + callback: (_ /*threadId*/, valueFromCallJs) => { + callAggregate += valueFromCallJs; + } + }); /** * Calculate the expected results. */ const expected = threads.reduce((p, threadCallCount, threadId) => ( ++threadId, - p[0] += threadCallCount, - p[1] += threadCallCount * threadId ** 2, + ++p.threadCount, + p.callCount += threadCallCount, + p.aggregate += threadCallCount * threadId ** 2, p - ), [0, 0]); + ), { threadCount: 0, callCount: 0, aggregate: 0 }); - if (started) { + if (typeof startedActual === 'number') { const released = await tsfn.release(); const [callCountActual, aggregateActual] = tsfn.callCount(); - const [callCountExpected, aggregateExpected] = expected; - assert(callCountActual == callCountExpected, `The number of calls do not match: actual = ${callCountActual}, expected = ${callCountExpected}`); - assert(aggregateActual == aggregateExpected, `The aggregate of calls do not match: actual = ${aggregateActual}, expected = ${aggregateExpected}`); - return expected; + const { threadCount, callCount, aggregate } = expected; + assert(startedActual === threadCount, `The number of threads started do not match: actual = ${startedActual}, expected = ${threadCount}`) + assert(callCountActual === callCount, `The number of calls do not match: actual = ${callCountActual}, expected = ${callCount}`); + assert(aggregateActual === aggregate, `The aggregate of calls do not match: actual = ${aggregateActual}, expected = ${aggregate}`); + assert(aggregate === callAggregate, `The number aggregated by the JavaScript callback and the thread calculated aggregate do not match: actual ${aggregate}, expected = ${callAggregate}`) + return { released, ...expected, callAggregate }; } else { throw new Error('The TSFN failed to start'); } diff --git a/test/threadsafe_function_ex/test/threadsafe.cc b/test/threadsafe_function_ex/test/threadsafe.cc index f12e7e49b..cf3eddca8 100644 --- a/test/threadsafe_function_ex/test/threadsafe.cc +++ b/test/threadsafe_function_ex/test/threadsafe.cc @@ -1,7 +1,6 @@ #include #include #include "napi.h" -#include #if (NAPI_VERSION > 3) diff --git a/test/threadsafe_function_ex/util/util.h b/test/threadsafe_function_ex/util/util.h index 36472d81a..6f202601b 100644 --- a/test/threadsafe_function_ex/util/util.h +++ b/test/threadsafe_function_ex/util/util.h @@ -41,7 +41,7 @@ class TSFNWrapBase : public ObjectWrap { // TSFN finalizer. Resolves the Promise returned by `Release()` above. static void Finalizer(Napi::Env env, std::unique_ptr *deferred, - Context *ctx) { + Context * /*ctx*/) { if (deferred->get()) { (*deferred)->Resolve(Boolean::New(env, true)); deferred->release(); From d092a324ce65ab7308cd53c91c7aa614b95ad934 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Wed, 17 Jun 2020 20:28:10 +0200 Subject: [PATCH 25/39] doc: wip with tsfn documentation --- README.md | 8 +- doc/threadsafe.md | 123 +++++++++++++ doc/threadsafe_function.md | 51 ++---- doc/threadsafe_function_ex.md | 318 ++++++++++++++++++++++++++++++++++ 4 files changed, 456 insertions(+), 44 deletions(-) create mode 100644 doc/threadsafe.md create mode 100644 doc/threadsafe_function_ex.md diff --git a/README.md b/README.md index 206c7f108..c88104623 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ to ideas specified in the **ECMA262 Language Specification**. -node-addon-api is based on [N-API](https://nodejs.org/api/n-api.html) and supports using different N-API versions. -This allows addons built with it to run with Node.js versions which support the targeted N-API version. +node-addon-api is based on [N-API](https://nodejs.org/api/n-api.html) and supports using different N-API versions. +This allows addons built with it to run with Node.js versions which support the targeted N-API version. **However** the node-addon-api support model is to support only the active LTS Node.js versions. This means that every year there will be a new major which drops support for the Node.js LTS version which has gone out of service. @@ -116,7 +116,9 @@ The following is the documentation for node-addon-api. - [AsyncWorker](doc/async_worker.md) - [AsyncContext](doc/async_context.md) - [AsyncWorker Variants](doc/async_worker_variants.md) - - [Thread-safe Functions](doc/threadsafe_function.md) + - [Thread-safe Functions](doc/threadsafe.md) + - [ThreadSafeFunction](doc/threadsafe_function.md) + - [ThreadSafeFunctionEx](doc/threadsafe_function_ex.md) - [Promises](doc/promises.md) - [Version management](doc/version_management.md) diff --git a/doc/threadsafe.md b/doc/threadsafe.md new file mode 100644 index 000000000..70eb296ba --- /dev/null +++ b/doc/threadsafe.md @@ -0,0 +1,123 @@ +# Thread-safe Functions + +JavaScript functions can normally only be called from a native addon's main +thread. If an addon creates additional threads, then node-addon-api functions +that require a `Napi::Env`, `Napi::Value`, or `Napi::Reference` must not be +called from those threads. + +When an addon has additional threads and JavaScript functions need to be invoked +based on the processing completed by those threads, those threads must +communicate with the addon's main thread so that the main thread can invoke the +JavaScript function on their behalf. The thread-safe function APIs provide an +easy way to do this. These APIs provide two types -- +[`Napi::ThreadSafeFunction`](threadsafe_function.md) and +[`Napi::ThreadSafeFunctionEx`](threadsafe_function_ex.md) -- as well as APIs to +create, destroy, and call objects of this type. The differences between the two +are subtle and are [highlighted below](#implementation-differences). Regardless +of which type you choose, the API between the two are similar. + +`Napi::ThreadSafeFunction[Ex]::New()` creates a persistent reference that holds +a JavaScript function which can be called from multiple threads. The calls +happen asynchronously. This means that values with which the JavaScript callback +is to be called will be placed in a queue, and, for each value in the queue, a +call will eventually be made to the JavaScript function. + +`Napi::ThreadSafeFunction[Ex]` objects are destroyed when every thread which +uses the object has called `Release()` or has received a return status of +`napi_closing` in response to a call to `BlockingCall()` or `NonBlockingCall()`. +The queue is emptied before the `Napi::ThreadSafeFunction[Ex]` is destroyed. It +is important that `Release()` be the last API call made in conjunction with a +given `Napi::ThreadSafeFunction[Ex]`, because after the call completes, there is +no guarantee that the `Napi::ThreadSafeFunction[Ex]` is still allocated. For the +same reason it is also important that no more use be made of a thread-safe +function after receiving a return value of `napi_closing` in response to a call +to `BlockingCall()` or `NonBlockingCall()`. Data associated with the +`Napi::ThreadSafeFunction[Ex]` can be freed in its `Finalizer` callback which +was passed to `ThreadSafeFunction[Ex]::New()`. + +Once the number of threads making use of a `Napi::ThreadSafeFunction[Ex]` +reaches zero, no further threads can start making use of it by calling +`Acquire()`. In fact, all subsequent API calls associated with it, except +`Release()`, will return an error value of `napi_closing`. + +## Implementation Differences + +The choice between `Napi::ThreadSafeFunction` and `Napi::ThreadSafeFunctionEx` +depends largely on how you plan to execute your native C++ code (the "callback") +on the Node thread. + +### [`Napi::ThreadSafeFunction`](threadsafe_function.md) + +This API is designed without N-API 5 native support for [optional JavaScript + function callback feature](https://github.com/nodejs/node/commit/53297e66cb). + `::New` methods that do not have a `Function` parameter will construct a + _new_, no-op `Function` on the environment to pass to the underlying N-API + call. + +This API has some dynamic functionality, in that: +- The `[Non]BlockingCall()` methods provide a `Napi::Function` parameter as the + callback to run when processing the data item on the main thread -- the + `CallJs` callback. Since the callback is a parameter, it can be changed for + every call. +- Different C++ data types may be passed with each call of `[Non]BlockingCall()` + to match the specific data type as specified in the `CallJs` callback. + +However, this functionality comes with some **additional overhead** and +situational **memory leaks**: +- The API acts as a "middle-man" between the underlying + `napi_threadsafe_function`, and dynamically constructs a wrapper for your + callback on the heap for every call to `[Non]BlockingCall()`. +- In acting in this "middle-man" fashion, the API will call the underlying "make + call" N-API method on this packaged item. If the API has determined the + threadsafe function is no longer accessible (eg. all threads have Released yet + there are still items on the queue), **the callback passed to + [Non]BlockingCall will not execute**. This means it is impossible to perform + clean-up for calls that never execute their `CallJs` callback. **This may lead + to memory leaks** if you are dynamically allocating memory. +- The `CallJs` does not receive the threadsafe function's context as a + parameter. In order for the callback to access the context, it must have a + reference to either (1) the context directly, or (2) the threadsafe function + to call `GetContext()`. Furthermore, the `GetContext()` method is not + _type-safe_, as the method returns an object that can be "any-casted", instead + of having a static type. + +### [`Napi::ThreadSafeFunctionEx`](threadsafe_function_ex.md) + +The `ThreadSafeFunctionEx` class is a new implementation to address the +drawbacks listed above. The API is designed with N-API 5's support of an +optional function callback. The API will correctly allow developers to pass +`std::nullptr` instead of a `const Function&` for the callback function +specified in `::New`. It also provides helper APIs to _target_ N-API 4 and +construct a no-op `Function` **or** to target N-API 5 and "construct" an +`std::nullptr` callback. This allows a single codebase to use the same APIs, +with just a switch of the `NAPI_VERSION` compile-time constant. + +The removal of the dynamic call functionality has the additional side effects: +- The API does _not_ act as a "middle-man" compared to the non-`Ex`. Once Node + finalizes the threadsafe function, the `CallJs` callback will execute with an + empty `Napi::Env` for any remaining items on the queue. This provides the the + ability to handle any necessary clean up of the item's data. +- The callback _does_ receive the context as a parameter, so a call to + `GetContext()` is _not_ necessary. This context type is specified as the + **first type argument** specified to `::New`, ensuring type safety. +- The `New()` constructor accepts the `CallJs` callback as the **second type + argument**. The callback must be statically defined for the API to access it. + This affords the ability to statically pass the context as the correct type + across all methods. +- Only one C++ data type may be specified to every call to `[Non]BlockingCall()` + -- the **third type argument** specified to `::New`. Any "dynamic call data" + must be implemented by the user. + + +### Usage Suggestions + +In summary, it may be best to use `Napi::ThreadSafeFunctionEx` if: + +- static, compile-time support for targeting N-API 4 or 5+ with an optional + JavaScript callback feature is desired; +- the callback can have `static` storage class and will not change across calls + to `[Non]BlockingCall()`; +- cleanup of items' data is required (eg. deleting dynamically-allocated data + that is created at the caller level). + +Otherwise, `Napi::ThreadSafeFunction` may be a better choice. diff --git a/doc/threadsafe_function.md b/doc/threadsafe_function.md index 2bd8b67c9..0b3202929 100644 --- a/doc/threadsafe_function.md +++ b/doc/threadsafe_function.md @@ -1,41 +1,10 @@ # ThreadSafeFunction -JavaScript functions can normally only be called from a native addon's main -thread. If an addon creates additional threads, then node-addon-api functions -that require a `Napi::Env`, `Napi::Value`, or `Napi::Reference` must not be -called from those threads. - -When an addon has additional threads and JavaScript functions need to be invoked -based on the processing completed by those threads, those threads must -communicate with the addon's main thread so that the main thread can invoke the -JavaScript function on their behalf. The thread-safe function APIs provide an -easy way to do this. - -These APIs provide the type `Napi::ThreadSafeFunction` as well as APIs to -create, destroy, and call objects of this type. -`Napi::ThreadSafeFunction::New()` creates a persistent reference that holds a -JavaScript function which can be called from multiple threads. The calls happen -asynchronously. This means that values with which the JavaScript callback is to -be called will be placed in a queue, and, for each value in the queue, a call -will eventually be made to the JavaScript function. - -`Napi::ThreadSafeFunction` objects are destroyed when every thread which uses -the object has called `Release()` or has received a return status of -`napi_closing` in response to a call to `BlockingCall()` or `NonBlockingCall()`. -The queue is emptied before the `Napi::ThreadSafeFunction` is destroyed. It is -important that `Release()` be the last API call made in conjunction with a given -`Napi::ThreadSafeFunction`, because after the call completes, there is no -guarantee that the `Napi::ThreadSafeFunction` is still allocated. For the same -reason it is also important that no more use be made of a thread-safe function -after receiving a return value of `napi_closing` in response to a call to -`BlockingCall()` or `NonBlockingCall()`. Data associated with the -`Napi::ThreadSafeFunction` can be freed in its `Finalizer` callback which was -passed to `ThreadSafeFunction::New()`. - -Once the number of threads making use of a `Napi::ThreadSafeFunction` reaches -zero, no further threads can start making use of it by calling `Acquire()`. In -fact, all subsequent API calls associated with it, except `Release()`, will -return an error value of `napi_closing`. +The `Napi::ThreadSafeFunction` type provides APIs for threads to communicate +with the addon's main thread to invoke JavaScript functions on their behalf. +Documentation can be found for an [overview of the API](threadsafe.md), as well +as [differences between the two thread-safe function +APIs](threadsafe.md#implementation-differences). ## Methods @@ -93,6 +62,7 @@ New(napi_env env, - `initialThreadCount`: The initial number of threads, including the main thread, which will be making use of this function. - `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. + Can be retreived via `GetContext()`. - `[optional] finalizeCallback`: Function to call when the `ThreadSafeFunction` is being destroyed. This callback will be invoked on the main thread when the thread-safe function is about to be destroyed. It receives the context and the @@ -102,7 +72,6 @@ New(napi_env env, there be no threads left using the thread-safe function after the finalize callback completes. Must implement `void operator()(Env env, DataType* data, Context* hint)`, skipping `data` or `hint` if they are not provided. - Can be retreived via `GetContext()`. - `[optional] data`: Data to be passed to `finalizeCallback`. Returns a non-empty `Napi::ThreadSafeFunction` instance. @@ -110,7 +79,7 @@ Returns a non-empty `Napi::ThreadSafeFunction` instance. ### Acquire Add a thread to this thread-safe function object, indicating that a new thread -will start making use of the thread-safe function. +will start making use of the thread-safe function. ```cpp napi_status Napi::ThreadSafeFunction::Acquire() @@ -118,7 +87,7 @@ napi_status Napi::ThreadSafeFunction::Acquire() Returns one of: - `napi_ok`: The thread has successfully acquired the thread-safe function -for its use. +for its use. - `napi_closing`: The thread-safe function has been marked as closing via a previous call to `Abort()`. @@ -258,10 +227,10 @@ Value Start( const CallbackInfo& info ) // Create a native thread nativeThread = std::thread( [count] { auto callback = []( Napi::Env env, Function jsCallback, int* value ) { - // Transform native data into JS data, passing it to the provided + // Transform native data into JS data, passing it to the provided // `jsCallback` -- the TSFN's JavaScript function. jsCallback.Call( {Number::New( env, *value )} ); - + // We're finished with the data. delete value; }; diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md new file mode 100644 index 000000000..2657dd752 --- /dev/null +++ b/doc/threadsafe_function_ex.md @@ -0,0 +1,318 @@ +# TODO +- Document new N-API 5+ only methods +- Continue with examples + +# ThreadSafeFunctionEx + +The `Napi::ThreadSafeFunctionEx` type provides APIs for threads to communicate +with the addon's main thread to invoke JavaScript functions on their behalf. The +type is a three-argument templated class, each argument representing the type +of: +- `ContextType = std::nullptr_t`: The threadsafe function's context. By default, + a TSFN has no context. +- `DataType = void*`: The data to use in the native callback. By default, a TSFN + can accept *any* data type. +- `Callback = void*(Napi::Env, Napi::Function, ContextType*, DataType*)`: The + callback to run for each item added to the queue. + +Documentation can be found for an [overview of the API](threadsafe.md), as well +as [differences between the two thread-safe function +APIs](threadsafe.md#implementation-differences). + +## Methods + +### Constructor + +Creates a new empty instance of `Napi::ThreadSafeFunctionEx`. + +```cpp +Napi::Function::ThreadSafeFunctionEx::ThreadSafeFunctionEx(); +``` + +### Constructor + +Creates a new instance of the `Napi::ThreadSafeFunctionEx` object. + +```cpp +Napi::ThreadSafeFunctionEx::ThreadSafeFunctionEx(napi_threadsafe_function tsfn); +``` + +- `tsfn`: The `napi_threadsafe_function` which is a handle for an existing + thread-safe function. + +Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. + +### New + +Creates a new instance of the `Napi::ThreadSafeFunctionEx` object. + +```cpp +New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); +``` + +- `env`: The `napi_env` environment in which to construct the + `Napi::ThreadSafeFunction` object. +- `callback`: The `Function` to call from another thread. +- `resource`: An object associated with the async work that will be passed to + possible async_hooks init hooks. +- `resourceName`: A JavaScript string to provide an identifier for the kind of + resource that is being provided for diagnostic information exposed by the + async_hooks API. +- `maxQueueSize`: Maximum size of the queue. `0` for no limit. +- `initialThreadCount`: The initial number of threads, including the main + thread, which will be making use of this function. +- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. + Can be retreived via `GetContext()`. + +Returns a non-empty `Napi::ThreadSafeFunction` instance. + +### New + +Creates a new instance of the `Napi::ThreadSafeFunctionEx` object with a +finalizer that runs when the object is being destroyed. + +```cpp +New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); +``` + +- `env`: The `napi_env` environment in which to construct the + `Napi::ThreadSafeFunction` object. +- `callback`: The `Function` to call from another thread. +- `resource`: An object associated with the async work that will be passed to + possible async_hooks init hooks. +- `resourceName`: A JavaScript string to provide an identifier for the kind of + resource that is being provided for diagnostic information exposed by the + async_hooks API. +- `maxQueueSize`: Maximum size of the queue. `0` for no limit. +- `initialThreadCount`: The initial number of threads, including the main + thread, which will be making use of this function. +- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. + Can be retreived via `GetContext()`. +- `finalizeCallback`: Function to call when the `ThreadSafeFunctionEx` is being + destroyed. This callback will be invoked on the main thread when the + thread-safe function is about to be destroyed. It receives the context and the + finalize data given during construction (if given), and provides an + opportunity for cleaning up after the threads e.g. by calling + `uv_thread_join()`. It is important that, aside from the main loop thread, + there be no threads left using the thread-safe function after the finalize + callback completes. Must implement `void operator()(Env env, DataType* data, + ContextType* hint)`. +- `[optional] data`: Data to be passed to `finalizeCallback`. + +Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. + +### Acquire + +Add a thread to this thread-safe function object, indicating that a new thread +will start making use of the thread-safe function. + +```cpp +napi_status Napi::ThreadSafeFunctionEx::Acquire() +``` + +Returns one of: +- `napi_ok`: The thread has successfully acquired the thread-safe function for + its use. +- `napi_closing`: The thread-safe function has been marked as closing via a + previous call to `Abort()`. + +### Release + +Indicate that an existing thread will stop making use of the thread-safe +function. A thread should call this API when it stops making use of this +thread-safe function. Using any thread-safe APIs after having called this API +has undefined results in the current thread, as it may have been destroyed. + +```cpp +napi_status Napi::ThreadSafeFunctionEx::Release() +``` + +Returns one of: +- `napi_ok`: The thread-safe function has been successfully released. +- `napi_invalid_arg`: The thread-safe function's thread-count is zero. +- `napi_generic_failure`: A generic error occurred when attemping to release the + thread-safe function. + +### Abort + +"Abort" the thread-safe function. This will cause all subsequent APIs associated +with the thread-safe function except `Release()` to return `napi_closing` even +before its reference count reaches zero. In particular, `BlockingCall` and +`NonBlockingCall()` will return `napi_closing`, thus informing the threads that +it is no longer possible to make asynchronous calls to the thread-safe function. +This can be used as a criterion for terminating the thread. Upon receiving a +return value of `napi_closing` from a thread-safe function call a thread must +make no further use of the thread-safe function because it is no longer +guaranteed to be allocated. + +```cpp +napi_status Napi::ThreadSafeFunctionEx::Abort() +``` + +Returns one of: +- `napi_ok`: The thread-safe function has been successfully aborted. +- `napi_invalid_arg`: The thread-safe function's thread-count is zero. +- `napi_generic_failure`: A generic error occurred when attemping to abort the + thread-safe function. + +### BlockingCall / NonBlockingCall + +Calls the Javascript function in either a blocking or non-blocking fashion. +- `BlockingCall()`: the API blocks until space becomes available in the queue. + Will never block if the thread-safe function was created with a maximum queue + size of `0`. +- `NonBlockingCall()`: will return `napi_queue_full` if the queue was full, + preventing data from being successfully added to the queue. + +```cpp +napi_status Napi::ThreadSafeFunctionEx::BlockingCall(DataType* data = nullptr) const + +napi_status Napi::ThreadSafeFunctionEx::NonBlockingCall(DataType* data = nullptr) const +``` + +- `[optional] data`: Data to pass to the callback which was passed to + `ThreadSafeFunctionEx::New()`. +- `[optional] callback`: C++ function that is invoked on the main thread. The + callback receives the `ThreadSafeFunction`'s JavaScript callback function to + call as an `Napi::Function` in its parameters and the `DataType*` data pointer + (if provided). Must implement `void operator()(Napi::Env env, Function + jsCallback, DataType* data)`, skipping `data` if not provided. It is not + necessary to call into JavaScript via `MakeCallback()` because N-API runs + `callback` in a context appropriate for callbacks. + +Returns one of: +- `napi_ok`: The call was successfully added to the queue. +- `napi_queue_full`: The queue was full when trying to call in a non-blocking + method. +- `napi_closing`: The thread-safe function is aborted and cannot accept more + calls. +- `napi_invalid_arg`: The thread-safe function is closed. +- `napi_generic_failure`: A generic error occurred when attemping to add to the + queue. + +## Example + +```cpp +#include +#include +#include + +using namespace Napi; + +std::thread nativeThread; + +struct ContextType { + int threadId; +}; + +using DataType = int; + +using ThreadSafeFunctionEx = tsfn; + +Value Start( const CallbackInfo& info ) +{ + Napi::Env env = info.Env(); + + if ( info.Length() < 2 ) + { + throw TypeError::New( env, "Expected two arguments" ); + } + else if ( !info[0].IsFunction() ) + { + throw TypeError::New( env, "Expected first arg to be function" ); + } + else if ( !info[1].IsNumber() ) + { + throw TypeError::New( env, "Expected second arg to be number" ); + } + + int count = info[1].As().Int32Value(); + + // Create a ThreadSafeFunction + tsfn = ThreadSafeFunction::New( + env, + info[0].As(), // JavaScript function called asynchronously + "Resource Name", // Name + 0, // Unlimited queue + 1, // Only one thread will use this initially + []( Napi::Env ) { // Finalizer used to clean threads up + nativeThread.join(); + } ); + + // Create a native thread + nativeThread = std::thread( [count] { + auto callback = []( Napi::Env env, Function jsCallback, int* value ) { + // Transform native data into JS data, passing it to the provided + // `jsCallback` -- the TSFN's JavaScript function. + jsCallback.Call( {Number::New( env, *value )} ); + + // We're finished with the data. + delete value; + }; + + for ( int i = 0; i < count; i++ ) + { + // Create new data + int* value = new int( clock() ); + + // Perform a blocking call + napi_status status = tsfn.BlockingCall( value, callback ); + if ( status != napi_ok ) + { + // Handle error + break; + } + + std::this_thread::sleep_for( std::chrono::seconds( 1 ) ); + } + + // Release the thread-safe function + tsfn.Release(); + } ); + + return Boolean::New(env, true); +} + +Napi::Object Init( Napi::Env env, Object exports ) +{ + exports.Set( "start", Function::New( env, Start ) ); + return exports; +} + +NODE_API_MODULE( clock, Init ) +``` + +The above code can be used from JavaScript as follows: + +```js +const { start } = require('bindings')('clock'); + +start(function () { + console.log("JavaScript callback called with arguments", Array.from(arguments)); +}, 5); +``` + +When executed, the output will show the value of `clock()` five times at one +second intervals: + +``` +JavaScript callback called with arguments [ 84745 ] +JavaScript callback called with arguments [ 103211 ] +JavaScript callback called with arguments [ 104516 ] +JavaScript callback called with arguments [ 105104 ] +JavaScript callback called with arguments [ 105691 ] +``` From 75dd4221240bbca2a9bce01498db93d1050cffec Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 21 Jun 2020 15:27:39 +0200 Subject: [PATCH 26/39] doc,test: finish TSFNEx --- doc/threadsafe.md | 8 +- doc/threadsafe_function_ex.md | 241 +++---- test/binding.gyp | 7 +- test/threadsafe_function_ex/test/basic.cc | 93 ++- test/threadsafe_function_ex/test/basic.js | 79 +- test/threadsafe_function_ex/test/example.cc | 681 +++++++++++++----- test/threadsafe_function_ex/test/example.js | 334 ++++++++- .../threadsafe_function_ex/util/TestRunner.js | 32 +- 8 files changed, 1064 insertions(+), 411 deletions(-) diff --git a/doc/threadsafe.md b/doc/threadsafe.md index 70eb296ba..86f63906b 100644 --- a/doc/threadsafe.md +++ b/doc/threadsafe.md @@ -69,14 +69,14 @@ situational **memory leaks**: callback on the heap for every call to `[Non]BlockingCall()`. - In acting in this "middle-man" fashion, the API will call the underlying "make call" N-API method on this packaged item. If the API has determined the - threadsafe function is no longer accessible (eg. all threads have Released yet + thread-safe function is no longer accessible (eg. all threads have released yet there are still items on the queue), **the callback passed to [Non]BlockingCall will not execute**. This means it is impossible to perform clean-up for calls that never execute their `CallJs` callback. **This may lead to memory leaks** if you are dynamically allocating memory. -- The `CallJs` does not receive the threadsafe function's context as a +- The `CallJs` does not receive the thread-safe function's context as a parameter. In order for the callback to access the context, it must have a - reference to either (1) the context directly, or (2) the threadsafe function + reference to either (1) the context directly, or (2) the thread-safe function to call `GetContext()`. Furthermore, the `GetContext()` method is not _type-safe_, as the method returns an object that can be "any-casted", instead of having a static type. @@ -94,7 +94,7 @@ with just a switch of the `NAPI_VERSION` compile-time constant. The removal of the dynamic call functionality has the additional side effects: - The API does _not_ act as a "middle-man" compared to the non-`Ex`. Once Node - finalizes the threadsafe function, the `CallJs` callback will execute with an + finalizes the thread-safe function, the `CallJs` callback will execute with an empty `Napi::Env` for any remaining items on the queue. This provides the the ability to handle any necessary clean up of the item's data. - The callback _does_ receive the context as a parameter, so a call to diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md index 2657dd752..15188a96c 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/threadsafe_function_ex.md @@ -1,19 +1,17 @@ -# TODO -- Document new N-API 5+ only methods -- Continue with examples - # ThreadSafeFunctionEx The `Napi::ThreadSafeFunctionEx` type provides APIs for threads to communicate with the addon's main thread to invoke JavaScript functions on their behalf. The type is a three-argument templated class, each argument representing the type of: -- `ContextType = std::nullptr_t`: The threadsafe function's context. By default, +- `ContextType = std::nullptr_t`: The thread-safe function's context. By default, a TSFN has no context. - `DataType = void*`: The data to use in the native callback. By default, a TSFN can accept *any* data type. -- `Callback = void*(Napi::Env, Napi::Function, ContextType*, DataType*)`: The - callback to run for each item added to the queue. +- `Callback = void*(Napi::Env, Napi::Function jsCallback, ContextType*, + DataType*)`: The callback to run for each item added to the queue. If no + `Callback` is given, the API will call the function `jsCallback` with no + arguments. Documentation can be found for an [overview of the API](threadsafe.md), as well as [differences between the two thread-safe function @@ -40,46 +38,20 @@ Napi::ThreadSafeFunctionEx::ThreadSafeFunctionE - `tsfn`: The `napi_threadsafe_function` which is a handle for an existing thread-safe function. -Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. - -### New - -Creates a new instance of the `Napi::ThreadSafeFunctionEx` object. - -```cpp -New(napi_env env, - const Function& callback, - const Object& resource, - ResourceString resourceName, - size_t maxQueueSize, - size_t initialThreadCount, - ContextType* context = nullptr); -``` - -- `env`: The `napi_env` environment in which to construct the - `Napi::ThreadSafeFunction` object. -- `callback`: The `Function` to call from another thread. -- `resource`: An object associated with the async work that will be passed to - possible async_hooks init hooks. -- `resourceName`: A JavaScript string to provide an identifier for the kind of - resource that is being provided for diagnostic information exposed by the - async_hooks API. -- `maxQueueSize`: Maximum size of the queue. `0` for no limit. -- `initialThreadCount`: The initial number of threads, including the main - thread, which will be making use of this function. -- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. - Can be retreived via `GetContext()`. - -Returns a non-empty `Napi::ThreadSafeFunction` instance. +Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. To ensure the API +statically handles the correct return type for `GetContext()` and +`[Non]BlockingCall()`, pass the proper type arguments to +`Napi::ThreadSafeFunctionEx`. ### New -Creates a new instance of the `Napi::ThreadSafeFunctionEx` object with a -finalizer that runs when the object is being destroyed. +Creates a new instance of the `Napi::ThreadSafeFunctionEx` object. The `New` +function has several overloads for the various optional parameters: skip the +optional parameter for that specific overload. ```cpp New(napi_env env, - const Function& callback, + CallbackType callback, const Object& resource, ResourceString resourceName, size_t maxQueueSize, @@ -91,9 +63,9 @@ New(napi_env env, - `env`: The `napi_env` environment in which to construct the `Napi::ThreadSafeFunction` object. -- `callback`: The `Function` to call from another thread. -- `resource`: An object associated with the async work that will be passed to - possible async_hooks init hooks. +- `[optional] callback`: The `Function` to call from another thread. +- `[optional] resource`: An object associated with the async work that will be + passed to possible async_hooks init hooks. - `resourceName`: A JavaScript string to provide an identifier for the kind of resource that is being provided for diagnostic information exposed by the async_hooks API. @@ -102,19 +74,31 @@ New(napi_env env, thread, which will be making use of this function. - `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. Can be retreived via `GetContext()`. -- `finalizeCallback`: Function to call when the `ThreadSafeFunctionEx` is being - destroyed. This callback will be invoked on the main thread when the - thread-safe function is about to be destroyed. It receives the context and the - finalize data given during construction (if given), and provides an - opportunity for cleaning up after the threads e.g. by calling - `uv_thread_join()`. It is important that, aside from the main loop thread, - there be no threads left using the thread-safe function after the finalize - callback completes. Must implement `void operator()(Env env, DataType* data, - ContextType* hint)`. +- `[optional] finalizeCallback`: Function to call when the + `ThreadSafeFunctionEx` is being destroyed. This callback will be invoked on + the main thread when the thread-safe function is about to be destroyed. It + receives the context and the finalize data given during construction (if + given), and provides an opportunity for cleaning up after the threads e.g. by + calling `uv_thread_join()`. It is important that, aside from the main loop + thread, there be no threads left using the thread-safe function after the + finalize callback completes. Must implement `void operator()(Env env, + DataType* data, ContextType* hint)`. - `[optional] data`: Data to be passed to `finalizeCallback`. Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. +Depending on the targetted `NAPI_VERSION`, the API has different implementations +for `CallbackType callback`. + +When targetting version 4, `CallbackType` is: +- `const Function&` +- skipped, in which case the API creates a new no-op `Function` + +When targetting version 5+, `CallbackType` is: +- `const Function&` +- `std::nullptr_t` +- skipped, in which case the API passes `std::nullptr` + ### Acquire Add a thread to this thread-safe function object, indicating that a new thread @@ -206,113 +190,54 @@ Returns one of: ## Example -```cpp -#include -#include -#include - -using namespace Napi; - -std::thread nativeThread; - -struct ContextType { - int threadId; -}; - -using DataType = int; - -using ThreadSafeFunctionEx = tsfn; - -Value Start( const CallbackInfo& info ) -{ - Napi::Env env = info.Env(); - - if ( info.Length() < 2 ) - { - throw TypeError::New( env, "Expected two arguments" ); - } - else if ( !info[0].IsFunction() ) - { - throw TypeError::New( env, "Expected first arg to be function" ); - } - else if ( !info[1].IsNumber() ) - { - throw TypeError::New( env, "Expected second arg to be number" ); - } - - int count = info[1].As().Int32Value(); - - // Create a ThreadSafeFunction - tsfn = ThreadSafeFunction::New( - env, - info[0].As(), // JavaScript function called asynchronously - "Resource Name", // Name - 0, // Unlimited queue - 1, // Only one thread will use this initially - []( Napi::Env ) { // Finalizer used to clean threads up - nativeThread.join(); - } ); - - // Create a native thread - nativeThread = std::thread( [count] { - auto callback = []( Napi::Env env, Function jsCallback, int* value ) { - // Transform native data into JS data, passing it to the provided - // `jsCallback` -- the TSFN's JavaScript function. - jsCallback.Call( {Number::New( env, *value )} ); - - // We're finished with the data. - delete value; - }; - - for ( int i = 0; i < count; i++ ) - { - // Create new data - int* value = new int( clock() ); - - // Perform a blocking call - napi_status status = tsfn.BlockingCall( value, callback ); - if ( status != napi_ok ) - { - // Handle error - break; - } - - std::this_thread::sleep_for( std::chrono::seconds( 1 ) ); - } - - // Release the thread-safe function - tsfn.Release(); - } ); - - return Boolean::New(env, true); -} - -Napi::Object Init( Napi::Env env, Object exports ) -{ - exports.Set( "start", Function::New( env, Start ) ); - return exports; -} - -NODE_API_MODULE( clock, Init ) -``` - -The above code can be used from JavaScript as follows: +For an in-line documented example, please see the ThreadSafeFunctionEx CI tests hosted here. +- [test/threadsafe_function_ex/test/example.js](../test/threadsafe_function_ex/test/example.js) +- [test/threadsafe_function_ex/test/example.cc](../test/threadsafe_function_ex/test/example.cc) -```js -const { start } = require('bindings')('clock'); - -start(function () { - console.log("JavaScript callback called with arguments", Array.from(arguments)); -}, 5); -``` +The example will create multiple set of threads. Each thread calls into +JavaScript with a numeric `base` value (deterministically calculated by the +thread id), with Node returning either a `number` or `Promise` that +resolves to `base * base`. -When executed, the output will show the value of `clock()` five times at one -second intervals: +From the root of the `node-addon-api` repository: ``` -JavaScript callback called with arguments [ 84745 ] -JavaScript callback called with arguments [ 103211 ] -JavaScript callback called with arguments [ 104516 ] -JavaScript callback called with arguments [ 105104 ] -JavaScript callback called with arguments [ 105691 ] +Usage: node ./test/threadsafe_function_ex/test/example.js [options] + + -c, --calls The number of calls each thread should make (number[]). + -a, --acquire [factor] Acquire a new set of `factor` call threads, using the + same `calls` definition. + -d, --call-delay The delay on callback resolution that each thread should + have (number[]). This is achieved via a delayed Promise + resolution in the JavaScript callback provided to the + TSFN. Using large delays here will cause all threads to + bottle-neck. + -D, --thread-delay The delay that each thread should have prior to making a + call (number[]). Using large delays here will cause the + individual thread to bottle-neck. + -l, --log-call Display console.log-based logging messages. + -L, --log-thread Display std::cout-based logging messages. + -n, --no-callback Do not use a JavaScript callback. + -e, --callback-error [thread[.call]] Cause an error to occur in the JavaScript callback for + the given thread's call (if provided; first thread's + first call otherwise). + + When not provided: + - defaults to [1,2,3,4,5] + - [factor] defaults to 1 + - defaults to [400,200,100,50,0] + - defaults to [400,200,100,50,0] + + +Examples: + + -c [1,2,3] -l -L + + Creates three threads that makes one, two, and three calls each, respectively. + + -c [5,5] -d [5000,5000] -D [0,0] -l -L + + Creates two threads that make five calls each. In this scenario, the threads will be + blocked primarily on waiting for the callback to resolve, as each thread's call takes + 5000 milliseconds. ``` diff --git a/test/binding.gyp b/test/binding.gyp index 8bfa59e3e..3a7ab89c9 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -2,6 +2,9 @@ 'target_defaults': { 'includes': ['../common.gypi'], 'sources': [ + 'threadsafe_function_ex/test/basic.cc', + 'threadsafe_function_ex/test/example.cc', + 'threadsafe_function_ex/test/threadsafe.cc', 'addon_data.cc', 'arraybuffer.cc', 'asynccontext.cc', @@ -35,9 +38,7 @@ 'object/set_property.cc', 'promise.cc', 'run_script.cc', - 'threadsafe_function_ex/test/basic.cc', - 'threadsafe_function_ex/test/example.cc', - 'threadsafe_function_ex/test/threadsafe.cc', + 'threadsafe_function/threadsafe_function_ctx.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', 'threadsafe_function/threadsafe_function_ptr.cc', diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc index e39b1f7b7..9c9b5e636 100644 --- a/test/threadsafe_function_ex/test/basic.cc +++ b/test/threadsafe_function_ex/test/basic.cc @@ -212,21 +212,20 @@ struct DataType { // CallJs callback function provided to `napi_create_threadsafe_function`. It is // _NOT_ used by `Napi::ThreadSafeFunctionEx<>`, which is why these arguments // are napi_*. -static void CallJs(napi_env env, napi_value /*jsCallback*/, void * /*context*/, +static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, void *data) { DataType *casted = static_cast(data); if (env != nullptr) { + if (jsCallback != nullptr) { + Function(env, jsCallback).Call(0, nullptr); + } if (data != nullptr) { - napi_value undefined; - napi_status status = napi_get_undefined(env, &undefined); - if (status != napi_ok) { - NAPI_THROW_VOID( - Error::New(env, "Could not get undefined from environment")); - } if (casted->reject) { - casted->deferred.Reject(undefined); + casted->deferred.Reject( + String::New(env, "The CallJs has rejected the promise")); } else { - casted->deferred.Resolve(undefined); + casted->deferred.Resolve( + String::New(env, "The CallJs has resolved the promise")); } } } @@ -256,34 +255,24 @@ class TSFNWrap : public base { TSFNWrap(const CallbackInfo &info) : base(info) { auto env = info.Env(); -#if NAPI_VERSION == 4 - napi_threadsafe_function napi_tsfn; - // A threadsafe function on N-API 4 still requires a callback function, so - // this uses the `EmptyFunctionFactory` helper method to return a no-op - // Function. - auto status = napi_create_threadsafe_function( - info.Env(), TSFN::EmptyFunctionFactory(env), nullptr, - String::From(info.Env(), "Test"), 0, 1, nullptr, nullptr, nullptr, - CallJs, &napi_tsfn); - if (status != napi_ok) { - NAPI_THROW_VOID(Error::New(env, "Could not create TSFN.")); + if (info.Length() < 1 || !info[0].IsFunction()) { + NAPI_THROW_VOID(Napi::TypeError::New( + env, "Invalid arguments: Expected arg0 = function")); } - _tsfn = TSFN(napi_tsfn); -#else + napi_threadsafe_function napi_tsfn; - // A threadsafe function may be `nullptr` on N-API 5+ as long as a `CallJS` - // is present. + // A threadsafe function on N-API 4 still requires a callback function, so + // this uses the `EmptyFunctionFactory` helper method to return a no-op + // Function on N-API 5+. auto status = napi_create_threadsafe_function( - info.Env(), nullptr, nullptr, String::From(info.Env(), "Test"), 0, 1, + info.Env(), info[0], nullptr, String::From(info.Env(), "Test"), 0, 1, nullptr, Finalizer, this, CallJs, &napi_tsfn); if (status != napi_ok) { - NAPI_THROW_VOID( - Error::New(env, "Could not get undefined from environment")); + NAPI_THROW_VOID(Error::New(env, "Could not create TSFN.")); } _tsfn = TSFN(napi_tsfn); -#endif } static std::array, 2> InstanceMethods() { @@ -292,10 +281,50 @@ class TSFNWrap : public base { } Napi::Value Call(const CallbackInfo &info) { - auto *data = - new DataType{Promise::Deferred::New(info.Env()), info[0].ToBoolean()}; - _tsfn.NonBlockingCall(data); - return data->deferred.Promise(); + Napi::Env env = info.Env(); + if (info.Length() < 1) { + NAPI_THROW(Napi::TypeError::New( + env, "Invalid arguments: Expected arg0 = number [0,5]"), + Value()); + } + auto arg0 = info[0]; + if (!arg0.IsNumber()) { + NAPI_THROW(Napi::TypeError::New( + env, "Invalid arguments: Expected arg0 = number [0,5]"), + Value()); + } + auto mode = info[0].ToNumber().Int32Value(); + switch (mode) { + // Use node-addon-api to send a call that either resolves or rejects the + // promise in the data. + case 0: + case 1: { + auto *data = new DataType{Promise::Deferred::New(env), mode == 1}; + _tsfn.NonBlockingCall(data); + return data->deferred.Promise(); + } + // Use node-addon-api to send a call with no data + case 2: { + _tsfn.NonBlockingCall(); + return Boolean::New(env, true); + } + // Use napi to send a call that either resolves or rejects the promise in + // the data. + case 3: + case 4: { + auto *data = new DataType{Promise::Deferred::New(env), mode == 4}; + napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); + return data->deferred.Promise(); + } + // Use napi to send a call with no data + case 5: { + napi_call_threadsafe_function(_tsfn, nullptr, napi_tsfn_nonblocking); + return Boolean::New(env, true); + } + } + NAPI_THROW(Napi::TypeError::New( + env, "Invalid arguments: Expected arg0 = number [0,5]"), + Value()); }; private: diff --git a/test/threadsafe_function_ex/test/basic.js b/test/threadsafe_function_ex/test/basic.js index 432856ddf..f5bfc678b 100644 --- a/test/threadsafe_function_ex/test/basic.js +++ b/test/threadsafe_function_ex/test/basic.js @@ -37,7 +37,7 @@ class BasicTest extends TestRunner { * the test. * - Asserts the contexts are the same as the context passed during threadsafe * function construction in two places: - * - (A) Makes one call, and waits for call to complete. + * - (A) Makes one call, and waits for call to complete. * - (B) Asserts that the context returns from the API's `GetContext()` */ async context({ TSFNWrap }) { @@ -54,10 +54,10 @@ class BasicTest extends TestRunner { * that handles all of its JavaScript processing on the callJs instead of the * callback. * - Creates a threadsafe function with no JavaScript context or callback. - * - Makes one call, waiting for completion. The internal `CallJs` resolves the call if jsCallback is empty, otherwise rejects. + * - Makes one call, waiting for completion. The internal `CallJs` resolves + * the call if jsCallback is empty, otherwise rejects. */ async empty({ TSFNWrap }) { - debugger; if (typeof TSFNWrap === 'function') { const tsfn = new TSFNWrap(); await tsfn.call(); @@ -66,6 +66,79 @@ class BasicTest extends TestRunner { return true; } + /** + * A `ThreadSafeFunctionEx` can be constructed with an existing + * napi_threadsafe_function. + * - Creates a native napi_threadsafe_function with no context, using the + * jsCallback passed from this test. + * - Makes six calls: + * - Use node-addon-api's `NonBlockingCall` *OR* napi's + * `napi_call_threadsafe_function` _cross_ + * - With data that resolves *OR rejects on CallJs + * - With no data that rejects on CallJs + * - Releases the TSFN. + */ + async existing({ TSFNWrap }) { + + /** + * Called by the TSFN's jsCallback below. + * @type {function|undefined} + */ + let currentCallback = undefined; + + const tsfn = new TSFNWrap(function () { + if (typeof currentCallback === 'function') { + currentCallback.apply(undefined, arguments); + } + }); + /** + * The input argument to `tsfn.call()`: 0-2: + * ThreadSafeFunctionEx.NonBlockingCall(data) with... + * - 0: data, resolve promise in CallJs + * - 1: data, reject promise in CallJs + * - 2: data = nullptr 3-5: napi_call_threadsafe_function(data, + * napi_tsfn_nonblocking) with... + * - 3: data, resolve promise in CallJs + * - 4: data, reject promise in CallJs + * - 5: data = nullptr + * @type {[0,1,2,3,4,5]} + */ + const input = [0, 1, 2, 3, 4, 5]; + + let caught = false; + + while (input.length) { + // Perform a call that resolves + await tsfn.call(input.shift()); + + // Perform a call that rejects + caught = false; + try { + await tsfn.call(input.shift()); + } catch (e) { + caught = true; + } finally { + assert(caught, "The rejection was not caught"); + } + + // Perform a call with no data + caught = false; + await new Promise((resolve, reject) => { + currentCallback = () => { + resolve(); + reject = undefined; + }; + tsfn.call(input.shift()); + setTimeout(() => { + if (reject) { + reject(new Error("tsfn.call() timed out")); + } + }, 0); + }); + } + return await tsfn.release(); + } + /** * A `ThreadSafeFunctionEx<>` can be constructed with no type arguments. * - Creates a threadsafe function with no context or callback or callJs. diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc index 83293667c..b0063a5cd 100644 --- a/test/threadsafe_function_ex/test/example.cc +++ b/test/threadsafe_function_ex/test/example.cc @@ -3,11 +3,18 @@ #include #include #include +#include #include #include -static constexpr auto DEFAULT_THREAD_COUNT = 10U; -static constexpr auto DEFAULT_CALL_COUNT = 2; +using ThreadExitHandler = void (*)(size_t threadId); + +struct ThreadOptions { + size_t threadId; + int calls; + int callDelay; + int threadDelay; +}; static struct { bool logCall = true; // Uses JS console.log to output when the TSFN is @@ -16,22 +23,6 @@ static struct { // NonBlockingCall() request has finished. } DefaultOptions; // Options from Start() -/** - * @brief Macro used specifically to support the dual CI test / documentation - * example setup. Exceptions are always thrown as JavaScript exceptions when - * running in example mode. - * - */ -#define TSFN_THROW(tsfnWrap, e, ...) \ - if (tsfnWrap->cppExceptions) { \ - do { \ - (e).ThrowAsJavaScriptException(); \ - return __VA_ARGS__; \ - } while (0); \ - } else { \ - NAPI_THROW(e, __VA_ARGS__); \ - } - #if (NAPI_VERSION > 3) using namespace Napi; @@ -43,186 +34,298 @@ class TSFNWrap; // Context of the TSFN. using Context = TSFNWrap; -// Data passed (as pointer) to [Non]BlockingCall +// Data returned to a thread when it requests a TSFN call. This example uses +// promises to synchronize between threads. Since this example needs to be built +// with exceptions both enabled and disabled, we will always use a static +// "positive" result, and dynamically at run-time determine if it failed. +// Otherwise, we could use std::promise.set_exception to handle errors. +struct CallJsResult { + int result; + bool isFinalized; + std::string error; +}; + +// The structure of data we send to `CallJs` struct Data { - std::promise promise; + std::promise promise; uint32_t threadId; bool logCall; - uint32_t base; + int callDelay; + uint32_t base; // The "input" data, which CallJs will calculate `base * base` + int callId; // The call id (unique to the thread) }; -using DataType = std::unique_ptr; -// CallJs callback function +// Data passed (as pointer) to [Non]BlockingCall is shared among multiple +// threads (native thread and Node thread). +using DataType = std::shared_ptr; + +// When providing the `CallJs` result back to the thread, we pass information +// about where and how the result came (for logging). +enum ResultLocation { + NUMBER, // The callback returned a number + PROMISE, // The callback returned a Promise that resolved to a number + DEFAULT // There was no callback provided to TSFN::New +}; + +// CallJs callback function, used to transform the native C data to JS data. static void CallJs(Napi::Env env, Napi::Function jsCallback, Context * /*context*/, DataType *dataPtr) { + // If we have data if (dataPtr != nullptr) { - auto &data = *dataPtr; + std::weak_ptr weakData(*dataPtr); + // Create concrete reference to our DataType. + auto &data(*dataPtr); + + // The success handler ran by the following `CallJs` function. + auto handleResult = [=](Napi::Env env, int calculated, + ResultLocation location) { + // auto &data(*dataPtr); + if (auto data = + weakData + .lock()) { // Has to be copied into a shared_ptr before usage + // std::cout << *data << "\n"; + if (data->logCall) { + std::string message( + "[Thread " + std::to_string(data->threadId) + + "] [CallJs ] [Call " + std::to_string(data->callId) + + "] Receive answer: result = " + std::to_string(calculated) + + (location == ResultLocation::NUMBER + ? " (as number)" + : location == ResultLocation::PROMISE ? " (as Promise)" + : " (as default)")); + + auto console = env.Global().Get("console").As(); + console.Get("log").As().Call(console, + {String::New(env, message)}); + } + // Resolve the `std::promise` awaited on in the child thread. + data->promise.set_value(CallJsResult{calculated, false, ""}); + // Free the data. + // delete dataPtr; + } + }; + + // The error handler ran by the following `CallJs` function. + + auto handleError = [=](const std::string &what) { + if (auto data = + weakData + .lock()) { // Has to be copied into a shared_ptr before usage + // Resolve the `std::promise` awaited on in the child thread with an + // "errored success" value. Instead of erroring at the thread level, + // this could also return the default result. + data->promise.set_value(CallJsResult{0, false, what}); + } + + // // Free the data. + // delete dataPtr; + }; + if (env != nullptr) { - auto calculated = data->base * data->base; + // If the callback was provided at construction time via TSFN::New if (!jsCallback.IsEmpty()) { - auto value = jsCallback.Call({Number::New(env, data->threadId), Number::New(env, calculated)}); + // Call the callback + auto value = jsCallback.Call( + {Number::New(env, data->threadId), Number::New(env, data->callId), + Number::New(env, data->logCall), Number::New(env, data->base), + Number::New(env, data->callDelay)}); + + // Check if the callback failed if (env.IsExceptionPending()) { const auto &error = env.GetAndClearPendingException(); - data->promise.set_exception( - std::make_exception_ptr(std::runtime_error(error.Message()))); - } else if (value.IsNumber()) { - calculated = value.ToNumber(); + handleError(error.Message()); + } + + // Check for an immediate number result + else if (value.IsNumber()) { + handleResult(env, value.ToNumber(), ResultLocation::NUMBER); + } + + // Check for a Promise result + else if (value.IsPromise()) { + + // Construct the Promise.then and Promise.catch handlers. These could + // also be a statically-defined `Function`s. + + // Promise.then handler. + auto promiseHandlerThen = Function::New(env, [=](const CallbackInfo + &info) { + // Check for Promise result + if (info.Length() < 1 || !info[0].IsNumber()) { + handleError( + "Expected callback Promise resolution to be of type number"); + } else { + auto result = info[0].ToNumber().Int32Value(); + handleResult(info.Env(), result, ResultLocation::PROMISE); + } + }); + + // Promise.catch handler. + auto promiseHandlerCatch = + Function::New(env, [&](const CallbackInfo &info) { + if (info.Length() < 1 || !info[0].IsObject()) { + handleError("Unknown error in callback handler"); + } else { + auto errorAsValue(info[0] + .As() + .Get("toString") + .As() + .Call(info[0], {})); + handleError(errorAsValue.ToString()); + } + }); + + // Execute the JavaScript equivalent of `promise.then.call(promise, + // promiseHandlerThen).catch.call(promise, promiseHandlerCatch);` + value.As() + .Get("then") + .As() + .Call(value, {promiseHandlerThen}) + .As() + .Get("catch") + .As() + .Call(value, {promiseHandlerCatch}); + } + // When using N-API 4, the callback is a valid no-op Function that + // returns `undefined`. This also allows the callback itself to return + // `undefined` to take the default result. + else if (value.IsUndefined()) { + handleResult(env, data->base * data->base, ResultLocation::DEFAULT); + } else { + handleError("Expected callback return to be of type number " + "| Promise"); } } - if (data->logCall) { - std::string message("Thread " + std::to_string(data->threadId) + - " CallJs resolving std::promise"); - auto console = env.Global().Get("console").As(); - console.Get("log").As().Call(console, - {String::New(env, message)}); + + // If no callback provided, handle with default result that the callback + // would have provided. + else { + handleResult(env, data->base * data->base, ResultLocation::DEFAULT); } - data->promise.set_value(calculated); - } else { - data->promise.set_exception(std::make_exception_ptr( - std::runtime_error("TSFN has been finalized."))); + } + // If `env` is nullptr, then all threads have called finished their usage of + // the TSFN (either by calling `Release` or making a call and receiving + // `napi_closing`). In this scenario, it is not allowed to call into + // JavaScript, as the TSFN has been finalized. + else { + handleError("The TSFN has been finalized."); } } - // We do NOT delete data as it is a unique_ptr held by the calling thread. } // Full type of the ThreadSafeFunctionEx using TSFN = ThreadSafeFunctionEx; - using base = tsfnutil::TSFNWrapBase; // A JS-accessible wrap that holds the TSFN. class TSFNWrap : public base { public: - TSFNWrap(const CallbackInfo &info) : base(info) { - if (info.Length() > 0 && info[0].IsObject()) { - auto arg0 = info[0].ToObject(); - if (arg0.Has("cppExceptions")) { - auto cppExceptions = arg0.Get("cppExceptions"); - if (cppExceptions.IsBoolean()) { - cppExceptions = cppExceptions.As(); - } else { - // We explicitly use the addon's except/noexcept settings here, since - // we don't have a valid setting. - Napi::TypeError::New(Env(), "cppExceptions is not a boolean") - .ThrowAsJavaScriptException(); - } - } - } - } + TSFNWrap(const CallbackInfo &info) : base(info) {} + ~TSFNWrap() { - for (auto &thread : finalizerData->threads) { + for (auto &thread : finalizerData.threads) { + // The TSFNWrap destructor runs when our ObjectWrap'd instance is + // garbage-collected. This should never happen with proper usage of + // `await` on `tsfn.release()`! if (thread.joinable()) { thread.join(); } } } - struct FinalizerData { - std::vector threads; - std::unique_ptr deferred; - }; - - // The finalizer data is shared, because we want to join the threads if our - // TSFNWrap object gets garbage-collected and there are still active threads. - using FinalizerDataType = std::shared_ptr; - -#define THREADLOG(X) \ - if (context->logThread) { \ - std::cout << X; \ - } - static void threadEntry(size_t threadId, TSFN tsfn, uint32_t callCount, - Context *context) { - using namespace std::chrono_literals; - - THREADLOG("Thread " << threadId << " starting...\n") - - for (auto i = 0U; i < callCount; ++i) { - auto data = std::make_unique(); - data->base = threadId + 1; - data->threadId = threadId; - data->logCall = context->logCall; - THREADLOG("Thread " << threadId << " making call, base = " << data->base - << "\n") - - tsfn.NonBlockingCall(&data); - auto future = data->promise.get_future(); - auto result = future.get(); - context->callSucceeded(result); - THREADLOG("Thread " << threadId << " got result: " << result << "\n") - } - - THREADLOG("Thread " << threadId << " finished.\n\n") - tsfn.Release(); - } -#undef THREADLOG - - static std::array, 4> InstanceMethods() { + static std::array, 5> InstanceMethods() { return {{InstanceMethod("getContext", &TSFNWrap::GetContext), InstanceMethod("start", &TSFNWrap::Start), + InstanceMethod("acquire", &TSFNWrap::Acquire), InstanceMethod("callCount", &TSFNWrap::CallCount), InstanceMethod("release", &TSFNWrap::Release)}}; } - bool cppExceptions = false; - bool logThread; - bool logCall; + bool logThread = DefaultOptions.logThread; + bool logCall = DefaultOptions.logCall; + bool hasEmptyCallback; std::atomic_uint succeededCalls; std::atomic_int aggregate; - FinalizerDataType finalizerData; + // The structure of the data send to the finalizer. + struct FinalizerDataType { + std::vector threads; + std::vector outstandingCalls; + std::mutex + callMutex; // To protect multi-threaded accesses to `outstandingCalls` + std::unique_ptr deferred; + } finalizerData; + + // Used for logging. + std::mutex logMutex; Napi::Value Start(const CallbackInfo &info) { Napi::Env env = info.Env(); if (_tsfn) { - TSFN_THROW(this, Napi::Error::New(Env(), "TSFN already exists."), - Value()); + NAPI_THROW(Napi::Error::New(Env(), "TSFN already exists."), Value()); } // Creates a list to hold how many times each thread should make a call. - std::vector callCounts; + std::vector callCounts; // The JS-provided callback to execute for each call (if provided) Function callback; - finalizerData = std::make_shared(); - - logThread = DefaultOptions.logThread; - if (info.Length() > 0 && info[0].IsObject()) { auto arg0 = info[0].ToObject(); + + if (arg0.Has("callback")) { + auto cb = arg0.Get("callback"); + if (cb.IsUndefined()) { + // An empty callback option will create a valid no-op function on + // N-API 4 or leave `callback` as `std::nullptr` on N-API 5+. + callback = TSFN::FunctionOrEmpty(env, callback); + } else if (cb.IsFunction()) { + callback = cb.As(); + } else { + NAPI_THROW(Napi::TypeError::New( + Env(), "Invalid arguments: callback is not a " + "function. See StartOptions definition."), + Value()); + } + } + + hasEmptyCallback = callback.IsEmpty(); + + // Ensure proper parameters and add to our list of threads. if (arg0.Has("threads")) { Napi::Value threads = arg0.Get("threads"); if (threads.IsArray()) { Napi::Array threadsArray = threads.As(); for (auto i = 0U; i < threadsArray.Length(); ++i) { Napi::Value elem = threadsArray.Get(i); - if (elem.IsNumber()) { - callCounts.push_back(elem.As().Int32Value()); + if (elem.IsObject()) { + Object o = elem.ToObject(); + if (!(o.Has("calls") && o.Has("callDelay") && + o.Has("threadDelay"))) { + NAPI_THROW(Napi::TypeError::New( + Env(), "Invalid arguments. See " + "StartOptions.threads definition."), + Value()); + } + callCounts.push_back(ThreadOptions{ + callCounts.size(), o.Get("calls").ToNumber(), + hasEmptyCallback ? -1 : o.Get("callDelay").ToNumber(), + o.Get("threadDelay").ToNumber()}); } else { - TSFN_THROW(this, Napi::TypeError::New(Env(), "Invalid arguments"), + NAPI_THROW(Napi::TypeError::New( + Env(), "Invalid arguments. See " + "StartOptions.threads definition."), Value()); } } - } else if (threads.IsNumber()) { - auto threadCount = threads.As().Int32Value(); - for (auto i = 0; i < threadCount; ++i) { - callCounts.push_back(DEFAULT_CALL_COUNT); - } - } else { - TSFN_THROW(this, Napi::TypeError::New(Env(), "Invalid arguments"), - Value()); - } - } - - if (arg0.Has("callback")) { - auto cb = arg0.Get("callback"); - if (cb.IsFunction()) { - callback = cb.As(); } else { - TSFN_THROW(this, - Napi::TypeError::New(Env(), "Callback is not a function"), - Value()); + NAPI_THROW( + Napi::TypeError::New( + Env(), + "Invalid arguments. See StartOptions.threads definition."), + Value()); } } @@ -231,8 +334,9 @@ class TSFNWrap : public base { if (logCallOption.IsBoolean()) { logCall = logCallOption.As(); } else { - TSFN_THROW(this, - Napi::TypeError::New(Env(), "logCall is not a boolean"), + NAPI_THROW(Napi::TypeError::New( + Env(), "Invalid arguments: logCall is not a boolean. " + "See StartOptions definition."), Value()); } } @@ -242,24 +346,16 @@ class TSFNWrap : public base { if (logThreadOption.IsBoolean()) { logThread = logThreadOption.As(); } else { - TSFN_THROW(this, - Napi::TypeError::New(Env(), "logThread is not a boolean"), + NAPI_THROW(Napi::TypeError::New( + Env(), "Invalid arguments: logThread is not a " + "boolean. See StartOptions definition."), Value()); } } } - // Apply default arguments - if (callCounts.size() == 0) { - for (auto i = 0U; i < DEFAULT_THREAD_COUNT; ++i) { - callCounts.push_back(DEFAULT_CALL_COUNT); - } - } - const auto threadCount = callCounts.size(); - auto *finalizerDataPtr = new FinalizerDataType(finalizerData); - succeededCalls = 0; aggregate = 0; _tsfn = TSFN::New( @@ -271,49 +367,121 @@ class TSFNWrap : public base { threadCount + 1, // size_t initialThreadCount, +1 for Node thread this, // Context* context, Finalizer, // Finalizer finalizer - finalizerDataPtr // FinalizerDataType* data + &finalizerData // FinalizerDataType* data ); - for (auto threadId = 0U; threadId < threadCount; ++threadId) { - finalizerData->threads.push_back(std::thread(threadEntry, threadId, _tsfn, - callCounts[threadId], this)); + if (logThread) { + std::cout << "[Starting] Starting example with options: {\n[Starting] " + "Log Call = " + << (logCall ? "true" : "false") + << ",\n[Starting] Log Thread = " + << (logThread ? "true" : "false") + << ",\n[Starting] Callback = " + << (hasEmptyCallback ? "[empty]" : "function") + << ",\n[Starting] Threads = [\n"; + for (auto &threadOption : callCounts) { + std::cout << "[Starting] " << threadOption.threadId + << " -> { Calls: " << threadOption.calls << ", Call Delay: " + << (threadOption.callDelay == -1 + ? "[no callback]" + : std::to_string(threadOption.callDelay)) + << ", Thread Delay: " << threadOption.threadDelay << " },\n"; + } + std::cout << "[Starting] ]\n[Starting] }\n"; + } + + // for (auto threadId = 0U; threadId < threadCount; ++threadId) { + for (auto &threadOption : callCounts) { + finalizerData.threads.push_back( + std::thread(threadEntry, _tsfn, threadOption, this)); } return Number::New(env, threadCount); }; - // TSFN finalizer. Joins the threads and resolves the Promise returned by - // `Release()` above. - static void Finalizer(Napi::Env env, FinalizerDataType *finalizeData, - Context *ctx) { + Napi::Value Acquire(const CallbackInfo &info) { + Napi::Env env = info.Env(); - if (ctx->logThread) { - std::cout << "Finalizer joining threads\n"; + if (!_tsfn) { + NAPI_THROW(Napi::Error::New(Env(), "TSFN does not exist."), Value()); } - for (auto &thread : (*finalizeData)->threads) { - if (thread.joinable()) { - thread.join(); + + // Creates a list to hold how many times each thread should make a call. + std::vector callCounts; + if (info.Length() > 0 && info[0].IsArray()) { + Napi::Array threadsArray = info[0].As(); + for (auto i = 0U; i < threadsArray.Length(); ++i) { + Napi::Value elem = threadsArray.Get(i); + if (elem.IsObject()) { + Object o = elem.ToObject(); + if (!(o.Has("calls") && o.Has("callDelay") && o.Has("threadDelay"))) { + NAPI_THROW(Napi::TypeError::New(Env(), + "Invalid arguments. See " + "StartOptions.threads definition."), + Value()); + } + callCounts.push_back(ThreadOptions{ + callCounts.size() + finalizerData.threads.size(), + o.Get("calls").ToNumber(), + hasEmptyCallback ? -1 : o.Get("callDelay").ToNumber(), + o.Get("threadDelay").ToNumber()}); + } else { + NAPI_THROW(Napi::TypeError::New(Env(), + "Invalid arguments. See " + "StartOptions.threads definition."), + Value()); + } } + } else { + NAPI_THROW( + Napi::TypeError::New( + Env(), "Invalid arguments. See StartOptions.threads definition."), + Value()); } - ctx->clearTSFN(); - if (ctx->logThread) { - std::cout << "Finished finalizing threads.\n"; + + if (logThread) { + for (auto &threadOption : callCounts) { + std::cout << "[Acquire ] " << threadOption.threadId + << " -> { Calls: " << threadOption.calls << ", Call Delay: " + << (threadOption.callDelay == -1 + ? "[no callback]" + : std::to_string(threadOption.callDelay)) + << ", Thread Delay: " << threadOption.threadDelay << " },\n"; + } + std::cout << "[Acquire ] ]\n[Acquire ] }\n"; } - (*finalizeData)->deferred->Resolve(Boolean::New(env, true)); - delete finalizeData; + auto started = 0U; + + for (auto &threadOption : callCounts) { + // The `Acquire` call may be called from any thread, but we do it here to + // avoid a race condition where the thread starts but the TSFN has been + // finalized. + auto status = _tsfn.Acquire(); + if (status == napi_ok) { + finalizerData.threads.push_back( + std::thread(threadEntry, _tsfn, threadOption, this)); + ++started; + } + } + return Number::New(env, started); } + + // Release the TSFN from the Node thread. This will return a `Promise` that + // resolves in the Finalizer. Napi::Value Release(const CallbackInfo &info) { - if (finalizerData->deferred) { - return finalizerData->deferred->Promise(); + if (finalizerData.deferred) { + return finalizerData.deferred->Promise(); } - finalizerData->deferred.reset( + finalizerData.deferred.reset( new Promise::Deferred(Promise::Deferred::New(info.Env()))); _tsfn.Release(); - return finalizerData->deferred->Promise(); + return finalizerData.deferred->Promise(); }; + // Returns an array corresponding to the amount of succeeded calls and the sum + // aggregate. Napi::Value CallCount(const CallbackInfo &info) { Napi::Env env(info.Env()); @@ -323,17 +491,196 @@ class TSFNWrap : public base { return results; }; + // Returns the TSFN's context. Napi::Value GetContext(const CallbackInfo &) { return _tsfn.GetContext()->Value(); }; + // The thread entry point. It receives as arguments the TSFN, the call + // options, and the context. + static void threadEntry(TSFN tsfn, ThreadOptions options, Context *context) { +#define THREADLOG(X) \ + if (context->logThread) { \ + std::lock_guard lock(context->logMutex); \ + std::cout << "[Thread " << threadId << "] [Native ] " \ + << (data->callId == -1 \ + ? "" \ + : "[Call " + std::to_string(data->callId) + "] ") \ + << X; \ + } + +#define THREADLOG_MAIN(X) \ + if (context->logThread) { \ + std::lock_guard lock(context->logMutex); \ + std::cout << "[Thread " << threadId << "] [Native ] " << X; \ + } + using namespace std::chrono_literals; + auto threadId = options.threadId; + + // To help with simultaneous threads using the logging mechanism, we'll + // delay at thread start. + std::this_thread::sleep_for(threadId * 10ms); + THREADLOG_MAIN("Thread " << threadId << " started.\n") + + enum ThreadState { + // Starting stating. + Running, + + // When all requests have been completed. + Release, + + // If a `NonBlockingCall` results in a Promise but while waiting + // for the resolution, the TSFN is finalized. + AlreadyFinalized, + + // If a `NonBlockingCall` receiving `napi_closing`, we do *NOT* `Release` + // it. + Closing + } state = ThreadState::Running; + + for (auto i = 0; state == Running; ++i) { + + if (i >= options.calls) { + state = Release; + break; + } + + DataType data(context->makeNewCall(threadId, i, context->logCall, + options.callDelay)); + + if (options.threadDelay > 0 && i > 0) { + THREADLOG("Delay for " << options.threadDelay + << "ms before next call\n") + std::this_thread::sleep_for(options.threadDelay * 1ms); + } + + THREADLOG("Performing call request: base = " << data->base << "\n") + + auto status = tsfn.NonBlockingCall(&data); + + if (status == napi_ok) { + auto future = data->promise.get_future(); + auto result = future.get(); + if (result.error.length() == 0) { + context->callSucceeded(data, result.result); + THREADLOG("Receive answer: result = " << result.result << "\n") + continue; + } else if (result.isFinalized) { + THREADLOG("Application Error: The TSFN has been finalized.\n") + // If the Finalizer has canceled this request, we do not call + // `Release()`. + state = AlreadyFinalized; + } + } else if (status == napi_closing) { + // A thread **MUST NOT** call `Abort()` or `Release()` if we receive an + // `napi_closing` call. + THREADLOG("N-API Error: The thread-safe function is aborted and " + "cannot accept more calls.\n") + state = Closing; + } else if (status == napi_queue_full) { + // The example will finish this thread's use of the TSFN if it is full. + THREADLOG("N-API Error: The queue was full when trying to call in a " + "non-blocking method.\n") + state = Release; + } else if (status == napi_invalid_arg) { + THREADLOG("N-API Error: The thread-safe function is closed.\n") + state = AlreadyFinalized; + } else { + THREADLOG("N-API Error: A generic error occurred when attemping to " + "add to the queue.\n") + state = AlreadyFinalized; + } + context->callFailed(data); + } + + THREADLOG_MAIN("Thread " << threadId << " finished. State: " + << (state == Closing ? "Closing" + : state == AlreadyFinalized + ? "Already Finalized" + : "Release") + << "\n") + + if (state == Release) { + tsfn.Release(); + } +#undef THREADLOG +#undef THREADLOG_MAIN + } + + // TSFN finalizer. Joins the threads and resolves the Promise returned by + // `Release()` above. + static void Finalizer(Napi::Env env, FinalizerDataType *finalizeDataPtr, + Context *ctx) { + + auto &finalizeData(*finalizeDataPtr); + auto outstanding = finalizeData.outstandingCalls.size(); + if (ctx->logThread) { + std::cout << "[Finalize] [Native ] Joining threads (" << outstanding + << " outstanding requests)...\n"; + } + if (outstanding > 0) { + for (auto &request : finalizeData.outstandingCalls) { + request->promise.set_value( + CallJsResult{-1, true, "The TSFN has been finalized."}); + } + } + for (auto &thread : finalizeData.threads) { + thread.join(); + } + + ctx->clearTSFN(); + if (ctx->logThread) { + std::cout << "[Finalize] [Native ] Threads joined.\n"; + } + + finalizeData.deferred->Resolve(Boolean::New(env, true)); + } + // This method does not run on the Node thread. void clearTSFN() { _tsfn = TSFN(); } // This method does not run on the Node thread. - void callSucceeded(int result) { + void callSucceeded(DataType data, int result) { + std::lock_guard lock(finalizerData.callMutex); succeededCalls++; aggregate += result; + + auto &calls = finalizerData.outstandingCalls; + auto it = std::find_if( + calls.begin(), calls.end(), + [&](std::shared_ptr const &p) { return p.get() == data.get(); }); + + if (it != calls.end()) { + calls.erase(it); + } + } + + // This method does not run on the Node thread. + void callFailed(DataType data) { + std::lock_guard lock(finalizerData.callMutex); + auto &calls = finalizerData.outstandingCalls; + auto it = + std::find_if(calls.begin(), calls.end(), + [&](std::shared_ptr const &p) { return p == data; }); + + if (it != calls.end()) { + calls.erase(it); + } + } + + DataType makeNewCall(size_t threadId, int callId, bool logCall, + int callDelay) { + // x + // auto &calls(finalizerData.outstandingCalls); + finalizerData.outstandingCalls.emplace_back(std::make_shared()); + auto data(finalizerData.outstandingCalls.back()); + data->threadId = threadId; + data->logCall = logCall; + data->callDelay = callDelay; + data->base = threadId + 1; + data->callId = callId; + + return data; } }; } // namespace example diff --git a/test/threadsafe_function_ex/test/example.js b/test/threadsafe_function_ex/test/example.js index d47cba236..2afa36de8 100644 --- a/test/threadsafe_function_ex/test/example.js +++ b/test/threadsafe_function_ex/test/example.js @@ -1,47 +1,337 @@ // @ts-check 'use strict'; const assert = require('assert'); - const { TestRunner } = require('../util/TestRunner'); +/** + * @typedef {(threadId: number, callId: number, logCall: boolean, value: number, callDelay: + * number)=>number|Promise} TSFNCallback + */ + +/** + * @typedef {Object} ThreadOptions + * @property {number} calls + * @property {number} callDelay + * @property {number} threadDelay + */ + +/** + * The options when starting the addon's TSFN. + * @typedef {Object} StartOptions + * @property {ThreadOptions[]} threads + * @property {boolean} [logCall] If `true`, log messages via `console.log`. + * @property {boolean} [logThread] If `true`, log messages via `std::cout`. + * @property {number} [acquireFactor] Acquire a new set of \`acquireFactor\` + * call threads. `NAPI_CPP_EXCEPTIONS`, allowing errors to be caught as + * exceptions. + * @property {TSFNCallback} callback The callback provided to the threadsafe + * function. + * @property {[number,number]} [callbackError] Tuple of `[threadId, callId]` to + * cause an error on +*/ + +/** + * Returns test options. + * @type {() => { options: StartOptions, calls: { aggregate: number } }} + */ +const getTestDetails = () => { + const TEST_CALLS = [1, 2, 3, 4, 5]; + const TEST_ACQUIRE = 1; + const TEST_CALL_DELAY = [400, 200, 100, 50, 0] + const TEST_THREAD_DELAY = TEST_CALL_DELAY.map(_ => _); + const TEST_LOG_CALL = false; + const TEST_LOG_THREAD = false; + const TEST_NO_CALLBACK = false; + + /** @type {[number, number] | undefined} [threadId, callId] */ + const TEST_CALLBACK_ERROR = undefined; + + // Set options as defaults + let testCalls = TEST_CALLS; + let testAcquire = TEST_ACQUIRE; + let testCallDelay = TEST_CALL_DELAY; + let testThreadDelay = TEST_THREAD_DELAY; + let testLogCall = TEST_LOG_CALL; + let testLogThread = TEST_LOG_THREAD; + let testNoCallback = TEST_NO_CALLBACK; + let testCallbackError = TEST_CALLBACK_ERROR; + + let args = process.argv.slice(2); + let arg; + + const showHelp = () => { + console.log( + ` +Usage: ${process.argv0} .${process.argv[1].replace(process.cwd(), '')} [options] + + -c, --calls The number of calls each thread should make (number[]). + -a, --acquire [factor] Acquire a new set of \`factor\` call threads, using the + same \`calls\` definition. + -d, --call-delay The delay on callback resolution that each thread should + have (number[]). This is achieved via a delayed Promise + resolution in the JavaScript callback provided to the + TSFN. Using large delays here will cause all threads to + bottle-neck. + -D, --thread-delay The delay that each thread should have prior to making a + call (number[]). Using large delays here will cause the + individual thread to bottle-neck. + -l, --log-call Display console.log-based logging messages. + -L, --log-thread Display std::cout-based logging messages. + -n, --no-callback Do not use a JavaScript callback. + -e, --callback-error [thread[.call]] Cause an error to occur in the JavaScript callback for + the given thread's call (if provided; first thread's + first call otherwise). + + When not provided: + - defaults to [${TEST_CALLS}] + - [factor] defaults to ${TEST_ACQUIRE} + - defaults to [${TEST_CALL_DELAY}] + - defaults to [${TEST_THREAD_DELAY}] + + +Examples: + + -c [1,2,3] -l -L + + Creates three threads that makes one, two, and three calls each, respectively. + + -c [5,5] -d [5000,5000] -D [0,0] -l -L + + Creates two threads that make five calls each. In this scenario, the threads will be + blocked primarily on waiting for the callback to resolve, as each thread's call takes + 5000 milliseconds. +` + ); + return undefined; + }; + + while ((arg = args.shift())) { + switch (arg) { + case "-h": + case "--help": + return showHelp(); + + case "--calls": + case "-c": + try { + testCalls = JSON.parse(args.shift()); + } catch (ex) { /* ignore */ } + break; + + case "--acquire": + case "-a": + testAcquire = parseInt(args[0]); + if (!isNaN(testAcquire)) { + args.shift(); + } else { + testAcquire = TEST_ACQUIRE; + } + break; + + case "--call-delay": + case "-d": + try { + testCallDelay = JSON.parse(args.shift()); + } catch (ex) { /* ignore */ } + break; + + case "--thread-delay": + case "-D": + try { + testThreadDelay = JSON.parse(args.shift()); + } catch (ex) { /* ignore */ } + break; + + case "--log-call": + case "-l": + testLogCall = true; + break; + + case "--log-thread": + case "-L": + testLogThread = true; + break; + + case "--no-callback": + case "-n": + testNoCallback = true; + break; + + case "-e": + case "--callback-error": + try { + if (!args[0].startsWith("-")) { + const split = args.shift().split(/\./); + testCallbackError = [parseInt(split[0], 10) || 0, parseInt(split[1], 10) || 0]; + } + } + catch (ex) { /*ignore*/ } + finally { + if (!testCallbackError) { + testCallbackError = [0, 0]; + } + } + break; + + default: + console.error("Unknown option:", arg); + return showHelp(); + } + } + + if (testCallbackError && testNoCallback) { + console.error("--error cannot be used in conjunction with --no-callback"); + return undefined; + } + + testCalls = Array.isArray(testCalls) ? testCalls : TEST_CALLS; + + const calls = { aggregate: testNoCallback ? null : 0 }; + + /** + * The JavaScript callback provided to our TSFN. + * @callback TSFNCallback + * @param {number} threadId Thread Id + * @param {number} callId Call Id + * @param {boolean} logCall If true, log messages to console regarding this + * call. + * @param {number} base The input as calculated from CallJs + * @param {number} callDelay If `> 0`, return a `Promise` that resolves with + * `value` after `callDelay` milliseconds. Otherwise, return a `number` + * whose value is `value`. + */ + + /** @type {undefined | TSFNCallback} */ + const callback = testNoCallback ? undefined : (threadId, callId, logCall, base, callDelay) => { + // Calculate the result value as `base * base`. + const value = base * base; + + // Add the value to our call aggregate + calls.aggregate += value; + + if (testCallbackError !== undefined && testCallbackError[0] === threadId && testCallbackError[1] === callId) { + return new Error(`Test throw error for ${threadId}.${callId}`); + } + + // If `callDelay > 0`, then return a Promise that resolves with `value` after + // `callDelay` milliseconds. + if (callDelay > 0) { + // Logging messages. + if (logCall) { + console.log(`[Thread ${threadId}] [Callback] [Call ${callId}] Receive request: base = ${base}, delay = ${callDelay}ms`); + } + + const start = Date.now(); + + return new Promise(resolve => setTimeout(() => { + // Logging messages. + if (logCall) { + console.log(`[Thread ${threadId}] [Callback] [Call ${callId}] Answer request: base = ${base}, value = ${value} after ${Date.now() - start}ms`); + } + resolve(value); + }, callDelay)); + } + + // Otherwise, return a `number` whose value is `value`. + else { + // Logging messages. + if (logCall) { + console.log(`[Thread ${threadId}] [Callback] [Call ${callId}] Receive, answer request: base = ${base}, value = ${value}`); + } + return value; + } + }; + + return { + options: { + // Construct `ThreadOption[] threads` from `number[] testCalls` + threads: testCalls.map((callCount, index) => ({ + calls: callCount, + callDelay: testCallDelay !== null && typeof testCallDelay[index] === 'number' ? testCallDelay[index] : 0, + threadDelay: testThreadDelay !== null && typeof testThreadDelay[index] === 'number' ? testThreadDelay[index] : 0, + })), + logCall: testLogCall, + logThread: testLogThread, + acquireFactor: testAcquire, + callback, + callbackError: testCallbackError + }, + calls + }; +} class ExampleTest extends TestRunner { async example({ TSFNWrap }) { + + /** + * @typedef {Object} TSFNWrap + * @property {(opts: StartOptions) => number} start Start the TSFN. Returns + * the number of threads started. + * @property {() => Promise} release Release the TSFN. + * @property {() => [number, number]} callCount Returns the call aggregates + * as counted by the TSFN: + * - `[0]`: The sum of the number of calls by each thread. + * - `[1]`: The sum of the `value`s returned by each call by each thread. + * @property {(threads: ThreadOptions[]) => number} acquire + */ + + /** @type {TSFNWrap} */ const tsfn = new TSFNWrap(); - const threads = [1, 2, 3, 4, 5]; - let callAggregate = 0; - const startedActual = await tsfn.start({ - threads, - logThread: false, - logCall: false, + const testDetails = getTestDetails(); + if (testDetails === undefined) { + throw new Error("No test details"); + } + const { options } = testDetails; + const { acquireFactor, threads, callback, callbackError } = options; - callback: (_ /*threadId*/, valueFromCallJs) => { - callAggregate += valueFromCallJs; - } - }); + /** + * Start the TSFN with the given options. This will create the TSFN with initial + * thread count of `threads.length + 1` (+1 due to the Node thread using the TSFN) + */ + const startedActual = tsfn.start(options); + + /** + * The initial + */ + const threadsPerSet = threads.length; /** - * Calculate the expected results. + * Calculate the expected results. Create a new list of thread options by + * concatinating `threads` by `acquireFactor` times. */ - const expected = threads.reduce((p, threadCallCount, threadId) => ( + const startedThreads = [...new Array(acquireFactor)].map(_ => threads).reduce((p, c) => p.concat(c), []); + + const expected = startedThreads.reduce((p, threadCallCount, threadId) => ( ++threadId, ++p.threadCount, - p.callCount += threadCallCount, - p.aggregate += threadCallCount * threadId ** 2, + p.callCount += threadCallCount.calls, + p.aggregate += threadCallCount.calls * threadId ** 2, + p.callbackAggregate = p.callbackAggregate === null ? null : p.aggregate, p - ), { threadCount: 0, callCount: 0, aggregate: 0 }); + ), { threadCount: 0, callCount: 0, aggregate: 0, callbackAggregate: callback ? 0 : null }); if (typeof startedActual === 'number') { + const { threadCount, callCount, aggregate, callbackAggregate } = expected; + assert(startedActual === threadsPerSet, `The number of threads when starting the TSFN do not match: actual = ${startedActual}, expected = ${threadsPerSet}`) + for (let i = 1; i < acquireFactor; ++i) { + const acquiredActual = tsfn.acquire(threads); + assert(acquiredActual === threadsPerSet, `The number of threads when acquiring a new set of threads do not match: actual = ${acquiredActual}, expected = ${threadsPerSet}`) + } const released = await tsfn.release(); const [callCountActual, aggregateActual] = tsfn.callCount(); - const { threadCount, callCount, aggregate } = expected; - assert(startedActual === threadCount, `The number of threads started do not match: actual = ${startedActual}, expected = ${threadCount}`) - assert(callCountActual === callCount, `The number of calls do not match: actual = ${callCountActual}, expected = ${callCount}`); - assert(aggregateActual === aggregate, `The aggregate of calls do not match: actual = ${aggregateActual}, expected = ${aggregate}`); - assert(aggregate === callAggregate, `The number aggregated by the JavaScript callback and the thread calculated aggregate do not match: actual ${aggregate}, expected = ${callAggregate}`) - return { released, ...expected, callAggregate }; + const { calls } = testDetails; + const { aggregate: actualCallAggregate } = calls; + if (!callbackError) { + assert(callCountActual === callCount, `The number of calls do not match: actual = ${callCountActual}, expected = ${callCount}`); + assert(aggregateActual === aggregate, `The aggregate of calls do not match: actual = ${aggregateActual}, expected = ${aggregate}`); + assert(actualCallAggregate === callbackAggregate, `The number aggregated by the JavaScript callback and the thread calculated aggregate do not match: actual ${actualCallAggregate}, expected = ${aggregate}`) + return { released, ...expected }; + } + // The test runner erases the last line, so write an empty line. + if (options.logCall) { console.log(); } + return released; } else { throw new Error('The TSFN failed to start'); } diff --git a/test/threadsafe_function_ex/util/TestRunner.js b/test/threadsafe_function_ex/util/TestRunner.js index e01b42480..8b425ab65 100644 --- a/test/threadsafe_function_ex/util/TestRunner.js +++ b/test/threadsafe_function_ex/util/TestRunner.js @@ -4,9 +4,6 @@ const assert = require('assert'); const { basename, extname } = require('path'); const buildType = process.config.target_defaults.default_configuration; -// If you pass certain test names as argv, run those only. -const cmdlineTests = process.argv.length > 2 ? process.argv.slice(2) : null; - const pad = (what, targetLength = 20, padString = ' ', padLeft) => { const padder = (pad, str) => { if (typeof str === 'undefined') @@ -20,6 +17,12 @@ const pad = (what, targetLength = 20, padString = ' ', padLeft) => { return padder(padString.repeat(targetLength), String(what)); } +/** + * If `true`, always show results as interactive. See constructor for more + * information. +*/ +const SHOW_OUTPUT = false; + /** * Test runner helper class. Each static method's name corresponds to the * namespace the test as defined in the native addon. Each test specifics are @@ -29,12 +32,6 @@ const pad = (what, targetLength = 20, padString = ' ', padLeft) => { */ class TestRunner { - /** - * If `true`, always show results as interactive. See constructor for more - * information. - */ - static SHOW_OUTPUT = false; - /** * @param {string} bindingKey The key to use when accessing the binding. * @param {string} filename Name of file that the current TestRunner instance @@ -46,7 +43,7 @@ class TestRunner { constructor(bindingKey, filename) { this.bindingKey = bindingKey; this.filename = filename; - this.interactive = TestRunner.SHOW_OUTPUT || filename === require.main.filename; + this.interactive = SHOW_OUTPUT || filename === require.main.filename; this.specName = `${this.bindingKey}/${basename(this.filename, extname(this.filename))}`; } @@ -84,8 +81,6 @@ class TestRunner { // Interactive mode prints start and end messages if (this.interactive) { - - /** @typedef {[string, string | null | number, boolean, string, any]} State [label, time, isNoExcept, nsName, returnValue] */ /** @type {State} */ @@ -114,31 +109,25 @@ class TestRunner { this.log(stateLine()); }; - const runTest = (cmdlineTests == null || cmdlineTests.indexOf(nsName) > -1); - - if (ns && typeof runner[nsName] === 'function' && runTest) { + if (ns && typeof runner[nsName] === 'function') { setState('Running test', null, isNoExcept, nsName, undefined); const start = Date.now(); const returnValue = await runner[nsName](ns); - await this.dummy(); setState('Finished test', Date.now() - start, isNoExcept, nsName, returnValue); } else { setState('Skipping test', '-', isNoExcept, nsName, undefined); } - } else { + } else if (ns) { console.log(`Running test '${this.specName}/${nsName}' ${isNoExcept ? '[noexcept]' : ''}`); await runner[nsName](ns); - await this.dummy(); } } } } - dummy() { return new Promise(resolve => setTimeout(resolve, 50)); } - /** * Print to console only when using interactive mode. - * + * * @param {boolean} newLine If true, end with a new line. * @param {any[]} what What to print */ @@ -158,7 +147,6 @@ class TestRunner { log(...what) { this.print(true, ...what); } - } module.exports = { From 0d84cf7be4b785b231f68cb2cd0f53c0b34dcd83 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 27 Jul 2020 18:07:20 +0200 Subject: [PATCH 27/39] Test with longer timeout --- test/threadsafe_function_ex/test/basic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/threadsafe_function_ex/test/basic.js b/test/threadsafe_function_ex/test/basic.js index f5bfc678b..780116954 100644 --- a/test/threadsafe_function_ex/test/basic.js +++ b/test/threadsafe_function_ex/test/basic.js @@ -133,7 +133,7 @@ class BasicTest extends TestRunner { if (reject) { reject(new Error("tsfn.call() timed out")); } - }, 0); + }, 1000); }); } return await tsfn.release(); From 807fb27c4f24ebb5fc6237deb30e2424e13186af Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 9 Aug 2020 19:02:02 +0200 Subject: [PATCH 28/39] Apply suggestions from code review Co-authored-by: Gabriel Schulhof --- doc/threadsafe.md | 22 +++++++++++----------- doc/threadsafe_function.md | 2 +- doc/threadsafe_function_ex.md | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/threadsafe.md b/doc/threadsafe.md index 86f63906b..b3fda8b54 100644 --- a/doc/threadsafe.md +++ b/doc/threadsafe.md @@ -14,7 +14,7 @@ easy way to do this. These APIs provide two types -- [`Napi::ThreadSafeFunctionEx`](threadsafe_function_ex.md) -- as well as APIs to create, destroy, and call objects of this type. The differences between the two are subtle and are [highlighted below](#implementation-differences). Regardless -of which type you choose, the API between the two are similar. +of which type you choose, the APIs between the two are similar. `Napi::ThreadSafeFunction[Ex]::New()` creates a persistent reference that holds a JavaScript function which can be called from multiple threads. The calls @@ -44,11 +44,11 @@ reaches zero, no further threads can start making use of it by calling The choice between `Napi::ThreadSafeFunction` and `Napi::ThreadSafeFunctionEx` depends largely on how you plan to execute your native C++ code (the "callback") -on the Node thread. +on the Node.js thread. ### [`Napi::ThreadSafeFunction`](threadsafe_function.md) -This API is designed without N-API 5 native support for [optional JavaScript +This API is designed without N-API 5 native support for [the optional JavaScript function callback feature](https://github.com/nodejs/node/commit/53297e66cb). `::New` methods that do not have a `Function` parameter will construct a _new_, no-op `Function` on the environment to pass to the underlying N-API @@ -62,12 +62,12 @@ This API has some dynamic functionality, in that: - Different C++ data types may be passed with each call of `[Non]BlockingCall()` to match the specific data type as specified in the `CallJs` callback. -However, this functionality comes with some **additional overhead** and +Note that this functionality comes with some **additional overhead** and situational **memory leaks**: -- The API acts as a "middle-man" between the underlying +- The API acts as a "broker" between the underlying `napi_threadsafe_function`, and dynamically constructs a wrapper for your callback on the heap for every call to `[Non]BlockingCall()`. -- In acting in this "middle-man" fashion, the API will call the underlying "make +- In acting in this "broker" fashion, the API will call the underlying "make call" N-API method on this packaged item. If the API has determined the thread-safe function is no longer accessible (eg. all threads have released yet there are still items on the queue), **the callback passed to @@ -88,15 +88,15 @@ drawbacks listed above. The API is designed with N-API 5's support of an optional function callback. The API will correctly allow developers to pass `std::nullptr` instead of a `const Function&` for the callback function specified in `::New`. It also provides helper APIs to _target_ N-API 4 and -construct a no-op `Function` **or** to target N-API 5 and "construct" an +construct a no-op `Function` **or** to target N-API 5 and "construct" a `std::nullptr` callback. This allows a single codebase to use the same APIs, with just a switch of the `NAPI_VERSION` compile-time constant. -The removal of the dynamic call functionality has the additional side effects: -- The API does _not_ act as a "middle-man" compared to the non-`Ex`. Once Node +The removal of the dynamic call functionality has the following implications: +- The API does _not_ act as a "broker" compared to the non-`Ex`. Once Node.js finalizes the thread-safe function, the `CallJs` callback will execute with an - empty `Napi::Env` for any remaining items on the queue. This provides the the - ability to handle any necessary clean up of the item's data. + empty `Napi::Env` for any remaining items on the queue. This provides the + ability to handle any necessary cleanup of the item's data. - The callback _does_ receive the context as a parameter, so a call to `GetContext()` is _not_ necessary. This context type is specified as the **first type argument** specified to `::New`, ensuring type safety. diff --git a/doc/threadsafe_function.md b/doc/threadsafe_function.md index 0b3202929..7c323fd4a 100644 --- a/doc/threadsafe_function.md +++ b/doc/threadsafe_function.md @@ -62,7 +62,7 @@ New(napi_env env, - `initialThreadCount`: The initial number of threads, including the main thread, which will be making use of this function. - `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. - Can be retreived via `GetContext()`. + It can be retreived by calling `GetContext()`. - `[optional] finalizeCallback`: Function to call when the `ThreadSafeFunction` is being destroyed. This callback will be invoked on the main thread when the thread-safe function is about to be destroyed. It receives the context and the diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md index 15188a96c..ad25abd21 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/threadsafe_function_ex.md @@ -8,7 +8,7 @@ of: a TSFN has no context. - `DataType = void*`: The data to use in the native callback. By default, a TSFN can accept *any* data type. -- `Callback = void*(Napi::Env, Napi::Function jsCallback, ContextType*, +- `Callback = void(*)(Napi::Env, Napi::Function jsCallback, ContextType*, DataType*)`: The callback to run for each item added to the queue. If no `Callback` is given, the API will call the function `jsCallback` with no arguments. @@ -73,7 +73,7 @@ New(napi_env env, - `initialThreadCount`: The initial number of threads, including the main thread, which will be making use of this function. - `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. - Can be retreived via `GetContext()`. + It can be retreived via `GetContext()`. - `[optional] finalizeCallback`: Function to call when the `ThreadSafeFunctionEx` is being destroyed. This callback will be invoked on the main thread when the thread-safe function is about to be destroyed. It From fd6a2b40f27c4d12bb9cae4c474100825c3b6cd9 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sun, 9 Aug 2020 19:49:07 +0200 Subject: [PATCH 29/39] Additional changes from review --- doc/threadsafe.md | 6 +++--- doc/threadsafe_function_ex.md | 24 +++++++++-------------- napi-inl.h | 2 +- test/index.js | 1 + test/threadsafe_function_ex/README.md | 4 +--- test/threadsafe_function_ex/test/basic.cc | 11 +++++------ test/threadsafe_function_ex/test/basic.js | 2 +- 7 files changed, 21 insertions(+), 29 deletions(-) diff --git a/doc/threadsafe.md b/doc/threadsafe.md index b3fda8b54..4254fc8cc 100644 --- a/doc/threadsafe.md +++ b/doc/threadsafe.md @@ -99,14 +99,14 @@ The removal of the dynamic call functionality has the following implications: ability to handle any necessary cleanup of the item's data. - The callback _does_ receive the context as a parameter, so a call to `GetContext()` is _not_ necessary. This context type is specified as the - **first type argument** specified to `::New`, ensuring type safety. + **first template argument** specified to `::New`, ensuring type safety. - The `New()` constructor accepts the `CallJs` callback as the **second type argument**. The callback must be statically defined for the API to access it. This affords the ability to statically pass the context as the correct type across all methods. - Only one C++ data type may be specified to every call to `[Non]BlockingCall()` - -- the **third type argument** specified to `::New`. Any "dynamic call data" - must be implemented by the user. + -- the **third template argument** specified to `::New`. Any "dynamic call + data" must be implemented by the user. ### Usage Suggestions diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md index ad25abd21..f1c42599b 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/threadsafe_function_ex.md @@ -40,7 +40,7 @@ Napi::ThreadSafeFunctionEx::ThreadSafeFunctionE Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. To ensure the API statically handles the correct return type for `GetContext()` and -`[Non]BlockingCall()`, pass the proper type arguments to +`[Non]BlockingCall()`, pass the proper template arguments to `Napi::ThreadSafeFunctionEx`. ### New @@ -90,14 +90,15 @@ Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. Depending on the targetted `NAPI_VERSION`, the API has different implementations for `CallbackType callback`. -When targetting version 4, `CallbackType` is: -- `const Function&` -- skipped, in which case the API creates a new no-op `Function` +When targetting version 4, `callback` may be: +- of type `const Function&` +- not provided as an parameter, in which case the API creates a new no-op +`Function` -When targetting version 5+, `CallbackType` is: -- `const Function&` -- `std::nullptr_t` -- skipped, in which case the API passes `std::nullptr` +When targetting version 5+, `callback` may be: +- of type `const Function&` +- of type `std::nullptr_t` +- not provided as an parameter, in which case the API passes `std::nullptr` ### Acquire @@ -170,13 +171,6 @@ napi_status Napi::ThreadSafeFunctionEx::NonBloc - `[optional] data`: Data to pass to the callback which was passed to `ThreadSafeFunctionEx::New()`. -- `[optional] callback`: C++ function that is invoked on the main thread. The - callback receives the `ThreadSafeFunction`'s JavaScript callback function to - call as an `Napi::Function` in its parameters and the `DataType*` data pointer - (if provided). Must implement `void operator()(Napi::Env env, Function - jsCallback, DataType* data)`, skipping `data` if not provided. It is not - necessary to call into JavaScript via `MakeCallback()` because N-API runs - `callback` in a context appropriate for callbacks. Returns one of: - `napi_ok`: The call was successfully added to the queue. diff --git a/napi-inl.h b/napi-inl.h index 3f3f11464..46090470c 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4399,7 +4399,7 @@ inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { // ThreadSafeFunctionEx class //////////////////////////////////////////////////////////////////////////////// -// Starting with NAPI 4, the JavaScript function `func` parameter of +// Starting with NAPI 5, the JavaScript function `func` parameter of // `napi_create_threadsafe_function` is optional. #if NAPI_VERSION > 4 // static, with Callback [missing] Resource [missing] Finalizer [missing] diff --git a/test/index.js b/test/index.js index 86939af84..fc285f132 100644 --- a/test/index.js +++ b/test/index.js @@ -43,6 +43,7 @@ let testModules = [ 'object/set_property', 'promise', 'run_script', + 'threadsafe_function_ex', 'threadsafe_function/threadsafe_function_ctx', 'threadsafe_function/threadsafe_function_existing_tsfn', 'threadsafe_function/threadsafe_function_ptr', diff --git a/test/threadsafe_function_ex/README.md b/test/threadsafe_function_ex/README.md index 139838ae5..c5c74db07 100644 --- a/test/threadsafe_function_ex/README.md +++ b/test/threadsafe_function_ex/README.md @@ -1,5 +1,3 @@ # Napi::ThreadSafeFunctionEx tests -|Spec|Test|Native|Node|Description| -|----|---|---|---|---| -|call \ No newline at end of file +TODO \ No newline at end of file diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc index 9c9b5e636..9f8488d20 100644 --- a/test/threadsafe_function_ex/test/basic.cc +++ b/test/threadsafe_function_ex/test/basic.cc @@ -263,9 +263,6 @@ class TSFNWrap : public base { napi_threadsafe_function napi_tsfn; - // A threadsafe function on N-API 4 still requires a callback function, so - // this uses the `EmptyFunctionFactory` helper method to return a no-op - // Function on N-API 5+. auto status = napi_create_threadsafe_function( info.Env(), info[0], nullptr, String::From(info.Env(), "Test"), 0, 1, nullptr, Finalizer, this, CallJs, &napi_tsfn); @@ -349,8 +346,8 @@ namespace simple { using ContextType = std::nullptr_t; // Full type of our ThreadSafeFunctionEx. We don't specify the `ContextType` -// here (even though the _default_ for the type argument is `std::nullptr_t`) to -// demonstrate construction with no type arguments. +// here (even though the _default_ for the template argument is +// `std::nullptr_t`) to demonstrate construction with no template arguments. using TSFN = ThreadSafeFunctionEx<>; class TSFNWrap; @@ -363,7 +360,9 @@ class TSFNWrap : public base { auto env = info.Env(); #if NAPI_VERSION == 4 - // A threadsafe function on N-API 4 still requires a callback function. + // A threadsafe function on N-API 4 still requires a callback function, so + // this uses the `EmptyFunctionFactory` helper method to return a no-op + // Function on N-API 4. _tsfn = TSFN::New( env, // napi_env env, TSFN::EmptyFunctionFactory( diff --git a/test/threadsafe_function_ex/test/basic.js b/test/threadsafe_function_ex/test/basic.js index 780116954..d45091530 100644 --- a/test/threadsafe_function_ex/test/basic.js +++ b/test/threadsafe_function_ex/test/basic.js @@ -140,7 +140,7 @@ class BasicTest extends TestRunner { } /** - * A `ThreadSafeFunctionEx<>` can be constructed with no type arguments. + * A `ThreadSafeFunctionEx<>` can be constructed with no template arguments. * - Creates a threadsafe function with no context or callback or callJs. * - The node-addon-api 'no callback' feature is implemented by passing either * a no-op `Function` on N-API 4 or `std::nullptr` on N-API 5+ to the From ad9333e5c3aa575ff91db09bdfd4aea53288bd05 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 18 Aug 2020 17:26:57 +0200 Subject: [PATCH 30/39] test: tsfnex uses ported tsfn tests --- doc/threadsafe_function_ex.md | 147 ++-- test/binding.cc | 20 +- test/binding.gyp | 9 +- test/index.js | 7 +- test/threadsafe_function_ex/README.md | 3 - test/threadsafe_function_ex/index.js | 13 - test/threadsafe_function_ex/test/basic.cc | 432 ----------- test/threadsafe_function_ex/test/basic.js | 161 ---- test/threadsafe_function_ex/test/example.cc | 694 ------------------ test/threadsafe_function_ex/test/example.js | 342 --------- .../threadsafe_function_ex/test/threadsafe.js | 182 ----- .../threadsafe.cc => threadsafe_function.cc} | 2 +- .../threadsafe_function.js | 194 +++++ .../threadsafe_function_ctx.cc | 62 ++ .../threadsafe_function_ctx.js | 14 + .../threadsafe_function_existing_tsfn.cc | 114 +++ .../threadsafe_function_existing_tsfn.js | 17 + .../threadsafe_function_ptr.cc | 28 + .../threadsafe_function_ptr.js | 10 + .../threadsafe_function_sum.cc | 220 ++++++ .../threadsafe_function_sum.js | 61 ++ .../threadsafe_function_unref.cc | 44 ++ .../threadsafe_function_unref.js | 55 ++ .../threadsafe_function_ex/util/TestRunner.js | 154 ---- test/threadsafe_function_ex/util/util.h | 62 -- 25 files changed, 945 insertions(+), 2102 deletions(-) delete mode 100644 test/threadsafe_function_ex/README.md delete mode 100644 test/threadsafe_function_ex/index.js delete mode 100644 test/threadsafe_function_ex/test/basic.cc delete mode 100644 test/threadsafe_function_ex/test/basic.js delete mode 100644 test/threadsafe_function_ex/test/example.cc delete mode 100644 test/threadsafe_function_ex/test/example.js delete mode 100644 test/threadsafe_function_ex/test/threadsafe.js rename test/threadsafe_function_ex/{test/threadsafe.cc => threadsafe_function.cc} (99%) create mode 100644 test/threadsafe_function_ex/threadsafe_function.js create mode 100644 test/threadsafe_function_ex/threadsafe_function_ctx.cc create mode 100644 test/threadsafe_function_ex/threadsafe_function_ctx.js create mode 100644 test/threadsafe_function_ex/threadsafe_function_existing_tsfn.cc create mode 100644 test/threadsafe_function_ex/threadsafe_function_existing_tsfn.js create mode 100644 test/threadsafe_function_ex/threadsafe_function_ptr.cc create mode 100644 test/threadsafe_function_ex/threadsafe_function_ptr.js create mode 100644 test/threadsafe_function_ex/threadsafe_function_sum.cc create mode 100644 test/threadsafe_function_ex/threadsafe_function_sum.js create mode 100644 test/threadsafe_function_ex/threadsafe_function_unref.cc create mode 100644 test/threadsafe_function_ex/threadsafe_function_unref.js delete mode 100644 test/threadsafe_function_ex/util/TestRunner.js delete mode 100644 test/threadsafe_function_ex/util/util.h diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md index f1c42599b..ffc637217 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/threadsafe_function_ex.md @@ -82,7 +82,7 @@ New(napi_env env, calling `uv_thread_join()`. It is important that, aside from the main loop thread, there be no threads left using the thread-safe function after the finalize callback completes. Must implement `void operator()(Env env, - DataType* data, ContextType* hint)`. + FinalizerDataType* data, ContextType* hint)`. - `[optional] data`: Data to be passed to `finalizeCallback`. Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. @@ -182,56 +182,109 @@ Returns one of: - `napi_generic_failure`: A generic error occurred when attemping to add to the queue. + ## Example -For an in-line documented example, please see the ThreadSafeFunctionEx CI tests hosted here. -- [test/threadsafe_function_ex/test/example.js](../test/threadsafe_function_ex/test/example.js) -- [test/threadsafe_function_ex/test/example.cc](../test/threadsafe_function_ex/test/example.cc) +```cpp +#include +#include +#include + +using namespace Napi; + +std::thread nativeThread; +ThreadSafeFunction tsfn; + +Value Start( const CallbackInfo& info ) +{ + Napi::Env env = info.Env(); + + if ( info.Length() < 2 ) + { + throw TypeError::New( env, "Expected two arguments" ); + } + else if ( !info[0].IsFunction() ) + { + throw TypeError::New( env, "Expected first arg to be function" ); + } + else if ( !info[1].IsNumber() ) + { + throw TypeError::New( env, "Expected second arg to be number" ); + } + + int count = info[1].As().Int32Value(); + + // Create a ThreadSafeFunction + tsfn = ThreadSafeFunction::New( + env, + info[0].As(), // JavaScript function called asynchronously + "Resource Name", // Name + 0, // Unlimited queue + 1, // Only one thread will use this initially + []( Napi::Env ) { // Finalizer used to clean threads up + nativeThread.join(); + } ); + + // Create a native thread + nativeThread = std::thread( [count] { + auto callback = []( Napi::Env env, Function jsCallback, int* value ) { + // Transform native data into JS data, passing it to the provided + // `jsCallback` -- the TSFN's JavaScript function. + jsCallback.Call( {Number::New( env, *value )} ); + + // We're finished with the data. + delete value; + }; + + for ( int i = 0; i < count; i++ ) + { + // Create new data + int* value = new int( clock() ); + + // Perform a blocking call + napi_status status = tsfn.BlockingCall( value, callback ); + if ( status != napi_ok ) + { + // Handle error + break; + } + + std::this_thread::sleep_for( std::chrono::seconds( 1 ) ); + } + + // Release the thread-safe function + tsfn.Release(); + } ); + + return Boolean::New(env, true); +} + +Napi::Object Init( Napi::Env env, Object exports ) +{ + exports.Set( "start", Function::New( env, Start ) ); + return exports; +} + +NODE_API_MODULE( clock, Init ) +``` + +The above code can be used from JavaScript as follows: + +```js +const { start } = require('bindings')('clock'); -The example will create multiple set of threads. Each thread calls into -JavaScript with a numeric `base` value (deterministically calculated by the -thread id), with Node returning either a `number` or `Promise` that -resolves to `base * base`. +start(function () { + console.log("JavaScript callback called with arguments", Array.from(arguments)); +}, 5); +``` -From the root of the `node-addon-api` repository: +When executed, the output will show the value of `clock()` five times at one +second intervals: ``` -Usage: node ./test/threadsafe_function_ex/test/example.js [options] - - -c, --calls The number of calls each thread should make (number[]). - -a, --acquire [factor] Acquire a new set of `factor` call threads, using the - same `calls` definition. - -d, --call-delay The delay on callback resolution that each thread should - have (number[]). This is achieved via a delayed Promise - resolution in the JavaScript callback provided to the - TSFN. Using large delays here will cause all threads to - bottle-neck. - -D, --thread-delay The delay that each thread should have prior to making a - call (number[]). Using large delays here will cause the - individual thread to bottle-neck. - -l, --log-call Display console.log-based logging messages. - -L, --log-thread Display std::cout-based logging messages. - -n, --no-callback Do not use a JavaScript callback. - -e, --callback-error [thread[.call]] Cause an error to occur in the JavaScript callback for - the given thread's call (if provided; first thread's - first call otherwise). - - When not provided: - - defaults to [1,2,3,4,5] - - [factor] defaults to 1 - - defaults to [400,200,100,50,0] - - defaults to [400,200,100,50,0] - - -Examples: - - -c [1,2,3] -l -L - - Creates three threads that makes one, two, and three calls each, respectively. - - -c [5,5] -d [5000,5000] -D [0,0] -l -L - - Creates two threads that make five calls each. In this scenario, the threads will be - blocked primarily on waiting for the callback to resolve, as each thread's call takes - 5000 milliseconds. +JavaScript callback called with arguments [ 84745 ] +JavaScript callback called with arguments [ 103211 ] +JavaScript callback called with arguments [ 104516 ] +JavaScript callback called with arguments [ 105104 ] +JavaScript callback called with arguments [ 105691 ] ``` diff --git a/test/binding.cc b/test/binding.cc index cde830d29..fe3a3cd5b 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -49,9 +49,12 @@ Object InitThreadSafeFunctionPtr(Env env); Object InitThreadSafeFunctionSum(Env env); Object InitThreadSafeFunctionUnref(Env env); Object InitThreadSafeFunction(Env env); -Object InitThreadSafeFunctionExBasic(Env env); -Object InitThreadSafeFunctionExExample(Env env); -Object InitThreadSafeFunctionExThreadSafe(Env env); +Object InitThreadSafeFunctionExCtx(Env env); +Object InitThreadSafeFunctionExExistingTsfn(Env env); +Object InitThreadSafeFunctionExPtr(Env env); +Object InitThreadSafeFunctionExSum(Env env); +Object InitThreadSafeFunctionExUnref(Env env); +Object InitThreadSafeFunctionEx(Env env); #endif Object InitTypedArray(Env env); Object InitObjectWrap(Env env); @@ -111,10 +114,13 @@ Object Init(Env env, Object exports) { exports.Set("threadsafe_function_ptr", InitThreadSafeFunctionPtr(env)); exports.Set("threadsafe_function_sum", InitThreadSafeFunctionSum(env)); exports.Set("threadsafe_function_unref", InitThreadSafeFunctionUnref(env)); - exports.Set("threadsafe_function", InitThreadSafeFunction(env)); - exports.Set("threadsafe_function_ex_basic", InitThreadSafeFunctionExBasic(env)); - exports.Set("threadsafe_function_ex_example", InitThreadSafeFunctionExExample(env)); - exports.Set("threadsafe_function_ex_threadsafe", InitThreadSafeFunctionExThreadSafe(env)); + exports.Set("threadsafe_function", InitThreadSafeFunctionEx(env)); + exports.Set("threadsafe_function_ex_ctx", InitThreadSafeFunctionExCtx(env)); + exports.Set("threadsafe_function_ex_existing_tsfn", InitThreadSafeFunctionExExistingTsfn(env)); + exports.Set("threadsafe_function_ex_ptr", InitThreadSafeFunctionExPtr(env)); + exports.Set("threadsafe_function_ex_sum", InitThreadSafeFunctionExSum(env)); + exports.Set("threadsafe_function_ex_unref", InitThreadSafeFunctionExUnref(env)); + exports.Set("threadsafe_function_ex", InitThreadSafeFunctionEx(env)); #endif exports.Set("typedarray", InitTypedArray(env)); exports.Set("objectwrap", InitObjectWrap(env)); diff --git a/test/binding.gyp b/test/binding.gyp index f1e46114f..702799d11 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -36,9 +36,12 @@ 'object/set_property.cc', 'promise.cc', 'run_script.cc', - 'threadsafe_function_ex/test/basic.cc', - 'threadsafe_function_ex/test/example.cc', - 'threadsafe_function_ex/test/threadsafe.cc', + 'threadsafe_function_ex/threadsafe_function_ctx.cc', + 'threadsafe_function_ex/threadsafe_function_existing_tsfn.cc', + 'threadsafe_function_ex/threadsafe_function_ptr.cc', + 'threadsafe_function_ex/threadsafe_function_sum.cc', + 'threadsafe_function_ex/threadsafe_function_unref.cc', + 'threadsafe_function_ex/threadsafe_function.cc', 'threadsafe_function/threadsafe_function_ctx.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', 'threadsafe_function/threadsafe_function_ptr.cc', diff --git a/test/index.js b/test/index.js index fc285f132..adf6925cf 100644 --- a/test/index.js +++ b/test/index.js @@ -43,7 +43,12 @@ let testModules = [ 'object/set_property', 'promise', 'run_script', - 'threadsafe_function_ex', + 'threadsafe_function_ex/threadsafe_function_ctx', + 'threadsafe_function_ex/threadsafe_function_existing_tsfn', + 'threadsafe_function_ex/threadsafe_function_ptr', + 'threadsafe_function_ex/threadsafe_function_sum', + 'threadsafe_function_ex/threadsafe_function_unref', + 'threadsafe_function_ex/threadsafe_function', 'threadsafe_function/threadsafe_function_ctx', 'threadsafe_function/threadsafe_function_existing_tsfn', 'threadsafe_function/threadsafe_function_ptr', diff --git a/test/threadsafe_function_ex/README.md b/test/threadsafe_function_ex/README.md deleted file mode 100644 index c5c74db07..000000000 --- a/test/threadsafe_function_ex/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Napi::ThreadSafeFunctionEx tests - -TODO \ No newline at end of file diff --git a/test/threadsafe_function_ex/index.js b/test/threadsafe_function_ex/index.js deleted file mode 100644 index 917d1b060..000000000 --- a/test/threadsafe_function_ex/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const tests = [ - 'threadsafe', - 'basic', - 'example' -]; - -// Threadsafe tests must run synchronously. If two threaded-tests are running -// and one fails, Node may exit while `std::thread`s are running. -module.exports = (async () => { - for (const test of tests) { - await require(`./test/${test}`); - } -})(); diff --git a/test/threadsafe_function_ex/test/basic.cc b/test/threadsafe_function_ex/test/basic.cc deleted file mode 100644 index 9f8488d20..000000000 --- a/test/threadsafe_function_ex/test/basic.cc +++ /dev/null @@ -1,432 +0,0 @@ -#include "../util/util.h" -#include "napi.h" -#include - -#if (NAPI_VERSION > 3) - -using namespace Napi; - -namespace call { - -// Context of the TSFN. -using ContextType = std::nullptr_t; - -// Data passed (as pointer) to [Non]BlockingCall -struct DataType { - Reference data; - Promise::Deferred deferred; -}; - -// CallJs callback function -static void CallJs(Napi::Env env, Napi::Function jsCallback, - ContextType * /*context*/, DataType *data) { - if (!(env == nullptr || jsCallback == nullptr)) { - if (data != nullptr) { - jsCallback.Call(env.Undefined(), {data->data.Value()}); - data->deferred.Resolve(data->data.Value()); - } - } - if (data != nullptr) { - delete data; - } -} - -// Full type of the ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; - -class TSFNWrap; -using base = tsfnutil::TSFNWrapBase; - -// A JS-accessible wrap that holds the TSFN. -class TSFNWrap : public base { -public: - TSFNWrap(const CallbackInfo &info) : base(info) { - Napi::Env env = info.Env(); - _tsfn = TSFN::New(env, // napi_env env, - info[0].As(), // const Function& callback, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - nullptr, // ContextType* context - base::Finalizer, // Finalizer finalizer - &_deferred // FinalizerDataType* data - ); - } - - static std::array, 2> InstanceMethods() { - return {{InstanceMethod("release", &TSFNWrap::Release), - InstanceMethod("call", &TSFNWrap::Call)}}; - } - - Napi::Value Call(const CallbackInfo &info) { - Napi::Env env = info.Env(); - DataType *data = - new DataType{Napi::Reference(Persistent(info[0])), - Promise::Deferred::New(env)}; - _tsfn.NonBlockingCall(data); - return data->deferred.Promise(); - }; -}; - -} // namespace call - -namespace context { - -// Context of the TSFN. -using ContextType = Reference; - -// Data passed (as pointer) to [Non]BlockingCall -using DataType = Promise::Deferred; - -// CallJs callback function -static void CallJs(Napi::Env env, Napi::Function /*jsCallback*/, - ContextType *context, DataType *data) { - if (env != nullptr) { - if (data != nullptr) { - data->Resolve(context->Value()); - } - } - if (data != nullptr) { - delete data; - } -} - -// Full type of the ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; - -class TSFNWrap; -using base = tsfnutil::TSFNWrapBase; - -// A JS-accessible wrap that holds the TSFN. -class TSFNWrap : public base { -public: - TSFNWrap(const CallbackInfo &info) : base(info) { - Napi::Env env = info.Env(); - - ContextType *context = new ContextType(Persistent(info[0])); - - _tsfn = - TSFN::New(env, // napi_env env, - TSFN::EmptyFunctionFactory(env), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - context, // ContextType* context, - base::Finalizer, // Finalizer finalizer - &_deferred // FinalizerDataType* data - ); - } - - static std::array, 3> InstanceMethods() { - return {{InstanceMethod("call", &TSFNWrap::Call), - InstanceMethod("getContext", &TSFNWrap::GetContext), - InstanceMethod("release", &TSFNWrap::Release)}}; - } - - Napi::Value Call(const CallbackInfo &info) { - auto *callData = new DataType(info.Env()); - _tsfn.NonBlockingCall(callData); - return callData->Promise(); - }; - - Napi::Value GetContext(const CallbackInfo &) { - return _tsfn.GetContext()->Value(); - }; -}; -} // namespace context - -namespace empty { -#if NAPI_VERSION > 4 - -// Context of the TSFN. -using ContextType = std::nullptr_t; - -// Data passed (as pointer) to [Non]BlockingCall -struct DataType { - Promise::Deferred deferred; -}; - -// CallJs callback function -static void CallJs(Napi::Env env, Function jsCallback, - ContextType * /*context*/, DataType *data) { - if (env != nullptr) { - if (data != nullptr) { - if (jsCallback.IsEmpty()) { - data->deferred.Resolve(Boolean::New(env, true)); - } else { - data->deferred.Reject(String::New(env, "jsCallback is not empty")); - } - } - } - if (data != nullptr) { - delete data; - } -} - -// Full type of the ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; - -class TSFNWrap; -using base = tsfnutil::TSFNWrapBase; - -// A JS-accessible wrap that holds the TSFN. -class TSFNWrap : public base { -public: - TSFNWrap(const CallbackInfo &info) : base(info) { - - auto env = info.Env(); - _tsfn = TSFN::New(env, // napi_env env, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - 1, // size_t initialThreadCount, - nullptr, // ContextType* context - base::Finalizer, // Finalizer finalizer - &_deferred // FinalizerDataType* data - ); - } - - static std::array, 2> InstanceMethods() { - return {{InstanceMethod("release", &TSFNWrap::Release), - InstanceMethod("call", &TSFNWrap::Call)}}; - } - - Napi::Value Call(const CallbackInfo &info) { - auto data = new DataType{Promise::Deferred::New(info.Env())}; - _tsfn.NonBlockingCall(data); - return data->deferred.Promise(); - }; -}; - -#endif -} // namespace empty - -namespace existing { - -// Data passed (as pointer) to [Non]BlockingCall -struct DataType { - Promise::Deferred deferred; - bool reject; -}; - -// CallJs callback function provided to `napi_create_threadsafe_function`. It is -// _NOT_ used by `Napi::ThreadSafeFunctionEx<>`, which is why these arguments -// are napi_*. -static void CallJs(napi_env env, napi_value jsCallback, void * /*context*/, - void *data) { - DataType *casted = static_cast(data); - if (env != nullptr) { - if (jsCallback != nullptr) { - Function(env, jsCallback).Call(0, nullptr); - } - if (data != nullptr) { - if (casted->reject) { - casted->deferred.Reject( - String::New(env, "The CallJs has rejected the promise")); - } else { - casted->deferred.Resolve( - String::New(env, "The CallJs has resolved the promise")); - } - } - } - if (casted != nullptr) { - delete casted; - } -} - -// This test creates a native napi_threadsafe_function itself, whose `context` -// parameter is the `TSFNWrap` object. We forward-declare, so we can use -// it as an argument inside `ThreadSafeFunctionEx<>`. This also allows us to -// statically get the correct type when using `tsfn.GetContext()`. The converse -// is true: if the ContextType does _not_ match that provided to the underlying -// napi_create_threadsafe_function, then the static type will be incorrect. -class TSFNWrap; - -// Context of the TSFN. -using ContextType = TSFNWrap; - -// Full type of our ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; -using base = tsfnutil::TSFNWrapBase; - -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public base { -public: - TSFNWrap(const CallbackInfo &info) : base(info) { - - auto env = info.Env(); - - if (info.Length() < 1 || !info[0].IsFunction()) { - NAPI_THROW_VOID(Napi::TypeError::New( - env, "Invalid arguments: Expected arg0 = function")); - } - - napi_threadsafe_function napi_tsfn; - - auto status = napi_create_threadsafe_function( - info.Env(), info[0], nullptr, String::From(info.Env(), "Test"), 0, 1, - nullptr, Finalizer, this, CallJs, &napi_tsfn); - if (status != napi_ok) { - NAPI_THROW_VOID(Error::New(env, "Could not create TSFN.")); - } - _tsfn = TSFN(napi_tsfn); - } - - static std::array, 2> InstanceMethods() { - return {{InstanceMethod("release", &TSFNWrap::Release), - InstanceMethod("call", &TSFNWrap::Call)}}; - } - - Napi::Value Call(const CallbackInfo &info) { - Napi::Env env = info.Env(); - if (info.Length() < 1) { - NAPI_THROW(Napi::TypeError::New( - env, "Invalid arguments: Expected arg0 = number [0,5]"), - Value()); - } - auto arg0 = info[0]; - if (!arg0.IsNumber()) { - NAPI_THROW(Napi::TypeError::New( - env, "Invalid arguments: Expected arg0 = number [0,5]"), - Value()); - } - auto mode = info[0].ToNumber().Int32Value(); - switch (mode) { - // Use node-addon-api to send a call that either resolves or rejects the - // promise in the data. - case 0: - case 1: { - auto *data = new DataType{Promise::Deferred::New(env), mode == 1}; - _tsfn.NonBlockingCall(data); - return data->deferred.Promise(); - } - // Use node-addon-api to send a call with no data - case 2: { - _tsfn.NonBlockingCall(); - return Boolean::New(env, true); - } - // Use napi to send a call that either resolves or rejects the promise in - // the data. - case 3: - case 4: { - auto *data = new DataType{Promise::Deferred::New(env), mode == 4}; - napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); - return data->deferred.Promise(); - } - // Use napi to send a call with no data - case 5: { - napi_call_threadsafe_function(_tsfn, nullptr, napi_tsfn_nonblocking); - return Boolean::New(env, true); - } - } - NAPI_THROW(Napi::TypeError::New( - env, "Invalid arguments: Expected arg0 = number [0,5]"), - Value()); - }; - -private: - // This test uses a custom napi (NOT node-addon-api) TSFN finalizer. - static void Finalizer(napi_env env, void * /*data*/, void *ctx) { - TSFNWrap *tsfn = static_cast(ctx); - tsfn->Finalizer(env); - } - - // Clean up the TSFNWrap by resolving the promise. - void Finalizer(napi_env e) { - if (_deferred) { - _deferred->Resolve(Boolean::New(e, true)); - _deferred.release(); - } - } -}; - -} // namespace existing -namespace simple { - -using ContextType = std::nullptr_t; - -// Full type of our ThreadSafeFunctionEx. We don't specify the `ContextType` -// here (even though the _default_ for the template argument is -// `std::nullptr_t`) to demonstrate construction with no template arguments. -using TSFN = ThreadSafeFunctionEx<>; - -class TSFNWrap; -using base = tsfnutil::TSFNWrapBase; - -// A JS-accessible wrap that holds a TSFN. -class TSFNWrap : public base { -public: - TSFNWrap(const CallbackInfo &info) : base(info) { - - auto env = info.Env(); -#if NAPI_VERSION == 4 - // A threadsafe function on N-API 4 still requires a callback function, so - // this uses the `EmptyFunctionFactory` helper method to return a no-op - // Function on N-API 4. - _tsfn = TSFN::New( - env, // napi_env env, - TSFN::EmptyFunctionFactory( - env), // N-API 5+: nullptr; else: const Function& callback, - "Test", // ResourceString resourceName, - 1, // size_t maxQueueSize, - 1 // size_t initialThreadCount - ); -#else - _tsfn = TSFN::New(env, // napi_env env, - "Test", // ResourceString resourceName, - 1, // size_t maxQueueSize, - 1 // size_t initialThreadCount - ); -#endif - } - - static std::array, 2> InstanceMethods() { - return {{InstanceMethod("release", &TSFNWrap::Release), - InstanceMethod("call", &TSFNWrap::Call)}}; - } - - // Since this test spec has no CALLBACK, CONTEXT, or FINALIZER. We have no way - // to know when the underlying ThreadSafeFunction has been finalized. - Napi::Value Release(const CallbackInfo &info) { - _tsfn.Release(); - return String::New(info.Env(), "TSFN may not have finalized."); - }; - - Napi::Value Call(const CallbackInfo &info) { - _tsfn.NonBlockingCall(); - return info.Env().Undefined(); - }; -}; - -} // namespace simple - -Object InitThreadSafeFunctionExBasic(Env env) { - -// A list of v4+ enabled spec namespaces. -#define V4_EXPORTS(V) \ - V(call) \ - V(simple) \ - V(existing) \ - V(context) - -// A list of v5+ enables spec namespaces. -#define V5_EXPORTS(V) V(empty) - -#if NAPI_VERSION == 4 -#define EXPORTS(V) V4_EXPORTS(V) -#else -#define EXPORTS(V) \ - V4_EXPORTS(V) \ - V5_EXPORTS(V) -#endif - - Object exports(Object::New(env)); - -#define V(modname) modname::TSFNWrap::Init(env, exports, #modname); - EXPORTS(V) -#undef V - - return exports; -} - -#endif diff --git a/test/threadsafe_function_ex/test/basic.js b/test/threadsafe_function_ex/test/basic.js deleted file mode 100644 index d45091530..000000000 --- a/test/threadsafe_function_ex/test/basic.js +++ /dev/null @@ -1,161 +0,0 @@ -// @ts-check -'use strict'; -const assert = require('assert'); - -const { TestRunner } = require('../util/TestRunner'); - -/** - * A "basic" test spec. This spec does NOT use threads, and is primarily used to - * verify the API. - */ -class BasicTest extends TestRunner { - /** - * This test ensures the data sent to the NonBlockingCall and the data - * received in the JavaScript callback are the same. - * - Creates a contexted threadsafe function with callback. - * - Makes one call, and waits for call to complete. - * - The callback forwards the item's data to the given JavaScript function in - * the test. - * - Asserts the data is the same. - */ - async call({ TSFNWrap }) { - const data = {}; - const tsfn = new TSFNWrap(tsfnData => { - assert(data === tsfnData, "Data in and out of tsfn call do not equal"); - }); - await tsfn.call(data); - return await tsfn.release(); - } - - /** - * The context provided to the threadsafe function's constructor is accessible - * on both (A) the threadsafe function's callback as well as (B) the - * threadsafe function itself. This test ensures the context across all three - * are the same. - * - Creates a contexted threadsafe function with callback. - * - The callback forwards the item's data to the given JavaScript function in - * the test. - * - Asserts the contexts are the same as the context passed during threadsafe - * function construction in two places: - * - (A) Makes one call, and waits for call to complete. - * - (B) Asserts that the context returns from the API's `GetContext()` - */ - async context({ TSFNWrap }) { - const ctx = {}; - const tsfn = new TSFNWrap(ctx); - assert(ctx === await tsfn.call(), "getContextByCall context not equal"); - assert(ctx === tsfn.getContext(), "getContextFromTsfn context not equal"); - return await tsfn.release(); - } - - /** - * **ONLY ON N-API 5+**. The optional JavaScript function callback feature is - * not available in N-API <= 4. This test creates uses a threadsafe function - * that handles all of its JavaScript processing on the callJs instead of the - * callback. - * - Creates a threadsafe function with no JavaScript context or callback. - * - Makes one call, waiting for completion. The internal `CallJs` resolves - * the call if jsCallback is empty, otherwise rejects. - */ - async empty({ TSFNWrap }) { - if (typeof TSFNWrap === 'function') { - const tsfn = new TSFNWrap(); - await tsfn.call(); - return await tsfn.release(); - } - return true; - } - - /** - * A `ThreadSafeFunctionEx` can be constructed with an existing - * napi_threadsafe_function. - * - Creates a native napi_threadsafe_function with no context, using the - * jsCallback passed from this test. - * - Makes six calls: - * - Use node-addon-api's `NonBlockingCall` *OR* napi's - * `napi_call_threadsafe_function` _cross_ - * - With data that resolves *OR rejects on CallJs - * - With no data that rejects on CallJs - * - Releases the TSFN. - */ - async existing({ TSFNWrap }) { - - /** - * Called by the TSFN's jsCallback below. - * @type {function|undefined} - */ - let currentCallback = undefined; - - const tsfn = new TSFNWrap(function () { - if (typeof currentCallback === 'function') { - currentCallback.apply(undefined, arguments); - } - }); - /** - * The input argument to `tsfn.call()`: 0-2: - * ThreadSafeFunctionEx.NonBlockingCall(data) with... - * - 0: data, resolve promise in CallJs - * - 1: data, reject promise in CallJs - * - 2: data = nullptr 3-5: napi_call_threadsafe_function(data, - * napi_tsfn_nonblocking) with... - * - 3: data, resolve promise in CallJs - * - 4: data, reject promise in CallJs - * - 5: data = nullptr - * @type {[0,1,2,3,4,5]} - */ - const input = [0, 1, 2, 3, 4, 5]; - - let caught = false; - - while (input.length) { - // Perform a call that resolves - await tsfn.call(input.shift()); - - // Perform a call that rejects - caught = false; - try { - await tsfn.call(input.shift()); - } catch (e) { - caught = true; - } finally { - assert(caught, "The rejection was not caught"); - } - - // Perform a call with no data - caught = false; - await new Promise((resolve, reject) => { - currentCallback = () => { - resolve(); - reject = undefined; - }; - tsfn.call(input.shift()); - setTimeout(() => { - if (reject) { - reject(new Error("tsfn.call() timed out")); - } - }, 1000); - }); - } - return await tsfn.release(); - } - - /** - * A `ThreadSafeFunctionEx<>` can be constructed with no template arguments. - * - Creates a threadsafe function with no context or callback or callJs. - * - The node-addon-api 'no callback' feature is implemented by passing either - * a no-op `Function` on N-API 4 or `std::nullptr` on N-API 5+ to the - * underlying `napi_create_threadsafe_function` call. - * - Makes one call, releases, then waits for finalization. - * - Inherently ignores the state of the item once it has been added to the - * queue. Since there are no callbacks or context, it is impossible to - * capture the state. - */ - async simple({ TSFNWrap }) { - const tsfn = new TSFNWrap(); - tsfn.call(); - return await tsfn.release(); - } - -} - -module.exports = new BasicTest('threadsafe_function_ex_basic', __filename).start(); diff --git a/test/threadsafe_function_ex/test/example.cc b/test/threadsafe_function_ex/test/example.cc deleted file mode 100644 index b0063a5cd..000000000 --- a/test/threadsafe_function_ex/test/example.cc +++ /dev/null @@ -1,694 +0,0 @@ -#include "../util/util.h" -#include "napi.h" -#include -#include -#include -#include -#include -#include - -using ThreadExitHandler = void (*)(size_t threadId); - -struct ThreadOptions { - size_t threadId; - int calls; - int callDelay; - int threadDelay; -}; - -static struct { - bool logCall = true; // Uses JS console.log to output when the TSFN is - // processing the NonBlockingCall(). - bool logThread = false; // Uses native std::cout to output when the thread's - // NonBlockingCall() request has finished. -} DefaultOptions; // Options from Start() - -#if (NAPI_VERSION > 3) - -using namespace Napi; - -namespace example { - -class TSFNWrap; - -// Context of the TSFN. -using Context = TSFNWrap; - -// Data returned to a thread when it requests a TSFN call. This example uses -// promises to synchronize between threads. Since this example needs to be built -// with exceptions both enabled and disabled, we will always use a static -// "positive" result, and dynamically at run-time determine if it failed. -// Otherwise, we could use std::promise.set_exception to handle errors. -struct CallJsResult { - int result; - bool isFinalized; - std::string error; -}; - -// The structure of data we send to `CallJs` -struct Data { - std::promise promise; - uint32_t threadId; - bool logCall; - int callDelay; - uint32_t base; // The "input" data, which CallJs will calculate `base * base` - int callId; // The call id (unique to the thread) -}; - -// Data passed (as pointer) to [Non]BlockingCall is shared among multiple -// threads (native thread and Node thread). -using DataType = std::shared_ptr; - -// When providing the `CallJs` result back to the thread, we pass information -// about where and how the result came (for logging). -enum ResultLocation { - NUMBER, // The callback returned a number - PROMISE, // The callback returned a Promise that resolved to a number - DEFAULT // There was no callback provided to TSFN::New -}; - -// CallJs callback function, used to transform the native C data to JS data. -static void CallJs(Napi::Env env, Napi::Function jsCallback, - Context * /*context*/, DataType *dataPtr) { - // If we have data - if (dataPtr != nullptr) { - std::weak_ptr weakData(*dataPtr); - // Create concrete reference to our DataType. - auto &data(*dataPtr); - - // The success handler ran by the following `CallJs` function. - auto handleResult = [=](Napi::Env env, int calculated, - ResultLocation location) { - // auto &data(*dataPtr); - if (auto data = - weakData - .lock()) { // Has to be copied into a shared_ptr before usage - // std::cout << *data << "\n"; - if (data->logCall) { - std::string message( - "[Thread " + std::to_string(data->threadId) + - "] [CallJs ] [Call " + std::to_string(data->callId) + - "] Receive answer: result = " + std::to_string(calculated) + - (location == ResultLocation::NUMBER - ? " (as number)" - : location == ResultLocation::PROMISE ? " (as Promise)" - : " (as default)")); - - auto console = env.Global().Get("console").As(); - console.Get("log").As().Call(console, - {String::New(env, message)}); - } - // Resolve the `std::promise` awaited on in the child thread. - data->promise.set_value(CallJsResult{calculated, false, ""}); - // Free the data. - // delete dataPtr; - } - }; - - // The error handler ran by the following `CallJs` function. - - auto handleError = [=](const std::string &what) { - if (auto data = - weakData - .lock()) { // Has to be copied into a shared_ptr before usage - // Resolve the `std::promise` awaited on in the child thread with an - // "errored success" value. Instead of erroring at the thread level, - // this could also return the default result. - data->promise.set_value(CallJsResult{0, false, what}); - } - - // // Free the data. - // delete dataPtr; - }; - - if (env != nullptr) { - // If the callback was provided at construction time via TSFN::New - if (!jsCallback.IsEmpty()) { - // Call the callback - auto value = jsCallback.Call( - {Number::New(env, data->threadId), Number::New(env, data->callId), - Number::New(env, data->logCall), Number::New(env, data->base), - Number::New(env, data->callDelay)}); - - // Check if the callback failed - if (env.IsExceptionPending()) { - const auto &error = env.GetAndClearPendingException(); - handleError(error.Message()); - } - - // Check for an immediate number result - else if (value.IsNumber()) { - handleResult(env, value.ToNumber(), ResultLocation::NUMBER); - } - - // Check for a Promise result - else if (value.IsPromise()) { - - // Construct the Promise.then and Promise.catch handlers. These could - // also be a statically-defined `Function`s. - - // Promise.then handler. - auto promiseHandlerThen = Function::New(env, [=](const CallbackInfo - &info) { - // Check for Promise result - if (info.Length() < 1 || !info[0].IsNumber()) { - handleError( - "Expected callback Promise resolution to be of type number"); - } else { - auto result = info[0].ToNumber().Int32Value(); - handleResult(info.Env(), result, ResultLocation::PROMISE); - } - }); - - // Promise.catch handler. - auto promiseHandlerCatch = - Function::New(env, [&](const CallbackInfo &info) { - if (info.Length() < 1 || !info[0].IsObject()) { - handleError("Unknown error in callback handler"); - } else { - auto errorAsValue(info[0] - .As() - .Get("toString") - .As() - .Call(info[0], {})); - handleError(errorAsValue.ToString()); - } - }); - - // Execute the JavaScript equivalent of `promise.then.call(promise, - // promiseHandlerThen).catch.call(promise, promiseHandlerCatch);` - value.As() - .Get("then") - .As() - .Call(value, {promiseHandlerThen}) - .As() - .Get("catch") - .As() - .Call(value, {promiseHandlerCatch}); - } - // When using N-API 4, the callback is a valid no-op Function that - // returns `undefined`. This also allows the callback itself to return - // `undefined` to take the default result. - else if (value.IsUndefined()) { - handleResult(env, data->base * data->base, ResultLocation::DEFAULT); - } else { - handleError("Expected callback return to be of type number " - "| Promise"); - } - } - - // If no callback provided, handle with default result that the callback - // would have provided. - else { - handleResult(env, data->base * data->base, ResultLocation::DEFAULT); - } - } - // If `env` is nullptr, then all threads have called finished their usage of - // the TSFN (either by calling `Release` or making a call and receiving - // `napi_closing`). In this scenario, it is not allowed to call into - // JavaScript, as the TSFN has been finalized. - else { - handleError("The TSFN has been finalized."); - } - } -} - -// Full type of the ThreadSafeFunctionEx -using TSFN = ThreadSafeFunctionEx; -using base = tsfnutil::TSFNWrapBase; - -// A JS-accessible wrap that holds the TSFN. -class TSFNWrap : public base { -public: - TSFNWrap(const CallbackInfo &info) : base(info) {} - - ~TSFNWrap() { - for (auto &thread : finalizerData.threads) { - // The TSFNWrap destructor runs when our ObjectWrap'd instance is - // garbage-collected. This should never happen with proper usage of - // `await` on `tsfn.release()`! - if (thread.joinable()) { - thread.join(); - } - } - } - - static std::array, 5> InstanceMethods() { - return {{InstanceMethod("getContext", &TSFNWrap::GetContext), - InstanceMethod("start", &TSFNWrap::Start), - InstanceMethod("acquire", &TSFNWrap::Acquire), - InstanceMethod("callCount", &TSFNWrap::CallCount), - InstanceMethod("release", &TSFNWrap::Release)}}; - } - - bool logThread = DefaultOptions.logThread; - bool logCall = DefaultOptions.logCall; - bool hasEmptyCallback; - std::atomic_uint succeededCalls; - std::atomic_int aggregate; - - // The structure of the data send to the finalizer. - struct FinalizerDataType { - std::vector threads; - std::vector outstandingCalls; - std::mutex - callMutex; // To protect multi-threaded accesses to `outstandingCalls` - std::unique_ptr deferred; - } finalizerData; - - // Used for logging. - std::mutex logMutex; - - Napi::Value Start(const CallbackInfo &info) { - Napi::Env env = info.Env(); - - if (_tsfn) { - NAPI_THROW(Napi::Error::New(Env(), "TSFN already exists."), Value()); - } - - // Creates a list to hold how many times each thread should make a call. - std::vector callCounts; - - // The JS-provided callback to execute for each call (if provided) - Function callback; - - if (info.Length() > 0 && info[0].IsObject()) { - auto arg0 = info[0].ToObject(); - - if (arg0.Has("callback")) { - auto cb = arg0.Get("callback"); - if (cb.IsUndefined()) { - // An empty callback option will create a valid no-op function on - // N-API 4 or leave `callback` as `std::nullptr` on N-API 5+. - callback = TSFN::FunctionOrEmpty(env, callback); - } else if (cb.IsFunction()) { - callback = cb.As(); - } else { - NAPI_THROW(Napi::TypeError::New( - Env(), "Invalid arguments: callback is not a " - "function. See StartOptions definition."), - Value()); - } - } - - hasEmptyCallback = callback.IsEmpty(); - - // Ensure proper parameters and add to our list of threads. - if (arg0.Has("threads")) { - Napi::Value threads = arg0.Get("threads"); - if (threads.IsArray()) { - Napi::Array threadsArray = threads.As(); - for (auto i = 0U; i < threadsArray.Length(); ++i) { - Napi::Value elem = threadsArray.Get(i); - if (elem.IsObject()) { - Object o = elem.ToObject(); - if (!(o.Has("calls") && o.Has("callDelay") && - o.Has("threadDelay"))) { - NAPI_THROW(Napi::TypeError::New( - Env(), "Invalid arguments. See " - "StartOptions.threads definition."), - Value()); - } - callCounts.push_back(ThreadOptions{ - callCounts.size(), o.Get("calls").ToNumber(), - hasEmptyCallback ? -1 : o.Get("callDelay").ToNumber(), - o.Get("threadDelay").ToNumber()}); - } else { - NAPI_THROW(Napi::TypeError::New( - Env(), "Invalid arguments. See " - "StartOptions.threads definition."), - Value()); - } - } - } else { - NAPI_THROW( - Napi::TypeError::New( - Env(), - "Invalid arguments. See StartOptions.threads definition."), - Value()); - } - } - - if (arg0.Has("logCall")) { - auto logCallOption = arg0.Get("logCall"); - if (logCallOption.IsBoolean()) { - logCall = logCallOption.As(); - } else { - NAPI_THROW(Napi::TypeError::New( - Env(), "Invalid arguments: logCall is not a boolean. " - "See StartOptions definition."), - Value()); - } - } - - if (arg0.Has("logThread")) { - auto logThreadOption = arg0.Get("logThread"); - if (logThreadOption.IsBoolean()) { - logThread = logThreadOption.As(); - } else { - NAPI_THROW(Napi::TypeError::New( - Env(), "Invalid arguments: logThread is not a " - "boolean. See StartOptions definition."), - Value()); - } - } - } - - const auto threadCount = callCounts.size(); - - succeededCalls = 0; - aggregate = 0; - _tsfn = TSFN::New( - env, // napi_env env, - TSFN::FunctionOrEmpty(env, callback), // const Function& callback, - Value(), // const Object& resource, - "Test", // ResourceString resourceName, - 0, // size_t maxQueueSize, - threadCount + 1, // size_t initialThreadCount, +1 for Node thread - this, // Context* context, - Finalizer, // Finalizer finalizer - &finalizerData // FinalizerDataType* data - ); - - if (logThread) { - std::cout << "[Starting] Starting example with options: {\n[Starting] " - "Log Call = " - << (logCall ? "true" : "false") - << ",\n[Starting] Log Thread = " - << (logThread ? "true" : "false") - << ",\n[Starting] Callback = " - << (hasEmptyCallback ? "[empty]" : "function") - << ",\n[Starting] Threads = [\n"; - for (auto &threadOption : callCounts) { - std::cout << "[Starting] " << threadOption.threadId - << " -> { Calls: " << threadOption.calls << ", Call Delay: " - << (threadOption.callDelay == -1 - ? "[no callback]" - : std::to_string(threadOption.callDelay)) - << ", Thread Delay: " << threadOption.threadDelay << " },\n"; - } - std::cout << "[Starting] ]\n[Starting] }\n"; - } - - // for (auto threadId = 0U; threadId < threadCount; ++threadId) { - for (auto &threadOption : callCounts) { - finalizerData.threads.push_back( - std::thread(threadEntry, _tsfn, threadOption, this)); - } - - return Number::New(env, threadCount); - }; - - Napi::Value Acquire(const CallbackInfo &info) { - Napi::Env env = info.Env(); - - if (!_tsfn) { - NAPI_THROW(Napi::Error::New(Env(), "TSFN does not exist."), Value()); - } - - // Creates a list to hold how many times each thread should make a call. - std::vector callCounts; - if (info.Length() > 0 && info[0].IsArray()) { - Napi::Array threadsArray = info[0].As(); - for (auto i = 0U; i < threadsArray.Length(); ++i) { - Napi::Value elem = threadsArray.Get(i); - if (elem.IsObject()) { - Object o = elem.ToObject(); - if (!(o.Has("calls") && o.Has("callDelay") && o.Has("threadDelay"))) { - NAPI_THROW(Napi::TypeError::New(Env(), - "Invalid arguments. See " - "StartOptions.threads definition."), - Value()); - } - callCounts.push_back(ThreadOptions{ - callCounts.size() + finalizerData.threads.size(), - o.Get("calls").ToNumber(), - hasEmptyCallback ? -1 : o.Get("callDelay").ToNumber(), - o.Get("threadDelay").ToNumber()}); - } else { - NAPI_THROW(Napi::TypeError::New(Env(), - "Invalid arguments. See " - "StartOptions.threads definition."), - Value()); - } - } - } else { - NAPI_THROW( - Napi::TypeError::New( - Env(), "Invalid arguments. See StartOptions.threads definition."), - Value()); - } - - if (logThread) { - for (auto &threadOption : callCounts) { - std::cout << "[Acquire ] " << threadOption.threadId - << " -> { Calls: " << threadOption.calls << ", Call Delay: " - << (threadOption.callDelay == -1 - ? "[no callback]" - : std::to_string(threadOption.callDelay)) - << ", Thread Delay: " << threadOption.threadDelay << " },\n"; - } - std::cout << "[Acquire ] ]\n[Acquire ] }\n"; - } - - auto started = 0U; - - for (auto &threadOption : callCounts) { - // The `Acquire` call may be called from any thread, but we do it here to - // avoid a race condition where the thread starts but the TSFN has been - // finalized. - auto status = _tsfn.Acquire(); - if (status == napi_ok) { - finalizerData.threads.push_back( - std::thread(threadEntry, _tsfn, threadOption, this)); - ++started; - } - } - return Number::New(env, started); - } - - - // Release the TSFN from the Node thread. This will return a `Promise` that - // resolves in the Finalizer. - Napi::Value Release(const CallbackInfo &info) { - if (finalizerData.deferred) { - return finalizerData.deferred->Promise(); - } - finalizerData.deferred.reset( - new Promise::Deferred(Promise::Deferred::New(info.Env()))); - _tsfn.Release(); - return finalizerData.deferred->Promise(); - }; - - // Returns an array corresponding to the amount of succeeded calls and the sum - // aggregate. - Napi::Value CallCount(const CallbackInfo &info) { - Napi::Env env(info.Env()); - - auto results = Array::New(env, 2); - results.Set("0", Number::New(env, succeededCalls)); - results.Set("1", Number::New(env, aggregate)); - return results; - }; - - // Returns the TSFN's context. - Napi::Value GetContext(const CallbackInfo &) { - return _tsfn.GetContext()->Value(); - }; - - // The thread entry point. It receives as arguments the TSFN, the call - // options, and the context. - static void threadEntry(TSFN tsfn, ThreadOptions options, Context *context) { -#define THREADLOG(X) \ - if (context->logThread) { \ - std::lock_guard lock(context->logMutex); \ - std::cout << "[Thread " << threadId << "] [Native ] " \ - << (data->callId == -1 \ - ? "" \ - : "[Call " + std::to_string(data->callId) + "] ") \ - << X; \ - } - -#define THREADLOG_MAIN(X) \ - if (context->logThread) { \ - std::lock_guard lock(context->logMutex); \ - std::cout << "[Thread " << threadId << "] [Native ] " << X; \ - } - using namespace std::chrono_literals; - auto threadId = options.threadId; - - // To help with simultaneous threads using the logging mechanism, we'll - // delay at thread start. - std::this_thread::sleep_for(threadId * 10ms); - THREADLOG_MAIN("Thread " << threadId << " started.\n") - - enum ThreadState { - // Starting stating. - Running, - - // When all requests have been completed. - Release, - - // If a `NonBlockingCall` results in a Promise but while waiting - // for the resolution, the TSFN is finalized. - AlreadyFinalized, - - // If a `NonBlockingCall` receiving `napi_closing`, we do *NOT* `Release` - // it. - Closing - } state = ThreadState::Running; - - for (auto i = 0; state == Running; ++i) { - - if (i >= options.calls) { - state = Release; - break; - } - - DataType data(context->makeNewCall(threadId, i, context->logCall, - options.callDelay)); - - if (options.threadDelay > 0 && i > 0) { - THREADLOG("Delay for " << options.threadDelay - << "ms before next call\n") - std::this_thread::sleep_for(options.threadDelay * 1ms); - } - - THREADLOG("Performing call request: base = " << data->base << "\n") - - auto status = tsfn.NonBlockingCall(&data); - - if (status == napi_ok) { - auto future = data->promise.get_future(); - auto result = future.get(); - if (result.error.length() == 0) { - context->callSucceeded(data, result.result); - THREADLOG("Receive answer: result = " << result.result << "\n") - continue; - } else if (result.isFinalized) { - THREADLOG("Application Error: The TSFN has been finalized.\n") - // If the Finalizer has canceled this request, we do not call - // `Release()`. - state = AlreadyFinalized; - } - } else if (status == napi_closing) { - // A thread **MUST NOT** call `Abort()` or `Release()` if we receive an - // `napi_closing` call. - THREADLOG("N-API Error: The thread-safe function is aborted and " - "cannot accept more calls.\n") - state = Closing; - } else if (status == napi_queue_full) { - // The example will finish this thread's use of the TSFN if it is full. - THREADLOG("N-API Error: The queue was full when trying to call in a " - "non-blocking method.\n") - state = Release; - } else if (status == napi_invalid_arg) { - THREADLOG("N-API Error: The thread-safe function is closed.\n") - state = AlreadyFinalized; - } else { - THREADLOG("N-API Error: A generic error occurred when attemping to " - "add to the queue.\n") - state = AlreadyFinalized; - } - context->callFailed(data); - } - - THREADLOG_MAIN("Thread " << threadId << " finished. State: " - << (state == Closing ? "Closing" - : state == AlreadyFinalized - ? "Already Finalized" - : "Release") - << "\n") - - if (state == Release) { - tsfn.Release(); - } -#undef THREADLOG -#undef THREADLOG_MAIN - } - - // TSFN finalizer. Joins the threads and resolves the Promise returned by - // `Release()` above. - static void Finalizer(Napi::Env env, FinalizerDataType *finalizeDataPtr, - Context *ctx) { - - auto &finalizeData(*finalizeDataPtr); - auto outstanding = finalizeData.outstandingCalls.size(); - if (ctx->logThread) { - std::cout << "[Finalize] [Native ] Joining threads (" << outstanding - << " outstanding requests)...\n"; - } - if (outstanding > 0) { - for (auto &request : finalizeData.outstandingCalls) { - request->promise.set_value( - CallJsResult{-1, true, "The TSFN has been finalized."}); - } - } - for (auto &thread : finalizeData.threads) { - thread.join(); - } - - ctx->clearTSFN(); - if (ctx->logThread) { - std::cout << "[Finalize] [Native ] Threads joined.\n"; - } - - finalizeData.deferred->Resolve(Boolean::New(env, true)); - } - - // This method does not run on the Node thread. - void clearTSFN() { _tsfn = TSFN(); } - - // This method does not run on the Node thread. - void callSucceeded(DataType data, int result) { - std::lock_guard lock(finalizerData.callMutex); - succeededCalls++; - aggregate += result; - - auto &calls = finalizerData.outstandingCalls; - auto it = std::find_if( - calls.begin(), calls.end(), - [&](std::shared_ptr const &p) { return p.get() == data.get(); }); - - if (it != calls.end()) { - calls.erase(it); - } - } - - // This method does not run on the Node thread. - void callFailed(DataType data) { - std::lock_guard lock(finalizerData.callMutex); - auto &calls = finalizerData.outstandingCalls; - auto it = - std::find_if(calls.begin(), calls.end(), - [&](std::shared_ptr const &p) { return p == data; }); - - if (it != calls.end()) { - calls.erase(it); - } - } - - DataType makeNewCall(size_t threadId, int callId, bool logCall, - int callDelay) { - // x - // auto &calls(finalizerData.outstandingCalls); - finalizerData.outstandingCalls.emplace_back(std::make_shared()); - auto data(finalizerData.outstandingCalls.back()); - data->threadId = threadId; - data->logCall = logCall; - data->callDelay = callDelay; - data->base = threadId + 1; - data->callId = callId; - - return data; - } -}; -} // namespace example - -Object InitThreadSafeFunctionExExample(Env env) { - auto exports(Object::New(env)); - example::TSFNWrap::Init(env, exports, "example"); - return exports; -} - -#endif diff --git a/test/threadsafe_function_ex/test/example.js b/test/threadsafe_function_ex/test/example.js deleted file mode 100644 index 2afa36de8..000000000 --- a/test/threadsafe_function_ex/test/example.js +++ /dev/null @@ -1,342 +0,0 @@ -// @ts-check -'use strict'; -const assert = require('assert'); -const { TestRunner } = require('../util/TestRunner'); - -/** - * @typedef {(threadId: number, callId: number, logCall: boolean, value: number, callDelay: - * number)=>number|Promise} TSFNCallback - */ - -/** - * @typedef {Object} ThreadOptions - * @property {number} calls - * @property {number} callDelay - * @property {number} threadDelay - */ - -/** - * The options when starting the addon's TSFN. - * @typedef {Object} StartOptions - * @property {ThreadOptions[]} threads - * @property {boolean} [logCall] If `true`, log messages via `console.log`. - * @property {boolean} [logThread] If `true`, log messages via `std::cout`. - * @property {number} [acquireFactor] Acquire a new set of \`acquireFactor\` - * call threads. `NAPI_CPP_EXCEPTIONS`, allowing errors to be caught as - * exceptions. - * @property {TSFNCallback} callback The callback provided to the threadsafe - * function. - * @property {[number,number]} [callbackError] Tuple of `[threadId, callId]` to - * cause an error on -*/ - -/** - * Returns test options. - * @type {() => { options: StartOptions, calls: { aggregate: number } }} - */ -const getTestDetails = () => { - const TEST_CALLS = [1, 2, 3, 4, 5]; - const TEST_ACQUIRE = 1; - const TEST_CALL_DELAY = [400, 200, 100, 50, 0] - const TEST_THREAD_DELAY = TEST_CALL_DELAY.map(_ => _); - const TEST_LOG_CALL = false; - const TEST_LOG_THREAD = false; - const TEST_NO_CALLBACK = false; - - /** @type {[number, number] | undefined} [threadId, callId] */ - const TEST_CALLBACK_ERROR = undefined; - - // Set options as defaults - let testCalls = TEST_CALLS; - let testAcquire = TEST_ACQUIRE; - let testCallDelay = TEST_CALL_DELAY; - let testThreadDelay = TEST_THREAD_DELAY; - let testLogCall = TEST_LOG_CALL; - let testLogThread = TEST_LOG_THREAD; - let testNoCallback = TEST_NO_CALLBACK; - let testCallbackError = TEST_CALLBACK_ERROR; - - let args = process.argv.slice(2); - let arg; - - const showHelp = () => { - console.log( - ` -Usage: ${process.argv0} .${process.argv[1].replace(process.cwd(), '')} [options] - - -c, --calls The number of calls each thread should make (number[]). - -a, --acquire [factor] Acquire a new set of \`factor\` call threads, using the - same \`calls\` definition. - -d, --call-delay The delay on callback resolution that each thread should - have (number[]). This is achieved via a delayed Promise - resolution in the JavaScript callback provided to the - TSFN. Using large delays here will cause all threads to - bottle-neck. - -D, --thread-delay The delay that each thread should have prior to making a - call (number[]). Using large delays here will cause the - individual thread to bottle-neck. - -l, --log-call Display console.log-based logging messages. - -L, --log-thread Display std::cout-based logging messages. - -n, --no-callback Do not use a JavaScript callback. - -e, --callback-error [thread[.call]] Cause an error to occur in the JavaScript callback for - the given thread's call (if provided; first thread's - first call otherwise). - - When not provided: - - defaults to [${TEST_CALLS}] - - [factor] defaults to ${TEST_ACQUIRE} - - defaults to [${TEST_CALL_DELAY}] - - defaults to [${TEST_THREAD_DELAY}] - - -Examples: - - -c [1,2,3] -l -L - - Creates three threads that makes one, two, and three calls each, respectively. - - -c [5,5] -d [5000,5000] -D [0,0] -l -L - - Creates two threads that make five calls each. In this scenario, the threads will be - blocked primarily on waiting for the callback to resolve, as each thread's call takes - 5000 milliseconds. -` - ); - return undefined; - }; - - while ((arg = args.shift())) { - switch (arg) { - case "-h": - case "--help": - return showHelp(); - - case "--calls": - case "-c": - try { - testCalls = JSON.parse(args.shift()); - } catch (ex) { /* ignore */ } - break; - - case "--acquire": - case "-a": - testAcquire = parseInt(args[0]); - if (!isNaN(testAcquire)) { - args.shift(); - } else { - testAcquire = TEST_ACQUIRE; - } - break; - - case "--call-delay": - case "-d": - try { - testCallDelay = JSON.parse(args.shift()); - } catch (ex) { /* ignore */ } - break; - - case "--thread-delay": - case "-D": - try { - testThreadDelay = JSON.parse(args.shift()); - } catch (ex) { /* ignore */ } - break; - - case "--log-call": - case "-l": - testLogCall = true; - break; - - case "--log-thread": - case "-L": - testLogThread = true; - break; - - case "--no-callback": - case "-n": - testNoCallback = true; - break; - - case "-e": - case "--callback-error": - try { - if (!args[0].startsWith("-")) { - const split = args.shift().split(/\./); - testCallbackError = [parseInt(split[0], 10) || 0, parseInt(split[1], 10) || 0]; - } - } - catch (ex) { /*ignore*/ } - finally { - if (!testCallbackError) { - testCallbackError = [0, 0]; - } - } - break; - - default: - console.error("Unknown option:", arg); - return showHelp(); - } - } - - if (testCallbackError && testNoCallback) { - console.error("--error cannot be used in conjunction with --no-callback"); - return undefined; - } - - testCalls = Array.isArray(testCalls) ? testCalls : TEST_CALLS; - - const calls = { aggregate: testNoCallback ? null : 0 }; - - /** - * The JavaScript callback provided to our TSFN. - * @callback TSFNCallback - * @param {number} threadId Thread Id - * @param {number} callId Call Id - * @param {boolean} logCall If true, log messages to console regarding this - * call. - * @param {number} base The input as calculated from CallJs - * @param {number} callDelay If `> 0`, return a `Promise` that resolves with - * `value` after `callDelay` milliseconds. Otherwise, return a `number` - * whose value is `value`. - */ - - /** @type {undefined | TSFNCallback} */ - const callback = testNoCallback ? undefined : (threadId, callId, logCall, base, callDelay) => { - // Calculate the result value as `base * base`. - const value = base * base; - - // Add the value to our call aggregate - calls.aggregate += value; - - if (testCallbackError !== undefined && testCallbackError[0] === threadId && testCallbackError[1] === callId) { - return new Error(`Test throw error for ${threadId}.${callId}`); - } - - // If `callDelay > 0`, then return a Promise that resolves with `value` after - // `callDelay` milliseconds. - if (callDelay > 0) { - // Logging messages. - if (logCall) { - console.log(`[Thread ${threadId}] [Callback] [Call ${callId}] Receive request: base = ${base}, delay = ${callDelay}ms`); - } - - const start = Date.now(); - - return new Promise(resolve => setTimeout(() => { - // Logging messages. - if (logCall) { - console.log(`[Thread ${threadId}] [Callback] [Call ${callId}] Answer request: base = ${base}, value = ${value} after ${Date.now() - start}ms`); - } - resolve(value); - }, callDelay)); - } - - // Otherwise, return a `number` whose value is `value`. - else { - // Logging messages. - if (logCall) { - console.log(`[Thread ${threadId}] [Callback] [Call ${callId}] Receive, answer request: base = ${base}, value = ${value}`); - } - return value; - } - }; - - return { - options: { - // Construct `ThreadOption[] threads` from `number[] testCalls` - threads: testCalls.map((callCount, index) => ({ - calls: callCount, - callDelay: testCallDelay !== null && typeof testCallDelay[index] === 'number' ? testCallDelay[index] : 0, - threadDelay: testThreadDelay !== null && typeof testThreadDelay[index] === 'number' ? testThreadDelay[index] : 0, - })), - logCall: testLogCall, - logThread: testLogThread, - acquireFactor: testAcquire, - callback, - callbackError: testCallbackError - }, - calls - }; -} - -class ExampleTest extends TestRunner { - - async example({ TSFNWrap }) { - - /** - * @typedef {Object} TSFNWrap - * @property {(opts: StartOptions) => number} start Start the TSFN. Returns - * the number of threads started. - * @property {() => Promise} release Release the TSFN. - * @property {() => [number, number]} callCount Returns the call aggregates - * as counted by the TSFN: - * - `[0]`: The sum of the number of calls by each thread. - * - `[1]`: The sum of the `value`s returned by each call by each thread. - * @property {(threads: ThreadOptions[]) => number} acquire - */ - - /** @type {TSFNWrap} */ - const tsfn = new TSFNWrap(); - - const testDetails = getTestDetails(); - if (testDetails === undefined) { - throw new Error("No test details"); - } - const { options } = testDetails; - const { acquireFactor, threads, callback, callbackError } = options; - - /** - * Start the TSFN with the given options. This will create the TSFN with initial - * thread count of `threads.length + 1` (+1 due to the Node thread using the TSFN) - */ - const startedActual = tsfn.start(options); - - /** - * The initial - */ - const threadsPerSet = threads.length; - - /** - * Calculate the expected results. Create a new list of thread options by - * concatinating `threads` by `acquireFactor` times. - */ - const startedThreads = [...new Array(acquireFactor)].map(_ => threads).reduce((p, c) => p.concat(c), []); - - const expected = startedThreads.reduce((p, threadCallCount, threadId) => ( - ++threadId, - ++p.threadCount, - p.callCount += threadCallCount.calls, - p.aggregate += threadCallCount.calls * threadId ** 2, - p.callbackAggregate = p.callbackAggregate === null ? null : p.aggregate, - p - ), { threadCount: 0, callCount: 0, aggregate: 0, callbackAggregate: callback ? 0 : null }); - - if (typeof startedActual === 'number') { - const { threadCount, callCount, aggregate, callbackAggregate } = expected; - assert(startedActual === threadsPerSet, `The number of threads when starting the TSFN do not match: actual = ${startedActual}, expected = ${threadsPerSet}`) - for (let i = 1; i < acquireFactor; ++i) { - const acquiredActual = tsfn.acquire(threads); - assert(acquiredActual === threadsPerSet, `The number of threads when acquiring a new set of threads do not match: actual = ${acquiredActual}, expected = ${threadsPerSet}`) - } - const released = await tsfn.release(); - const [callCountActual, aggregateActual] = tsfn.callCount(); - const { calls } = testDetails; - const { aggregate: actualCallAggregate } = calls; - if (!callbackError) { - assert(callCountActual === callCount, `The number of calls do not match: actual = ${callCountActual}, expected = ${callCount}`); - assert(aggregateActual === aggregate, `The aggregate of calls do not match: actual = ${aggregateActual}, expected = ${aggregate}`); - assert(actualCallAggregate === callbackAggregate, `The number aggregated by the JavaScript callback and the thread calculated aggregate do not match: actual ${actualCallAggregate}, expected = ${aggregate}`) - return { released, ...expected }; - } - // The test runner erases the last line, so write an empty line. - if (options.logCall) { console.log(); } - return released; - } else { - throw new Error('The TSFN failed to start'); - } - } - -} - -module.exports = new ExampleTest('threadsafe_function_ex_example', __filename).start(); diff --git a/test/threadsafe_function_ex/test/threadsafe.js b/test/threadsafe_function_ex/test/threadsafe.js deleted file mode 100644 index 2d807b040..000000000 --- a/test/threadsafe_function_ex/test/threadsafe.js +++ /dev/null @@ -1,182 +0,0 @@ -'use strict'; - -const buildType = process.config.target_defaults.default_configuration; -const assert = require('assert'); -const common = require('../../common'); - -module.exports = run() - .catch((e) => { - console.error(`Test failed!`, e); - process.exit(1); - }); - -async function run() { - console.log(`Running tests in .${__filename.replace(process.cwd(),'')}`); - await test(require(`../../build/${buildType}/binding.node`)); - await test(require(`../../build/${buildType}/binding_noexcept.node`)); -} - -/** - * This spec replicates the non-`Ex` multi-threaded spec using the `Ex` API. - */ -function test(binding) { - const expectedArray = (function(arrayLength) { - const result = []; - for (let index = 0; index < arrayLength; index++) { - result.push(arrayLength - 1 - index); - } - return result; - })(binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH); - - function testWithJSMarshaller({ - threadStarter, - quitAfter, - abort, - maxQueueSize, - launchSecondary }) { - return new Promise((resolve) => { - const array = []; - binding.threadsafe_function_ex_threadsafe[threadStarter](function testCallback(value) { - array.push(value); - if (array.length === quitAfter) { - setImmediate(() => { - binding.threadsafe_function_ex_threadsafe.stopThread(common.mustCall(() => { - resolve(array); - }), !!abort); - }); - } - }, !!abort, !!launchSecondary, maxQueueSize); - if (threadStarter === 'startThreadNonblocking') { - // Let's make this thread really busy for a short while to ensure that - // the queue fills and the thread receives a napi_queue_full. - const start = Date.now(); - while (Date.now() - start < 200); - } - }); - } - - return new Promise(function testWithoutJSMarshaller(resolve) { - let callCount = 0; - binding.threadsafe_function_ex_threadsafe.startThreadNoNative(function testCallback() { - callCount++; - - // The default call-into-JS implementation passes no arguments. - assert.strictEqual(arguments.length, 0); - if (callCount === binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH) { - setImmediate(() => { - binding.threadsafe_function_ex_threadsafe.stopThread(common.mustCall(() => { - resolve(); - }), false); - }); - } - }, false /* abort */, false /* launchSecondary */, - binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE); - }) - - // Start the thread in blocking mode, and assert that all values are passed. - // Quit after it's done. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThread', - maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, - quitAfter: binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH - })) - .then((result) => assert.deepStrictEqual(result, expectedArray)) - - // Start the thread in blocking mode with an infinite queue, and assert that - // all values are passed. Quit after it's done. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThread', - maxQueueSize: 0, - quitAfter: binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH - })) - .then((result) => assert.deepStrictEqual(result, expectedArray)) - - // Start the thread in non-blocking mode, and assert that all values are - // passed. Quit after it's done. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThreadNonblocking', - maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, - quitAfter: binding.threadsafe_function_ex_threadsafe.ARRAY_LENGTH - })) - .then((result) => assert.deepStrictEqual(result, expectedArray)) - - // Start the thread in blocking mode, and assert that all values are passed. - // Quit early, but let the thread finish. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThread', - maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, - quitAfter: 1 - })) - .then((result) => assert.deepStrictEqual(result, expectedArray)) - - // Start the thread in blocking mode with an infinite queue, and assert that - // all values are passed. Quit early, but let the thread finish. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThread', - maxQueueSize: 0, - quitAfter: 1 - })) - .then((result) => assert.deepStrictEqual(result, expectedArray)) - - - // Start the thread in non-blocking mode, and assert that all values are - // passed. Quit early, but let the thread finish. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThreadNonblocking', - maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, - quitAfter: 1 - })) - .then((result) => assert.deepStrictEqual(result, expectedArray)) - - // Start the thread in blocking mode, and assert that all values are passed. - // Quit early, but let the thread finish. Launch a secondary thread to test - // the reference counter incrementing functionality. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThread', - quitAfter: 1, - maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, - launchSecondary: true - })) - .then((result) => assert.deepStrictEqual(result, expectedArray)) - - // Start the thread in non-blocking mode, and assert that all values are - // passed. Quit early, but let the thread finish. Launch a secondary thread - // to test the reference counter incrementing functionality. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThreadNonblocking', - quitAfter: 1, - maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, - launchSecondary: true - })) - .then((result) => assert.deepStrictEqual(result, expectedArray)) - - // Start the thread in blocking mode, and assert that it could not finish. - // Quit early by aborting. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThread', - quitAfter: 1, - maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, - abort: true - })) - .then((result) => assert.strictEqual(result.indexOf(0), -1)) - - // Start the thread in blocking mode with an infinite queue, and assert that - // it could not finish. Quit early by aborting. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThread', - quitAfter: 1, - maxQueueSize: 0, - abort: true - })) - .then((result) => assert.strictEqual(result.indexOf(0), -1)) - - // Start the thread in non-blocking mode, and assert that it could not finish. - // Quit early and aborting. - .then(() => testWithJSMarshaller({ - threadStarter: 'startThreadNonblocking', - quitAfter: 1, - maxQueueSize: binding.threadsafe_function_ex_threadsafe.MAX_QUEUE_SIZE, - abort: true - })) - .then((result) => assert.strictEqual(result.indexOf(0), -1)) -} diff --git a/test/threadsafe_function_ex/test/threadsafe.cc b/test/threadsafe_function_ex/threadsafe_function.cc similarity index 99% rename from test/threadsafe_function_ex/test/threadsafe.cc rename to test/threadsafe_function_ex/threadsafe_function.cc index cf3eddca8..cb604c17e 100644 --- a/test/threadsafe_function_ex/test/threadsafe.cc +++ b/test/threadsafe_function_ex/threadsafe_function.cc @@ -174,7 +174,7 @@ static Value StartThreadNoNative(const CallbackInfo& info) { return StartThreadInternal(info, ThreadSafeFunctionInfo::DEFAULT); } -Object InitThreadSafeFunctionExThreadSafe(Env env) { +Object InitThreadSafeFunctionEx(Env env) { for (size_t index = 0; index < ARRAY_LENGTH; index++) { ints[index] = index; } diff --git a/test/threadsafe_function_ex/threadsafe_function.js b/test/threadsafe_function_ex/threadsafe_function.js new file mode 100644 index 000000000..9be4b26dd --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function.js @@ -0,0 +1,194 @@ +'use strict'; + +const buildType = process.config.target_defaults.default_configuration; +const assert = require('assert'); +const common = require('../common'); + +module.exports = (async function() { + await test(require(`../build/${buildType}/binding.node`)); + await test(require(`../build/${buildType}/binding_noexcept.node`)); +})(); + +async function test(binding) { + const expectedArray = (function(arrayLength) { + const result = []; + for (let index = 0; index < arrayLength; index++) { + result.push(arrayLength - 1 - index); + } + return result; + })(binding.threadsafe_function_ex.ARRAY_LENGTH); + + function testWithJSMarshaller({ + threadStarter, + quitAfter, + abort, + maxQueueSize, + launchSecondary }) { + return new Promise((resolve) => { + const array = []; + binding.threadsafe_function_ex[threadStarter](function testCallback(value) { + array.push(value); + if (array.length === quitAfter) { + setImmediate(() => { + binding.threadsafe_function_ex.stopThread(common.mustCall(() => { + resolve(array); + }), !!abort); + }); + } + }, !!abort, !!launchSecondary, maxQueueSize); + if (threadStarter === 'startThreadNonblocking') { + // Let's make this thread really busy for a short while to ensure that + // the queue fills and the thread receives a napi_queue_full. + const start = Date.now(); + while (Date.now() - start < 200); + } + }); + } + + await new Promise(function testWithoutJSMarshaller(resolve) { + let callCount = 0; + binding.threadsafe_function_ex.startThreadNoNative(function testCallback() { + callCount++; + + // The default call-into-JS implementation passes no arguments. + assert.strictEqual(arguments.length, 0); + if (callCount === binding.threadsafe_function_ex.ARRAY_LENGTH) { + setImmediate(() => { + binding.threadsafe_function_ex.stopThread(common.mustCall(() => { + resolve(); + }), false); + }); + } + }, false /* abort */, false /* launchSecondary */, + binding.threadsafe_function_ex.MAX_QUEUE_SIZE); + }); + + // Start the thread in blocking mode, and assert that all values are passed. + // Quit after it's done. + assert.deepStrictEqual( + await testWithJSMarshaller({ + threadStarter: 'startThread', + maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + quitAfter: binding.threadsafe_function_ex.ARRAY_LENGTH + }), + expectedArray, + ); + + // Start the thread in blocking mode with an infinite queue, and assert that + // all values are passed. Quit after it's done. + assert.deepStrictEqual( + await testWithJSMarshaller({ + threadStarter: 'startThread', + maxQueueSize: 0, + quitAfter: binding.threadsafe_function_ex.ARRAY_LENGTH + }), + expectedArray, + ); + + // Start the thread in non-blocking mode, and assert that all values are + // passed. Quit after it's done. + assert.deepStrictEqual( + await testWithJSMarshaller({ + threadStarter: 'startThreadNonblocking', + maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + quitAfter: binding.threadsafe_function_ex.ARRAY_LENGTH + }), + expectedArray, + ); + + // Start the thread in blocking mode, and assert that all values are passed. + // Quit early, but let the thread finish. + assert.deepStrictEqual( + await testWithJSMarshaller({ + threadStarter: 'startThread', + maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + quitAfter: 1 + }), + expectedArray, + ); + + // Start the thread in blocking mode with an infinite queue, and assert that + // all values are passed. Quit early, but let the thread finish. + assert.deepStrictEqual( + await testWithJSMarshaller({ + threadStarter: 'startThread', + maxQueueSize: 0, + quitAfter: 1 + }), + expectedArray, + ); + + + // Start the thread in non-blocking mode, and assert that all values are + // passed. Quit early, but let the thread finish. + assert.deepStrictEqual( + await testWithJSMarshaller({ + threadStarter: 'startThreadNonblocking', + maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + quitAfter: 1 + }), + expectedArray, + ); + + // Start the thread in blocking mode, and assert that all values are passed. + // Quit early, but let the thread finish. Launch a secondary thread to test + // the reference counter incrementing functionality. + assert.deepStrictEqual( + await testWithJSMarshaller({ + threadStarter: 'startThread', + quitAfter: 1, + maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + launchSecondary: true + }), + expectedArray, + ); + + // Start the thread in non-blocking mode, and assert that all values are + // passed. Quit early, but let the thread finish. Launch a secondary thread + // to test the reference counter incrementing functionality. + assert.deepStrictEqual( + await testWithJSMarshaller({ + threadStarter: 'startThreadNonblocking', + quitAfter: 1, + maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + launchSecondary: true + }), + expectedArray, + ); + + // Start the thread in blocking mode, and assert that it could not finish. + // Quit early by aborting. + assert.strictEqual( + (await testWithJSMarshaller({ + threadStarter: 'startThread', + quitAfter: 1, + maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + abort: true + })).indexOf(0), + -1, + ); + + // Start the thread in blocking mode with an infinite queue, and assert that + // it could not finish. Quit early by aborting. + assert.strictEqual( + (await testWithJSMarshaller({ + threadStarter: 'startThread', + quitAfter: 1, + maxQueueSize: 0, + abort: true + })).indexOf(0), + -1, + ); + + // Start the thread in non-blocking mode, and assert that it could not finish. + // Quit early and aborting. + assert.strictEqual( + (await testWithJSMarshaller({ + threadStarter: 'startThreadNonblocking', + quitAfter: 1, + maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + abort: true + })).indexOf(0), + -1, + ); +} \ No newline at end of file diff --git a/test/threadsafe_function_ex/threadsafe_function_ctx.cc b/test/threadsafe_function_ex/threadsafe_function_ctx.cc new file mode 100644 index 000000000..f186fc845 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_ctx.cc @@ -0,0 +1,62 @@ +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +using ContextType = Reference; +using TSFN = ThreadSafeFunctionEx; + +namespace { + +class TSFNWrap : public ObjectWrap { +public: + static Object Init(Napi::Env env, Object exports); + TSFNWrap(const CallbackInfo &info); + + Napi::Value GetContext(const CallbackInfo & /*info*/) { + ContextType *ctx = _tsfn.GetContext(); + return ctx->Value(); + }; + + Napi::Value Release(const CallbackInfo &info) { + Napi::Env env = info.Env(); + _deferred = std::unique_ptr(new Promise::Deferred(env)); + _tsfn.Release(); + return _deferred->Promise(); + }; + +private: + TSFN _tsfn; + std::unique_ptr _deferred; +}; + +Object TSFNWrap::Init(Napi::Env env, Object exports) { + Function func = + DefineClass(env, "TSFNWrap", + {InstanceMethod("getContext", &TSFNWrap::GetContext), + InstanceMethod("release", &TSFNWrap::Release)}); + + exports.Set("TSFNWrap", func); + return exports; +} + +TSFNWrap::TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { + ContextType *_ctx = new ContextType; + *_ctx = Persistent(info[0]); + + _tsfn = TSFN::New(info.Env(), this->Value(), "Test", 1, 1, _ctx, + [this](Napi::Env env, void *, ContextType *ctx) { + _deferred->Resolve(env.Undefined()); + ctx->Reset(); + delete ctx; + }); +} + +} // namespace + +Object InitThreadSafeFunctionExCtx(Env env) { + return TSFNWrap::Init(env, Object::New(env)); +} + +#endif diff --git a/test/threadsafe_function_ex/threadsafe_function_ctx.js b/test/threadsafe_function_ex/threadsafe_function_ctx.js new file mode 100644 index 000000000..2651586a0 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_ctx.js @@ -0,0 +1,14 @@ +'use strict'; + +const assert = require('assert'); +const buildType = process.config.target_defaults.default_configuration; + +module.exports = test(require(`../build/${buildType}/binding.node`)) + .then(() => test(require(`../build/${buildType}/binding_noexcept.node`))); + +async function test(binding) { + const ctx = { }; + const tsfn = new binding.threadsafe_function_ctx.TSFNWrap(ctx); + assert(tsfn.getContext() === ctx); + await tsfn.release(); +} diff --git a/test/threadsafe_function_ex/threadsafe_function_existing_tsfn.cc b/test/threadsafe_function_ex/threadsafe_function_existing_tsfn.cc new file mode 100644 index 000000000..fc28a06c5 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_existing_tsfn.cc @@ -0,0 +1,114 @@ +#include "napi.h" +#include + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace { + +struct TestContext { + TestContext(Promise::Deferred &&deferred) + : deferred(std::move(deferred)), callData(nullptr){}; + + napi_threadsafe_function tsfn; + Promise::Deferred deferred; + double *callData; + + ~TestContext() { + if (callData != nullptr) + delete callData; + }; +}; + +using TSFN = ThreadSafeFunctionEx; + +void FinalizeCB(napi_env env, void * /*finalizeData */, void *context) { + TestContext *testContext = static_cast(context); + if (testContext->callData != nullptr) { + testContext->deferred.Resolve(Number::New(env, *testContext->callData)); + } else { + testContext->deferred.Resolve(Napi::Env(env).Undefined()); + } + delete testContext; +} + +void CallJSWithData(napi_env env, napi_value /* callback */, void *context, + void *data) { + TestContext *testContext = static_cast(context); + testContext->callData = static_cast(data); + + napi_status status = + napi_release_threadsafe_function(testContext->tsfn, napi_tsfn_release); + + NAPI_THROW_IF_FAILED_VOID(env, status); +} + +void CallJSNoData(napi_env env, napi_value /* callback */, void *context, + void * /*data*/) { + TestContext *testContext = static_cast(context); + testContext->callData = nullptr; + + napi_status status = + napi_release_threadsafe_function(testContext->tsfn, napi_tsfn_release); + + NAPI_THROW_IF_FAILED_VOID(env, status); +} + +static Value TestCall(const CallbackInfo &info) { + Napi::Env env = info.Env(); + bool isBlocking = false; + bool hasData = false; + if (info.Length() > 0) { + Object opts = info[0].As(); + if (opts.Has("blocking")) { + isBlocking = opts.Get("blocking").ToBoolean(); + } + if (opts.Has("data")) { + hasData = opts.Get("data").ToBoolean(); + } + } + + // Allow optional callback passed from JS. Useful for testing. + Function cb = Function::New(env, [](const CallbackInfo & /*info*/) {}); + + TestContext *testContext = new TestContext(Napi::Promise::Deferred(env)); + + napi_status status = napi_create_threadsafe_function( + env, cb, Object::New(env), String::New(env, "Test"), 0, 1, + nullptr, /*finalize data*/ + FinalizeCB, testContext, hasData ? CallJSWithData : CallJSNoData, + &testContext->tsfn); + + NAPI_THROW_IF_FAILED(env, status, Value()); + + TSFN wrapped = TSFN(testContext->tsfn); + + // Test the four napi_threadsafe_function direct-accessing calls + if (isBlocking) { + if (hasData) { + wrapped.BlockingCall(new double(std::rand())); + } else { + wrapped.BlockingCall(nullptr); + } + } else { + if (hasData) { + wrapped.NonBlockingCall(new double(std::rand())); + } else { + wrapped.NonBlockingCall(nullptr); + } + } + + return testContext->deferred.Promise(); +} + +} // namespace + +Object InitThreadSafeFunctionExExistingTsfn(Env env) { + Object exports = Object::New(env); + exports["testCall"] = Function::New(env, TestCall); + + return exports; +} + +#endif diff --git a/test/threadsafe_function_ex/threadsafe_function_existing_tsfn.js b/test/threadsafe_function_ex/threadsafe_function_existing_tsfn.js new file mode 100644 index 000000000..d42517d9f --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_existing_tsfn.js @@ -0,0 +1,17 @@ +'use strict'; + +const assert = require('assert'); + +const buildType = process.config.target_defaults.default_configuration; + +module.exports = test(require(`../build/${buildType}/binding.node`)) + .then(() => test(require(`../build/${buildType}/binding_noexcept.node`))); + +async function test(binding) { + const testCall = binding.threadsafe_function_ex_existing_tsfn.testCall; + + assert.strictEqual(typeof await testCall({ blocking: true, data: true }), "number"); + assert.strictEqual(typeof await testCall({ blocking: true, data: false }), "undefined"); + assert.strictEqual(typeof await testCall({ blocking: false, data: true }), "number"); + assert.strictEqual(typeof await testCall({ blocking: false, data: false }), "undefined"); +} diff --git a/test/threadsafe_function_ex/threadsafe_function_ptr.cc b/test/threadsafe_function_ex/threadsafe_function_ptr.cc new file mode 100644 index 000000000..c8810ce50 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_ptr.cc @@ -0,0 +1,28 @@ +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace { + +using TSFN = ThreadSafeFunctionEx<>; + +static Value Test(const CallbackInfo& info) { + Object resource = info[0].As(); + Function cb = info[1].As(); + TSFN tsfn = TSFN::New(info.Env(), cb, resource, "Test", 1, 1); + tsfn.Release(); + return info.Env().Undefined(); +} + +} + +Object InitThreadSafeFunctionExPtr(Env env) { + Object exports = Object::New(env); + exports["test"] = Function::New(env, Test); + + return exports; +} + +#endif diff --git a/test/threadsafe_function_ex/threadsafe_function_ptr.js b/test/threadsafe_function_ex/threadsafe_function_ptr.js new file mode 100644 index 000000000..1276d0930 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_ptr.js @@ -0,0 +1,10 @@ +'use strict'; + +const buildType = process.config.target_defaults.default_configuration; + +test(require(`../build/${buildType}/binding.node`)); +test(require(`../build/${buildType}/binding_noexcept.node`)); + +function test(binding) { + binding.threadsafe_function_ex_ptr.test({}, () => {}); +} diff --git a/test/threadsafe_function_ex/threadsafe_function_sum.cc b/test/threadsafe_function_ex/threadsafe_function_sum.cc new file mode 100644 index 000000000..69c8a8fb2 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_sum.cc @@ -0,0 +1,220 @@ +#include "napi.h" +#include +#include +#include +#include + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace { + +struct TestData { + + TestData(Promise::Deferred &&deferred) : deferred(std::move(deferred)){}; + + // Native Promise returned to JavaScript + Promise::Deferred deferred; + + // List of threads created for test. This list only ever accessed via main + // thread. + std::vector threads = {}; + + // These variables are only accessed from the main thread. + bool mainWantsRelease = false; + size_t expected_calls = 0; + + static void CallJs(Napi::Env env, Function callback, TestData *testData, + double *data) { + + // This lambda runs on the main thread so it's OK to access the variables + // `expected_calls` and `mainWantsRelease`. + testData->expected_calls--; + if (testData->expected_calls == 0 && testData->mainWantsRelease) + testData->tsfn.Release(); + callback.Call({Number::New(env, *data)}); + delete data; + } + + ThreadSafeFunctionEx tsfn; +}; + +using TSFN = ThreadSafeFunctionEx; + +void FinalizerCallback(Napi::Env env, void *, TestData *finalizeData) { + for (size_t i = 0; i < finalizeData->threads.size(); ++i) { + finalizeData->threads[i].join(); + } + finalizeData->deferred.Resolve(Boolean::New(env, true)); + delete finalizeData; +} + +/** + * See threadsafe_function_sum.js for descriptions of the tests in this file + */ + +void entryWithTSFN(TSFN tsfn, int threadId) { + std::this_thread::sleep_for(std::chrono::milliseconds(std::rand() % 100 + 1)); + tsfn.BlockingCall(new double(threadId)); + tsfn.Release(); +} + +static Value TestWithTSFN(const CallbackInfo &info) { + int threadCount = info[0].As().Int32Value(); + Function cb = info[1].As(); + + // We pass the test data to the Finalizer for cleanup. The finalizer is + // responsible for deleting this data as well. + TestData *testData = new TestData(Promise::Deferred::New(info.Env())); + + TSFN tsfn = TSFN::New( + info.Env(), cb, "Test", 0, threadCount, testData, + std::function(FinalizerCallback), testData); + + for (int i = 0; i < threadCount; ++i) { + // A copy of the ThreadSafeFunction will go to the thread entry point + testData->threads.push_back(std::thread(entryWithTSFN, tsfn, i)); + } + + return testData->deferred.Promise(); +} + +// Task instance created for each new std::thread +class DelayedTSFNTask { +public: + // Each instance has its own tsfn + TSFN tsfn; + + // Thread-safety + std::mutex mtx; + std::condition_variable cv; + + // Entry point for std::thread + void entryDelayedTSFN(int threadId) { + std::unique_lock lk(mtx); + cv.wait(lk); + tsfn.BlockingCall(new double(threadId)); + tsfn.Release(); + }; +}; + +struct TestDataDelayed : TestData { + + TestDataDelayed(Promise::Deferred &&deferred) + : TestData(std::move(deferred)){}; + ~TestDataDelayed() { taskInsts.clear(); }; + + // List of DelayedTSFNThread instances + std::vector> taskInsts = {}; +}; + +void FinalizerCallbackDelayed(Napi::Env env, TestDataDelayed *finalizeData, + TestData *) { + for (size_t i = 0; i < finalizeData->threads.size(); ++i) { + finalizeData->threads[i].join(); + } + finalizeData->deferred.Resolve(Boolean::New(env, true)); + delete finalizeData; +} + +static Value TestDelayedTSFN(const CallbackInfo &info) { + int threadCount = info[0].As().Int32Value(); + Function cb = info[1].As(); + + TestDataDelayed *testData = + new TestDataDelayed(Promise::Deferred::New(info.Env())); + + testData->tsfn = TSFN::New(info.Env(), cb, "Test", 0, threadCount, testData, + std::function( + FinalizerCallbackDelayed), + testData); + + for (int i = 0; i < threadCount; ++i) { + testData->taskInsts.push_back( + std::unique_ptr(new DelayedTSFNTask())); + testData->threads.push_back(std::thread(&DelayedTSFNTask::entryDelayedTSFN, + testData->taskInsts.back().get(), + i)); + } + std::this_thread::sleep_for(std::chrono::milliseconds(std::rand() % 100 + 1)); + + for (auto &task : testData->taskInsts) { + std::lock_guard lk(task->mtx); + task->tsfn = testData->tsfn; + task->cv.notify_all(); + } + + return testData->deferred.Promise(); +} + +void AcquireFinalizerCallback(Napi::Env env, TestData *finalizeData, + TestData *context) { + (void)context; + for (size_t i = 0; i < finalizeData->threads.size(); ++i) { + finalizeData->threads[i].join(); + } + finalizeData->deferred.Resolve(Boolean::New(env, true)); + delete finalizeData; +} + +void entryAcquire(TSFN tsfn, int threadId) { + tsfn.Acquire(); + std::this_thread::sleep_for(std::chrono::milliseconds(std::rand() % 100 + 1)); + tsfn.BlockingCall(new double(threadId)); + tsfn.Release(); +} + +static Value CreateThread(const CallbackInfo &info) { + TestData *testData = static_cast(info.Data()); + // Counting expected calls like this only works because on the JS side this + // binding is called from a synchronous loop. This means the main loop has no + // chance to run the tsfn JS callback before we've counted how many threads + // the JS intends to create. + testData->expected_calls++; + TSFN tsfn = testData->tsfn; + int threadId = testData->threads.size(); + // A copy of the ThreadSafeFunction will go to the thread entry point + testData->threads.push_back(std::thread(entryAcquire, tsfn, threadId)); + return Number::New(info.Env(), threadId); +} + +static Value StopThreads(const CallbackInfo &info) { + TestData *testData = static_cast(info.Data()); + testData->mainWantsRelease = true; + return info.Env().Undefined(); +} + +static Value TestAcquire(const CallbackInfo &info) { + Function cb = info[0].As(); + Napi::Env env = info.Env(); + + // We pass the test data to the Finalizer for cleanup. The finalizer is + // responsible for deleting this data as well. + TestData *testData = new TestData(Promise::Deferred::New(info.Env())); + + testData->tsfn = TSFN::New(env, cb, "Test", 0, 1, testData, + std::function( + AcquireFinalizerCallback), + testData); + + Object result = Object::New(env); + result["createThread"] = + Function::New(env, CreateThread, "createThread", testData); + result["stopThreads"] = + Function::New(env, StopThreads, "stopThreads", testData); + result["promise"] = testData->deferred.Promise(); + + return result; +} +} // namespace + +Object InitThreadSafeFunctionExSum(Env env) { + Object exports = Object::New(env); + exports["testDelayedTSFN"] = Function::New(env, TestDelayedTSFN); + exports["testWithTSFN"] = Function::New(env, TestWithTSFN); + exports["testAcquire"] = Function::New(env, TestAcquire); + return exports; +} + +#endif diff --git a/test/threadsafe_function_ex/threadsafe_function_sum.js b/test/threadsafe_function_ex/threadsafe_function_sum.js new file mode 100644 index 000000000..ef7162c25 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_sum.js @@ -0,0 +1,61 @@ +'use strict'; +const assert = require('assert'); +const buildType = process.config.target_defaults.default_configuration; + +/** + * + * ThreadSafeFunction Tests: Thread Id Sums + * + * Every native C++ function that utilizes the TSFN will call the registered + * callback with the thread id. Passing Array.prototype.push with a bound array + * will push the thread id to the array. Therefore, starting `N` threads, we + * will expect the sum of all elements in the array to be `(N-1) * (N) / 2` (as + * thread IDs are 0-based) + * + * We check different methods of passing a ThreadSafeFunction around multiple + * threads: + * - `testWithTSFN`: The main thread creates the TSFN. Then, it creates + * threads, passing the TSFN at thread construction. The number of threads is + * static (known at TSFN creation). + * - `testDelayedTSFN`: The main thread creates threads, passing a promise to a + * TSFN at construction. Then, it creates the TSFN, and resolves each + * threads' promise. The number of threads is static. + * - `testAcquire`: The native binding returns a function to start a new. A + * call to this function will return `false` once `N` calls have been made. + * Each thread will acquire its own use of the TSFN, call it, and then + * release. + */ + +const THREAD_COUNT = 5; +const EXPECTED_SUM = (THREAD_COUNT - 1) * (THREAD_COUNT) / 2; + +module.exports = test(require(`../build/${buildType}/binding.node`)) + .then(() => test(require(`../build/${buildType}/binding_noexcept.node`))); + +/** @param {number[]} N */ +const sum = (N) => N.reduce((sum, n) => sum + n, 0); + +function test(binding) { + async function check(bindingFunction) { + const calls = []; + const result = await bindingFunction(THREAD_COUNT, Array.prototype.push.bind(calls)); + assert.ok(result); + assert.equal(sum(calls), EXPECTED_SUM); + } + + async function checkAcquire() { + const calls = []; + const { promise, createThread, stopThreads } = binding.threadsafe_function_ex_sum.testAcquire(Array.prototype.push.bind(calls)); + for (let i = 0; i < THREAD_COUNT; i++) { + createThread(); + } + stopThreads(); + const result = await promise; + assert.ok(result); + assert.equal(sum(calls), EXPECTED_SUM); + } + + return check(binding.threadsafe_function_ex_sum.testDelayedTSFN) + .then(() => check(binding.threadsafe_function_ex_sum.testWithTSFN)) + .then(() => checkAcquire()); +} diff --git a/test/threadsafe_function_ex/threadsafe_function_unref.cc b/test/threadsafe_function_ex/threadsafe_function_unref.cc new file mode 100644 index 000000000..2e2041380 --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_unref.cc @@ -0,0 +1,44 @@ +#include "napi.h" + +#if (NAPI_VERSION > 3) + +using namespace Napi; + +namespace { + +using TSFN = ThreadSafeFunctionEx<>; +using ContextType = std::nullptr_t; +using FinalizerDataType = void; +static Value TestUnref(const CallbackInfo& info) { + Napi::Env env = info.Env(); + Object global = env.Global(); + Object resource = info[0].As(); + Function cb = info[1].As(); + Function setTimeout = global.Get("setTimeout").As(); + TSFN* tsfn = new TSFN; + + *tsfn = TSFN::New(info.Env(), cb, resource, "Test", 1, 1, nullptr, [tsfn](Napi::Env /* env */, FinalizerDataType*, ContextType*) { + delete tsfn; + }, static_cast(nullptr)); + + tsfn->BlockingCall(); + + setTimeout.Call( global, { + Function::New(env, [tsfn](const CallbackInfo& info) { + tsfn->Unref(info.Env()); + }), + Number::New(env, 100) + }); + + return info.Env().Undefined(); +} + +} + +Object InitThreadSafeFunctionExUnref(Env env) { + Object exports = Object::New(env); + exports["testUnref"] = Function::New(env, TestUnref); + return exports; +} + +#endif diff --git a/test/threadsafe_function_ex/threadsafe_function_unref.js b/test/threadsafe_function_ex/threadsafe_function_unref.js new file mode 100644 index 000000000..eee3fcf8f --- /dev/null +++ b/test/threadsafe_function_ex/threadsafe_function_unref.js @@ -0,0 +1,55 @@ +'use strict'; + +const assert = require('assert'); +const buildType = process.config.target_defaults.default_configuration; + +const isMainProcess = process.argv[1] != __filename; + +/** + * In order to test that the event loop exits even with an active TSFN, we need + * to spawn a new process for the test. + * - Main process: spawns new node instance, executing this script + * - Child process: creates TSFN. Native module Unref's via setTimeout after some time but does NOT call Release. + * + * Main process should expect child process to exit. + */ + +if (isMainProcess) { + module.exports = test(`../build/${buildType}/binding.node`) + .then(() => test(`../build/${buildType}/binding_noexcept.node`)); +} else { + test(process.argv[2]); +} + +function test(bindingFile) { + if (isMainProcess) { + // Main process + return new Promise((resolve, reject) => { + const child = require('../napi_child').spawn(process.argv[0], [ + '--expose-gc', __filename, bindingFile + ], { stdio: 'inherit' }); + + let timeout = setTimeout( function() { + child.kill(); + timeout = 0; + reject(new Error("Expected child to die")); + }, 5000); + + child.on("error", (err) => { + clearTimeout(timeout); + timeout = 0; + reject(new Error(err)); + }) + + child.on("close", (code) => { + if (timeout) clearTimeout(timeout); + assert.strictEqual(code, 0, "Expected return value 0"); + resolve(); + }); + }); + } else { + // Child process + const binding = require(bindingFile); + binding.threadsafe_function_ex_unref.testUnref({}, () => { }); + } +} diff --git a/test/threadsafe_function_ex/util/TestRunner.js b/test/threadsafe_function_ex/util/TestRunner.js deleted file mode 100644 index 8b425ab65..000000000 --- a/test/threadsafe_function_ex/util/TestRunner.js +++ /dev/null @@ -1,154 +0,0 @@ -// @ts-check -'use strict'; -const assert = require('assert'); -const { basename, extname } = require('path'); -const buildType = process.config.target_defaults.default_configuration; - -const pad = (what, targetLength = 20, padString = ' ', padLeft) => { - const padder = (pad, str) => { - if (typeof str === 'undefined') - return pad; - if (padLeft) { - return (pad + str).slice(-pad.length); - } else { - return (str + pad).substring(0, pad.length); - } - }; - return padder(padString.repeat(targetLength), String(what)); -} - -/** - * If `true`, always show results as interactive. See constructor for more - * information. -*/ -const SHOW_OUTPUT = false; - -/** - * Test runner helper class. Each static method's name corresponds to the - * namespace the test as defined in the native addon. Each test specifics are - * documented on the individual method. The async test handler runs - * synchronously in the series of all tests so the test **MUST** wait on the - * finalizer. Otherwise, the test runner will assume the test completed. - */ -class TestRunner { - - /** - * @param {string} bindingKey The key to use when accessing the binding. - * @param {string} filename Name of file that the current TestRunner instance - * is being constructed. This determines how to log to console: - * - When the test is running as the current module, output is shown on both - * start and stop of test in an 'interactive' styling. - * - Otherwise, the output is more of a CI-like styling. - */ - constructor(bindingKey, filename) { - this.bindingKey = bindingKey; - this.filename = filename; - this.interactive = SHOW_OUTPUT || filename === require.main.filename; - this.specName = `${this.bindingKey}/${basename(this.filename, extname(this.filename))}`; - } - - async start() { - try { - this.log(`Running tests in .${this.filename.replace(process.cwd(), '')}:\n`); - // Run tests in both except and noexcept - await this.run(false); - await this.run(true); - } catch (ex) { - console.error(`Test failed!`, ex); - process.exit(1); - } - } - - /** - * @param {boolean} isNoExcept If true, use the 'noexcept' binding. - */ - async run(isNoExcept) { - const binding = require(`../../build/${buildType}/binding${isNoExcept ? '_noexcept' : ''}.node`); - const { bindingKey } = this; - const spec = binding[bindingKey]; - const runner = this; - - // If we can't find the key in the binding, error. - if (!spec) { - throw new Error(`Could not find '${bindingKey}' in binding.`); - } - - // A 'test' is defined as any function on the prototype of this object. - for (const nsName of Object.getOwnPropertyNames(Object.getPrototypeOf(this))) { - if (nsName !== 'constructor') { - const ns = spec[nsName]; - - // Interactive mode prints start and end messages - if (this.interactive) { - - /** @typedef {[string, string | null | number, boolean, string, any]} State [label, time, isNoExcept, nsName, returnValue] */ - - /** @type {State} */ - let state = [undefined, undefined, undefined, undefined, undefined] - - const stateLine = () => { - const [label, time, isNoExcept, nsName, returnValue] = state; - const except = () => pad(isNoExcept ? '[noexcept]' : '', 12); - const timeStr = () => time == null ? '...' : `${time}${typeof time === 'number' ? 'ms' : ''}`; - return `${pad(nsName, 10)} ${except()}| ${pad(timeStr(), 8)}| ${pad(label, 15)}${returnValue === undefined ? '' : `(return: ${JSON.stringify(returnValue)})`}`; - }; - - /** - * @param {string} label - * @param {string | number} time - * @param {boolean} isNoExcept - * @param {string} nsName - * @param {any} returnValue - */ - const setState = (label, time, isNoExcept, nsName, returnValue) => { - if (state[1] === null) { - // Move to last line - this.print(false, `\x1b[1A`); - } - state = [label, time, isNoExcept, nsName, returnValue]; - this.log(stateLine()); - }; - - if (ns && typeof runner[nsName] === 'function') { - setState('Running test', null, isNoExcept, nsName, undefined); - const start = Date.now(); - const returnValue = await runner[nsName](ns); - setState('Finished test', Date.now() - start, isNoExcept, nsName, returnValue); - } else { - setState('Skipping test', '-', isNoExcept, nsName, undefined); - } - } else if (ns) { - console.log(`Running test '${this.specName}/${nsName}' ${isNoExcept ? '[noexcept]' : ''}`); - await runner[nsName](ns); - } - } - } - } - - /** - * Print to console only when using interactive mode. - * - * @param {boolean} newLine If true, end with a new line. - * @param {any[]} what What to print - */ - print(newLine, ...what) { - if (this.interactive) { - let target, method; - target = newLine ? console : process.stdout; - method = target === console ? 'log' : 'write'; - return target[method].apply(target, what); - } - } - - /** - * Log to console only when using interactive mode. - * @param {string[]} what - */ - log(...what) { - this.print(true, ...what); - } -} - -module.exports = { - TestRunner -}; diff --git a/test/threadsafe_function_ex/util/util.h b/test/threadsafe_function_ex/util/util.h deleted file mode 100644 index 6f202601b..000000000 --- a/test/threadsafe_function_ex/util/util.h +++ /dev/null @@ -1,62 +0,0 @@ -#include -#include "napi.h" - -#if (NAPI_VERSION > 3) - -using namespace Napi; - -namespace tsfnutil { -template -class TSFNWrapBase : public ObjectWrap { -public: - - - static void Init(Napi::Env env, Object exports, const std::string &ns) { - // Get methods defined by child - auto methods(TSFNWrapImpl::InstanceMethods()); - - // Create a vector, since DefineClass doesn't accept arrays. - std::vector> methodsVec(methods.begin(), methods.end()); - - auto locals(Object::New(env)); - locals.Set("TSFNWrap", ObjectWrap::DefineClass(env, "TSFNWrap", methodsVec)); - exports.Set(ns, locals); - } - - // Release the TSFN. Returns a Promise that is resolved in the TSFN's - // finalizer. - // NOTE: the 'simple' test overrides this method, because it has no finalizer. - Napi::Value Release(const CallbackInfo &info) { - if (_deferred) { - return _deferred->Promise(); - } - - auto env = info.Env(); - _deferred.reset(new Promise::Deferred(Promise::Deferred::New(env))); - - _tsfn.Release(); - return _deferred->Promise(); - }; - - // TSFN finalizer. Resolves the Promise returned by `Release()` above. - static void Finalizer(Napi::Env env, - std::unique_ptr *deferred, - Context * /*ctx*/) { - if (deferred->get()) { - (*deferred)->Resolve(Boolean::New(env, true)); - deferred->release(); - } - } - - - TSFNWrapBase(const CallbackInfo &callbackInfo) - : ObjectWrap(callbackInfo) {} - -protected: - TSFN _tsfn; - std::unique_ptr _deferred; -}; - -} // namespace tsfnutil - -#endif From e2239558351631fa01830fa825393da77ead711a Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 18 Aug 2020 23:10:35 +0200 Subject: [PATCH 31/39] doc: tsfnex example uses ported tsfn example --- doc/threadsafe_function.md | 2 +- doc/threadsafe_function_ex.md | 85 +++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/doc/threadsafe_function.md b/doc/threadsafe_function.md index 7c323fd4a..055478d84 100644 --- a/doc/threadsafe_function.md +++ b/doc/threadsafe_function.md @@ -71,7 +71,7 @@ New(napi_env env, `uv_thread_join()`. It is important that, aside from the main loop thread, there be no threads left using the thread-safe function after the finalize callback completes. Must implement `void operator()(Env env, DataType* data, - Context* hint)`, skipping `data` or `hint` if they are not provided. + ContextType* hint)`, skipping `data` or `hint` if they are not provided. - `[optional] data`: Data to be passed to `finalizeCallback`. Returns a non-empty `Napi::ThreadSafeFunction` instance. diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md index ffc637217..a3aa7b48a 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/threadsafe_function_ex.md @@ -192,8 +192,14 @@ Returns one of: using namespace Napi; +using Context = Reference; +using DataType = int; +void CallJs( Napi::Env env, Function callback, Context* context, DataType* data ); +using TSFN = ThreadSafeFunctionEx; +using FinalizerDataType = void; + std::thread nativeThread; -ThreadSafeFunction tsfn; +TSFN tsfn; Value Start( const CallbackInfo& info ) { @@ -214,35 +220,32 @@ Value Start( const CallbackInfo& info ) int count = info[1].As().Int32Value(); + // Create a new context set to the the receiver (ie, `this`) of the function + // call + Context* context = new Reference( Persistent( info.This() ) ); + // Create a ThreadSafeFunction - tsfn = ThreadSafeFunction::New( - env, - info[0].As(), // JavaScript function called asynchronously - "Resource Name", // Name - 0, // Unlimited queue - 1, // Only one thread will use this initially - []( Napi::Env ) { // Finalizer used to clean threads up - nativeThread.join(); - } ); + tsfn = TSFN::New( env, + info[0].As(), // JavaScript function called asynchronously + "Resource Name", // Name + 0, // Unlimited queue + 1, // Only one thread will use this initially + context, + []( Napi::Env, FinalizerDataType*, + Context* ctx ) { // Finalizer used to clean threads up + nativeThread.join(); + delete ctx; + } ); // Create a native thread nativeThread = std::thread( [count] { - auto callback = []( Napi::Env env, Function jsCallback, int* value ) { - // Transform native data into JS data, passing it to the provided - // `jsCallback` -- the TSFN's JavaScript function. - jsCallback.Call( {Number::New( env, *value )} ); - - // We're finished with the data. - delete value; - }; - for ( int i = 0; i < count; i++ ) { // Create new data int* value = new int( clock() ); // Perform a blocking call - napi_status status = tsfn.BlockingCall( value, callback ); + napi_status status = tsfn.BlockingCall( value ); if ( status != napi_ok ) { // Handle error @@ -256,7 +259,29 @@ Value Start( const CallbackInfo& info ) tsfn.Release(); } ); - return Boolean::New(env, true); + return Boolean::New( env, true ); +} + +// Transform native data into JS data, passing it to the provided +// `callback` -- the TSFN's JavaScript function. +void CallJs( Napi::Env env, Function callback, Context* context, DataType* data ) +{ + // Is the JavaScript environment still available to call into, eg. the TSFN is + // not aborted + if ( env != nullptr ) + { + // On N-API 5+, the `callback` parameter is optional; however, this example + // does ensure a callback is provided. + if ( callback != nullptr ) + { + callback.Call( context->Value(), {Number::New( env, *data )} ); + } + } + if ( data != nullptr ) + { + // We're finished with the data. + delete data; + } } Napi::Object Init( Napi::Env env, Object exports ) @@ -273,18 +298,20 @@ The above code can be used from JavaScript as follows: ```js const { start } = require('bindings')('clock'); -start(function () { - console.log("JavaScript callback called with arguments", Array.from(arguments)); +start.call(new Date(), function (clock) { + const context = this; + console.log(context, clock); }, 5); ``` When executed, the output will show the value of `clock()` five times at one -second intervals: +second intervals, prefixed with the TSFN's context -- `start`'s receiver (ie, +`new Date()`): ``` -JavaScript callback called with arguments [ 84745 ] -JavaScript callback called with arguments [ 103211 ] -JavaScript callback called with arguments [ 104516 ] -JavaScript callback called with arguments [ 105104 ] -JavaScript callback called with arguments [ 105691 ] +2020-08-18T21:04:25.116Z 49824 +2020-08-18T21:04:25.116Z 62493 +2020-08-18T21:04:25.116Z 62919 +2020-08-18T21:04:25.116Z 63228 +2020-08-18T21:04:25.116Z 63531 ``` From 3405b2bf39793a573d420584ea81a411e5127634 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 18 Aug 2020 23:35:53 +0200 Subject: [PATCH 32/39] src,doc: final cleanup --- doc/threadsafe_function_ex.md | 4 ++-- napi-inl.h | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md index a3aa7b48a..497484397 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/threadsafe_function_ex.md @@ -92,13 +92,13 @@ for `CallbackType callback`. When targetting version 4, `callback` may be: - of type `const Function&` -- not provided as an parameter, in which case the API creates a new no-op +- not provided as a parameter, in which case the API creates a new no-op `Function` When targetting version 5+, `callback` may be: - of type `const Function&` - of type `std::nullptr_t` -- not provided as an parameter, in which case the API passes `std::nullptr` +- not provided as a parameter, in which case the API passes `std::nullptr` ### Acquire diff --git a/napi-inl.h b/napi-inl.h index 46090470c..79a47a5cd 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -13,7 +13,6 @@ #include #include #include -#include namespace Napi { From 151a914c99a7b2644b25799223be9d954a4ece57 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Fri, 9 Oct 2020 11:23:37 +0200 Subject: [PATCH 33/39] Apply documentation suggestions from code review Documentation updates Co-authored-by: Gabriel Schulhof --- doc/threadsafe_function_ex.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md index 497484397..b0df5a8c0 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/threadsafe_function_ex.md @@ -7,7 +7,7 @@ of: - `ContextType = std::nullptr_t`: The thread-safe function's context. By default, a TSFN has no context. - `DataType = void*`: The data to use in the native callback. By default, a TSFN - can accept *any* data type. + can accept any data type. - `Callback = void(*)(Napi::Env, Napi::Function jsCallback, ContextType*, DataType*)`: The callback to run for each item added to the queue. If no `Callback` is given, the API will call the function `jsCallback` with no @@ -102,7 +102,7 @@ When targetting version 5+, `callback` may be: ### Acquire -Add a thread to this thread-safe function object, indicating that a new thread +Adds a thread to this thread-safe function object, indicating that a new thread will start making use of the thread-safe function. ```cpp @@ -117,10 +117,10 @@ Returns one of: ### Release -Indicate that an existing thread will stop making use of the thread-safe +Indicates that an existing thread will stop making use of the thread-safe function. A thread should call this API when it stops making use of this thread-safe function. Using any thread-safe APIs after having called this API -has undefined results in the current thread, as it may have been destroyed. +has undefined results in the current thread, as the thread-safe function may have been destroyed. ```cpp napi_status Napi::ThreadSafeFunctionEx::Release() @@ -134,7 +134,7 @@ Returns one of: ### Abort -"Abort" the thread-safe function. This will cause all subsequent APIs associated +"Aborts" the thread-safe function. This will cause all subsequent APIs associated with the thread-safe function except `Release()` to return `napi_closing` even before its reference count reaches zero. In particular, `BlockingCall` and `NonBlockingCall()` will return `napi_closing`, thus informing the threads that @@ -173,11 +173,10 @@ napi_status Napi::ThreadSafeFunctionEx::NonBloc `ThreadSafeFunctionEx::New()`. Returns one of: -- `napi_ok`: The call was successfully added to the queue. +- `napi_ok`: `data` was successfully added to the queue. - `napi_queue_full`: The queue was full when trying to call in a non-blocking method. -- `napi_closing`: The thread-safe function is aborted and cannot accept more - calls. +- `napi_closing`: The thread-safe function is aborted and no further calls can be made. - `napi_invalid_arg`: The thread-safe function is closed. - `napi_generic_failure`: A generic error occurred when attemping to add to the queue. From 59f27dac9ae37f252e2f31164d56d0bd6ee0a7d6 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Sat, 10 Oct 2020 02:26:47 +0200 Subject: [PATCH 34/39] Fix common.gypi --- common.gypi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common.gypi b/common.gypi index 9e35b384a..9be254f0b 100644 --- a/common.gypi +++ b/common.gypi @@ -1,6 +1,6 @@ { 'variables': { - 'NAPI_VERSION%': " Date: Sat, 10 Oct 2020 02:35:30 +0200 Subject: [PATCH 35/39] Additional changes from review --- doc/threadsafe.md | 9 +- doc/threadsafe_function_ex.md | 97 +++++++++---------- .../threadsafe_function.js | 8 +- 3 files changed, 53 insertions(+), 61 deletions(-) diff --git a/doc/threadsafe.md b/doc/threadsafe.md index 4254fc8cc..b867c8cab 100644 --- a/doc/threadsafe.md +++ b/doc/threadsafe.md @@ -93,10 +93,11 @@ construct a no-op `Function` **or** to target N-API 5 and "construct" a with just a switch of the `NAPI_VERSION` compile-time constant. The removal of the dynamic call functionality has the following implications: -- The API does _not_ act as a "broker" compared to the non-`Ex`. Once Node.js - finalizes the thread-safe function, the `CallJs` callback will execute with an - empty `Napi::Env` for any remaining items on the queue. This provides the - ability to handle any necessary cleanup of the item's data. +- The API does _not_ act as a "broker" compared to the + `Napi::ThreadSafeFunction`. Once Node.js finalizes the thread-safe function, + the `CallJs` callback will execute with an empty `Napi::Env` for any remaining + items on the queue. This provides the ability to handle any necessary cleanup + of the item's data. - The callback _does_ receive the context as a parameter, so a call to `GetContext()` is _not_ necessary. This context type is specified as the **first template argument** specified to `::New`, ensuring type safety. diff --git a/doc/threadsafe_function_ex.md b/doc/threadsafe_function_ex.md index b0df5a8c0..fdc56ecae 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/threadsafe_function_ex.md @@ -120,7 +120,8 @@ Returns one of: Indicates that an existing thread will stop making use of the thread-safe function. A thread should call this API when it stops making use of this thread-safe function. Using any thread-safe APIs after having called this API -has undefined results in the current thread, as the thread-safe function may have been destroyed. +has undefined results in the current thread, as the thread-safe function may +have been destroyed. ```cpp napi_status Napi::ThreadSafeFunctionEx::Release() @@ -176,7 +177,8 @@ Returns one of: - `napi_ok`: `data` was successfully added to the queue. - `napi_queue_full`: The queue was full when trying to call in a non-blocking method. -- `napi_closing`: The thread-safe function is aborted and no further calls can be made. +- `napi_closing`: The thread-safe function is aborted and no further calls can + be made. - `napi_invalid_arg`: The thread-safe function is closed. - `napi_generic_failure`: A generic error occurred when attemping to add to the queue. @@ -186,110 +188,99 @@ Returns one of: ```cpp #include -#include #include +#include using namespace Napi; using Context = Reference; using DataType = int; -void CallJs( Napi::Env env, Function callback, Context* context, DataType* data ); +void CallJs(Napi::Env env, Function callback, Context *context, DataType *data); using TSFN = ThreadSafeFunctionEx; using FinalizerDataType = void; std::thread nativeThread; TSFN tsfn; -Value Start( const CallbackInfo& info ) -{ +Value Start(const CallbackInfo &info) { Napi::Env env = info.Env(); - if ( info.Length() < 2 ) - { - throw TypeError::New( env, "Expected two arguments" ); - } - else if ( !info[0].IsFunction() ) - { - throw TypeError::New( env, "Expected first arg to be function" ); - } - else if ( !info[1].IsNumber() ) - { - throw TypeError::New( env, "Expected second arg to be number" ); + if (info.Length() < 2) { + throw TypeError::New(env, "Expected two arguments"); + } else if (!info[0].IsFunction()) { + throw TypeError::New(env, "Expected first arg to be function"); + } else if (!info[1].IsNumber()) { + throw TypeError::New(env, "Expected second arg to be number"); } int count = info[1].As().Int32Value(); // Create a new context set to the the receiver (ie, `this`) of the function // call - Context* context = new Reference( Persistent( info.This() ) ); + Context *context = new Reference(Persistent(info.This())); // Create a ThreadSafeFunction - tsfn = TSFN::New( env, - info[0].As(), // JavaScript function called asynchronously - "Resource Name", // Name - 0, // Unlimited queue - 1, // Only one thread will use this initially - context, - []( Napi::Env, FinalizerDataType*, - Context* ctx ) { // Finalizer used to clean threads up - nativeThread.join(); - delete ctx; - } ); + tsfn = TSFN::New( + env, + info[0].As(), // JavaScript function called asynchronously + "Resource Name", // Name + 0, // Unlimited queue + 1, // Only one thread will use this initially + context, + [](Napi::Env, FinalizerDataType *, + Context *ctx) { // Finalizer used to clean threads up + nativeThread.join(); + delete ctx; + }); // Create a native thread - nativeThread = std::thread( [count] { - for ( int i = 0; i < count; i++ ) - { + nativeThread = std::thread([count] { + for (int i = 0; i < count; i++) { // Create new data - int* value = new int( clock() ); + int *value = new int(clock()); // Perform a blocking call - napi_status status = tsfn.BlockingCall( value ); - if ( status != napi_ok ) - { + napi_status status = tsfn.BlockingCall(value); + if (status != napi_ok) { // Handle error break; } - std::this_thread::sleep_for( std::chrono::seconds( 1 ) ); + std::this_thread::sleep_for(std::chrono::seconds(1)); } // Release the thread-safe function tsfn.Release(); - } ); + }); - return Boolean::New( env, true ); + return Boolean::New(env, true); } // Transform native data into JS data, passing it to the provided // `callback` -- the TSFN's JavaScript function. -void CallJs( Napi::Env env, Function callback, Context* context, DataType* data ) -{ +void CallJs(Napi::Env env, Function callback, Context *context, + DataType *data) { // Is the JavaScript environment still available to call into, eg. the TSFN is // not aborted - if ( env != nullptr ) - { + if (env != nullptr) { // On N-API 5+, the `callback` parameter is optional; however, this example // does ensure a callback is provided. - if ( callback != nullptr ) - { - callback.Call( context->Value(), {Number::New( env, *data )} ); + if (callback != nullptr) { + callback.Call(context->Value(), {Number::New(env, *data)}); } } - if ( data != nullptr ) - { + if (data != nullptr) { // We're finished with the data. delete data; } } -Napi::Object Init( Napi::Env env, Object exports ) -{ - exports.Set( "start", Function::New( env, Start ) ); +Napi::Object Init(Napi::Env env, Object exports) { + exports.Set("start", Function::New(env, Start)); return exports; } -NODE_API_MODULE( clock, Init ) +NODE_API_MODULE(clock, Init) ``` The above code can be used from JavaScript as follows: @@ -304,7 +295,7 @@ start.call(new Date(), function (clock) { ``` When executed, the output will show the value of `clock()` five times at one -second intervals, prefixed with the TSFN's context -- `start`'s receiver (ie, +second intervals, prefixed with the TSFN's context -- `start`'s receiver (ie, `new Date()`): ``` diff --git a/test/threadsafe_function_ex/threadsafe_function.js b/test/threadsafe_function_ex/threadsafe_function.js index 9be4b26dd..80a03840e 100644 --- a/test/threadsafe_function_ex/threadsafe_function.js +++ b/test/threadsafe_function_ex/threadsafe_function.js @@ -4,13 +4,13 @@ const buildType = process.config.target_defaults.default_configuration; const assert = require('assert'); const common = require('../common'); -module.exports = (async function() { +module.exports = (async function () { await test(require(`../build/${buildType}/binding.node`)); await test(require(`../build/${buildType}/binding_noexcept.node`)); })(); async function test(binding) { - const expectedArray = (function(arrayLength) { + const expectedArray = (function (arrayLength) { const result = []; for (let index = 0; index < arrayLength; index++) { result.push(arrayLength - 1 - index); @@ -60,7 +60,7 @@ async function test(binding) { }); } }, false /* abort */, false /* launchSecondary */, - binding.threadsafe_function_ex.MAX_QUEUE_SIZE); + binding.threadsafe_function_ex.MAX_QUEUE_SIZE); }); // Start the thread in blocking mode, and assert that all values are passed. @@ -191,4 +191,4 @@ async function test(binding) { })).indexOf(0), -1, ); -} \ No newline at end of file +} From 7a13f861ab32c91e6f5e6dee0f87d4406749a153 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Thu, 29 Oct 2020 08:45:19 +0100 Subject: [PATCH 36/39] doc: fix additional typo --- doc/threadsafe_function.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/threadsafe_function.md b/doc/threadsafe_function.md index c6416641c..cb4e7a374 100644 --- a/doc/threadsafe_function.md +++ b/doc/threadsafe_function.md @@ -106,7 +106,7 @@ napi_status Napi::ThreadSafeFunction::Release() Returns one of: - `napi_ok`: The thread-safe function has been successfully released. - `napi_invalid_arg`: The thread-safe function's thread-count is zero. -- `napi_generic_failure`: A generic error occurred when attemping to release +- `napi_generic_failure`: A generic error occurred when attempting to release the thread-safe function. ### Abort From 4abe7cfe30b1bfdda57846351b2fae05f2cb4891 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Fri, 6 Nov 2020 16:16:56 +0100 Subject: [PATCH 37/39] test: rename tsfnex test files --- test/binding.gyp | 12 ++++++------ test/index.js | 12 ++++++------ ...eadsafe_function.cc => threadsafe_function_ex.cc} | 0 ...eadsafe_function.js => threadsafe_function_ex.js} | 0 ...function_ctx.cc => threadsafe_function_ex_ctx.cc} | 0 ...function_ctx.js => threadsafe_function_ex_ctx.js} | 0 ...fn.cc => threadsafe_function_ex_existing_tsfn.cc} | 0 ...fn.js => threadsafe_function_ex_existing_tsfn.js} | 0 ...function_ptr.cc => threadsafe_function_ex_ptr.cc} | 0 ...function_ptr.js => threadsafe_function_ex_ptr.js} | 0 ...function_sum.cc => threadsafe_function_ex_sum.cc} | 0 ...function_sum.js => threadsafe_function_ex_sum.js} | 0 ...tion_unref.cc => threadsafe_function_ex_unref.cc} | 0 ...tion_unref.js => threadsafe_function_ex_unref.js} | 0 14 files changed, 12 insertions(+), 12 deletions(-) rename test/threadsafe_function_ex/{threadsafe_function.cc => threadsafe_function_ex.cc} (100%) rename test/threadsafe_function_ex/{threadsafe_function.js => threadsafe_function_ex.js} (100%) rename test/threadsafe_function_ex/{threadsafe_function_ctx.cc => threadsafe_function_ex_ctx.cc} (100%) rename test/threadsafe_function_ex/{threadsafe_function_ctx.js => threadsafe_function_ex_ctx.js} (100%) rename test/threadsafe_function_ex/{threadsafe_function_existing_tsfn.cc => threadsafe_function_ex_existing_tsfn.cc} (100%) rename test/threadsafe_function_ex/{threadsafe_function_existing_tsfn.js => threadsafe_function_ex_existing_tsfn.js} (100%) rename test/threadsafe_function_ex/{threadsafe_function_ptr.cc => threadsafe_function_ex_ptr.cc} (100%) rename test/threadsafe_function_ex/{threadsafe_function_ptr.js => threadsafe_function_ex_ptr.js} (100%) rename test/threadsafe_function_ex/{threadsafe_function_sum.cc => threadsafe_function_ex_sum.cc} (100%) rename test/threadsafe_function_ex/{threadsafe_function_sum.js => threadsafe_function_ex_sum.js} (100%) rename test/threadsafe_function_ex/{threadsafe_function_unref.cc => threadsafe_function_ex_unref.cc} (100%) rename test/threadsafe_function_ex/{threadsafe_function_unref.js => threadsafe_function_ex_unref.js} (100%) diff --git a/test/binding.gyp b/test/binding.gyp index 702799d11..7f64e7f51 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -36,12 +36,12 @@ 'object/set_property.cc', 'promise.cc', 'run_script.cc', - 'threadsafe_function_ex/threadsafe_function_ctx.cc', - 'threadsafe_function_ex/threadsafe_function_existing_tsfn.cc', - 'threadsafe_function_ex/threadsafe_function_ptr.cc', - 'threadsafe_function_ex/threadsafe_function_sum.cc', - 'threadsafe_function_ex/threadsafe_function_unref.cc', - 'threadsafe_function_ex/threadsafe_function.cc', + 'threadsafe_function_ex/threadsafe_function_ex_ctx.cc', + 'threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.cc', + 'threadsafe_function_ex/threadsafe_function_ex_ptr.cc', + 'threadsafe_function_ex/threadsafe_function_ex_sum.cc', + 'threadsafe_function_ex/threadsafe_function_ex_unref.cc', + 'threadsafe_function_ex/threadsafe_function_ex.cc', 'threadsafe_function/threadsafe_function_ctx.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', 'threadsafe_function/threadsafe_function_ptr.cc', diff --git a/test/index.js b/test/index.js index 3ea8f9842..060070590 100644 --- a/test/index.js +++ b/test/index.js @@ -44,12 +44,12 @@ let testModules = [ 'object/set_property', 'promise', 'run_script', - 'threadsafe_function_ex/threadsafe_function_ctx', - 'threadsafe_function_ex/threadsafe_function_existing_tsfn', - 'threadsafe_function_ex/threadsafe_function_ptr', - 'threadsafe_function_ex/threadsafe_function_sum', - 'threadsafe_function_ex/threadsafe_function_unref', - 'threadsafe_function_ex/threadsafe_function', + 'threadsafe_function_ex/threadsafe_function_ex_ctx', + 'threadsafe_function_ex/threadsafe_function_ex_existing_tsfn', + 'threadsafe_function_ex/threadsafe_function_ex_ptr', + 'threadsafe_function_ex/threadsafe_function_ex_sum', + 'threadsafe_function_ex/threadsafe_function_ex_unref', + 'threadsafe_function_ex/threadsafe_function_ex', 'threadsafe_function/threadsafe_function_ctx', 'threadsafe_function/threadsafe_function_existing_tsfn', 'threadsafe_function/threadsafe_function_ptr', diff --git a/test/threadsafe_function_ex/threadsafe_function.cc b/test/threadsafe_function_ex/threadsafe_function_ex.cc similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function.cc rename to test/threadsafe_function_ex/threadsafe_function_ex.cc diff --git a/test/threadsafe_function_ex/threadsafe_function.js b/test/threadsafe_function_ex/threadsafe_function_ex.js similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function.js rename to test/threadsafe_function_ex/threadsafe_function_ex.js diff --git a/test/threadsafe_function_ex/threadsafe_function_ctx.cc b/test/threadsafe_function_ex/threadsafe_function_ex_ctx.cc similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_ctx.cc rename to test/threadsafe_function_ex/threadsafe_function_ex_ctx.cc diff --git a/test/threadsafe_function_ex/threadsafe_function_ctx.js b/test/threadsafe_function_ex/threadsafe_function_ex_ctx.js similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_ctx.js rename to test/threadsafe_function_ex/threadsafe_function_ex_ctx.js diff --git a/test/threadsafe_function_ex/threadsafe_function_existing_tsfn.cc b/test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.cc similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_existing_tsfn.cc rename to test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.cc diff --git a/test/threadsafe_function_ex/threadsafe_function_existing_tsfn.js b/test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.js similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_existing_tsfn.js rename to test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.js diff --git a/test/threadsafe_function_ex/threadsafe_function_ptr.cc b/test/threadsafe_function_ex/threadsafe_function_ex_ptr.cc similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_ptr.cc rename to test/threadsafe_function_ex/threadsafe_function_ex_ptr.cc diff --git a/test/threadsafe_function_ex/threadsafe_function_ptr.js b/test/threadsafe_function_ex/threadsafe_function_ex_ptr.js similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_ptr.js rename to test/threadsafe_function_ex/threadsafe_function_ex_ptr.js diff --git a/test/threadsafe_function_ex/threadsafe_function_sum.cc b/test/threadsafe_function_ex/threadsafe_function_ex_sum.cc similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_sum.cc rename to test/threadsafe_function_ex/threadsafe_function_ex_sum.cc diff --git a/test/threadsafe_function_ex/threadsafe_function_sum.js b/test/threadsafe_function_ex/threadsafe_function_ex_sum.js similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_sum.js rename to test/threadsafe_function_ex/threadsafe_function_ex_sum.js diff --git a/test/threadsafe_function_ex/threadsafe_function_unref.cc b/test/threadsafe_function_ex/threadsafe_function_ex_unref.cc similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_unref.cc rename to test/threadsafe_function_ex/threadsafe_function_ex_unref.cc diff --git a/test/threadsafe_function_ex/threadsafe_function_unref.js b/test/threadsafe_function_ex/threadsafe_function_ex_unref.js similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_unref.js rename to test/threadsafe_function_ex/threadsafe_function_ex_unref.js From c24c455ced57a3c9ac4818482dc6d4f246f591e7 Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:02:16 +0100 Subject: [PATCH 38/39] Rename to TypedThreadSafeFunction --- README.md | 2 +- doc/threadsafe.md | 58 +++++----- ...ion_ex.md => typed_threadsafe_function.md} | 74 ++++++------ napi-inl.h | 106 +++++++++--------- napi.h | 26 ++--- test/binding.cc | 26 ++--- test/binding.gyp | 12 +- test/index.js | 12 +- .../typed_threadsafe_function.cc} | 4 +- .../typed_threadsafe_function.js} | 36 +++--- .../typed_threadsafe_function_ctx.cc} | 4 +- .../typed_threadsafe_function_ctx.js} | 0 ...yped_threadsafe_function_existing_tsfn.cc} | 4 +- ...yped_threadsafe_function_existing_tsfn.js} | 4 +- .../typed_threadsafe_function_ptr.cc} | 4 +- .../typed_threadsafe_function_ptr.js} | 2 +- .../typed_threadsafe_function_sum.cc} | 6 +- .../typed_threadsafe_function_sum.js} | 6 +- .../typed_threadsafe_function_unref.cc} | 4 +- .../typed_threadsafe_function_unref.js} | 2 +- 20 files changed, 196 insertions(+), 196 deletions(-) rename doc/{threadsafe_function_ex.md => typed_threadsafe_function.md} (78%) rename test/{threadsafe_function_ex/threadsafe_function_ex.cc => typed_threadsafe_function/typed_threadsafe_function.cc} (97%) rename test/{threadsafe_function_ex/threadsafe_function_ex.js => typed_threadsafe_function/typed_threadsafe_function.js} (79%) rename test/{threadsafe_function_ex/threadsafe_function_ex_ctx.cc => typed_threadsafe_function/typed_threadsafe_function_ctx.cc} (93%) rename test/{threadsafe_function_ex/threadsafe_function_ex_ctx.js => typed_threadsafe_function/typed_threadsafe_function_ctx.js} (100%) rename test/{threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.cc => typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc} (96%) rename test/{threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.js => typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.js} (89%) rename test/{threadsafe_function_ex/threadsafe_function_ex_ptr.cc => typed_threadsafe_function/typed_threadsafe_function_ptr.cc} (83%) rename test/{threadsafe_function_ex/threadsafe_function_ex_ptr.js => typed_threadsafe_function/typed_threadsafe_function_ptr.js} (79%) rename test/{threadsafe_function_ex/threadsafe_function_ex_sum.cc => typed_threadsafe_function/typed_threadsafe_function_sum.cc} (97%) rename test/{threadsafe_function_ex/threadsafe_function_ex_sum.js => typed_threadsafe_function/typed_threadsafe_function_sum.js} (88%) rename test/{threadsafe_function_ex/threadsafe_function_ex_unref.cc => typed_threadsafe_function/typed_threadsafe_function_unref.cc} (91%) rename test/{threadsafe_function_ex/threadsafe_function_ex_unref.js => typed_threadsafe_function/typed_threadsafe_function_unref.js} (95%) diff --git a/README.md b/README.md index ff4319553..6b0df4deb 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ The following is the documentation for node-addon-api. - [AsyncWorker Variants](doc/async_worker_variants.md) - [Thread-safe Functions](doc/threadsafe.md) - [ThreadSafeFunction](doc/threadsafe_function.md) - - [ThreadSafeFunctionEx](doc/threadsafe_function_ex.md) + - [TypedThreadSafeFunction](doc/typed_threadsafe_function.md) - [Promises](doc/promises.md) - [Version management](doc/version_management.md) diff --git a/doc/threadsafe.md b/doc/threadsafe.md index b867c8cab..22bb399fc 100644 --- a/doc/threadsafe.md +++ b/doc/threadsafe.md @@ -11,40 +11,40 @@ communicate with the addon's main thread so that the main thread can invoke the JavaScript function on their behalf. The thread-safe function APIs provide an easy way to do this. These APIs provide two types -- [`Napi::ThreadSafeFunction`](threadsafe_function.md) and -[`Napi::ThreadSafeFunctionEx`](threadsafe_function_ex.md) -- as well as APIs to -create, destroy, and call objects of this type. The differences between the two -are subtle and are [highlighted below](#implementation-differences). Regardless -of which type you choose, the APIs between the two are similar. +[`Napi::TypedThreadSafeFunction`](typed_threadsafe_function.md) -- as well as +APIs to create, destroy, and call objects of this type. The differences between +the two are subtle and are [highlighted below](#implementation-differences). +Regardless of which type you choose, the APIs between the two are similar. -`Napi::ThreadSafeFunction[Ex]::New()` creates a persistent reference that holds -a JavaScript function which can be called from multiple threads. The calls +`Napi::[Typed]ThreadSafeFunction::New()` creates a persistent reference that +holds a JavaScript function which can be called from multiple threads. The calls happen asynchronously. This means that values with which the JavaScript callback is to be called will be placed in a queue, and, for each value in the queue, a call will eventually be made to the JavaScript function. -`Napi::ThreadSafeFunction[Ex]` objects are destroyed when every thread which +`Napi::[Typed]ThreadSafeFunction` objects are destroyed when every thread which uses the object has called `Release()` or has received a return status of `napi_closing` in response to a call to `BlockingCall()` or `NonBlockingCall()`. -The queue is emptied before the `Napi::ThreadSafeFunction[Ex]` is destroyed. It -is important that `Release()` be the last API call made in conjunction with a -given `Napi::ThreadSafeFunction[Ex]`, because after the call completes, there is -no guarantee that the `Napi::ThreadSafeFunction[Ex]` is still allocated. For the -same reason it is also important that no more use be made of a thread-safe -function after receiving a return value of `napi_closing` in response to a call -to `BlockingCall()` or `NonBlockingCall()`. Data associated with the -`Napi::ThreadSafeFunction[Ex]` can be freed in its `Finalizer` callback which -was passed to `ThreadSafeFunction[Ex]::New()`. - -Once the number of threads making use of a `Napi::ThreadSafeFunction[Ex]` +The queue is emptied before the `Napi::[Typed]ThreadSafeFunction` is destroyed. +It is important that `Release()` be the last API call made in conjunction with a +given `Napi::[Typed]ThreadSafeFunction`, because after the call completes, there +is no guarantee that the `Napi::[Typed]ThreadSafeFunction` is still allocated. +For the same reason it is also important that no more use be made of a +thread-safe function after receiving a return value of `napi_closing` in +response to a call to `BlockingCall()` or `NonBlockingCall()`. Data associated +with the `Napi::[Typed]ThreadSafeFunction` can be freed in its `Finalizer` +callback which was passed to `[Typed]ThreadSafeFunction::New()`. + +Once the number of threads making use of a `Napi::[Typed]ThreadSafeFunction` reaches zero, no further threads can start making use of it by calling `Acquire()`. In fact, all subsequent API calls associated with it, except `Release()`, will return an error value of `napi_closing`. ## Implementation Differences -The choice between `Napi::ThreadSafeFunction` and `Napi::ThreadSafeFunctionEx` -depends largely on how you plan to execute your native C++ code (the "callback") -on the Node.js thread. +The choice between `Napi::ThreadSafeFunction` and +`Napi::TypedThreadSafeFunction` depends largely on how you plan to execute your +native C++ code (the "callback") on the Node.js thread. ### [`Napi::ThreadSafeFunction`](threadsafe_function.md) @@ -64,13 +64,13 @@ This API has some dynamic functionality, in that: Note that this functionality comes with some **additional overhead** and situational **memory leaks**: -- The API acts as a "broker" between the underlying - `napi_threadsafe_function`, and dynamically constructs a wrapper for your - callback on the heap for every call to `[Non]BlockingCall()`. +- The API acts as a "broker" between the underlying `napi_threadsafe_function`, + and dynamically constructs a wrapper for your callback on the heap for every + call to `[Non]BlockingCall()`. - In acting in this "broker" fashion, the API will call the underlying "make call" N-API method on this packaged item. If the API has determined the - thread-safe function is no longer accessible (eg. all threads have released yet - there are still items on the queue), **the callback passed to + thread-safe function is no longer accessible (eg. all threads have released + yet there are still items on the queue), **the callback passed to [Non]BlockingCall will not execute**. This means it is impossible to perform clean-up for calls that never execute their `CallJs` callback. **This may lead to memory leaks** if you are dynamically allocating memory. @@ -81,9 +81,9 @@ situational **memory leaks**: _type-safe_, as the method returns an object that can be "any-casted", instead of having a static type. -### [`Napi::ThreadSafeFunctionEx`](threadsafe_function_ex.md) +### [`Napi::TypedThreadSafeFunction`](typed_threadsafe_function.md) -The `ThreadSafeFunctionEx` class is a new implementation to address the +The `TypedThreadSafeFunction` class is a new implementation to address the drawbacks listed above. The API is designed with N-API 5's support of an optional function callback. The API will correctly allow developers to pass `std::nullptr` instead of a `const Function&` for the callback function @@ -112,7 +112,7 @@ The removal of the dynamic call functionality has the following implications: ### Usage Suggestions -In summary, it may be best to use `Napi::ThreadSafeFunctionEx` if: +In summary, it may be best to use `Napi::TypedThreadSafeFunction` if: - static, compile-time support for targeting N-API 4 or 5+ with an optional JavaScript callback feature is desired; diff --git a/doc/threadsafe_function_ex.md b/doc/typed_threadsafe_function.md similarity index 78% rename from doc/threadsafe_function_ex.md rename to doc/typed_threadsafe_function.md index fdc56ecae..e0d29807f 100644 --- a/doc/threadsafe_function_ex.md +++ b/doc/typed_threadsafe_function.md @@ -1,11 +1,11 @@ -# ThreadSafeFunctionEx - -The `Napi::ThreadSafeFunctionEx` type provides APIs for threads to communicate -with the addon's main thread to invoke JavaScript functions on their behalf. The -type is a three-argument templated class, each argument representing the type -of: -- `ContextType = std::nullptr_t`: The thread-safe function's context. By default, - a TSFN has no context. +# TypedThreadSafeFunction + +The `Napi::TypedThreadSafeFunction` type provides APIs for threads to +communicate with the addon's main thread to invoke JavaScript functions on their +behalf. The type is a three-argument templated class, each argument representing +the type of: +- `ContextType = std::nullptr_t`: The thread-safe function's context. By + default, a TSFN has no context. - `DataType = void*`: The data to use in the native callback. By default, a TSFN can accept any data type. - `Callback = void(*)(Napi::Env, Napi::Function jsCallback, ContextType*, @@ -21,31 +21,31 @@ APIs](threadsafe.md#implementation-differences). ### Constructor -Creates a new empty instance of `Napi::ThreadSafeFunctionEx`. +Creates a new empty instance of `Napi::TypedThreadSafeFunction`. ```cpp -Napi::Function::ThreadSafeFunctionEx::ThreadSafeFunctionEx(); +Napi::Function::TypedThreadSafeFunction::TypedThreadSafeFunction(); ``` ### Constructor -Creates a new instance of the `Napi::ThreadSafeFunctionEx` object. +Creates a new instance of the `Napi::TypedThreadSafeFunction` object. ```cpp -Napi::ThreadSafeFunctionEx::ThreadSafeFunctionEx(napi_threadsafe_function tsfn); +Napi::TypedThreadSafeFunction::TypedThreadSafeFunction(napi_threadsafe_function tsfn); ``` - `tsfn`: The `napi_threadsafe_function` which is a handle for an existing thread-safe function. -Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. To ensure the API +Returns a non-empty `Napi::TypedThreadSafeFunction` instance. To ensure the API statically handles the correct return type for `GetContext()` and `[Non]BlockingCall()`, pass the proper template arguments to -`Napi::ThreadSafeFunctionEx`. +`Napi::TypedThreadSafeFunction`. ### New -Creates a new instance of the `Napi::ThreadSafeFunctionEx` object. The `New` +Creates a new instance of the `Napi::TypedThreadSafeFunction` object. The `New` function has several overloads for the various optional parameters: skip the optional parameter for that specific overload. @@ -72,11 +72,11 @@ New(napi_env env, - `maxQueueSize`: Maximum size of the queue. `0` for no limit. - `initialThreadCount`: The initial number of threads, including the main thread, which will be making use of this function. -- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. - It can be retreived via `GetContext()`. +- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. It + can be retreived via `GetContext()`. - `[optional] finalizeCallback`: Function to call when the - `ThreadSafeFunctionEx` is being destroyed. This callback will be invoked on - the main thread when the thread-safe function is about to be destroyed. It + `TypedThreadSafeFunction` is being destroyed. This callback will be invoked + on the main thread when the thread-safe function is about to be destroyed. It receives the context and the finalize data given during construction (if given), and provides an opportunity for cleaning up after the threads e.g. by calling `uv_thread_join()`. It is important that, aside from the main loop @@ -85,7 +85,7 @@ New(napi_env env, FinalizerDataType* data, ContextType* hint)`. - `[optional] data`: Data to be passed to `finalizeCallback`. -Returns a non-empty `Napi::ThreadSafeFunctionEx` instance. +Returns a non-empty `Napi::TypedThreadSafeFunction` instance. Depending on the targetted `NAPI_VERSION`, the API has different implementations for `CallbackType callback`. @@ -93,7 +93,7 @@ for `CallbackType callback`. When targetting version 4, `callback` may be: - of type `const Function&` - not provided as a parameter, in which case the API creates a new no-op -`Function` + `Function` When targetting version 5+, `callback` may be: - of type `const Function&` @@ -106,7 +106,7 @@ Adds a thread to this thread-safe function object, indicating that a new thread will start making use of the thread-safe function. ```cpp -napi_status Napi::ThreadSafeFunctionEx::Acquire() +napi_status Napi::TypedThreadSafeFunction::Acquire() ``` Returns one of: @@ -124,7 +124,7 @@ has undefined results in the current thread, as the thread-safe function may have been destroyed. ```cpp -napi_status Napi::ThreadSafeFunctionEx::Release() +napi_status Napi::TypedThreadSafeFunction::Release() ``` Returns one of: @@ -135,18 +135,18 @@ Returns one of: ### Abort -"Aborts" the thread-safe function. This will cause all subsequent APIs associated -with the thread-safe function except `Release()` to return `napi_closing` even -before its reference count reaches zero. In particular, `BlockingCall` and -`NonBlockingCall()` will return `napi_closing`, thus informing the threads that -it is no longer possible to make asynchronous calls to the thread-safe function. -This can be used as a criterion for terminating the thread. Upon receiving a -return value of `napi_closing` from a thread-safe function call a thread must -make no further use of the thread-safe function because it is no longer -guaranteed to be allocated. +"Aborts" the thread-safe function. This will cause all subsequent APIs +associated with the thread-safe function except `Release()` to return +`napi_closing` even before its reference count reaches zero. In particular, +`BlockingCall` and `NonBlockingCall()` will return `napi_closing`, thus +informing the threads that it is no longer possible to make asynchronous calls +to the thread-safe function. This can be used as a criterion for terminating the +thread. Upon receiving a return value of `napi_closing` from a thread-safe +function call a thread must make no further use of the thread-safe function +because it is no longer guaranteed to be allocated. ```cpp -napi_status Napi::ThreadSafeFunctionEx::Abort() +napi_status Napi::TypedThreadSafeFunction::Abort() ``` Returns one of: @@ -165,13 +165,13 @@ Calls the Javascript function in either a blocking or non-blocking fashion. preventing data from being successfully added to the queue. ```cpp -napi_status Napi::ThreadSafeFunctionEx::BlockingCall(DataType* data = nullptr) const +napi_status Napi::TypedThreadSafeFunction::BlockingCall(DataType* data = nullptr) const -napi_status Napi::ThreadSafeFunctionEx::NonBlockingCall(DataType* data = nullptr) const +napi_status Napi::TypedThreadSafeFunction::NonBlockingCall(DataType* data = nullptr) const ``` - `[optional] data`: Data to pass to the callback which was passed to - `ThreadSafeFunctionEx::New()`. + `TypedThreadSafeFunction::New()`. Returns one of: - `napi_ok`: `data` was successfully added to the queue. @@ -196,7 +196,7 @@ using namespace Napi; using Context = Reference; using DataType = int; void CallJs(Napi::Env env, Function callback, Context *context, DataType *data); -using TSFN = ThreadSafeFunctionEx; +using TSFN = TypedThreadSafeFunction; using FinalizerDataType = void; std::thread nativeThread; diff --git a/napi-inl.h b/napi-inl.h index 624f36c84..55273b662 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4377,7 +4377,7 @@ inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { #if (NAPI_VERSION > 3 && !defined(__wasm32__)) //////////////////////////////////////////////////////////////////////////////// -// ThreadSafeFunctionEx class +// TypedThreadSafeFunction class //////////////////////////////////////////////////////////////////////////////// // Starting with NAPI 5, the JavaScript function `func` parameter of @@ -4387,11 +4387,11 @@ inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { template template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( napi_env env, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; napi_status status = napi_create_threadsafe_function( env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, @@ -4399,7 +4399,7 @@ ThreadSafeFunctionEx::New( CallJsInternal, &tsfn._tsfn); if (status != napi_ok) { NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); + TypedThreadSafeFunction()); } return tsfn; @@ -4409,11 +4409,11 @@ ThreadSafeFunctionEx::New( template template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( napi_env env, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; napi_status status = napi_create_threadsafe_function( env, nullptr, resource, String::From(env, resourceName), maxQueueSize, @@ -4421,7 +4421,7 @@ ThreadSafeFunctionEx::New( &tsfn._tsfn); if (status != napi_ok) { NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); + TypedThreadSafeFunction()); } return tsfn; @@ -4432,12 +4432,12 @@ template template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( napi_env env, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data) { - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; auto *finalizeData = new details::ThreadSafeFinalize( @@ -4451,7 +4451,7 @@ ThreadSafeFunctionEx::New( if (status != napi_ok) { delete finalizeData; NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); + TypedThreadSafeFunction()); } return tsfn; @@ -4462,12 +4462,12 @@ template template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( napi_env env, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data) { - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; auto *finalizeData = new details::ThreadSafeFinalize( @@ -4481,7 +4481,7 @@ ThreadSafeFunctionEx::New( if (status != napi_ok) { delete finalizeData; NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); + TypedThreadSafeFunction()); } return tsfn; @@ -4492,11 +4492,11 @@ ThreadSafeFunctionEx::New( template template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( napi_env env, const Function &callback, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; napi_status status = napi_create_threadsafe_function( env, callback, nullptr, String::From(env, resourceName), maxQueueSize, @@ -4504,7 +4504,7 @@ ThreadSafeFunctionEx::New( &tsfn._tsfn); if (status != napi_ok) { NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); + TypedThreadSafeFunction()); } return tsfn; @@ -4514,12 +4514,12 @@ ThreadSafeFunctionEx::New( template template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( napi_env env, const Function &callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; napi_status status = napi_create_threadsafe_function( env, callback, resource, String::From(env, resourceName), maxQueueSize, @@ -4527,7 +4527,7 @@ ThreadSafeFunctionEx::New( &tsfn._tsfn); if (status != napi_ok) { NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); + TypedThreadSafeFunction()); } return tsfn; @@ -4538,12 +4538,12 @@ template template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( napi_env env, const Function &callback, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data) { - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; auto *finalizeData = new details::ThreadSafeFinalize( @@ -4557,7 +4557,7 @@ ThreadSafeFunctionEx::New( if (status != napi_ok) { delete finalizeData; NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); + TypedThreadSafeFunction()); } return tsfn; @@ -4568,18 +4568,18 @@ template template -inline ThreadSafeFunctionEx -ThreadSafeFunctionEx::New( +inline TypedThreadSafeFunction +TypedThreadSafeFunction::New( napi_env env, CallbackType callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data) { - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; auto *finalizeData = new details::ThreadSafeFinalize( {data, finalizeCallback}); napi_status status = napi_create_threadsafe_function( - env, details::DefaultCallbackWrapper>(env, callback), resource, + env, details::DefaultCallbackWrapper>(env, callback), resource, String::From(env, resourceName), maxQueueSize, initialThreadCount, finalizeData, details::ThreadSafeFinalize:: @@ -4588,7 +4588,7 @@ ThreadSafeFunctionEx::New( if (status != napi_ok) { delete finalizeData; NAPI_THROW_IF_FAILED(env, status, - ThreadSafeFunctionEx()); + TypedThreadSafeFunction()); } return tsfn; @@ -4596,19 +4596,19 @@ ThreadSafeFunctionEx::New( template -inline ThreadSafeFunctionEx::ThreadSafeFunctionEx() +inline TypedThreadSafeFunction::TypedThreadSafeFunction() : _tsfn() {} template -inline ThreadSafeFunctionEx:: - ThreadSafeFunctionEx(napi_threadsafe_function tsfn) +inline TypedThreadSafeFunction:: + TypedThreadSafeFunction(napi_threadsafe_function tsfn) : _tsfn(tsfn) {} template -inline ThreadSafeFunctionEx:: +inline TypedThreadSafeFunction:: operator napi_threadsafe_function() const { return _tsfn; } @@ -4616,7 +4616,7 @@ operator napi_threadsafe_function() const { template inline napi_status -ThreadSafeFunctionEx::BlockingCall( +TypedThreadSafeFunction::BlockingCall( DataType *data) const { return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); } @@ -4624,7 +4624,7 @@ ThreadSafeFunctionEx::BlockingCall( template inline napi_status -ThreadSafeFunctionEx::NonBlockingCall( +TypedThreadSafeFunction::NonBlockingCall( DataType *data) const { return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); } @@ -4632,7 +4632,7 @@ ThreadSafeFunctionEx::NonBlockingCall( template inline void -ThreadSafeFunctionEx::Ref(napi_env env) const { +TypedThreadSafeFunction::Ref(napi_env env) const { if (_tsfn != nullptr) { napi_status status = napi_ref_threadsafe_function(env, _tsfn); NAPI_THROW_IF_FAILED_VOID(env, status); @@ -4642,7 +4642,7 @@ ThreadSafeFunctionEx::Ref(napi_env env) const { template inline void -ThreadSafeFunctionEx::Unref(napi_env env) const { +TypedThreadSafeFunction::Unref(napi_env env) const { if (_tsfn != nullptr) { napi_status status = napi_unref_threadsafe_function(env, _tsfn); NAPI_THROW_IF_FAILED_VOID(env, status); @@ -4652,31 +4652,31 @@ ThreadSafeFunctionEx::Unref(napi_env env) const { template inline napi_status -ThreadSafeFunctionEx::Acquire() const { +TypedThreadSafeFunction::Acquire() const { return napi_acquire_threadsafe_function(_tsfn); } template inline napi_status -ThreadSafeFunctionEx::Release() { +TypedThreadSafeFunction::Release() { return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); } template inline napi_status -ThreadSafeFunctionEx::Abort() { +TypedThreadSafeFunction::Abort() { return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); } template inline ContextType * -ThreadSafeFunctionEx::GetContext() const { +TypedThreadSafeFunction::GetContext() const { void *context; napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); - NAPI_FATAL_IF_FAILED(status, "ThreadSafeFunctionEx::GetContext", + NAPI_FATAL_IF_FAILED(status, "TypedThreadSafeFunction::GetContext", "napi_get_threadsafe_function_context"); return static_cast(context); } @@ -4684,7 +4684,7 @@ ThreadSafeFunctionEx::GetContext() const { // static template -void ThreadSafeFunctionEx::CallJsInternal( +void TypedThreadSafeFunction::CallJsInternal( napi_env env, napi_value jsCallback, void *context, void *data) { details::CallJsWrapper( env, jsCallback, context, data); @@ -4695,7 +4695,7 @@ void ThreadSafeFunctionEx::CallJsInternal( template Napi::Function -ThreadSafeFunctionEx::EmptyFunctionFactory( +TypedThreadSafeFunction::EmptyFunctionFactory( Napi::Env env) { return Napi::Function::New(env, [](const CallbackInfo &cb) {}); } @@ -4704,7 +4704,7 @@ ThreadSafeFunctionEx::EmptyFunctionFactory( template Napi::Function -ThreadSafeFunctionEx::FunctionOrEmpty( +TypedThreadSafeFunction::FunctionOrEmpty( Napi::Env env, Napi::Function &callback) { if (callback.IsEmpty()) { return EmptyFunctionFactory(env); @@ -4717,7 +4717,7 @@ ThreadSafeFunctionEx::FunctionOrEmpty( template std::nullptr_t -ThreadSafeFunctionEx::EmptyFunctionFactory( +TypedThreadSafeFunction::EmptyFunctionFactory( Napi::Env /*env*/) { return nullptr; } @@ -4726,7 +4726,7 @@ ThreadSafeFunctionEx::EmptyFunctionFactory( template Napi::Function -ThreadSafeFunctionEx::FunctionOrEmpty( +TypedThreadSafeFunction::FunctionOrEmpty( Napi::Env /*env*/, Napi::Function &callback) { return callback; } diff --git a/napi.h b/napi.h index 88474a31f..b42b4b707 100644 --- a/napi.h +++ b/napi.h @@ -2244,12 +2244,12 @@ namespace Napi { napi_threadsafe_function _tsfn; }; - // A ThreadSafeFunctionEx by default has no context (nullptr) and can accept + // A TypedThreadSafeFunction by default has no context (nullptr) and can accept // any type (void) to its CallJs. template - class ThreadSafeFunctionEx { + class TypedThreadSafeFunction { public: @@ -2270,7 +2270,7 @@ namespace Napi { // Creates a new threadsafe function with: // Callback [missing] Resource [missing] Finalizer [missing] template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context = nullptr); @@ -2278,7 +2278,7 @@ namespace Napi { // Creates a new threadsafe function with: // Callback [missing] Resource [passed] Finalizer [missing] template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context = nullptr); @@ -2288,7 +2288,7 @@ namespace Napi { // Callback [missing] Resource [missing] Finalizer [passed] template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data = nullptr); @@ -2298,7 +2298,7 @@ namespace Napi { // Callback [missing] Resource [passed] Finalizer [passed] template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data = nullptr); @@ -2308,7 +2308,7 @@ namespace Napi { // Creates a new threadsafe function with: // Callback [passed] Resource [missing] Finalizer [missing] template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, const Function &callback, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context = nullptr); @@ -2317,7 +2317,7 @@ namespace Napi { // Creates a new threadsafe function with: // Callback [passed] Resource [passed] Finalizer [missing] template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, const Function &callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context = nullptr); @@ -2327,7 +2327,7 @@ namespace Napi { // Callback [passed] Resource [missing] Finalizer [passed] template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, const Function &callback, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data = nullptr); @@ -2337,14 +2337,14 @@ namespace Napi { // Callback [passed] Resource [passed] Finalizer [passed] template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, CallbackType callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data = nullptr); - ThreadSafeFunctionEx(); - ThreadSafeFunctionEx( + TypedThreadSafeFunction(); + TypedThreadSafeFunction( napi_threadsafe_function tsFunctionValue); operator napi_threadsafe_function() const; @@ -2376,7 +2376,7 @@ namespace Napi { private: template - static ThreadSafeFunctionEx + static TypedThreadSafeFunction New(napi_env env, const Function &callback, const Object &resource, ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, ContextType *context, diff --git a/test/binding.cc b/test/binding.cc index fe3a3cd5b..b46cb4df8 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -49,12 +49,12 @@ Object InitThreadSafeFunctionPtr(Env env); Object InitThreadSafeFunctionSum(Env env); Object InitThreadSafeFunctionUnref(Env env); Object InitThreadSafeFunction(Env env); -Object InitThreadSafeFunctionExCtx(Env env); -Object InitThreadSafeFunctionExExistingTsfn(Env env); -Object InitThreadSafeFunctionExPtr(Env env); -Object InitThreadSafeFunctionExSum(Env env); -Object InitThreadSafeFunctionExUnref(Env env); -Object InitThreadSafeFunctionEx(Env env); +Object InitTypedThreadSafeFunctionCtx(Env env); +Object InitTypedThreadSafeFunctionExistingTsfn(Env env); +Object InitTypedThreadSafeFunctionPtr(Env env); +Object InitTypedThreadSafeFunctionSum(Env env); +Object InitTypedThreadSafeFunctionUnref(Env env); +Object InitTypedThreadSafeFunction(Env env); #endif Object InitTypedArray(Env env); Object InitObjectWrap(Env env); @@ -114,13 +114,13 @@ Object Init(Env env, Object exports) { exports.Set("threadsafe_function_ptr", InitThreadSafeFunctionPtr(env)); exports.Set("threadsafe_function_sum", InitThreadSafeFunctionSum(env)); exports.Set("threadsafe_function_unref", InitThreadSafeFunctionUnref(env)); - exports.Set("threadsafe_function", InitThreadSafeFunctionEx(env)); - exports.Set("threadsafe_function_ex_ctx", InitThreadSafeFunctionExCtx(env)); - exports.Set("threadsafe_function_ex_existing_tsfn", InitThreadSafeFunctionExExistingTsfn(env)); - exports.Set("threadsafe_function_ex_ptr", InitThreadSafeFunctionExPtr(env)); - exports.Set("threadsafe_function_ex_sum", InitThreadSafeFunctionExSum(env)); - exports.Set("threadsafe_function_ex_unref", InitThreadSafeFunctionExUnref(env)); - exports.Set("threadsafe_function_ex", InitThreadSafeFunctionEx(env)); + exports.Set("threadsafe_function", InitTypedThreadSafeFunction(env)); + exports.Set("typed_threadsafe_function_ctx", InitTypedThreadSafeFunctionCtx(env)); + exports.Set("typed_threadsafe_function_existing_tsfn", InitTypedThreadSafeFunctionExistingTsfn(env)); + exports.Set("typed_threadsafe_function_ptr", InitTypedThreadSafeFunctionPtr(env)); + exports.Set("typed_threadsafe_function_sum", InitTypedThreadSafeFunctionSum(env)); + exports.Set("typed_threadsafe_function_unref", InitTypedThreadSafeFunctionUnref(env)); + exports.Set("typed_threadsafe_function", InitTypedThreadSafeFunction(env)); #endif exports.Set("typedarray", InitTypedArray(env)); exports.Set("objectwrap", InitObjectWrap(env)); diff --git a/test/binding.gyp b/test/binding.gyp index 7f64e7f51..90dee4565 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -36,18 +36,18 @@ 'object/set_property.cc', 'promise.cc', 'run_script.cc', - 'threadsafe_function_ex/threadsafe_function_ex_ctx.cc', - 'threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.cc', - 'threadsafe_function_ex/threadsafe_function_ex_ptr.cc', - 'threadsafe_function_ex/threadsafe_function_ex_sum.cc', - 'threadsafe_function_ex/threadsafe_function_ex_unref.cc', - 'threadsafe_function_ex/threadsafe_function_ex.cc', 'threadsafe_function/threadsafe_function_ctx.cc', 'threadsafe_function/threadsafe_function_existing_tsfn.cc', 'threadsafe_function/threadsafe_function_ptr.cc', 'threadsafe_function/threadsafe_function_sum.cc', 'threadsafe_function/threadsafe_function_unref.cc', 'threadsafe_function/threadsafe_function.cc', + 'typed_threadsafe_function/typed_threadsafe_function_ctx.cc', + 'typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc', + 'typed_threadsafe_function/typed_threadsafe_function_ptr.cc', + 'typed_threadsafe_function/typed_threadsafe_function_sum.cc', + 'typed_threadsafe_function/typed_threadsafe_function_unref.cc', + 'typed_threadsafe_function/typed_threadsafe_function.cc', 'typedarray.cc', 'objectwrap.cc', 'objectwrap_constructor_exception.cc', diff --git a/test/index.js b/test/index.js index 060070590..3f8549999 100644 --- a/test/index.js +++ b/test/index.js @@ -44,18 +44,18 @@ let testModules = [ 'object/set_property', 'promise', 'run_script', - 'threadsafe_function_ex/threadsafe_function_ex_ctx', - 'threadsafe_function_ex/threadsafe_function_ex_existing_tsfn', - 'threadsafe_function_ex/threadsafe_function_ex_ptr', - 'threadsafe_function_ex/threadsafe_function_ex_sum', - 'threadsafe_function_ex/threadsafe_function_ex_unref', - 'threadsafe_function_ex/threadsafe_function_ex', 'threadsafe_function/threadsafe_function_ctx', 'threadsafe_function/threadsafe_function_existing_tsfn', 'threadsafe_function/threadsafe_function_ptr', 'threadsafe_function/threadsafe_function_sum', 'threadsafe_function/threadsafe_function_unref', 'threadsafe_function/threadsafe_function', + 'typed_threadsafe_function/typed_threadsafe_function_ctx', + 'typed_threadsafe_function/typed_threadsafe_function_existing_tsfn', + 'typed_threadsafe_function/typed_threadsafe_function_ptr', + 'typed_threadsafe_function/typed_threadsafe_function_sum', + 'typed_threadsafe_function/typed_threadsafe_function_unref', + 'typed_threadsafe_function/typed_threadsafe_function', 'typedarray', 'typedarray-bigint', 'objectwrap', diff --git a/test/threadsafe_function_ex/threadsafe_function_ex.cc b/test/typed_threadsafe_function/typed_threadsafe_function.cc similarity index 97% rename from test/threadsafe_function_ex/threadsafe_function_ex.cc rename to test/typed_threadsafe_function/typed_threadsafe_function.cc index cb604c17e..71f47b959 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function.cc @@ -36,7 +36,7 @@ static void TSFNCallJS(Env env, Function jsCallback, } } -using TSFN = ThreadSafeFunctionEx; +using TSFN = TypedThreadSafeFunction; static TSFN tsfn; // Thread data to transmit to JS @@ -174,7 +174,7 @@ static Value StartThreadNoNative(const CallbackInfo& info) { return StartThreadInternal(info, ThreadSafeFunctionInfo::DEFAULT); } -Object InitThreadSafeFunctionEx(Env env) { +Object InitTypedThreadSafeFunction(Env env) { for (size_t index = 0; index < ARRAY_LENGTH; index++) { ints[index] = index; } diff --git a/test/threadsafe_function_ex/threadsafe_function_ex.js b/test/typed_threadsafe_function/typed_threadsafe_function.js similarity index 79% rename from test/threadsafe_function_ex/threadsafe_function_ex.js rename to test/typed_threadsafe_function/typed_threadsafe_function.js index 80a03840e..7aa8cc2ad 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex.js +++ b/test/typed_threadsafe_function/typed_threadsafe_function.js @@ -16,7 +16,7 @@ async function test(binding) { result.push(arrayLength - 1 - index); } return result; - })(binding.threadsafe_function_ex.ARRAY_LENGTH); + })(binding.typed_threadsafe_function.ARRAY_LENGTH); function testWithJSMarshaller({ threadStarter, @@ -26,11 +26,11 @@ async function test(binding) { launchSecondary }) { return new Promise((resolve) => { const array = []; - binding.threadsafe_function_ex[threadStarter](function testCallback(value) { + binding.typed_threadsafe_function[threadStarter](function testCallback(value) { array.push(value); if (array.length === quitAfter) { setImmediate(() => { - binding.threadsafe_function_ex.stopThread(common.mustCall(() => { + binding.typed_threadsafe_function.stopThread(common.mustCall(() => { resolve(array); }), !!abort); }); @@ -47,20 +47,20 @@ async function test(binding) { await new Promise(function testWithoutJSMarshaller(resolve) { let callCount = 0; - binding.threadsafe_function_ex.startThreadNoNative(function testCallback() { + binding.typed_threadsafe_function.startThreadNoNative(function testCallback() { callCount++; // The default call-into-JS implementation passes no arguments. assert.strictEqual(arguments.length, 0); - if (callCount === binding.threadsafe_function_ex.ARRAY_LENGTH) { + if (callCount === binding.typed_threadsafe_function.ARRAY_LENGTH) { setImmediate(() => { - binding.threadsafe_function_ex.stopThread(common.mustCall(() => { + binding.typed_threadsafe_function.stopThread(common.mustCall(() => { resolve(); }), false); }); } }, false /* abort */, false /* launchSecondary */, - binding.threadsafe_function_ex.MAX_QUEUE_SIZE); + binding.typed_threadsafe_function.MAX_QUEUE_SIZE); }); // Start the thread in blocking mode, and assert that all values are passed. @@ -68,8 +68,8 @@ async function test(binding) { assert.deepStrictEqual( await testWithJSMarshaller({ threadStarter: 'startThread', - maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, - quitAfter: binding.threadsafe_function_ex.ARRAY_LENGTH + maxQueueSize: binding.typed_threadsafe_function.MAX_QUEUE_SIZE, + quitAfter: binding.typed_threadsafe_function.ARRAY_LENGTH }), expectedArray, ); @@ -80,7 +80,7 @@ async function test(binding) { await testWithJSMarshaller({ threadStarter: 'startThread', maxQueueSize: 0, - quitAfter: binding.threadsafe_function_ex.ARRAY_LENGTH + quitAfter: binding.typed_threadsafe_function.ARRAY_LENGTH }), expectedArray, ); @@ -90,8 +90,8 @@ async function test(binding) { assert.deepStrictEqual( await testWithJSMarshaller({ threadStarter: 'startThreadNonblocking', - maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, - quitAfter: binding.threadsafe_function_ex.ARRAY_LENGTH + maxQueueSize: binding.typed_threadsafe_function.MAX_QUEUE_SIZE, + quitAfter: binding.typed_threadsafe_function.ARRAY_LENGTH }), expectedArray, ); @@ -101,7 +101,7 @@ async function test(binding) { assert.deepStrictEqual( await testWithJSMarshaller({ threadStarter: 'startThread', - maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + maxQueueSize: binding.typed_threadsafe_function.MAX_QUEUE_SIZE, quitAfter: 1 }), expectedArray, @@ -124,7 +124,7 @@ async function test(binding) { assert.deepStrictEqual( await testWithJSMarshaller({ threadStarter: 'startThreadNonblocking', - maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + maxQueueSize: binding.typed_threadsafe_function.MAX_QUEUE_SIZE, quitAfter: 1 }), expectedArray, @@ -137,7 +137,7 @@ async function test(binding) { await testWithJSMarshaller({ threadStarter: 'startThread', quitAfter: 1, - maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + maxQueueSize: binding.typed_threadsafe_function.MAX_QUEUE_SIZE, launchSecondary: true }), expectedArray, @@ -150,7 +150,7 @@ async function test(binding) { await testWithJSMarshaller({ threadStarter: 'startThreadNonblocking', quitAfter: 1, - maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + maxQueueSize: binding.typed_threadsafe_function.MAX_QUEUE_SIZE, launchSecondary: true }), expectedArray, @@ -162,7 +162,7 @@ async function test(binding) { (await testWithJSMarshaller({ threadStarter: 'startThread', quitAfter: 1, - maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + maxQueueSize: binding.typed_threadsafe_function.MAX_QUEUE_SIZE, abort: true })).indexOf(0), -1, @@ -186,7 +186,7 @@ async function test(binding) { (await testWithJSMarshaller({ threadStarter: 'startThreadNonblocking', quitAfter: 1, - maxQueueSize: binding.threadsafe_function_ex.MAX_QUEUE_SIZE, + maxQueueSize: binding.typed_threadsafe_function.MAX_QUEUE_SIZE, abort: true })).indexOf(0), -1, diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_ctx.cc b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.cc similarity index 93% rename from test/threadsafe_function_ex/threadsafe_function_ex_ctx.cc rename to test/typed_threadsafe_function/typed_threadsafe_function_ctx.cc index f186fc845..4ec3e368b 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_ctx.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.cc @@ -5,7 +5,7 @@ using namespace Napi; using ContextType = Reference; -using TSFN = ThreadSafeFunctionEx; +using TSFN = TypedThreadSafeFunction; namespace { @@ -55,7 +55,7 @@ TSFNWrap::TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { } // namespace -Object InitThreadSafeFunctionExCtx(Env env) { +Object InitTypedThreadSafeFunctionCtx(Env env) { return TSFNWrap::Init(env, Object::New(env)); } diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_ctx.js b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js similarity index 100% rename from test/threadsafe_function_ex/threadsafe_function_ex_ctx.js rename to test/typed_threadsafe_function/typed_threadsafe_function_ctx.js diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.cc b/test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc similarity index 96% rename from test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.cc rename to test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc index fc28a06c5..799aacb9f 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc @@ -21,7 +21,7 @@ struct TestContext { }; }; -using TSFN = ThreadSafeFunctionEx; +using TSFN = TypedThreadSafeFunction; void FinalizeCB(napi_env env, void * /*finalizeData */, void *context) { TestContext *testContext = static_cast(context); @@ -104,7 +104,7 @@ static Value TestCall(const CallbackInfo &info) { } // namespace -Object InitThreadSafeFunctionExExistingTsfn(Env env) { +Object InitTypedThreadSafeFunctionExistingTsfn(Env env) { Object exports = Object::New(env); exports["testCall"] = Function::New(env, TestCall); diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.js b/test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.js similarity index 89% rename from test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.js rename to test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.js index d42517d9f..b6df669d4 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_existing_tsfn.js +++ b/test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.js @@ -8,8 +8,8 @@ module.exports = test(require(`../build/${buildType}/binding.node`)) .then(() => test(require(`../build/${buildType}/binding_noexcept.node`))); async function test(binding) { - const testCall = binding.threadsafe_function_ex_existing_tsfn.testCall; - + const testCall = binding.typed_threadsafe_function_existing_tsfn.testCall; + assert.strictEqual(typeof await testCall({ blocking: true, data: true }), "number"); assert.strictEqual(typeof await testCall({ blocking: true, data: false }), "undefined"); assert.strictEqual(typeof await testCall({ blocking: false, data: true }), "number"); diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_ptr.cc b/test/typed_threadsafe_function/typed_threadsafe_function_ptr.cc similarity index 83% rename from test/threadsafe_function_ex/threadsafe_function_ex_ptr.cc rename to test/typed_threadsafe_function/typed_threadsafe_function_ptr.cc index c8810ce50..27367e4ea 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_ptr.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_ptr.cc @@ -6,7 +6,7 @@ using namespace Napi; namespace { -using TSFN = ThreadSafeFunctionEx<>; +using TSFN = TypedThreadSafeFunction<>; static Value Test(const CallbackInfo& info) { Object resource = info[0].As(); @@ -18,7 +18,7 @@ static Value Test(const CallbackInfo& info) { } -Object InitThreadSafeFunctionExPtr(Env env) { +Object InitTypedThreadSafeFunctionPtr(Env env) { Object exports = Object::New(env); exports["test"] = Function::New(env, Test); diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_ptr.js b/test/typed_threadsafe_function/typed_threadsafe_function_ptr.js similarity index 79% rename from test/threadsafe_function_ex/threadsafe_function_ex_ptr.js rename to test/typed_threadsafe_function/typed_threadsafe_function_ptr.js index 1276d0930..47b187761 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_ptr.js +++ b/test/typed_threadsafe_function/typed_threadsafe_function_ptr.js @@ -6,5 +6,5 @@ test(require(`../build/${buildType}/binding.node`)); test(require(`../build/${buildType}/binding_noexcept.node`)); function test(binding) { - binding.threadsafe_function_ex_ptr.test({}, () => {}); + binding.typed_threadsafe_function_ptr.test({}, () => {}); } diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_sum.cc b/test/typed_threadsafe_function/typed_threadsafe_function_sum.cc similarity index 97% rename from test/threadsafe_function_ex/threadsafe_function_ex_sum.cc rename to test/typed_threadsafe_function/typed_threadsafe_function_sum.cc index 69c8a8fb2..6b33499a6 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_sum.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_sum.cc @@ -37,10 +37,10 @@ struct TestData { delete data; } - ThreadSafeFunctionEx tsfn; + TypedThreadSafeFunction tsfn; }; -using TSFN = ThreadSafeFunctionEx; +using TSFN = TypedThreadSafeFunction; void FinalizerCallback(Napi::Env env, void *, TestData *finalizeData) { for (size_t i = 0; i < finalizeData->threads.size(); ++i) { @@ -209,7 +209,7 @@ static Value TestAcquire(const CallbackInfo &info) { } } // namespace -Object InitThreadSafeFunctionExSum(Env env) { +Object InitTypedThreadSafeFunctionSum(Env env) { Object exports = Object::New(env); exports["testDelayedTSFN"] = Function::New(env, TestDelayedTSFN); exports["testWithTSFN"] = Function::New(env, TestWithTSFN); diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_sum.js b/test/typed_threadsafe_function/typed_threadsafe_function_sum.js similarity index 88% rename from test/threadsafe_function_ex/threadsafe_function_ex_sum.js rename to test/typed_threadsafe_function/typed_threadsafe_function_sum.js index ef7162c25..8f10476f6 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_sum.js +++ b/test/typed_threadsafe_function/typed_threadsafe_function_sum.js @@ -45,7 +45,7 @@ function test(binding) { async function checkAcquire() { const calls = []; - const { promise, createThread, stopThreads } = binding.threadsafe_function_ex_sum.testAcquire(Array.prototype.push.bind(calls)); + const { promise, createThread, stopThreads } = binding.typed_threadsafe_function_sum.testAcquire(Array.prototype.push.bind(calls)); for (let i = 0; i < THREAD_COUNT; i++) { createThread(); } @@ -55,7 +55,7 @@ function test(binding) { assert.equal(sum(calls), EXPECTED_SUM); } - return check(binding.threadsafe_function_ex_sum.testDelayedTSFN) - .then(() => check(binding.threadsafe_function_ex_sum.testWithTSFN)) + return check(binding.typed_threadsafe_function_sum.testDelayedTSFN) + .then(() => check(binding.typed_threadsafe_function_sum.testWithTSFN)) .then(() => checkAcquire()); } diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_unref.cc b/test/typed_threadsafe_function/typed_threadsafe_function_unref.cc similarity index 91% rename from test/threadsafe_function_ex/threadsafe_function_ex_unref.cc rename to test/typed_threadsafe_function/typed_threadsafe_function_unref.cc index 2e2041380..3f81611fc 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_unref.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_unref.cc @@ -6,7 +6,7 @@ using namespace Napi; namespace { -using TSFN = ThreadSafeFunctionEx<>; +using TSFN = TypedThreadSafeFunction<>; using ContextType = std::nullptr_t; using FinalizerDataType = void; static Value TestUnref(const CallbackInfo& info) { @@ -35,7 +35,7 @@ static Value TestUnref(const CallbackInfo& info) { } -Object InitThreadSafeFunctionExUnref(Env env) { +Object InitTypedThreadSafeFunctionUnref(Env env) { Object exports = Object::New(env); exports["testUnref"] = Function::New(env, TestUnref); return exports; diff --git a/test/threadsafe_function_ex/threadsafe_function_ex_unref.js b/test/typed_threadsafe_function/typed_threadsafe_function_unref.js similarity index 95% rename from test/threadsafe_function_ex/threadsafe_function_ex_unref.js rename to test/typed_threadsafe_function/typed_threadsafe_function_unref.js index eee3fcf8f..55b42a553 100644 --- a/test/threadsafe_function_ex/threadsafe_function_ex_unref.js +++ b/test/typed_threadsafe_function/typed_threadsafe_function_unref.js @@ -50,6 +50,6 @@ function test(bindingFile) { } else { // Child process const binding = require(bindingFile); - binding.threadsafe_function_ex_unref.testUnref({}, () => { }); + binding.typed_threadsafe_function_unref.testUnref({}, () => { }); } } From 5e5b9ce1b7023de2033cf74e383ebe48f792572d Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:34:02 +0100 Subject: [PATCH 39/39] Apply formatting changes --- napi-inl.h | 432 ++++++++++++------ napi.h | 163 ++++--- test/binding.cc | 15 +- .../typed_threadsafe_function.cc | 50 +- .../typed_threadsafe_function_ctx.cc | 30 +- ...typed_threadsafe_function_existing_tsfn.cc | 56 ++- .../typed_threadsafe_function_ptr.cc | 2 +- .../typed_threadsafe_function_sum.cc | 83 ++-- .../typed_threadsafe_function_unref.cc | 28 +- 9 files changed, 542 insertions(+), 317 deletions(-) diff --git a/napi-inl.h b/napi-inl.h index 396b8aeae..5a345df53 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -262,16 +262,17 @@ struct ThreadSafeFinalize { }; template -typename std::enable_if::type -static inline CallJsWrapper(napi_env env, napi_value jsCallback, void *context, void *data) { - call(env, Function(env, jsCallback), static_cast(context), - static_cast(data)); +typename std::enable_if::type static inline CallJsWrapper( + napi_env env, napi_value jsCallback, void* context, void* data) { + call(env, + Function(env, jsCallback), + static_cast(context), + static_cast(data)); } template -typename std::enable_if::type -static inline CallJsWrapper(napi_env env, napi_value jsCallback, void * /*context*/, - void * /*data*/) { +typename std::enable_if::type static inline CallJsWrapper( + napi_env env, napi_value jsCallback, void* /*context*/, void* /*data*/) { if (jsCallback != nullptr) { Function(env, jsCallback).Call(0, nullptr); } @@ -4399,104 +4400,156 @@ inline void AsyncWorker::OnWorkComplete(Napi::Env /*env*/, napi_status status) { // `napi_create_threadsafe_function` is optional. #if NAPI_VERSION > 4 // static, with Callback [missing] Resource [missing] Finalizer [missing] -template +template template inline TypedThreadSafeFunction TypedThreadSafeFunction::New( - napi_env env, ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context) { + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { TypedThreadSafeFunction tsfn; - napi_status status = napi_create_threadsafe_function( - env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, - initialThreadCount, nullptr, nullptr, context, - CallJsInternal, &tsfn._tsfn); + napi_status status = + napi_create_threadsafe_function(env, + nullptr, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status, - TypedThreadSafeFunction()); + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); } return tsfn; } // static, with Callback [missing] Resource [passed] Finalizer [missing] -template +template template inline TypedThreadSafeFunction TypedThreadSafeFunction::New( - napi_env env, const Object &resource, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { TypedThreadSafeFunction tsfn; - napi_status status = napi_create_threadsafe_function( - env, nullptr, resource, String::From(env, resourceName), maxQueueSize, - initialThreadCount, nullptr, nullptr, context, CallJsInternal, - &tsfn._tsfn); + napi_status status = + napi_create_threadsafe_function(env, + nullptr, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status, - TypedThreadSafeFunction()); + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); } return tsfn; } // static, with Callback [missing] Resource [missing] Finalizer [passed] -template -template +template inline TypedThreadSafeFunction TypedThreadSafeFunction::New( - napi_env env, ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context, Finalizer finalizeCallback, - FinalizerDataType *data) { + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { TypedThreadSafeFunction tsfn; - auto *finalizeData = new details::ThreadSafeFinalize( - {data, finalizeCallback}); + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); napi_status status = napi_create_threadsafe_function( - env, nullptr, nullptr, String::From(env, resourceName), maxQueueSize, - initialThreadCount, finalizeData, + env, + nullptr, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, details::ThreadSafeFinalize:: FinalizeFinalizeWrapperWithDataAndContext, - context, CallJsInternal, &tsfn._tsfn); + context, + CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, - TypedThreadSafeFunction()); + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); } return tsfn; } // static, with Callback [missing] Resource [passed] Finalizer [passed] -template -template +template inline TypedThreadSafeFunction TypedThreadSafeFunction::New( - napi_env env, const Object &resource, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data) { + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { TypedThreadSafeFunction tsfn; - auto *finalizeData = new details::ThreadSafeFinalize( - {data, finalizeCallback}); + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); napi_status status = napi_create_threadsafe_function( - env, nullptr, resource, String::From(env, resourceName), maxQueueSize, - initialThreadCount, finalizeData, + env, + nullptr, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, details::ThreadSafeFinalize:: FinalizeFinalizeWrapperWithDataAndContext, - context, CallJsInternal, &tsfn._tsfn); + context, + CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, - TypedThreadSafeFunction()); + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); } return tsfn; @@ -4504,223 +4557,296 @@ TypedThreadSafeFunction::New( #endif // static, with Callback [passed] Resource [missing] Finalizer [missing] -template +template template inline TypedThreadSafeFunction TypedThreadSafeFunction::New( - napi_env env, const Function &callback, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context) { + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { TypedThreadSafeFunction tsfn; - napi_status status = napi_create_threadsafe_function( - env, callback, nullptr, String::From(env, resourceName), maxQueueSize, - initialThreadCount, nullptr, nullptr, context, CallJsInternal, - &tsfn._tsfn); + napi_status status = + napi_create_threadsafe_function(env, + callback, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status, - TypedThreadSafeFunction()); + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); } return tsfn; } // static, with Callback [passed] Resource [passed] Finalizer [missing] -template +template template inline TypedThreadSafeFunction TypedThreadSafeFunction::New( - napi_env env, const Function &callback, const Object &resource, - ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, - ContextType *context) { + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context) { TypedThreadSafeFunction tsfn; - napi_status status = napi_create_threadsafe_function( - env, callback, resource, String::From(env, resourceName), maxQueueSize, - initialThreadCount, nullptr, nullptr, context, CallJsInternal, - &tsfn._tsfn); + napi_status status = + napi_create_threadsafe_function(env, + callback, + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + nullptr, + nullptr, + context, + CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { - NAPI_THROW_IF_FAILED(env, status, - TypedThreadSafeFunction()); + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); } return tsfn; } // static, with Callback [passed] Resource [missing] Finalizer [passed] -template -template +template inline TypedThreadSafeFunction TypedThreadSafeFunction::New( - napi_env env, const Function &callback, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data) { + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { TypedThreadSafeFunction tsfn; - auto *finalizeData = new details::ThreadSafeFinalize( - {data, finalizeCallback}); + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); napi_status status = napi_create_threadsafe_function( - env, callback, nullptr, String::From(env, resourceName), maxQueueSize, - initialThreadCount, finalizeData, + env, + callback, + nullptr, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, + finalizeData, details::ThreadSafeFinalize:: FinalizeFinalizeWrapperWithDataAndContext, - context, CallJsInternal, &tsfn._tsfn); + context, + CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, - TypedThreadSafeFunction()); + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); } return tsfn; } // static, with: Callback [passed] Resource [passed] Finalizer [passed] -template -template +template inline TypedThreadSafeFunction TypedThreadSafeFunction::New( - napi_env env, CallbackType callback, const Object &resource, - ResourceString resourceName, size_t maxQueueSize, size_t initialThreadCount, - ContextType *context, Finalizer finalizeCallback, FinalizerDataType *data) { + napi_env env, + CallbackType callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data) { TypedThreadSafeFunction tsfn; - auto *finalizeData = new details::ThreadSafeFinalize( - {data, finalizeCallback}); + auto* finalizeData = new details:: + ThreadSafeFinalize( + {data, finalizeCallback}); napi_status status = napi_create_threadsafe_function( - env, details::DefaultCallbackWrapper>(env, callback), resource, - String::From(env, resourceName), maxQueueSize, initialThreadCount, + env, + details::DefaultCallbackWrapper< + CallbackType, + TypedThreadSafeFunction>(env, + callback), + resource, + String::From(env, resourceName), + maxQueueSize, + initialThreadCount, finalizeData, details::ThreadSafeFinalize:: FinalizeFinalizeWrapperWithDataAndContext, - context, CallJsInternal, &tsfn._tsfn); + context, + CallJsInternal, + &tsfn._tsfn); if (status != napi_ok) { delete finalizeData; - NAPI_THROW_IF_FAILED(env, status, - TypedThreadSafeFunction()); + NAPI_THROW_IF_FAILED( + env, status, TypedThreadSafeFunction()); } return tsfn; } -template -inline TypedThreadSafeFunction::TypedThreadSafeFunction() +template +inline TypedThreadSafeFunction:: + TypedThreadSafeFunction() : _tsfn() {} -template +template inline TypedThreadSafeFunction:: TypedThreadSafeFunction(napi_threadsafe_function tsfn) : _tsfn(tsfn) {} -template +template inline TypedThreadSafeFunction:: operator napi_threadsafe_function() const { return _tsfn; } -template +template inline napi_status TypedThreadSafeFunction::BlockingCall( - DataType *data) const { + DataType* data) const { return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_blocking); } -template +template inline napi_status TypedThreadSafeFunction::NonBlockingCall( - DataType *data) const { + DataType* data) const { return napi_call_threadsafe_function(_tsfn, data, napi_tsfn_nonblocking); } -template -inline void -TypedThreadSafeFunction::Ref(napi_env env) const { +template +inline void TypedThreadSafeFunction::Ref( + napi_env env) const { if (_tsfn != nullptr) { napi_status status = napi_ref_threadsafe_function(env, _tsfn); NAPI_THROW_IF_FAILED_VOID(env, status); } } -template -inline void -TypedThreadSafeFunction::Unref(napi_env env) const { +template +inline void TypedThreadSafeFunction::Unref( + napi_env env) const { if (_tsfn != nullptr) { napi_status status = napi_unref_threadsafe_function(env, _tsfn); NAPI_THROW_IF_FAILED_VOID(env, status); } } -template +template inline napi_status TypedThreadSafeFunction::Acquire() const { return napi_acquire_threadsafe_function(_tsfn); } -template +template inline napi_status TypedThreadSafeFunction::Release() { return napi_release_threadsafe_function(_tsfn, napi_tsfn_release); } -template +template inline napi_status TypedThreadSafeFunction::Abort() { return napi_release_threadsafe_function(_tsfn, napi_tsfn_abort); } -template -inline ContextType * +template +inline ContextType* TypedThreadSafeFunction::GetContext() const { - void *context; + void* context; napi_status status = napi_get_threadsafe_function_context(_tsfn, &context); - NAPI_FATAL_IF_FAILED(status, "TypedThreadSafeFunction::GetContext", + NAPI_FATAL_IF_FAILED(status, + "TypedThreadSafeFunction::GetContext", "napi_get_threadsafe_function_context"); - return static_cast(context); + return static_cast(context); } // static -template +template void TypedThreadSafeFunction::CallJsInternal( - napi_env env, napi_value jsCallback, void *context, void *data) { + napi_env env, napi_value jsCallback, void* context, void* data) { details::CallJsWrapper( env, jsCallback, context, data); } #if NAPI_VERSION == 4 // static -template +template Napi::Function TypedThreadSafeFunction::EmptyFunctionFactory( Napi::Env env) { - return Napi::Function::New(env, [](const CallbackInfo &cb) {}); + return Napi::Function::New(env, [](const CallbackInfo& cb) {}); } // static -template +template Napi::Function TypedThreadSafeFunction::FunctionOrEmpty( - Napi::Env env, Napi::Function &callback) { + Napi::Env env, Napi::Function& callback) { if (callback.IsEmpty()) { return EmptyFunctionFactory(env); } @@ -4729,8 +4855,9 @@ TypedThreadSafeFunction::FunctionOrEmpty( #else // static -template +template std::nullptr_t TypedThreadSafeFunction::EmptyFunctionFactory( Napi::Env /*env*/) { @@ -4738,11 +4865,12 @@ TypedThreadSafeFunction::EmptyFunctionFactory( } // static -template +template Napi::Function TypedThreadSafeFunction::FunctionOrEmpty( - Napi::Env /*env*/, Napi::Function &callback) { + Napi::Env /*env*/, Napi::Function& callback) { return callback; } diff --git a/napi.h b/napi.h index 9d813778b..cf4410bd9 100644 --- a/napi.h +++ b/napi.h @@ -2249,15 +2249,14 @@ namespace Napi { napi_threadsafe_function _tsfn; }; - // A TypedThreadSafeFunction by default has no context (nullptr) and can accept - // any type (void) to its CallJs. - template + // A TypedThreadSafeFunction by default has no context (nullptr) and can + // accept any type (void) to its CallJs. + template class TypedThreadSafeFunction { - - public: - + public: // This API may only be called from the main thread. // Helper function that returns nullptr if running N-API 5+, otherwise a // non-empty, no-op Function. This provides the ability to specify at @@ -2268,85 +2267,123 @@ namespace Napi { #else static Napi::Function EmptyFunctionFactory(Napi::Env env); #endif - static Napi::Function FunctionOrEmpty(Napi::Env env, Napi::Function& callback); + static Napi::Function FunctionOrEmpty(Napi::Env env, + Napi::Function& callback); #if NAPI_VERSION > 4 // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [missing] Resource [missing] Finalizer [missing] template - static TypedThreadSafeFunction - New(napi_env env, ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context = nullptr); + static TypedThreadSafeFunction New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [missing] Resource [passed] Finalizer [missing] template - static TypedThreadSafeFunction - New(napi_env env, const Object &resource, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, - ContextType *context = nullptr); + static TypedThreadSafeFunction New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [missing] Resource [missing] Finalizer [passed] - template - static TypedThreadSafeFunction - New(napi_env env, ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data = nullptr); + static TypedThreadSafeFunction New( + napi_env env, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [missing] Resource [passed] Finalizer [passed] - template - static TypedThreadSafeFunction - New(napi_env env, const Object &resource, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data = nullptr); + static TypedThreadSafeFunction New( + napi_env env, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); #endif // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [passed] Resource [missing] Finalizer [missing] template - static TypedThreadSafeFunction - New(napi_env env, const Function &callback, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, - ContextType *context = nullptr); + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [passed] Resource [passed] Finalizer [missing] template - static TypedThreadSafeFunction - New(napi_env env, const Function &callback, const Object &resource, - ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context = nullptr); + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context = nullptr); // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [passed] Resource [missing] Finalizer [passed] - template - static TypedThreadSafeFunction - New(napi_env env, const Function &callback, ResourceString resourceName, - size_t maxQueueSize, size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data = nullptr); + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); // This API may only be called from the main thread. // Creates a new threadsafe function with: // Callback [passed] Resource [passed] Finalizer [passed] - template - static TypedThreadSafeFunction - New(napi_env env, CallbackType callback, const Object &resource, - ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data = nullptr); + template + static TypedThreadSafeFunction New( + napi_env env, + CallbackType callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data = nullptr); TypedThreadSafeFunction(); TypedThreadSafeFunction( @@ -2355,10 +2392,10 @@ namespace Napi { operator napi_threadsafe_function() const; // This API may be called from any thread. - napi_status BlockingCall(DataType *data = nullptr) const; + napi_status BlockingCall(DataType* data = nullptr) const; // This API may be called from any thread. - napi_status NonBlockingCall(DataType *data = nullptr) const; + napi_status NonBlockingCall(DataType* data = nullptr) const; // This API may only be called from the main thread. void Ref(napi_env env) const; @@ -2376,22 +2413,30 @@ namespace Napi { napi_status Abort(); // This API may be called from any thread. - ContextType *GetContext() const; + ContextType* GetContext() const; - private: - template - static TypedThreadSafeFunction - New(napi_env env, const Function &callback, const Object &resource, - ResourceString resourceName, size_t maxQueueSize, - size_t initialThreadCount, ContextType *context, - Finalizer finalizeCallback, FinalizerDataType *data, + static TypedThreadSafeFunction New( + napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data, napi_finalize wrapper); - static void CallJsInternal(napi_env env, napi_value jsCallback, - void *context, void *data); + static void CallJsInternal(napi_env env, + napi_value jsCallback, + void* context, + void* data); - protected: + protected: napi_threadsafe_function _tsfn; }; template diff --git a/test/binding.cc b/test/binding.cc index b46cb4df8..03661da35 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -115,11 +115,16 @@ Object Init(Env env, Object exports) { exports.Set("threadsafe_function_sum", InitThreadSafeFunctionSum(env)); exports.Set("threadsafe_function_unref", InitThreadSafeFunctionUnref(env)); exports.Set("threadsafe_function", InitTypedThreadSafeFunction(env)); - exports.Set("typed_threadsafe_function_ctx", InitTypedThreadSafeFunctionCtx(env)); - exports.Set("typed_threadsafe_function_existing_tsfn", InitTypedThreadSafeFunctionExistingTsfn(env)); - exports.Set("typed_threadsafe_function_ptr", InitTypedThreadSafeFunctionPtr(env)); - exports.Set("typed_threadsafe_function_sum", InitTypedThreadSafeFunctionSum(env)); - exports.Set("typed_threadsafe_function_unref", InitTypedThreadSafeFunctionUnref(env)); + exports.Set("typed_threadsafe_function_ctx", + InitTypedThreadSafeFunctionCtx(env)); + exports.Set("typed_threadsafe_function_existing_tsfn", + InitTypedThreadSafeFunctionExistingTsfn(env)); + exports.Set("typed_threadsafe_function_ptr", + InitTypedThreadSafeFunctionPtr(env)); + exports.Set("typed_threadsafe_function_sum", + InitTypedThreadSafeFunctionSum(env)); + exports.Set("typed_threadsafe_function_unref", + InitTypedThreadSafeFunctionUnref(env)); exports.Set("typed_threadsafe_function", InitTypedThreadSafeFunction(env)); #endif exports.Set("typedarray", InitTypedArray(env)); diff --git a/test/typed_threadsafe_function/typed_threadsafe_function.cc b/test/typed_threadsafe_function/typed_threadsafe_function.cc index 71f47b959..f9896db86 100644 --- a/test/typed_threadsafe_function/typed_threadsafe_function.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function.cc @@ -12,19 +12,17 @@ constexpr size_t MAX_QUEUE_SIZE = 2; static std::thread threads[2]; static struct ThreadSafeFunctionInfo { - enum CallType { - DEFAULT, - BLOCKING, - NON_BLOCKING - } type; + enum CallType { DEFAULT, BLOCKING, NON_BLOCKING } type; bool abort; bool startSecondary; FunctionReference jsFinalizeCallback; uint32_t maxQueueSize; } tsfnInfo; -static void TSFNCallJS(Env env, Function jsCallback, - ThreadSafeFunctionInfo * /* context */, int *data) { +static void TSFNCallJS(Env env, + Function jsCallback, + ThreadSafeFunctionInfo* /* context */, + int* data) { // A null environment signifies the threadsafe function has been finalized. if (!(env == nullptr || jsCallback == nullptr)) { // If called with no data @@ -82,24 +80,25 @@ static void DataSourceThread() { // chance to abort. auto start = std::chrono::high_resolution_clock::now(); constexpr auto MS_200 = std::chrono::milliseconds(200); - for (; std::chrono::high_resolution_clock::now() - start < MS_200;); + for (; std::chrono::high_resolution_clock::now() - start < MS_200;) + ; } switch (status) { - case napi_queue_full: - queueWasFull = true; - index++; - // fall through + case napi_queue_full: + queueWasFull = true; + index++; + // fall through - case napi_ok: - continue; + case napi_ok: + continue; - case napi_closing: - queueWasClosing = true; - break; + case napi_closing: + queueWasClosing = true; + break; - default: - Error::Fatal("DataSourceThread", "ThreadSafeFunction.*Call() failed"); + default: + Error::Fatal("DataSourceThread", "ThreadSafeFunction.*Call() failed"); } } @@ -141,14 +140,21 @@ static void JoinTheThreads(Env /* env */, } static Value StartThreadInternal(const CallbackInfo& info, - ThreadSafeFunctionInfo::CallType type) { + ThreadSafeFunctionInfo::CallType type) { tsfnInfo.type = type; tsfnInfo.abort = info[1].As(); tsfnInfo.startSecondary = info[2].As(); tsfnInfo.maxQueueSize = info[3].As().Uint32Value(); - tsfn = TSFN::New(info.Env(), info[0].As(), Object::New(info.Env()), - "Test", tsfnInfo.maxQueueSize, 2, &tsfnInfo, JoinTheThreads, threads); + tsfn = TSFN::New(info.Env(), + info[0].As(), + Object::New(info.Env()), + "Test", + tsfnInfo.maxQueueSize, + 2, + &tsfnInfo, + JoinTheThreads, + threads); threads[0] = std::thread(DataSourceThread); diff --git a/test/typed_threadsafe_function/typed_threadsafe_function_ctx.cc b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.cc index 4ec3e368b..ee70bb352 100644 --- a/test/typed_threadsafe_function/typed_threadsafe_function_ctx.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.cc @@ -10,30 +10,31 @@ using TSFN = TypedThreadSafeFunction; namespace { class TSFNWrap : public ObjectWrap { -public: + public: static Object Init(Napi::Env env, Object exports); - TSFNWrap(const CallbackInfo &info); + TSFNWrap(const CallbackInfo& info); - Napi::Value GetContext(const CallbackInfo & /*info*/) { - ContextType *ctx = _tsfn.GetContext(); + Napi::Value GetContext(const CallbackInfo& /*info*/) { + ContextType* ctx = _tsfn.GetContext(); return ctx->Value(); }; - Napi::Value Release(const CallbackInfo &info) { + Napi::Value Release(const CallbackInfo& info) { Napi::Env env = info.Env(); _deferred = std::unique_ptr(new Promise::Deferred(env)); _tsfn.Release(); return _deferred->Promise(); }; -private: + private: TSFN _tsfn; std::unique_ptr _deferred; }; Object TSFNWrap::Init(Napi::Env env, Object exports) { Function func = - DefineClass(env, "TSFNWrap", + DefineClass(env, + "TSFNWrap", {InstanceMethod("getContext", &TSFNWrap::GetContext), InstanceMethod("release", &TSFNWrap::Release)}); @@ -41,19 +42,24 @@ Object TSFNWrap::Init(Napi::Env env, Object exports) { return exports; } -TSFNWrap::TSFNWrap(const CallbackInfo &info) : ObjectWrap(info) { - ContextType *_ctx = new ContextType; +TSFNWrap::TSFNWrap(const CallbackInfo& info) : ObjectWrap(info) { + ContextType* _ctx = new ContextType; *_ctx = Persistent(info[0]); - _tsfn = TSFN::New(info.Env(), this->Value(), "Test", 1, 1, _ctx, - [this](Napi::Env env, void *, ContextType *ctx) { + _tsfn = TSFN::New(info.Env(), + this->Value(), + "Test", + 1, + 1, + _ctx, + [this](Napi::Env env, void*, ContextType* ctx) { _deferred->Resolve(env.Undefined()); ctx->Reset(); delete ctx; }); } -} // namespace +} // namespace Object InitTypedThreadSafeFunctionCtx(Env env) { return TSFNWrap::Init(env, Object::New(env)); diff --git a/test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc b/test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc index 799aacb9f..eccf87c93 100644 --- a/test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc @@ -1,5 +1,5 @@ -#include "napi.h" #include +#include "napi.h" #if (NAPI_VERSION > 3) @@ -8,23 +8,22 @@ using namespace Napi; namespace { struct TestContext { - TestContext(Promise::Deferred &&deferred) + TestContext(Promise::Deferred&& deferred) : deferred(std::move(deferred)), callData(nullptr){}; napi_threadsafe_function tsfn; Promise::Deferred deferred; - double *callData; + double* callData; ~TestContext() { - if (callData != nullptr) - delete callData; + if (callData != nullptr) delete callData; }; }; using TSFN = TypedThreadSafeFunction; -void FinalizeCB(napi_env env, void * /*finalizeData */, void *context) { - TestContext *testContext = static_cast(context); +void FinalizeCB(napi_env env, void* /*finalizeData */, void* context) { + TestContext* testContext = static_cast(context); if (testContext->callData != nullptr) { testContext->deferred.Resolve(Number::New(env, *testContext->callData)); } else { @@ -33,10 +32,12 @@ void FinalizeCB(napi_env env, void * /*finalizeData */, void *context) { delete testContext; } -void CallJSWithData(napi_env env, napi_value /* callback */, void *context, - void *data) { - TestContext *testContext = static_cast(context); - testContext->callData = static_cast(data); +void CallJSWithData(napi_env env, + napi_value /* callback */, + void* context, + void* data) { + TestContext* testContext = static_cast(context); + testContext->callData = static_cast(data); napi_status status = napi_release_threadsafe_function(testContext->tsfn, napi_tsfn_release); @@ -44,9 +45,11 @@ void CallJSWithData(napi_env env, napi_value /* callback */, void *context, NAPI_THROW_IF_FAILED_VOID(env, status); } -void CallJSNoData(napi_env env, napi_value /* callback */, void *context, - void * /*data*/) { - TestContext *testContext = static_cast(context); +void CallJSNoData(napi_env env, + napi_value /* callback */, + void* context, + void* /*data*/) { + TestContext* testContext = static_cast(context); testContext->callData = nullptr; napi_status status = @@ -55,7 +58,7 @@ void CallJSNoData(napi_env env, napi_value /* callback */, void *context, NAPI_THROW_IF_FAILED_VOID(env, status); } -static Value TestCall(const CallbackInfo &info) { +static Value TestCall(const CallbackInfo& info) { Napi::Env env = info.Env(); bool isBlocking = false; bool hasData = false; @@ -70,15 +73,22 @@ static Value TestCall(const CallbackInfo &info) { } // Allow optional callback passed from JS. Useful for testing. - Function cb = Function::New(env, [](const CallbackInfo & /*info*/) {}); + Function cb = Function::New(env, [](const CallbackInfo& /*info*/) {}); - TestContext *testContext = new TestContext(Napi::Promise::Deferred(env)); + TestContext* testContext = new TestContext(Napi::Promise::Deferred(env)); - napi_status status = napi_create_threadsafe_function( - env, cb, Object::New(env), String::New(env, "Test"), 0, 1, - nullptr, /*finalize data*/ - FinalizeCB, testContext, hasData ? CallJSWithData : CallJSNoData, - &testContext->tsfn); + napi_status status = + napi_create_threadsafe_function(env, + cb, + Object::New(env), + String::New(env, "Test"), + 0, + 1, + nullptr, /*finalize data*/ + FinalizeCB, + testContext, + hasData ? CallJSWithData : CallJSNoData, + &testContext->tsfn); NAPI_THROW_IF_FAILED(env, status, Value()); @@ -102,7 +112,7 @@ static Value TestCall(const CallbackInfo &info) { return testContext->deferred.Promise(); } -} // namespace +} // namespace Object InitTypedThreadSafeFunctionExistingTsfn(Env env) { Object exports = Object::New(env); diff --git a/test/typed_threadsafe_function/typed_threadsafe_function_ptr.cc b/test/typed_threadsafe_function/typed_threadsafe_function_ptr.cc index 27367e4ea..891fd560c 100644 --- a/test/typed_threadsafe_function/typed_threadsafe_function_ptr.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_ptr.cc @@ -16,7 +16,7 @@ static Value Test(const CallbackInfo& info) { return info.Env().Undefined(); } -} +} // namespace Object InitTypedThreadSafeFunctionPtr(Env env) { Object exports = Object::New(env); diff --git a/test/typed_threadsafe_function/typed_threadsafe_function_sum.cc b/test/typed_threadsafe_function/typed_threadsafe_function_sum.cc index 6b33499a6..9add259c4 100644 --- a/test/typed_threadsafe_function/typed_threadsafe_function_sum.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_sum.cc @@ -1,8 +1,8 @@ -#include "napi.h" #include #include #include #include +#include "napi.h" #if (NAPI_VERSION > 3) @@ -11,8 +11,7 @@ using namespace Napi; namespace { struct TestData { - - TestData(Promise::Deferred &&deferred) : deferred(std::move(deferred)){}; + TestData(Promise::Deferred&& deferred) : deferred(std::move(deferred)){}; // Native Promise returned to JavaScript Promise::Deferred deferred; @@ -25,9 +24,10 @@ struct TestData { bool mainWantsRelease = false; size_t expected_calls = 0; - static void CallJs(Napi::Env env, Function callback, TestData *testData, - double *data) { - + static void CallJs(Napi::Env env, + Function callback, + TestData* testData, + double* data) { // This lambda runs on the main thread so it's OK to access the variables // `expected_calls` and `mainWantsRelease`. testData->expected_calls--; @@ -42,7 +42,7 @@ struct TestData { using TSFN = TypedThreadSafeFunction; -void FinalizerCallback(Napi::Env env, void *, TestData *finalizeData) { +void FinalizerCallback(Napi::Env env, void*, TestData* finalizeData) { for (size_t i = 0; i < finalizeData->threads.size(); ++i) { finalizeData->threads[i].join(); } @@ -60,17 +60,23 @@ void entryWithTSFN(TSFN tsfn, int threadId) { tsfn.Release(); } -static Value TestWithTSFN(const CallbackInfo &info) { +static Value TestWithTSFN(const CallbackInfo& info) { int threadCount = info[0].As().Int32Value(); Function cb = info[1].As(); // We pass the test data to the Finalizer for cleanup. The finalizer is // responsible for deleting this data as well. - TestData *testData = new TestData(Promise::Deferred::New(info.Env())); - - TSFN tsfn = TSFN::New( - info.Env(), cb, "Test", 0, threadCount, testData, - std::function(FinalizerCallback), testData); + TestData* testData = new TestData(Promise::Deferred::New(info.Env())); + + TSFN tsfn = + TSFN::New(info.Env(), + cb, + "Test", + 0, + threadCount, + testData, + std::function(FinalizerCallback), + testData); for (int i = 0; i < threadCount; ++i) { // A copy of the ThreadSafeFunction will go to the thread entry point @@ -82,7 +88,7 @@ static Value TestWithTSFN(const CallbackInfo &info) { // Task instance created for each new std::thread class DelayedTSFNTask { -public: + public: // Each instance has its own tsfn TSFN tsfn; @@ -100,8 +106,7 @@ class DelayedTSFNTask { }; struct TestDataDelayed : TestData { - - TestDataDelayed(Promise::Deferred &&deferred) + TestDataDelayed(Promise::Deferred&& deferred) : TestData(std::move(deferred)){}; ~TestDataDelayed() { taskInsts.clear(); }; @@ -109,8 +114,9 @@ struct TestDataDelayed : TestData { std::vector> taskInsts = {}; }; -void FinalizerCallbackDelayed(Napi::Env env, TestDataDelayed *finalizeData, - TestData *) { +void FinalizerCallbackDelayed(Napi::Env env, + TestDataDelayed* finalizeData, + TestData*) { for (size_t i = 0; i < finalizeData->threads.size(); ++i) { finalizeData->threads[i].join(); } @@ -118,14 +124,19 @@ void FinalizerCallbackDelayed(Napi::Env env, TestDataDelayed *finalizeData, delete finalizeData; } -static Value TestDelayedTSFN(const CallbackInfo &info) { +static Value TestDelayedTSFN(const CallbackInfo& info) { int threadCount = info[0].As().Int32Value(); Function cb = info[1].As(); - TestDataDelayed *testData = + TestDataDelayed* testData = new TestDataDelayed(Promise::Deferred::New(info.Env())); - testData->tsfn = TSFN::New(info.Env(), cb, "Test", 0, threadCount, testData, + testData->tsfn = TSFN::New(info.Env(), + cb, + "Test", + 0, + threadCount, + testData, std::function( FinalizerCallbackDelayed), testData); @@ -139,7 +150,7 @@ static Value TestDelayedTSFN(const CallbackInfo &info) { } std::this_thread::sleep_for(std::chrono::milliseconds(std::rand() % 100 + 1)); - for (auto &task : testData->taskInsts) { + for (auto& task : testData->taskInsts) { std::lock_guard lk(task->mtx); task->tsfn = testData->tsfn; task->cv.notify_all(); @@ -148,8 +159,9 @@ static Value TestDelayedTSFN(const CallbackInfo &info) { return testData->deferred.Promise(); } -void AcquireFinalizerCallback(Napi::Env env, TestData *finalizeData, - TestData *context) { +void AcquireFinalizerCallback(Napi::Env env, + TestData* finalizeData, + TestData* context) { (void)context; for (size_t i = 0; i < finalizeData->threads.size(); ++i) { finalizeData->threads[i].join(); @@ -165,8 +177,8 @@ void entryAcquire(TSFN tsfn, int threadId) { tsfn.Release(); } -static Value CreateThread(const CallbackInfo &info) { - TestData *testData = static_cast(info.Data()); +static Value CreateThread(const CallbackInfo& info) { + TestData* testData = static_cast(info.Data()); // Counting expected calls like this only works because on the JS side this // binding is called from a synchronous loop. This means the main loop has no // chance to run the tsfn JS callback before we've counted how many threads @@ -179,21 +191,26 @@ static Value CreateThread(const CallbackInfo &info) { return Number::New(info.Env(), threadId); } -static Value StopThreads(const CallbackInfo &info) { - TestData *testData = static_cast(info.Data()); +static Value StopThreads(const CallbackInfo& info) { + TestData* testData = static_cast(info.Data()); testData->mainWantsRelease = true; return info.Env().Undefined(); } -static Value TestAcquire(const CallbackInfo &info) { +static Value TestAcquire(const CallbackInfo& info) { Function cb = info[0].As(); Napi::Env env = info.Env(); // We pass the test data to the Finalizer for cleanup. The finalizer is // responsible for deleting this data as well. - TestData *testData = new TestData(Promise::Deferred::New(info.Env())); - - testData->tsfn = TSFN::New(env, cb, "Test", 0, 1, testData, + TestData* testData = new TestData(Promise::Deferred::New(info.Env())); + + testData->tsfn = TSFN::New(env, + cb, + "Test", + 0, + 1, + testData, std::function( AcquireFinalizerCallback), testData); @@ -207,7 +224,7 @@ static Value TestAcquire(const CallbackInfo &info) { return result; } -} // namespace +} // namespace Object InitTypedThreadSafeFunctionSum(Env env) { Object exports = Object::New(env); diff --git a/test/typed_threadsafe_function/typed_threadsafe_function_unref.cc b/test/typed_threadsafe_function/typed_threadsafe_function_unref.cc index 3f81611fc..35345568d 100644 --- a/test/typed_threadsafe_function/typed_threadsafe_function_unref.cc +++ b/test/typed_threadsafe_function/typed_threadsafe_function_unref.cc @@ -17,23 +17,31 @@ static Value TestUnref(const CallbackInfo& info) { Function setTimeout = global.Get("setTimeout").As(); TSFN* tsfn = new TSFN; - *tsfn = TSFN::New(info.Env(), cb, resource, "Test", 1, 1, nullptr, [tsfn](Napi::Env /* env */, FinalizerDataType*, ContextType*) { - delete tsfn; - }, static_cast(nullptr)); + *tsfn = TSFN::New( + info.Env(), + cb, + resource, + "Test", + 1, + 1, + nullptr, + [tsfn](Napi::Env /* env */, FinalizerDataType*, ContextType*) { + delete tsfn; + }, + static_cast(nullptr)); tsfn->BlockingCall(); - setTimeout.Call( global, { - Function::New(env, [tsfn](const CallbackInfo& info) { - tsfn->Unref(info.Env()); - }), - Number::New(env, 100) - }); + setTimeout.Call( + global, + {Function::New( + env, [tsfn](const CallbackInfo& info) { tsfn->Unref(info.Env()); }), + Number::New(env, 100)}); return info.Env().Undefined(); } -} +} // namespace Object InitTypedThreadSafeFunctionUnref(Env env) { Object exports = Object::New(env);