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 001/482] 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 002/482] 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 003/482] 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 004/482] 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 005/482] 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 006/482] 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 007/482] 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 008/482] 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 009/482] 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 010/482] 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 011/482] 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 012/482] 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 013/482] 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 014/482] 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 015/482] 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 016/482] 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 017/482] 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 018/482] 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 019/482] 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 020/482] 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 021/482] 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 022/482] 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 023/482] 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 024/482] 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 025/482] 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 026/482] 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 027/482] 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 028/482] 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 029/482] 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 030/482] 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 031/482] 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 032/482] 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 033/482] 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 034/482] 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 035/482] 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 826e466ef67d75c9dad38637054759af61c2b529 Mon Sep 17 00:00:00 2001 From: nempoBu4 Date: Wed, 14 Oct 2020 04:14:52 +0600 Subject: [PATCH 036/482] Fixed example in addon.md. (#820) PR-URL: https://github.com/nodejs/node-addon-api/pull/820 Reviewed-By: Michael Dawson Reviewed-By: Gabriel Schulhof --- doc/addon.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/addon.md b/doc/addon.md index ee85e7d5e..26408ae55 100644 --- a/doc/addon.md +++ b/doc/addon.md @@ -41,9 +41,9 @@ class ExampleAddon : public Napi::Addon { // We can also attach plain objects to `exports`, and instance methods as // properties of those sub-objects. - InstanceValue("subObject", DefineProperties(Napi::Object::New(), { - InstanceMethod("decrement", &ExampleAddon::Decrement - })), napi_enumerable) + InstanceValue("subObject", DefineProperties(Napi::Object::New(env), { + InstanceMethod("decrement", &ExampleAddon::Decrement) + }), napi_enumerable) }); } private: @@ -80,7 +80,7 @@ The above code can be used from JavaScript as follows: const exampleAddon = require('bindings')('example_addon'); console.log(exampleAddon.increment()); // prints 43 console.log(exampleAddon.increment()); // prints 44 -consnole.log(exampleAddon.subObject.decrement()); // prints 43 +console.log(exampleAddon.subObject.decrement()); // prints 43 ``` When Node.js loads an instance of the add-on, a new instance of the class is From 898338300038fbbf11e13aefb4be979258494bde Mon Sep 17 00:00:00 2001 From: Ziqiu Zhao <39512431+ZzqiZQute@users.noreply.github.com> Date: Tue, 27 Oct 2020 03:10:18 +0800 Subject: [PATCH 037/482] Fix: some typos in the document (#825) * Update object_wrap.md --- doc/addon.md | 2 +- doc/object_wrap.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/addon.md b/doc/addon.md index 26408ae55..96dae718d 100644 --- a/doc/addon.md +++ b/doc/addon.md @@ -34,7 +34,7 @@ The `Napi::Addon` class can be used together with the `NODE_API_ADDON()` and class ExampleAddon : public Napi::Addon { public: ExampleAddon(Napi::Env env, Napi::Object exports) { - // In the constructor we declare the functions the add-on makes avaialable + // In the constructor we declare the functions the add-on makes available // to JavaScript. DefineAddon(exports, { InstanceMethod("increment", &ExampleAddon::Increment), diff --git a/doc/object_wrap.md b/doc/object_wrap.md index 7a791df11..7b8fc9c28 100644 --- a/doc/object_wrap.md +++ b/doc/object_wrap.md @@ -43,7 +43,7 @@ Napi::Object Example::Init(Napi::Env env, Napi::Object exports) { Napi::FunctionReference* constructor = new Napi::FunctionReference(); - // Create a peristent reference to the class constructor. This will allow + // Create a persistent reference to the class constructor. This will allow // a function called on a class prototype and a function // called on instance of a class to be distinguished from each other. *constructor = Napi::Persistent(func); From 35b65712c26a49285cdbe2b4d04e25a5eccbe719 Mon Sep 17 00:00:00 2001 From: Helio Frota <00hf11@gmail.com> Date: Mon, 26 Oct 2020 16:11:02 -0300 Subject: [PATCH 038/482] Fix: some typos in documentation (#826) --- doc/async_context.md | 2 +- doc/async_worker.md | 2 +- doc/boolean.md | 2 +- doc/checker-tool.md | 2 +- doc/creating_a_release.md | 2 +- doc/escapable_handle_scope.md | 2 +- doc/function.md | 4 ++-- doc/function_reference.md | 2 +- doc/handle_scope.md | 2 +- doc/number.md | 2 +- doc/object_lifetime_management.md | 2 +- doc/prebuild_tools.md | 4 ++-- doc/property_descriptor.md | 6 +++--- doc/threadsafe_function.md | 6 +++--- doc/version_management.md | 4 ++-- 15 files changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/async_context.md b/doc/async_context.md index 8e1f481c5..b217d336a 100644 --- a/doc/async_context.md +++ b/doc/async_context.md @@ -73,7 +73,7 @@ void MakeCallbackWithAsyncContext(const Napi::CallbackInfo& info) { Napi::Function callback = info[0].As(); Napi::Object resource = info[1].As(); - // Creat a new async context instance. + // Create a new async context instance. Napi::AsyncContext context(info.Env(), "async_context_test", resource); // Invoke the callback with the async context instance. diff --git a/doc/async_worker.md b/doc/async_worker.md index d7753ac8c..b6bb0cd67 100644 --- a/doc/async_worker.md +++ b/doc/async_worker.md @@ -395,7 +395,7 @@ class EchoWorker : public AsyncWorker { }; ``` -The `EchoWorker`'s contructor calls the base class' constructor to pass in the +The `EchoWorker`'s constructor calls the base class' constructor to pass in the callback that the `Napi::AsyncWorker` base class will store persistently. When the work on the `Napi::AsyncWorker::Execute` method is done the `Napi::AsyncWorker::OnOk` method is called and the results return back to diff --git a/doc/boolean.md b/doc/boolean.md index d07daa210..6a8ec3f98 100644 --- a/doc/boolean.md +++ b/doc/boolean.md @@ -18,7 +18,7 @@ Napi::Boolean::Boolean(); Returns a new _empty_ `Napi::Boolean` object. -### Contructor +### Constructor Creates a new instance of the `Napi::Boolean` object. diff --git a/doc/checker-tool.md b/doc/checker-tool.md index 499d3ab9d..135f13fd7 100644 --- a/doc/checker-tool.md +++ b/doc/checker-tool.md @@ -26,7 +26,7 @@ indicating for each addon whether it is an N-API addon. ``` The tool accepts the root directory from which to start checking for Node.js -native addons as a single optional command line parameter. If ommitted it will +native addons as a single optional command line parameter. If omitted it will start checking from the current directory (`.`). [checker tool]: ../tools/check-napi.js diff --git a/doc/creating_a_release.md b/doc/creating_a_release.md index beb67d287..5c8f8b025 100644 --- a/doc/creating_a_release.md +++ b/doc/creating_a_release.md @@ -36,7 +36,7 @@ the route folder of the repo launch the following command: ```bash > changelog-maker ``` -* Use the output generated by **changelog maker** to pdate the [CHANGELOG.md](https://github.com/nodejs/node-addon-api/blob/master/CHANGELOG.md) +* Use the output generated by **changelog maker** to update the [CHANGELOG.md](https://github.com/nodejs/node-addon-api/blob/master/CHANGELOG.md) following the style used in publishing the previous release. * Add any new contributors to the "contributors" section in the package.json diff --git a/doc/escapable_handle_scope.md b/doc/escapable_handle_scope.md index 978aab354..4f3e2d062 100644 --- a/doc/escapable_handle_scope.md +++ b/doc/escapable_handle_scope.md @@ -59,7 +59,7 @@ Napi::EscapableHandleScope::~EscapableHandleScope(); Deletes the `Napi::EscapableHandleScope` instance and allows any objects/handles created in the scope to be collected by the garbage collector. There is no -guarantee as to when the gargbage collector will do this. +guarantee as to when the garbage collector will do this. ### Escape diff --git a/doc/function.md b/doc/function.md index 610d1005c..ddc089cf7 100644 --- a/doc/function.md +++ b/doc/function.md @@ -218,7 +218,7 @@ Napi::Object Napi::Function::New(const std::initializer_list& args) ``` - `[in] args`: Initializer list of JavaScript values as `napi_value` representing -the arguments of the contructor function. +the arguments of the constructor function. Returns a new JavaScript object. @@ -245,7 +245,7 @@ object. Napi::Object Napi::Function::New(size_t argc, const napi_value* args) const; ``` -- `[in] argc`: The number of the arguments passed to the contructor function. +- `[in] argc`: The number of the arguments passed to the constructor function. - `[in] args`: Array of JavaScript values as `napi_value` representing the arguments of the constructor function. diff --git a/doc/function_reference.md b/doc/function_reference.md index 7b299b9bb..fa21830b8 100644 --- a/doc/function_reference.md +++ b/doc/function_reference.md @@ -73,7 +73,7 @@ Napi::Object Napi::FunctionReference::New(const std::initializer_list Date: Wed, 28 Oct 2020 22:14:41 +0100 Subject: [PATCH 039/482] Update object_reference.md (#827) --- doc/object_reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/object_reference.md b/doc/object_reference.md index 6e579e2d7..f2d8905a8 100644 --- a/doc/object_reference.md +++ b/doc/object_reference.md @@ -8,7 +8,7 @@ For more general information on references, please consult [`Napi::Reference`](r ```cpp #include -using namescape Napi; +using namespace Napi; void Init(Env env) { 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 040/482] 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 c79cabaed25fd341b46ddabf44a7cd1056bdc397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Tue, 3 Nov 2020 00:08:59 +0100 Subject: [PATCH 041/482] doc: avoid directing users to HTTP (#828) --- README.md | 4 ++-- doc/error.md | 2 +- doc/hierarchy.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f88ac6871..8e7ee391d 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ APIs exposed by node-addon-api are generally used to create and manipulate JavaScript values. Concepts and operations generally map to ideas specified in the **ECMA262 Language Specification**. -The [N-API Resource](http://nodejs.github.io/node-addon-examples/) offers an -excellent orientation and tips for developers just getting started with N-API +The [N-API Resource](https://nodejs.github.io/node-addon-examples/) offers an +excellent orientation and tips for developers just getting started with N-API and node-addon-api. - **[Setup](#setup)** diff --git a/doc/error.md b/doc/error.md index 7530262d7..08c156dd0 100644 --- a/doc/error.md +++ b/doc/error.md @@ -117,4 +117,4 @@ Returns a pointer to a null-terminated string that is used to identify the exception. This method can be used only if the exception mechanism is enabled. [`Napi::ObjectReference`]: ./object_reference.md -[`std::exception`]: http://cplusplus.com/reference/exception/exception/ +[`std::exception`]: https://cplusplus.com/reference/exception/exception/ diff --git a/doc/hierarchy.md b/doc/hierarchy.md index e40a65ff9..921f94a99 100644 --- a/doc/hierarchy.md +++ b/doc/hierarchy.md @@ -88,4 +88,4 @@ [`Napi::Uint8Array`]: ./typed_array_of.md [`Napi::Value`]: ./value.md [`Napi::VersionManagement`]: ./version_management.md -[`std::exception`]: http://cplusplus.com/reference/exception/exception/ +[`std::exception`]: https://cplusplus.com/reference/exception/exception/ From c9563caa25bfb208f4dd8f0f0e55f0861bf057dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Sun, 26 Jan 2020 00:30:38 -0400 Subject: [PATCH 042/482] src: add ArrayBuffer::Detach() and ::IsDetached() Refs: https://github.com/nodejs/node/pull/29768 Refs: https://github.com/nodejs/node/pull/30613 PR-URL: https://github.com/nodejs/node-addon-api/pull/659 Refs: https://github.com/nodejs/node/pull/29768 Refs: https://github.com/nodejs/node/pull/30613 Reviewed-By: Michael Dawson Reviewed-By: Gabriel Schulhof --- doc/array_buffer.md | 16 ++++++++++++++++ napi-inl.h | 14 ++++++++++++++ napi.h | 5 +++++ test/arraybuffer.cc | 38 +++++++++++++++++++++++++++++++------- test/arraybuffer.js | 5 +++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/doc/array_buffer.md b/doc/array_buffer.md index 988a839a4..346fe6ace 100644 --- a/doc/array_buffer.md +++ b/doc/array_buffer.md @@ -130,4 +130,20 @@ void* Napi::ArrayBuffer::Data() const; Returns a pointer the wrapped data. +### Detach + +```cpp +void Napi::ArrayBuffer::Detach(); +``` + +Invokes the `ArrayBuffer` detach operation on a detachable `ArrayBuffer`. + +### IsDetached + +```cpp +bool Napi::ArrayBuffer::IsDetached() const; +``` + +Returns `true` if this `ArrayBuffer` has been detached. + [`Napi::Object`]: ./object.md diff --git a/napi-inl.h b/napi-inl.h index 5ecdd5046..5866fd89c 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -1530,6 +1530,20 @@ inline size_t ArrayBuffer::ByteLength() { return length; } +#if NAPI_VERSION >= 7 +inline bool ArrayBuffer::IsDetached() const { + bool detached; + napi_status status = napi_is_detached_arraybuffer(_env, _value, &detached); + NAPI_THROW_IF_FAILED(_env, status, false); + return detached; +} + +inline void ArrayBuffer::Detach() { + napi_status status = napi_detach_arraybuffer(_env, _value); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} +#endif // NAPI_VERSION >= 7 + //////////////////////////////////////////////////////////////////////////////// // DataView class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index 76a62f659..945aac55e 100644 --- a/napi.h +++ b/napi.h @@ -822,6 +822,11 @@ namespace Napi { void* Data(); ///< Gets a pointer to the data buffer. size_t ByteLength(); ///< Gets the length of the array buffer in bytes. + +#if NAPI_VERSION >= 7 + bool IsDetached() const; + void Detach(); +#endif // NAPI_VERSION >= 7 }; /// A JavaScript typed-array value with unknown array type. diff --git a/test/arraybuffer.cc b/test/arraybuffer.cc index cc4f1f51c..4a1b2a34e 100644 --- a/test/arraybuffer.cc +++ b/test/arraybuffer.cc @@ -157,19 +157,43 @@ void CheckDetachUpdatesData(const CallbackInfo& info) { return; } - if (!info[1].IsFunction()) { - Error::New(info.Env(), "A function was expected.").ThrowAsJavaScriptException(); - return; - } - ArrayBuffer buffer = info[0].As(); - Function detach = info[1].As(); // This potentially causes the buffer to cache its data pointer and length. buffer.Data(); buffer.ByteLength(); - detach.Call({}); +#if NAPI_VERSION >= 7 + if (buffer.IsDetached()) { + Error::New(info.Env(), "Buffer should not be detached.").ThrowAsJavaScriptException(); + return; + } +#endif + + if (info.Length() == 2) { + // Detach externally (in JavaScript). + if (!info[1].IsFunction()) { + Error::New(info.Env(), "A function was expected.").ThrowAsJavaScriptException(); + return; + } + + Function detach = info[1].As(); + detach.Call({}); + } else { +#if NAPI_VERSION >= 7 + // Detach directly. + buffer.Detach(); +#else + return; +#endif + } + +#if NAPI_VERSION >= 7 + if (!buffer.IsDetached()) { + Error::New(info.Env(), "Buffer should be detached.").ThrowAsJavaScriptException(); + return; + } +#endif if (buffer.Data() != nullptr) { Error::New(info.Env(), "Incorrect data pointer.").ThrowAsJavaScriptException(); diff --git a/test/arraybuffer.js b/test/arraybuffer.js index 38d35d1e7..363de17d9 100644 --- a/test/arraybuffer.js +++ b/test/arraybuffer.js @@ -58,8 +58,13 @@ function test(binding) { 'ArrayBuffer updates data pointer and length when detached', () => { + // Detach the ArrayBuffer in JavaScript. const mem = new WebAssembly.Memory({ initial: 1 }); binding.arraybuffer.checkDetachUpdatesData(mem.buffer, () => mem.grow(1)); + + // Let C++ detach the ArrayBuffer. + const extBuffer = binding.arraybuffer.createExternalBuffer(); + binding.arraybuffer.checkDetachUpdatesData(extBuffer); }, ]); } 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 043/482] 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 1b52c28eb810ea46c2d5579cb16e8b6f1beb3869 Mon Sep 17 00:00:00 2001 From: mastergberry Date: Mon, 9 Nov 2020 17:57:35 +0100 Subject: [PATCH 044/482] Clean up AsyncProgressWorker documentation (#831) * Clean up AsyncProgressWorker documentation The old documentation was not very clear on how to use the API. It was missing javascript references, and was not necessarily following the best coding standards. These adjustments make it a bit more complete now by providing all three necessary parts, the worker, the hookup code, and the javascript usage of it. I have also gone ahead and rewritten the `AsyncProgressQueueWorker` example to show demonstration of the `FunctionReference` class and how to use multiple callbacks instead of one combined callback. --- doc/async_worker_variants.md | 181 +++++++++++++++++++++++++++-------- 1 file changed, 141 insertions(+), 40 deletions(-) diff --git a/doc/async_worker_variants.md b/doc/async_worker_variants.md index 8b892eaee..54007762c 100644 --- a/doc/async_worker_variants.md +++ b/doc/async_worker_variants.md @@ -243,7 +243,9 @@ be safely released. Note that `Napi::AsyncProgressWorker::ExecutionProcess::Send` merely guarantees **eventual** invocation of `Napi::AsyncProgressWorker::OnProgress`, which means multiple send might be coalesced into single invocation of `Napi::AsyncProgressWorker::OnProgress` -with latest data. +with latest data. If you would like to guarantee that there is one invocation of +`OnProgress` for every `Send` call, you should use the `Napi::AsyncProgressQueueWorker` +class instead which is documented further down this page. ```cpp void Napi::AsyncProgressWorker::ExecutionProcess::Send(const T* data, size_t count) const; @@ -269,7 +271,8 @@ function runs in the background out of the **event loop** thread and at the end the `Napi::AsyncProgressWorker::OnOK` or `Napi::AsyncProgressWorker::OnError` function will be called and are executed as part of the event loop. -The code below shows a basic example of the `Napi::AsyncProgressWorker` implementation: +The code below shows a basic example of the `Napi::AsyncProgressWorker` implementation along with an +example of how the counterpart in Javascript would appear: ```cpp #include @@ -281,28 +284,38 @@ using namespace Napi; class EchoWorker : public AsyncProgressWorker { public: - EchoWorker(Function& callback, std::string& echo) - : AsyncProgressWorker(callback), echo(echo) {} + EchoWorker(Function& okCallback, std::string& echo) + : AsyncProgressWorker(okCallback), echo(echo) {} ~EchoWorker() {} - // This code will be executed on the worker thread - void Execute(const ExecutionProgress& progress) { - // Need to simulate cpu heavy task - for (uint32_t i = 0; i < 100; ++i) { - progress.Send(&i, 1) - std::this_thread::sleep_for(std::chrono::seconds(1)); + + // This code will be executed on the worker thread + void Execute(const ExecutionProgress& progress) { + // Need to simulate cpu heavy task + // Note: This Send() call is not guaranteed to trigger an equal + // number of OnProgress calls (read documentation above for more info) + for (uint32_t i = 0; i < 100; ++i) { + progress.Send(&i, 1) + } + } + + void OnError(const Error &e) { + HandleScope scope(Env()); + // Pass error onto JS, no data for other parameters + Callback().Call({String::New(Env(), e.Message())}); } - } - void OnOK() { - HandleScope scope(Env()); - Callback().Call({Env().Null(), String::New(Env(), echo)}); - } + void OnOK() { + HandleScope scope(Env()); + // Pass no error, give back original data + Callback().Call({Env().Null(), String::New(Env(), echo)}); + } - void OnProgress(const uint32_t* data, size_t /* count */) { - HandleScope scope(Env()); - Callback().Call({Env().Null(), Env().Null(), Number::New(Env(), *data)}); - } + void OnProgress(const uint32_t* data, size_t /* count */) { + HandleScope scope(Env()); + // Pass no error, no echo data, but do pass on the progress data + Callback().Call({Env().Null(), Env().Null(), Number::New(Env(), *data)}); + } private: std::string echo; @@ -327,12 +340,23 @@ using namespace Napi; Value Echo(const CallbackInfo& info) { // We need to validate the arguments here - Function cb = info[1].As(); std::string in = info[0].As(); + Function cb = info[1].As(); EchoWorker* wk = new EchoWorker(cb, in); wk->Queue(); return info.Env().Undefined(); } + +// Register the native method for JS to access +Object Init(Env env, Object exports) +{ + exports.Set(String::New(env, "echo"), Function::New(env, Echo)); + + return exports; +} + +// Register our native addon +NODE_API_MODULE(nativeAddon, Init) ``` The implementation of a `Napi::AsyncProgressWorker` can be used by creating a @@ -341,6 +365,20 @@ asynchronous task ends and other data needed for the computation. Once created, the only other action needed is to call the `Napi::AsyncProgressWorker::Queue` method that will queue the created worker for execution. +Lastly, the following Javascript (ES6+) code would be associated the above example: + +```js +const { nativeAddon } = require('binding.node'); + +const exampleCallback = (errorResponse, okResponse, progressData) => { + // Use the data accordingly + // ... +}; + +// Call our native addon with the paramters of a string and a function +nativeAddon.echo("example", exampleCallback); +``` + # AsyncProgressQueueWorker `Napi::AsyncProgressQueueWorker` acts exactly like `Napi::AsyncProgressWorker` @@ -379,7 +417,9 @@ void Napi::AsyncProgressQueueWorker::ExecutionProcess::Send(const T* data, size_ ## Example -The code below shows a basic example of the `Napi::AsyncProgressQueueWorker` implementation: +The code below show an example of the `Napi::AsyncProgressQueueWorker` implementation, but +also demonsrates how to use multiple `Napi::Function`'s if you wish to provide multiple +callback functions for more object oriented code: ```cpp #include @@ -391,31 +431,55 @@ using namespace Napi; class EchoWorker : public AsyncProgressQueueWorker { public: - EchoWorker(Function& callback, std::string& echo) - : AsyncProgressQueueWorker(callback), echo(echo) {} + EchoWorker(Function& okCallback, Function& errorCallback, Function& progressCallback, std::string& echo) + : AsyncProgressQueueWorker(okCallback), echo(echo) { + // Set our function references to use them below + this->errorCallback.Reset(errorCallback, 1); + this->progressCallback.Reset(progressCallback, 1); + } ~EchoWorker() {} - // This code will be executed on the worker thread - void Execute(const ExecutionProgress& progress) { - // Need to simulate cpu heavy task - for (uint32_t i = 0; i < 100; ++i) { - progress.Send(&i, 1); - std::this_thread::sleep_for(std::chrono::seconds(1)); + + // This code will be executed on the worker thread + void Execute(const ExecutionProgress& progress) { + // Need to simulate cpu heavy task to demonstrate that + // every call to Send() will trigger an OnProgress function call + for (uint32_t i = 0; i < 100; ++i) { + progress.Send(&i, 1); + } } - } - void OnOK() { - HandleScope scope(Env()); - Callback().Call({Env().Null(), String::New(Env(), echo)}); - } + void OnOK() { + HandleScope scope(Env()); + // Call our onOkCallback in javascript with the data we were given originally + Callback().Call({String::New(Env(), echo)}); + } + + void OnError(const Error &e) { + HandleScope scope(Env()); + + // We call our callback provided in the constructor with 2 parameters + if (!this->errorCallback.IsEmpty()) { + // Call our onErrorCallback in javascript with the error message + this->errorCallback.Call(Receiver().Value(), {String::New(Env(), e.Message())}); + } + } - void OnProgress(const uint32_t* data, size_t /* count */) { - HandleScope scope(Env()); - Callback().Call({Env().Null(), Env().Null(), Number::New(Env(), *data)}); - } + void OnProgress(const uint32_t* data, size_t /* count */) { + HandleScope scope(Env()); + + if (!this->progressCallback.IsEmpty()) { + // Call our onProgressCallback in javascript with each integer from 0 to 99 (inclusive) + // as this function is triggered from the above Send() calls + this->progressCallback.Call(Receiver().Value(), {Number::New(Env(), *data)}); + } + } private: std::string echo; + FunctionReference progressCallback; + FunctionReference errorCallback; + }; ``` @@ -439,12 +503,25 @@ using namespace Napi; Value Echo(const CallbackInfo& info) { // We need to validate the arguments here. - Function cb = info[1].As(); std::string in = info[0].As(); - EchoWorker* wk = new EchoWorker(cb, in); + Function errorCb = info[1].As(); + Function okCb = info[2].As(); + Function progressCb = info[3].As(); + EchoWorker* wk = new EchoWorker(okCb, errorCb, progressCb, in); wk->Queue(); return info.Env().Undefined(); } + +// Register the native method for JS to access +Object Init(Env env, Object exports) +{ + exports.Set(String::New(env, "echo"), Function::New(env, Echo)); + + return exports; +} + +// Register our native addon +NODE_API_MODULE(nativeAddon, Init) ``` The implementation of a `Napi::AsyncProgressQueueWorker` can be used by creating a @@ -453,4 +530,28 @@ asynchronous task ends and other data needed for the computation. Once created, the only other action needed is to call the `Napi::AsyncProgressQueueWorker::Queue` method that will queue the created worker for execution. +Lastly, the following Javascript (ES6+) code would be associated the above example: + +```js +const { nativeAddon } = require('binding.node'); + +const onErrorCallback = (msg) => { + // Use the data accordingly + // ... +}; + +const onOkCallback = (echo) => { + // Use the data accordingly + // ... +}; + +const onProgressCallback = (num) => { + // Use the data accordingly + // ... +}; + +// Call our native addon with the paramters of a string and three callback functions +nativeAddon.echo("example", onErrorCallback, onOkCallback, onProgressCallback); +``` + [`Napi::AsyncWorker`]: ./async_worker.md From 2c02d317e51058692042406a550ffb24cee937db Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Mon, 9 Nov 2020 17:58:32 +0100 Subject: [PATCH 045/482] build: add pre-commit package for linting (#823) * build: add incremental clang-format checks * build: add pre-commit package for linting --- .clang-format | 111 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 1 + package.json | 9 +++- scripts/clang-format.js | 42 +++++++++++++++ 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 .clang-format create mode 100644 scripts/clang-format.js diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..4aad29c32 --- /dev/null +++ b/.clang-format @@ -0,0 +1,111 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 8 +UseTab: Never diff --git a/.travis.yml b/.travis.yml index 56f9c903c..642470330 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,7 @@ script: # Travis CI sets NVM_NODEJS_ORG_MIRROR, but it makes node-gyp fail to download headers for nightly builds. - unset NVM_NODEJS_ORG_MIRROR + - npm run lint - npm test after_success: - cpp-coveralls --gcov-options '\-lp' --build-root test/build --exclude test diff --git a/package.json b/package.json index 93f3fd81c..2d25d0aaa 100644 --- a/package.json +++ b/package.json @@ -264,8 +264,10 @@ "description": "Node.js API (N-API)", "devDependencies": { "benchmark": "^2.1.4", - "fs-extra": "^9.0.1", "bindings": "^1.5.0", + "clang-format": "^1.4.0", + "fs-extra": "^9.0.1", + "pre-commit": "^1.2.2", "safe-buffer": "^5.1.1" }, "directories": {}, @@ -300,7 +302,10 @@ "dev": "node test", "predev:incremental": "node-gyp configure build -C test --debug", "dev:incremental": "node test", - "doc": "doxygen doc/Doxyfile" + "doc": "doxygen doc/Doxyfile", + "lint": "node scripts/clang-format.js", + "lint:fix": "git-clang-format" }, + "pre-commit": "lint", "version": "3.0.2" } diff --git a/scripts/clang-format.js b/scripts/clang-format.js new file mode 100644 index 000000000..b44d47674 --- /dev/null +++ b/scripts/clang-format.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +const spawn = require('child_process').spawnSync; +const path = require('path'); + +const filesToCheck = ['*.h', '*.cc']; +const CLANG_FORMAT_START = process.env.CLANG_FORMAT_START || 'master'; + +function main(args) { + let clangFormatPath = path.dirname(require.resolve('clang-format')); + const options = ['--binary=node_modules/.bin/clang-format', '--style=file']; + + const gitClangFormatPath = path.join(clangFormatPath, + 'bin/git-clang-format'); + const result = spawn('python', [ + gitClangFormatPath, + ...options, + '--diff', + CLANG_FORMAT_START, + 'HEAD', + ...filesToCheck + ], { encoding: 'utf-8' }); + + if (result.error) { + console.error('Error running git-clang-format:', result.error); + return 2; + } + + const clangFormatOutput = result.stdout.trim(); + if (clangFormatOutput !== ('no modified files to format') && + clangFormatOutput !== ('clang-format did not modify any files')) { + console.error(clangFormatOutput); + const fixCmd = '"npm run lint:fix"'; + console.error(` + ERROR: please run ${fixCmd} to format changes in your commit`); + return 1; + } +} + +if (require.main === module) { + process.exitCode = main(process.argv.slice(2)); +} From 8fb5820557af11809dc1278518d2c96f001451bd Mon Sep 17 00:00:00 2001 From: legendecas Date: Thu, 8 Oct 2020 01:47:20 +0800 Subject: [PATCH 046/482] build: add incremental clang-format checks PR-URL: https://github.com/nodejs/node-addon-api/pull/819 Reviewed-By: Gabriel Schulhof Reviewed-By: Michael Dawson --- .github/workflows/linter.yml | 23 +++++++++++++++++++++++ .travis.yml | 1 - 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/linter.yml diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 000000000..3cbf00634 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,23 @@ +name: Style Checks + +on: [push, pull_request] + +jobs: + lint: + strategy: + matrix: + node-version: [14.x] + os: [ubuntu-latest] + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - run: git branch -a + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: CLANG_FORMAT_START=refs/remotes/origin/master npm run lint diff --git a/.travis.yml b/.travis.yml index 642470330..56f9c903c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,7 +54,6 @@ script: # Travis CI sets NVM_NODEJS_ORG_MIRROR, but it makes node-gyp fail to download headers for nightly builds. - unset NVM_NODEJS_ORG_MIRROR - - npm run lint - npm test after_success: - cpp-coveralls --gcov-options '\-lp' --build-root test/build --exclude test From 1427b3ef7853271bfdf9057c33ab9a03e7fc67b1 Mon Sep 17 00:00:00 2001 From: blagoev Date: Mon, 16 Nov 2020 23:09:02 +0200 Subject: [PATCH 047/482] src: create a HandleScope in FinalizeCallback Refs: https://github.com/nodejs/node-addon-api/pull/832 Seems like FinalizeCallback needs to create a HandleScope since it calls ObjectWrap::~ObjectWrap() which might need to create a temporary Object handle here https://github.com/nodejs/node-addon-api/blob/master/napi-inl.h#L3558 at Realm (https://github.com/realm/realm-js) we have crashes with stacktrace at this location. While fixed in core for later versions we still need for older versions of Node.js PR-URL: https://github.com/nodejs/node-addon-api/pull/832 Reviewed-By: Gabriel Schulhof Reviewed-By: Kevin Eady Reviewed-By: Michael Dawson Reviewed-By: NickNaso --- napi-inl.h | 1 + 1 file changed, 1 insertion(+) diff --git a/napi-inl.h b/napi-inl.h index 5866fd89c..ab7ece1c8 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -3977,6 +3977,7 @@ inline napi_value ObjectWrap::StaticSetterCallbackWrapper( template inline void ObjectWrap::FinalizeCallback(napi_env env, void* data, void* /*hint*/) { + HandleScope scope(env); T* instance = static_cast(data); instance->Finalize(Napi::Env(env)); delete instance; From 59c6a6aeb0bfd8e488fa0975af23f1219e41adde Mon Sep 17 00:00:00 2001 From: legendecas Date: Sat, 21 Nov 2020 03:43:56 +0800 Subject: [PATCH 048/482] fix: git-clang-format doesn't recognize no changes requested on given files (#835) --- package.json | 2 +- tools/README.md | 8 +++++++- {scripts => tools}/clang-format.js | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) rename {scripts => tools}/clang-format.js (92%) diff --git a/package.json b/package.json index 2d25d0aaa..85d6ea2d1 100644 --- a/package.json +++ b/package.json @@ -303,7 +303,7 @@ "predev:incremental": "node-gyp configure build -C test --debug", "dev:incremental": "node test", "doc": "doxygen doc/Doxyfile", - "lint": "node scripts/clang-format.js", + "lint": "node tools/clang-format.js", "lint:fix": "git-clang-format" }, "pre-commit": "lint", diff --git a/tools/README.md b/tools/README.md index 8d110609c..a1024d586 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,4 +1,10 @@ -# Migration Script +# Tools + +## clang-format + +The clang-format checking tools is designed to check changed lines of code compared to given git-refs. + +## Migration Script The migration tool is designed to reduce repetitive work in the migration process. However, the script is not aiming to convert every thing for you. There are usually some small fixes and major reconstruction required. diff --git a/scripts/clang-format.js b/tools/clang-format.js similarity index 92% rename from scripts/clang-format.js rename to tools/clang-format.js index b44d47674..3a2ba4a2f 100644 --- a/scripts/clang-format.js +++ b/tools/clang-format.js @@ -27,7 +27,8 @@ function main(args) { } const clangFormatOutput = result.stdout.trim(); - if (clangFormatOutput !== ('no modified files to format') && + if (clangFormatOutput !== '' && + clangFormatOutput !== ('no modified files to format') && clangFormatOutput !== ('clang-format did not modify any files')) { console.error(clangFormatOutput); const fixCmd = '"npm run lint:fix"'; From 6321f2ba1a66abca8abfb80011a93dc91bb8a729 Mon Sep 17 00:00:00 2001 From: raisinten Date: Thu, 19 Nov 2020 18:46:40 +0530 Subject: [PATCH 049/482] test: fix typos in addon_build/index.js PR-URL: https://github.com/nodejs/node-addon-api/pull/838 Reviewed-By: Michael Dawson Reviewed-By: Chengzhong Wu Reviewed-By: NickNaso --- test/addon_build/index.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/addon_build/index.js b/test/addon_build/index.js index 0d0c6ea2f..c410bf3a8 100644 --- a/test/addon_build/index.js +++ b/test/addon_build/index.js @@ -1,7 +1,7 @@ 'use strict'; const { promisify } = require('util'); -const exec = promisify(require('child_process').exec); +const exec = promisify(require('child_process').exec); const { copy, remove } = require('fs-extra'); const path = require('path'); const assert = require('assert') @@ -26,8 +26,8 @@ async function test(addon) { const { stderr, stdout } = await exec('npm install', { cwd: path.join(ADDONS_FOLDER, addon) }) - console.log(` >Runting test for: '${addon}'`); - // Disabled the checks on stderr and stdout because of this issuue on npm: + console.log(` >Running test for: '${addon}'`); + // Disabled the checks on stderr and stdout because of this issue on npm: // Stop using process.umask(): https://github.com/npm/cli/issues/1103 // We should enable the following checks again after the resolution of // the reported issue. @@ -41,7 +41,6 @@ async function test(addon) { assert.strictEqual(binding.noexcept.echo(103), 103); } - module.exports = (async function() { await beforeAll(addons); for (const addon of addons) { From 63b43f41254d144228fa8a1a8f229391ac0b9a9b Mon Sep 17 00:00:00 2001 From: raisinten Date: Thu, 19 Nov 2020 18:09:47 +0530 Subject: [PATCH 050/482] test: fix buildType bug objectwrap_worker_thread Since worker threads inherit non-process-specific options by default, the configuration needs to be shared via workerData. PR-URL: https://github.com/nodejs/node-addon-api/pull/837 Fixes: https://github.com/nodejs/node-addon-api/issues/834 Reviewed-By: Michael Dawson --- test/objectwrap_worker_thread.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/objectwrap_worker_thread.js b/test/objectwrap_worker_thread.js index 348309976..5e5e50b7e 100644 --- a/test/objectwrap_worker_thread.js +++ b/test/objectwrap_worker_thread.js @@ -1,14 +1,15 @@ 'use strict'; -const buildType = process.config.target_defaults.default_configuration; -const { Worker, isMainThread } = require('worker_threads'); +const { Worker, isMainThread, workerData } = require('worker_threads'); if (isMainThread) { - new Worker(__filename); + const buildType = process.config.target_defaults.default_configuration; + new Worker(__filename, { workerData: buildType }); } else { const test = binding => { new binding.objectwrap.Test(); }; + const buildType = workerData; test(require(`./build/${buildType}/binding.node`)); test(require(`./build/${buildType}/binding_noexcept.node`)); } 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 051/482] 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 052/482] 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); From 356e93d69a1a9c294e52ff1f28a5347becf38de2 Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Thu, 26 Nov 2020 18:08:41 -0500 Subject: [PATCH 053/482] test: fixup testing for specific N-API version (#840) Fixup testing for a specific N-API version and document. Reviewed-By: NickNaso PR-URL: https://github.com/nodejs/node-addon-api/pull/840 --- README.md | 9 +++++++++ test/index.js | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e7ee391d..41935336d 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,15 @@ npm install npm test --disable-deprecated ``` +To run the tests targetting a specific version of N-API run +``` +npm install +export NAPI_VERSION=X +npm test --NAPI_VERSION=X +``` + +where X is the version of N-API you want to target. + ### **Debug** To run the **node-addon-api** tests with `--debug` option: diff --git a/test/index.js b/test/index.js index ac95d88aa..ece32b3b4 100644 --- a/test/index.js +++ b/test/index.js @@ -62,7 +62,15 @@ let testModules = [ 'version_management' ]; -const napiVersion = Number(process.versions.napi) +let napiVersion = Number(process.versions.napi) +if (process.env.NAPI_VERSION) { + // we need this so that we don't try run tests that rely + // on methods that are not available in the NAPI_VERSION + // specified + napiVersion = process.env.NAPI_VERSION; +} +console.log('napiVersion:' + napiVersion); + const majorNodeVersion = process.versions.node.split('.')[0] if (napiVersion < 3) { From d9e11ff2c9a1a86548e4aac3f49e1f347e04d5e9 Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Tue, 1 Dec 2020 15:56:42 -0500 Subject: [PATCH 054/482] doc: add support info (#843) * doc: add support info PR-URL: https://github.com/nodejs/node-addon-api/pull/843 Reviewed-By: Chengzhong Wu Reviewed-By: NickNaso --- package-support.json | 21 +++++++++++++++++++++ package.json | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 package-support.json diff --git a/package-support.json b/package-support.json new file mode 100644 index 000000000..10d3607ac --- /dev/null +++ b/package-support.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "*", + "target": { + "node": "active" + }, + "response": { + "type": "time-permitting", + "paid": false, + "contact": { + "name": "node-addon-api team", + "url": "https://github.com/nodejs/node-addon-api/issues" + } + }, + "backing": [ { "project": "https://github.com/nodejs" }, + { "foundation": "https://openjsf.org/" } + ] + } + ] +} diff --git a/package.json b/package.json index 85d6ea2d1..343c1b624 100644 --- a/package.json +++ b/package.json @@ -307,5 +307,6 @@ "lint:fix": "git-clang-format" }, "pre-commit": "lint", - "version": "3.0.2" + "version": "3.0.2", + "support": true } From d5e37210cc6eeb3e4c86a037c81330f0d0b2939b Mon Sep 17 00:00:00 2001 From: legendecas Date: Fri, 4 Dec 2020 07:23:08 +0800 Subject: [PATCH 055/482] tools: print more instructions on clang-format check failed (#846) PR-URL: https://github.com/nodejs/node-addon-api/pull/846 Reviewed-By: Michael Dawson --- tools/clang-format.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/clang-format.js b/tools/clang-format.js index 3a2ba4a2f..026728b5c 100644 --- a/tools/clang-format.js +++ b/tools/clang-format.js @@ -33,7 +33,11 @@ function main(args) { console.error(clangFormatOutput); const fixCmd = '"npm run lint:fix"'; console.error(` - ERROR: please run ${fixCmd} to format changes in your commit`); + ERROR: please run ${fixCmd} to format changes in your commit + Note that when running the command locally, please keep your local + master branch and working branch up to date with nodejs/node-addon-api + to exclude un-related complains. + Or you can run "env CLANG_FORMAT_START=upstream/master ${fixCmd}".`); return 1; } } From 4bb680de4e2ed3775dce8ff5624e709a1718e752 Mon Sep 17 00:00:00 2001 From: Nikolai Vavilov Date: Mon, 7 Dec 2020 20:58:32 +0200 Subject: [PATCH 056/482] Remove misleading sentence (#847) The address-of operator is required for all class members. PR-URL: https://github.com/nodejs/node-addon-api/pull/847 Reviewed-By: Michael Dawson Reviewed-By: Gabriel Schulhof Reviewed-By: NickNaso --- tools/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/README.md b/tools/README.md index a1024d586..6b80e94f5 100644 --- a/tools/README.md +++ b/tools/README.md @@ -48,7 +48,7 @@ and define it as This way, the `Napi::ObjectWrap` constructor will be invoked after the object has been instantiated and `Napi::ObjectWrap` can use the `this` pointer to create a reference to the wrapped object. 2. Move your original constructor code into the new constructor. Delete your original constructor. - 3. In your class initialization function, associate native methods in the following way. The `&` character before methods is required because they are not static methods but instance methods. + 3. In your class initialization function, associate native methods in the following way. ``` Napi::FunctionReference constructor; From 528b9f6832645375caae7e4b567a203cd48c3252 Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Mon, 7 Dec 2020 15:02:38 -0500 Subject: [PATCH 057/482] test: remove sudo from travis (#850) Refs: https://github.com/nodejs/node-addon-api/issues/469 Signed-off-by: Michael Dawson --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 56f9c903c..76224c508 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,6 @@ matrix: fast_finish: true allow_failures: - env: NODEJS_VERSION=nightly -sudo: false cache: directories: - node_modules From b810466ae259c98238fc16aa549a67e020c37ae8 Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Wed, 9 Dec 2020 15:08:45 -0800 Subject: [PATCH 058/482] doc: clarify when to use N-API (#849) * doc: clarify when to use N-API Refs: https://github.com/nodejs/node-addon-api/issues/684 Signed-off-by: Michael Dawson --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index fd0a6cc11..86a82d427 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ the use of the C based [N-API](https://nodejs.org/dist/latest/docs/api/n-api.htm provided by Node.js when using C++. It provides a C++ object model and exception handling semantics with low overhead. +There are three options for implementing addons: N-API, nan, or direct +use of internal V8, libuv and Node.js libraries. Unless there is a need for +direct access to functionality which is not exposed by N-API as outlined +in [C/C++ addons](https://nodejs.org/dist/latest/docs/api/addons.html) +in Node.js core, use N-API. Refer to +[C/C++ addons with N-API](https://nodejs.org/dist/latest/docs/api/n-api.html) +for more information on N-API. + N-API is an ABI stable C interface provided by Node.js for building native addons. It is independent from the underlying JavaScript runtime (e.g. V8 or ChakraCore) and is maintained as part of Node.js itself. It is intended to insulate From b4a3364ad5fff1b043fe939d495f505fa812d79c Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Thu, 10 Dec 2020 06:51:52 -0800 Subject: [PATCH 059/482] doc: remove unused Doxygen file (#851) Refs: https://github.com/nodejs/node-addon-api/issues/368 Signed-off-by: Michael Dawson --- doc/Doxyfile | 2450 -------------------------------------------------- 1 file changed, 2450 deletions(-) delete mode 100644 doc/Doxyfile diff --git a/doc/Doxyfile b/doc/Doxyfile deleted file mode 100644 index 995090d1e..000000000 --- a/doc/Doxyfile +++ /dev/null @@ -1,2450 +0,0 @@ -# Doxyfile 1.8.13 - -# This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project. -# -# All text after a double hash (##) is considered a comment and is placed in -# front of the TAG it is preceding. -# -# All text after a single hash (#) is considered a comment and will be ignored. -# The format is: -# TAG = value [value, ...] -# For lists, items can also be appended using: -# TAG += value [value, ...] -# Values that contain spaces should be placed between quotes (\" \"). - -#--------------------------------------------------------------------------- -# Project related configuration options -#--------------------------------------------------------------------------- - -# This tag specifies the encoding used for all characters in the config file -# that follow. The default is UTF-8 which is also the encoding used for all text -# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv -# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv -# for the list of possible encodings. -# The default value is: UTF-8. - -DOXYFILE_ENCODING = UTF-8 - -# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by -# double-quotes, unless you are using Doxywizard) that should identify the -# project for which the documentation is generated. This name is used in the -# title of most generated pages and in a few other places. -# The default value is: My Project. - -PROJECT_NAME = N-API - -# The PROJECT_NUMBER tag can be used to enter a project or revision number. This -# could be handy for archiving the generated documentation or if some version -# control system is used. - -PROJECT_NUMBER = 1.3.0 - -# Using the PROJECT_BRIEF tag one can provide an optional one line description -# for a project that appears at the top of each page and should give viewer a -# quick idea about the purpose of the project. Keep the description short. - -PROJECT_BRIEF = "C++ wrapper classes for the ABI-stable C APIs for Node.js" - -# With the PROJECT_LOGO tag one can specify a logo or an icon that is included -# in the documentation. The maximum height of the logo should not exceed 55 -# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy -# the logo to the output directory. - -PROJECT_LOGO = - -# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path -# into which the generated documentation will be written. If a relative path is -# entered, it will be relative to the location where doxygen was started. If -# left blank the current directory will be used. - -OUTPUT_DIRECTORY = doc - -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -# directories (in 2 levels) under the output directory of each output format and -# will distribute the generated files over these directories. Enabling this -# option can be useful when feeding doxygen a huge amount of source files, where -# putting all generated files in the same directory would otherwise causes -# performance problems for the file system. -# The default value is: NO. - -CREATE_SUBDIRS = NO - -# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII -# characters to appear in the names of generated files. If set to NO, non-ASCII -# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode -# U+3044. -# The default value is: NO. - -ALLOW_UNICODE_NAMES = NO - -# The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all constant output in the proper language. -# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -# Ukrainian and Vietnamese. -# The default value is: English. - -OUTPUT_LANGUAGE = English - -# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member -# descriptions after the members that are listed in the file and class -# documentation (similar to Javadoc). Set to NO to disable this. -# The default value is: YES. - -BRIEF_MEMBER_DESC = YES - -# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief -# description of a member or function before the detailed description -# -# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the -# brief descriptions will be completely suppressed. -# The default value is: YES. - -REPEAT_BRIEF = YES - -# This tag implements a quasi-intelligent brief description abbreviator that is -# used to form the text in various listings. Each string in this list, if found -# as the leading text of the brief description, will be stripped from the text -# and the result, after processing the whole list, is used as the annotated -# text. Otherwise, the brief description is used as-is. If left blank, the -# following values are used ($name is automatically replaced with the name of -# the entity):The $name class, The $name widget, The $name file, is, provides, -# specifies, contains, represents, a, an and the. - -ABBREVIATE_BRIEF = "The $name class" \ - "The $name widget" \ - "The $name file" \ - is \ - provides \ - specifies \ - contains \ - represents \ - a \ - an \ - the - -# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# doxygen will generate a detailed section even if there is only a brief -# description. -# The default value is: NO. - -ALWAYS_DETAILED_SEC = NO - -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all -# inherited members of a class in the documentation of that class as if those -# members were ordinary class members. Constructors, destructors and assignment -# operators of the base classes will not be shown. -# The default value is: NO. - -INLINE_INHERITED_MEMB = NO - -# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path -# before files name in the file list and in the header files. If set to NO the -# shortest path that makes the file name unique will be used -# The default value is: YES. - -FULL_PATH_NAMES = YES - -# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. -# Stripping is only done if one of the specified strings matches the left-hand -# part of the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the path to -# strip. -# -# Note that you can specify absolute paths here, but also relative paths, which -# will be relative from the directory where doxygen is started. -# This tag requires that the tag FULL_PATH_NAMES is set to YES. - -STRIP_FROM_PATH = - -# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the -# path mentioned in the documentation of a class, which tells the reader which -# header file to include in order to use a class. If left blank only the name of -# the header file containing the class definition is used. Otherwise one should -# specify the list of include paths that are normally passed to the compiler -# using the -I flag. - -STRIP_FROM_INC_PATH = - -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but -# less readable) file names. This can be useful is your file systems doesn't -# support long names like on DOS, Mac, or CD-ROM. -# The default value is: NO. - -SHORT_NAMES = NO - -# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the -# first line (until the first dot) of a Javadoc-style comment as the brief -# description. If set to NO, the Javadoc-style will behave just like regular Qt- -# style comments (thus requiring an explicit @brief command for a brief -# description.) -# The default value is: NO. - -JAVADOC_AUTOBRIEF = YES - -# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first -# line (until the first dot) of a Qt-style comment as the brief description. If -# set to NO, the Qt-style will behave just like regular Qt-style comments (thus -# requiring an explicit \brief command for a brief description.) -# The default value is: NO. - -QT_AUTOBRIEF = NO - -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a -# multi-line C++ special comment block (i.e. a block of //! or /// comments) as -# a brief description. This used to be the default behavior. The new default is -# to treat a multi-line C++ comment block as a detailed description. Set this -# tag to YES if you prefer the old behavior instead. -# -# Note that setting this tag to YES also means that rational rose comments are -# not recognized any more. -# The default value is: NO. - -MULTILINE_CPP_IS_BRIEF = NO - -# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the -# documentation from any documented member that it re-implements. -# The default value is: YES. - -INHERIT_DOCS = YES - -# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new -# page for each member. If set to NO, the documentation of a member will be part -# of the file/class/namespace that contains it. -# The default value is: NO. - -SEPARATE_MEMBER_PAGES = NO - -# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen -# uses this value to replace tabs by spaces in code fragments. -# Minimum value: 1, maximum value: 16, default value: 4. - -TAB_SIZE = 4 - -# This tag can be used to specify a number of aliases that act as commands in -# the documentation. An alias has the form: -# name=value -# For example adding -# "sideeffect=@par Side Effects:\n" -# will allow you to put the command \sideeffect (or @sideeffect) in the -# documentation, which will result in a user-defined paragraph with heading -# "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines. - -ALIASES = - -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - -# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources -# only. Doxygen will then generate output that is more tailored for C. For -# instance, some of the names that are used will be different. The list of all -# members will be omitted, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_FOR_C = NO - -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or -# Python sources only. Doxygen will then generate output that is more tailored -# for that language. For instance, namespaces will be presented as packages, -# qualified scopes will look different, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_JAVA = NO - -# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran -# sources. Doxygen will then generate output that is tailored for Fortran. -# The default value is: NO. - -OPTIMIZE_FOR_FORTRAN = NO - -# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL -# sources. Doxygen will then generate output that is tailored for VHDL. -# The default value is: NO. - -OPTIMIZE_OUTPUT_VHDL = NO - -# Doxygen selects the parser to use depending on the extension of the files it -# parses. With this tag you can assign which parser to use for a given -# extension. Doxygen has a built-in mapping, but you can override or extend it -# using this tag. The format is ext=language, where ext is a file extension, and -# language is one of the parsers supported by doxygen: IDL, Java, Javascript, -# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: -# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: -# Fortran. In the later case the parser tries to guess whether the code is fixed -# or free formatted code, this is the default for Fortran type files), VHDL. For -# instance to make doxygen treat .inc files as Fortran files (default is PHP), -# and .f files as C (default is Fortran), use: inc=Fortran f=C. -# -# Note: For files without extension you can use no_extension as a placeholder. -# -# Note that for custom extensions you also need to set FILE_PATTERNS otherwise -# the files are not read by doxygen. - -EXTENSION_MAPPING = - -# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments -# according to the Markdown format, which allows for more readable -# documentation. See http://daringfireball.net/projects/markdown/ for details. -# The output of markdown processing is further processed by doxygen, so you can -# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in -# case of backward compatibilities issues. -# The default value is: YES. - -MARKDOWN_SUPPORT = YES - -# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up -# to that level are automatically included in the table of contents, even if -# they do not have an id attribute. -# Note: This feature currently applies only to Markdown headings. -# Minimum value: 0, maximum value: 99, default value: 0. -# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. - -TOC_INCLUDE_HEADINGS = 0 - -# When enabled doxygen tries to link words that correspond to documented -# classes, or namespaces to their corresponding documentation. Such a link can -# be prevented in individual cases by putting a % sign in front of the word or -# globally by setting AUTOLINK_SUPPORT to NO. -# The default value is: YES. - -AUTOLINK_SUPPORT = YES - -# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want -# to include (a tag file for) the STL sources as input, then you should set this -# tag to YES in order to let doxygen match functions declarations and -# definitions whose arguments contain STL classes (e.g. func(std::string); -# versus func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. -# The default value is: NO. - -BUILTIN_STL_SUPPORT = NO - -# If you use Microsoft's C++/CLI language, you should set this option to YES to -# enable parsing support. -# The default value is: NO. - -CPP_CLI_SUPPORT = NO - -# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: -# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen -# will parse them like normal C++ but will assume all classes use public instead -# of private inheritance when no explicit protection keyword is present. -# The default value is: NO. - -SIP_SUPPORT = NO - -# For Microsoft's IDL there are propget and propput attributes to indicate -# getter and setter methods for a property. Setting this option to YES will make -# doxygen to replace the get and set methods by a property in the documentation. -# This will only work if the methods are indeed getting or setting a simple -# type. If this is not the case, or you want to show the methods anyway, you -# should set this option to NO. -# The default value is: YES. - -IDL_PROPERTY_SUPPORT = YES - -# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES then doxygen will reuse the documentation of the first -# member in the group (if any) for the other members of the group. By default -# all members of a group must be documented explicitly. -# The default value is: NO. - -DISTRIBUTE_GROUP_DOC = NO - -# If one adds a struct or class to a group and this option is enabled, then also -# any nested class or struct is added to the same group. By default this option -# is disabled and one has to add nested compounds explicitly via \ingroup. -# The default value is: NO. - -GROUP_NESTED_COMPOUNDS = NO - -# Set the SUBGROUPING tag to YES to allow class member groups of the same type -# (for instance a group of public functions) to be put as a subgroup of that -# type (e.g. under the Public Functions section). Set it to NO to prevent -# subgrouping. Alternatively, this can be done per class using the -# \nosubgrouping command. -# The default value is: YES. - -SUBGROUPING = YES - -# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions -# are shown inside the group in which they are included (e.g. using \ingroup) -# instead of on a separate page (for HTML and Man pages) or section (for LaTeX -# and RTF). -# -# Note that this feature does not work in combination with -# SEPARATE_MEMBER_PAGES. -# The default value is: NO. - -INLINE_GROUPED_CLASSES = NO - -# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions -# with only public data fields or simple typedef fields will be shown inline in -# the documentation of the scope in which they are defined (i.e. file, -# namespace, or group documentation), provided this scope is documented. If set -# to NO, structs, classes, and unions are shown on a separate page (for HTML and -# Man pages) or section (for LaTeX and RTF). -# The default value is: NO. - -INLINE_SIMPLE_STRUCTS = NO - -# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or -# enum is documented as struct, union, or enum with the name of the typedef. So -# typedef struct TypeS {} TypeT, will appear in the documentation as a struct -# with name TypeT. When disabled the typedef will appear as a member of a file, -# namespace, or class. And the struct will be named TypeS. This can typically be -# useful for C code in case the coding convention dictates that all compound -# types are typedef'ed and only the typedef is referenced, never the tag name. -# The default value is: NO. - -TYPEDEF_HIDES_STRUCT = NO - -# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This -# cache is used to resolve symbols given their name and scope. Since this can be -# an expensive process and often the same symbol appears multiple times in the -# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small -# doxygen will become slower. If the cache is too large, memory is wasted. The -# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range -# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 -# symbols. At the end of a run doxygen will report the cache usage and suggest -# the optimal cache size from a speed point of view. -# Minimum value: 0, maximum value: 9, default value: 0. - -LOOKUP_CACHE_SIZE = 0 - -#--------------------------------------------------------------------------- -# Build related configuration options -#--------------------------------------------------------------------------- - -# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in -# documentation are documented, even if no documentation was available. Private -# class members and static file members will be hidden unless the -# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. -# Note: This will also disable the warnings about undocumented members that are -# normally produced when WARNINGS is set to YES. -# The default value is: NO. - -EXTRACT_ALL = YES - -# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will -# be included in the documentation. -# The default value is: NO. - -EXTRACT_PRIVATE = NO - -# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal -# scope will be included in the documentation. -# The default value is: NO. - -EXTRACT_PACKAGE = NO - -# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be -# included in the documentation. -# The default value is: NO. - -EXTRACT_STATIC = NO - -# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined -# locally in source files will be included in the documentation. If set to NO, -# only classes defined in header files are included. Does not have any effect -# for Java sources. -# The default value is: YES. - -EXTRACT_LOCAL_CLASSES = YES - -# This flag is only useful for Objective-C code. If set to YES, local methods, -# which are defined in the implementation section but not in the interface are -# included in the documentation. If set to NO, only methods in the interface are -# included. -# The default value is: NO. - -EXTRACT_LOCAL_METHODS = NO - -# If this flag is set to YES, the members of anonymous namespaces will be -# extracted and appear in the documentation as a namespace called -# 'anonymous_namespace{file}', where file will be replaced with the base name of -# the file that contains the anonymous namespace. By default anonymous namespace -# are hidden. -# The default value is: NO. - -EXTRACT_ANON_NSPACES = NO - -# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all -# undocumented members inside documented classes or files. If set to NO these -# members will be included in the various overviews, but no documentation -# section is generated. This option has no effect if EXTRACT_ALL is enabled. -# The default value is: NO. - -HIDE_UNDOC_MEMBERS = NO - -# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all -# undocumented classes that are normally visible in the class hierarchy. If set -# to NO, these classes will be included in the various overviews. This option -# has no effect if EXTRACT_ALL is enabled. -# The default value is: NO. - -HIDE_UNDOC_CLASSES = NO - -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend -# (class|struct|union) declarations. If set to NO, these declarations will be -# included in the documentation. -# The default value is: NO. - -HIDE_FRIEND_COMPOUNDS = NO - -# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any -# documentation blocks found inside the body of a function. If set to NO, these -# blocks will be appended to the function's detailed documentation block. -# The default value is: NO. - -HIDE_IN_BODY_DOCS = NO - -# The INTERNAL_DOCS tag determines if documentation that is typed after a -# \internal command is included. If the tag is set to NO then the documentation -# will be excluded. Set it to YES to include the internal documentation. -# The default value is: NO. - -INTERNAL_DOCS = NO - -# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file -# names in lower-case letters. If set to YES, upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# and Mac users are advised to set this option to NO. -# The default value is: system dependent. - -CASE_SENSE_NAMES = NO - -# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with -# their full class and namespace scopes in the documentation. If set to YES, the -# scope will be hidden. -# The default value is: NO. - -HIDE_SCOPE_NAMES = NO - -# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will -# append additional text to a page's title, such as Class Reference. If set to -# YES the compound reference will be hidden. -# The default value is: NO. - -HIDE_COMPOUND_REFERENCE= NO - -# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of -# the files that are included by a file in the documentation of that file. -# The default value is: YES. - -SHOW_INCLUDE_FILES = YES - -# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each -# grouped member an include statement to the documentation, telling the reader -# which file to include in order to use the member. -# The default value is: NO. - -SHOW_GROUPED_MEMB_INC = NO - -# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include -# files with double quotes in the documentation rather than with sharp brackets. -# The default value is: NO. - -FORCE_LOCAL_INCLUDES = NO - -# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the -# documentation for inline members. -# The default value is: YES. - -INLINE_INFO = YES - -# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the -# (detailed) documentation of file and class members alphabetically by member -# name. If set to NO, the members will appear in declaration order. -# The default value is: YES. - -SORT_MEMBER_DOCS = YES - -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief -# descriptions of file, namespace and class members alphabetically by member -# name. If set to NO, the members will appear in declaration order. Note that -# this will also influence the order of the classes in the class list. -# The default value is: NO. - -SORT_BRIEF_DOCS = NO - -# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the -# (brief and detailed) documentation of class members so that constructors and -# destructors are listed first. If set to NO the constructors will appear in the -# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. -# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief -# member documentation. -# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting -# detailed member documentation. -# The default value is: NO. - -SORT_MEMBERS_CTORS_1ST = NO - -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy -# of group names into alphabetical order. If set to NO the group names will -# appear in their defined order. -# The default value is: NO. - -SORT_GROUP_NAMES = NO - -# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by -# fully-qualified names, including namespaces. If set to NO, the class list will -# be sorted only by class name, not including the namespace part. -# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -# Note: This option applies only to the class list, not to the alphabetical -# list. -# The default value is: NO. - -SORT_BY_SCOPE_NAME = NO - -# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper -# type resolution of all parameters of a function it will reject a match between -# the prototype and the implementation of a member function even if there is -# only one candidate or it is obvious which candidate to choose by doing a -# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still -# accept a match between prototype and implementation in such cases. -# The default value is: NO. - -STRICT_PROTO_MATCHING = NO - -# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo -# list. This list is created by putting \todo commands in the documentation. -# The default value is: YES. - -GENERATE_TODOLIST = YES - -# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test -# list. This list is created by putting \test commands in the documentation. -# The default value is: YES. - -GENERATE_TESTLIST = YES - -# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug -# list. This list is created by putting \bug commands in the documentation. -# The default value is: YES. - -GENERATE_BUGLIST = YES - -# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) -# the deprecated list. This list is created by putting \deprecated commands in -# the documentation. -# The default value is: YES. - -GENERATE_DEPRECATEDLIST= YES - -# The ENABLED_SECTIONS tag can be used to enable conditional documentation -# sections, marked by \if ... \endif and \cond -# ... \endcond blocks. - -ENABLED_SECTIONS = - -# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the -# initial value of a variable or macro / define can have for it to appear in the -# documentation. If the initializer consists of more lines than specified here -# it will be hidden. Use a value of 0 to hide initializers completely. The -# appearance of the value of individual variables and macros / defines can be -# controlled using \showinitializer or \hideinitializer command in the -# documentation regardless of this setting. -# Minimum value: 0, maximum value: 10000, default value: 30. - -MAX_INITIALIZER_LINES = 30 - -# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at -# the bottom of the documentation of classes and structs. If set to YES, the -# list will mention the files that were used to generate the documentation. -# The default value is: YES. - -SHOW_USED_FILES = YES - -# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This -# will remove the Files entry from the Quick Index and from the Folder Tree View -# (if specified). -# The default value is: YES. - -SHOW_FILES = YES - -# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces -# page. This will remove the Namespaces entry from the Quick Index and from the -# Folder Tree View (if specified). -# The default value is: YES. - -SHOW_NAMESPACES = YES - -# The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from -# the version control system). Doxygen will invoke the program by executing (via -# popen()) the command command input-file, where command is the value of the -# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided -# by doxygen. Whatever the program writes to standard output is used as the file -# version. For an example see the documentation. - -FILE_VERSION_FILTER = - -# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed -# by doxygen. The layout file controls the global structure of the generated -# output files in an output format independent way. To create the layout file -# that represents doxygen's defaults, run doxygen with the -l option. You can -# optionally specify a file name after the option, if omitted DoxygenLayout.xml -# will be used as the name of the layout file. -# -# Note that if you run doxygen from a directory containing a file called -# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE -# tag is left empty. - -LAYOUT_FILE = - -# The CITE_BIB_FILES tag can be used to specify one or more bib files containing -# the reference definitions. This must be a list of .bib files. The .bib -# extension is automatically appended if omitted. This requires the bibtex tool -# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. -# For LaTeX the style of the bibliography can be controlled using -# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the -# search path. See also \cite for info how to create references. - -CITE_BIB_FILES = - -#--------------------------------------------------------------------------- -# Configuration options related to warning and progress messages -#--------------------------------------------------------------------------- - -# The QUIET tag can be used to turn on/off the messages that are generated to -# standard output by doxygen. If QUIET is set to YES this implies that the -# messages are off. -# The default value is: NO. - -QUIET = NO - -# The WARNINGS tag can be used to turn on/off the warning messages that are -# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES -# this implies that the warnings are on. -# -# Tip: Turn warnings on while writing the documentation. -# The default value is: YES. - -WARNINGS = YES - -# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate -# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag -# will automatically be disabled. -# The default value is: YES. - -WARN_IF_UNDOCUMENTED = YES - -# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some parameters -# in a documented function, or documenting parameters that don't exist or using -# markup commands wrongly. -# The default value is: YES. - -WARN_IF_DOC_ERROR = YES - -# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that -# are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong or incomplete -# parameter documentation, but not about the absence of documentation. -# The default value is: NO. - -WARN_NO_PARAMDOC = NO - -# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. -# The default value is: NO. - -WARN_AS_ERROR = NO - -# The WARN_FORMAT tag determines the format of the warning messages that doxygen -# can produce. The string should contain the $file, $line, and $text tags, which -# will be replaced by the file and line number from which the warning originated -# and the warning text. Optionally the format may contain $version, which will -# be replaced by the version of the file (if it could be obtained via -# FILE_VERSION_FILTER) -# The default value is: $file:$line: $text. - -WARN_FORMAT = "$file:$line: $text" - -# The WARN_LOGFILE tag can be used to specify a file to which warning and error -# messages should be written. If left blank the output is written to standard -# error (stderr). - -WARN_LOGFILE = - -#--------------------------------------------------------------------------- -# Configuration options related to the input files -#--------------------------------------------------------------------------- - -# The INPUT tag is used to specify the files and/or directories that contain -# documented source files. You may enter file names like myfile.cpp or -# directories like /usr/src/myproject. Separate the files or directories with -# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING -# Note: If this tag is empty the current directory is searched. - -INPUT = . - -# This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses -# libiconv (or the iconv built into libc) for the transcoding. See the libiconv -# documentation (see: http://www.gnu.org/software/libiconv) for the list of -# possible encodings. -# The default value is: UTF-8. - -INPUT_ENCODING = UTF-8 - -# If the value of the INPUT tag contains directories, you can use the -# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and -# *.h) to filter out the source-files in the directories. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# read by doxygen. -# -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, -# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf. - -FILE_PATTERNS = napi.h \ - README.md - -# The RECURSIVE tag can be used to specify whether or not subdirectories should -# be searched for input files as well. -# The default value is: NO. - -RECURSIVE = NO - -# The EXCLUDE tag can be used to specify files and/or directories that should be -# excluded from the INPUT source files. This way you can easily exclude a -# subdirectory from a directory tree whose root is specified with the INPUT tag. -# -# Note that relative paths are relative to the directory from which doxygen is -# run. - -EXCLUDE = - -# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or -# directories that are symbolic links (a Unix file system feature) are excluded -# from the input. -# The default value is: NO. - -EXCLUDE_SYMLINKS = NO - -# If the value of the INPUT tag contains directories, you can use the -# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -# certain files from those directories. -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories for example use the pattern */test/* - -EXCLUDE_PATTERNS = - -# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names -# (namespaces, classes, functions, etc.) that should be excluded from the -# output. The symbol name can be a fully qualified name, a word, or if the -# wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* - -EXCLUDE_SYMBOLS = - -# The EXAMPLE_PATH tag can be used to specify one or more files or directories -# that contain example code fragments that are included (see the \include -# command). - -EXAMPLE_PATH = - -# If the value of the EXAMPLE_PATH tag contains directories, you can use the -# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and -# *.h) to filter out the source-files in the directories. If left blank all -# files are included. - -EXAMPLE_PATTERNS = * - -# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -# searched for input files to be used with the \include or \dontinclude commands -# irrespective of the value of the RECURSIVE tag. -# The default value is: NO. - -EXAMPLE_RECURSIVE = NO - -# The IMAGE_PATH tag can be used to specify one or more files or directories -# that contain images that are to be included in the documentation (see the -# \image command). - -IMAGE_PATH = - -# The INPUT_FILTER tag can be used to specify a program that doxygen should -# invoke to filter for each input file. Doxygen will invoke the filter program -# by executing (via popen()) the command: -# -# -# -# where is the value of the INPUT_FILTER tag, and is the -# name of an input file. Doxygen will then use the output that the filter -# program writes to standard output. If FILTER_PATTERNS is specified, this tag -# will be ignored. -# -# Note that the filter must not add or remove lines; it is applied before the -# code is scanned, but not when the output code is generated. If lines are added -# or removed, the anchors will not be placed correctly. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. - -INPUT_FILTER = - -# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -# basis. Doxygen will compare the file name with each pattern and apply the -# filter if there is a match. The filters are a list of the form: pattern=filter -# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how -# filters are used. If the FILTER_PATTERNS tag is empty or if none of the -# patterns match the file name, INPUT_FILTER is applied. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. - -FILTER_PATTERNS = - -# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER) will also be used to filter the input files that are used for -# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). -# The default value is: NO. - -FILTER_SOURCE_FILES = NO - -# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file -# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and -# it is also possible to disable source filtering for a specific pattern using -# *.ext= (so without naming a filter). -# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. - -FILTER_SOURCE_PATTERNS = - -# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that -# is part of the input, its contents will be placed on the main page -# (index.html). This can be useful if you have a project on for instance GitHub -# and want to reuse the introduction page also for the doxygen output. - -USE_MDFILE_AS_MAINPAGE = README.md - -#--------------------------------------------------------------------------- -# Configuration options related to source browsing -#--------------------------------------------------------------------------- - -# If the SOURCE_BROWSER tag is set to YES then a list of source files will be -# generated. Documented entities will be cross-referenced with these sources. -# -# Note: To get rid of all source code in the generated output, make sure that -# also VERBATIM_HEADERS is set to NO. -# The default value is: NO. - -SOURCE_BROWSER = YES - -# Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. -# The default value is: NO. - -INLINE_SOURCES = NO - -# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any -# special comment blocks from generated source code fragments. Normal C, C++ and -# Fortran comments will always remain visible. -# The default value is: YES. - -STRIP_CODE_COMMENTS = YES - -# If the REFERENCED_BY_RELATION tag is set to YES then for each documented -# function all documented functions referencing it will be listed. -# The default value is: NO. - -REFERENCED_BY_RELATION = NO - -# If the REFERENCES_RELATION tag is set to YES then for each documented function -# all documented entities called/used by that function will be listed. -# The default value is: NO. - -REFERENCES_RELATION = NO - -# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set -# to YES then the hyperlinks from functions in REFERENCES_RELATION and -# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will -# link to the documentation. -# The default value is: YES. - -REFERENCES_LINK_SOURCE = YES - -# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the -# source code will show a tooltip with additional information such as prototype, -# brief description and links to the definition and documentation. Since this -# will make the HTML file larger and loading of large files a bit slower, you -# can opt to disable this feature. -# The default value is: YES. -# This tag requires that the tag SOURCE_BROWSER is set to YES. - -SOURCE_TOOLTIPS = YES - -# If the USE_HTAGS tag is set to YES then the references to source code will -# point to the HTML generated by the htags(1) tool instead of doxygen built-in -# source browser. The htags tool is part of GNU's global source tagging system -# (see http://www.gnu.org/software/global/global.html). You will need version -# 4.8.6 or higher. -# -# To use it do the following: -# - Install the latest version of global -# - Enable SOURCE_BROWSER and USE_HTAGS in the config file -# - Make sure the INPUT points to the root of the source tree -# - Run doxygen as normal -# -# Doxygen will invoke htags (and that will in turn invoke gtags), so these -# tools must be available from the command line (i.e. in the search path). -# -# The result: instead of the source browser generated by doxygen, the links to -# source code will now point to the output of htags. -# The default value is: NO. -# This tag requires that the tag SOURCE_BROWSER is set to YES. - -USE_HTAGS = NO - -# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a -# verbatim copy of the header file for each class for which an include is -# specified. Set to NO to disable this. -# See also: Section \class. -# The default value is: YES. - -VERBATIM_HEADERS = YES - -# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the -# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the -# cost of reduced performance. This can be particularly helpful with template -# rich C++ code for which doxygen's built-in parser lacks the necessary type -# information. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse-libclang=ON option for CMake. -# The default value is: NO. - -CLANG_ASSISTED_PARSING = NO - -# If clang assisted parsing is enabled you can provide the compiler with command -# line options that you would normally use when invoking the compiler. Note that -# the include paths will already be set by doxygen for the files and directories -# specified with INPUT and INCLUDE_PATH. -# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. - -CLANG_OPTIONS = - -#--------------------------------------------------------------------------- -# Configuration options related to the alphabetical class index -#--------------------------------------------------------------------------- - -# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all -# compounds will be generated. Enable this if the project contains a lot of -# classes, structs, unions or interfaces. -# The default value is: YES. - -ALPHABETICAL_INDEX = YES - -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -COLS_IN_ALPHA_INDEX = 5 - -# In case all classes in a project start with a common prefix, all classes will -# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag -# can be used to specify a prefix (or a list of prefixes) that should be ignored -# while generating the index headers. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -IGNORE_PREFIX = - -#--------------------------------------------------------------------------- -# Configuration options related to the HTML output -#--------------------------------------------------------------------------- - -# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output -# The default value is: YES. - -GENERATE_HTML = YES - -# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a -# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -# it. -# The default directory is: html. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_OUTPUT = html - -# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each -# generated HTML page (for example: .htm, .php, .asp). -# The default value is: .html. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FILE_EXTENSION = .html - -# The HTML_HEADER tag can be used to specify a user-defined HTML header file for -# each generated HTML page. If the tag is left blank doxygen will generate a -# standard header. -# -# To get valid HTML the header file that includes any scripts and style sheets -# that doxygen needs, which is dependent on the configuration options used (e.g. -# the setting GENERATE_TREEVIEW). It is highly recommended to start with a -# default header using -# doxygen -w html new_header.html new_footer.html new_stylesheet.css -# YourConfigFile -# and then modify the file new_header.html. See also section "Doxygen usage" -# for information on how to generate the default header that doxygen normally -# uses. -# Note: The header is subject to change so you typically have to regenerate the -# default header when upgrading to a newer version of doxygen. For a description -# of the possible markers and block names see the documentation. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_HEADER = - -# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each -# generated HTML page. If the tag is left blank doxygen will generate a standard -# footer. See HTML_HEADER for more information on how to generate a default -# footer and what special commands can be used inside the footer. See also -# section "Doxygen usage" for information on how to generate the default footer -# that doxygen normally uses. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FOOTER = - -# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style -# sheet that is used by each HTML page. It can be used to fine-tune the look of -# the HTML output. If left blank doxygen will generate a default style sheet. -# See also section "Doxygen usage" for information on how to generate the style -# sheet that doxygen normally uses. -# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as -# it is more robust and this tag (HTML_STYLESHEET) will in the future become -# obsolete. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_STYLESHEET = - -# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined -# cascading style sheets that are included after the standard style sheets -# created by doxygen. Using this option one can overrule certain style aspects. -# This is preferred over using HTML_STYLESHEET since it does not replace the -# standard style sheet and is therefore more robust against future updates. -# Doxygen will copy the style sheet files to the output directory. -# Note: The order of the extra style sheet files is of importance (e.g. the last -# style sheet in the list overrules the setting of the previous ones in the -# list). For an example see the documentation. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_EXTRA_STYLESHEET = - -# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or -# other source files which should be copied to the HTML output directory. Note -# that these files will be copied to the base HTML output directory. Use the -# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these -# files. In the HTML_STYLESHEET file, use the file name only. Also note that the -# files will be copied as-is; there are no commands or markers available. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_EXTRA_FILES = - -# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen -# will adjust the colors in the style sheet and background images according to -# this color. Hue is specified as an angle on a colorwheel, see -# http://en.wikipedia.org/wiki/Hue for more information. For instance the value -# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 -# purple, and 360 is red again. -# Minimum value: 0, maximum value: 359, default value: 220. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_HUE = 220 - -# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -# in the HTML output. For a value of 0 the output will use grayscales only. A -# value of 255 will produce the most vivid colors. -# Minimum value: 0, maximum value: 255, default value: 100. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_SAT = 100 - -# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the -# luminance component of the colors in the HTML output. Values below 100 -# gradually make the output lighter, whereas values above 100 make the output -# darker. The value divided by 100 is the actual gamma applied, so 80 represents -# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not -# change the gamma. -# Minimum value: 40, maximum value: 240, default value: 80. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_GAMMA = 80 - -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - -# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML -# documentation will contain sections that can be hidden and shown after the -# page has loaded. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_DYNAMIC_SECTIONS = NO - -# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries -# shown in the various tree structured indices initially; the user can expand -# and collapse entries dynamically later on. Doxygen will expand the tree to -# such a level that at most the specified number of entries are visible (unless -# a fully collapsed tree already exceeds this amount). So setting the number of -# entries 1 will produce a full collapsed tree by default. 0 is a special value -# representing an infinite number of entries and will result in a full expanded -# tree by default. -# Minimum value: 0, maximum value: 9999, default value: 100. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_INDEX_NUM_ENTRIES = 100 - -# If the GENERATE_DOCSET tag is set to YES, additional index files will be -# generated that can be used as input for Apple's Xcode 3 integrated development -# environment (see: http://developer.apple.com/tools/xcode/), introduced with -# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a -# Makefile in the HTML output directory. Running make will produce the docset in -# that directory and running make install will install the docset in -# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at -# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html -# for more information. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_DOCSET = NO - -# This tag determines the name of the docset feed. A documentation feed provides -# an umbrella under which multiple documentation sets from a single provider -# (such as a company or product suite) can be grouped. -# The default value is: Doxygen generated docs. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_FEEDNAME = "Doxygen generated docs" - -# This tag specifies a string that should uniquely identify the documentation -# set bundle. This should be a reverse domain-name style string, e.g. -# com.mycompany.MyDocSet. Doxygen will append .docset to the name. -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_BUNDLE_ID = org.doxygen.Project - -# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify -# the documentation publisher. This should be a reverse domain-name style -# string, e.g. com.mycompany.MyDocSet.documentation. -# The default value is: org.doxygen.Publisher. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_PUBLISHER_ID = org.doxygen.Publisher - -# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. -# The default value is: Publisher. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_PUBLISHER_NAME = Publisher - -# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three -# additional HTML index files: index.hhp, index.hhc, and index.hhk. The -# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on -# Windows. -# -# The HTML Help Workshop contains a compiler that can convert all HTML output -# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML -# files are now used as the Windows 98 help format, and will replace the old -# Windows help format (.hlp) on all Windows platforms in the future. Compressed -# HTML files also contain an index, a table of contents, and you can search for -# words in the documentation. The HTML workshop also contains a viewer for -# compressed HTML files. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_HTMLHELP = NO - -# The CHM_FILE tag can be used to specify the file name of the resulting .chm -# file. You can add a path in front of the file if the result should not be -# written to the html output directory. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -CHM_FILE = - -# The HHC_LOCATION tag can be used to specify the location (absolute path -# including file name) of the HTML help compiler (hhc.exe). If non-empty, -# doxygen will try to run the HTML help compiler on the generated index.hhp. -# The file has to be specified with full path. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -HHC_LOCATION = - -# The GENERATE_CHI flag controls if a separate .chi index file is generated -# (YES) or that it should be included in the master .chm file (NO). -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -GENERATE_CHI = NO - -# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) -# and project file content. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -CHM_INDEX_ENCODING = - -# The BINARY_TOC flag controls whether a binary table of contents is generated -# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it -# enables the Previous and Next buttons. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -BINARY_TOC = NO - -# The TOC_EXPAND flag can be set to YES to add extra items for group members to -# the table of contents of the HTML help documentation and to the tree view. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -TOC_EXPAND = NO - -# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and -# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that -# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help -# (.qch) of the generated HTML documentation. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_QHP = NO - -# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify -# the file name of the resulting .qch file. The path specified is relative to -# the HTML output folder. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QCH_FILE = - -# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help -# Project output. For more information please see Qt Help Project / Namespace -# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_NAMESPACE = org.doxygen.Project - -# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt -# Help Project output. For more information please see Qt Help Project / Virtual -# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- -# folders). -# The default value is: doc. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_VIRTUAL_FOLDER = doc - -# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom -# filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- -# filters). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_CUST_FILTER_NAME = - -# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the -# custom filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- -# filters). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_CUST_FILTER_ATTRS = - -# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this -# project's filter section matches. Qt Help Project / Filter Attributes (see: -# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_SECT_FILTER_ATTRS = - -# The QHG_LOCATION tag can be used to specify the location of Qt's -# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the -# generated .qhp file. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHG_LOCATION = - -# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be -# generated, together with the HTML files, they form an Eclipse help plugin. To -# install this plugin and make it available under the help contents menu in -# Eclipse, the contents of the directory containing the HTML and XML files needs -# to be copied into the plugins directory of eclipse. The name of the directory -# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. -# After copying Eclipse needs to be restarted before the help appears. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_ECLIPSEHELP = NO - -# A unique identifier for the Eclipse help plugin. When installing the plugin -# the directory name containing the HTML and XML files should also have this -# name. Each documentation set should have its own identifier. -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. - -ECLIPSE_DOC_ID = org.doxygen.Project - -# If you want full control over the layout of the generated HTML pages it might -# be necessary to disable the index and replace it with your own. The -# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top -# of each HTML page. A value of NO enables the index and the value YES disables -# it. Since the tabs in the index contain the same information as the navigation -# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -DISABLE_INDEX = NO - -# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index -# structure should be generated to display hierarchical information. If the tag -# value is set to YES, a side panel will be generated containing a tree-like -# index structure (just like the one that is generated for HTML Help). For this -# to work a browser that supports JavaScript, DHTML, CSS and frames is required -# (i.e. any modern browser). Windows users are probably better off using the -# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -# further fine-tune the look of the index. As an example, the default style -# sheet generated by doxygen has an example that shows how to put an image at -# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -# the same information as the tab index, you could consider setting -# DISABLE_INDEX to YES when enabling this option. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_TREEVIEW = NO - -# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that -# doxygen will group on one line in the generated HTML documentation. -# -# Note that a value of 0 will completely suppress the enum values from appearing -# in the overview section. -# Minimum value: 0, maximum value: 20, default value: 4. -# This tag requires that the tag GENERATE_HTML is set to YES. - -ENUM_VALUES_PER_LINE = 4 - -# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used -# to set the initial width (in pixels) of the frame in which the tree is shown. -# Minimum value: 0, maximum value: 1500, default value: 250. -# This tag requires that the tag GENERATE_HTML is set to YES. - -TREEVIEW_WIDTH = 250 - -# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to -# external symbols imported via tag files in a separate window. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -EXT_LINKS_IN_WINDOW = NO - -# Use this tag to change the font size of LaTeX formulas included as images in -# the HTML documentation. When you change the font size after a successful -# doxygen run you need to manually remove any form_*.png images from the HTML -# output directory to force them to be regenerated. -# Minimum value: 8, maximum value: 50, default value: 10. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_FONTSIZE = 10 - -# Use the FORMULA_TRANPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_TRANSPARENT = YES - -# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see -# http://www.mathjax.org) which uses client side Javascript for the rendering -# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX -# installed or if you want to formulas look prettier in the HTML output. When -# enabled you may also need to install MathJax separately and configure the path -# to it using the MATHJAX_RELPATH option. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -USE_MATHJAX = NO - -# When MathJax is enabled you can set the default output format to be used for -# the MathJax output. See the MathJax site (see: -# http://docs.mathjax.org/en/latest/output.html) for more details. -# Possible values are: HTML-CSS (which is slower, but has the best -# compatibility), NativeMML (i.e. MathML) and SVG. -# The default value is: HTML-CSS. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_FORMAT = HTML-CSS - -# When MathJax is enabled you need to specify the location relative to the HTML -# output directory using the MATHJAX_RELPATH option. The destination directory -# should contain the MathJax.js script. For instance, if the mathjax directory -# is located at the same level as the HTML output directory, then -# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax -# Content Delivery Network so you can quickly see the result without installing -# MathJax. However, it is strongly recommended to install a local copy of -# MathJax from http://www.mathjax.org before deployment. -# The default value is: http://cdn.mathjax.org/mathjax/latest. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest - -# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax -# extension names that should be enabled during MathJax rendering. For example -# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_EXTENSIONS = - -# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces -# of code that will be used on startup of the MathJax code. See the MathJax site -# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an -# example see the documentation. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_CODEFILE = - -# When the SEARCHENGINE tag is enabled doxygen will generate a search box for -# the HTML output. The underlying search engine uses javascript and DHTML and -# should work on any modern browser. Note that when using HTML help -# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) -# there is already a search function so this one should typically be disabled. -# For large projects the javascript based search engine can be slow, then -# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to -# search using the keyboard; to jump to the search box use + S -# (what the is depends on the OS and browser, but it is typically -# , /