From 6ccd5a8e65dd6c35e5924394d10a98bc21e9c582 Mon Sep 17 00:00:00 2001 From: legendecas <legendecas@gmail.com> Date: Wed, 7 Oct 2020 02:05:15 +0800 Subject: [PATCH] n-api: napi_make_callback emit async init with resource of async_context instead of emit async init with receiver of the callback. --- doc/api/n-api.md | 36 +++- src/node_api.cc | 187 +++++++++++++----- test/node-api/test_async_context/binding.c | 128 ++++++++++++ test/node-api/test_async_context/binding.gyp | 9 + .../test-gcable-callback.js | 65 ++++++ .../test-gcable.js} | 4 +- test/node-api/test_async_context/test.js | 63 ++++++ test/node-api/test_make_callback/binding.c | 45 +---- .../test_make_callback/test-async-hooks.js | 11 +- test/node-api/test_make_callback/test.js | 15 +- 10 files changed, 465 insertions(+), 98 deletions(-) create mode 100644 test/node-api/test_async_context/binding.c create mode 100644 test/node-api/test_async_context/binding.gyp create mode 100644 test/node-api/test_async_context/test-gcable-callback.js rename test/node-api/{test_make_callback/test-async-hooks-gcable.js => test_async_context/test-gcable.js} (95%) create mode 100644 test/node-api/test_async_context/test.js diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 287940d1d5485c..9f16d2d2933b90 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -5189,19 +5189,31 @@ napi_status napi_async_init(napi_env env, * `[in] env`: The environment that the API is invoked under. * `[in] async_resource`: Object associated with the async work - that will be passed to possible `async_hooks` [`init` hooks][]. - In order to retain ABI compatibility with previous versions, - passing `NULL` for `async_resource` does not result in an error. However, - this results in incorrect operation of async hooks for the - napi_async_context created. Potential issues include - loss of async context when using the AsyncLocalStorage API. -* `[in] async_resource_name`: Identifier for the kind of resource - that is being provided for diagnostic information exposed by the - `async_hooks` API. + that will be passed to possible `async_hooks` [`init` hooks][] and can be + accessed by [`async_hooks.executionAsyncResource()`][]. +* `[in] async_resource_name`: Identifier for the kind of resource that is being + provided for diagnostic information exposed by the `async_hooks` API. * `[out] result`: The initialized async context. Returns `napi_ok` if the API succeeded. +The `async_resource` object needs to be kept alive until +[`napi_async_destroy`][] to keep `async_hooks` related API acts correctly. In +order to retain ABI compatibility with previous versions, `napi_async_context`s +are not maintaining the strong reference to the `async_resource` objects to +avoid introducing causing memory leaks. However, if the `async_resource` is +garbage collected by JavaScript engine before the `napi_async_context` was +destroyed by `napi_async_destroy`, calling `napi_async_context` related APIs +like [`napi_open_callback_scope`][] and [`napi_make_callback`][] can cause +problems like loss of async context when using the `AsyncLocalStoage` API. + +In order to retain ABI compatibility with previous versions, passing `NULL` +for `async_resource` does not result in an error. However, this is not +recommended as this will result poor results with `async_hooks` +[`init` hooks][] and `async_hooks.executionAsyncResource()` as the resource is +now required by the underlying `async_hooks` implementation in order to provide +the linkage between async callbacks. + ### napi_async_destroy <!-- YAML added: v8.6.0 @@ -5288,7 +5300,9 @@ NAPI_EXTERN napi_status napi_open_callback_scope(napi_env env, * `[in] env`: The environment that the API is invoked under. * `[in] resource_object`: An object associated with the async work - that will be passed to possible `async_hooks` [`init` hooks][]. + that will be passed to possible `async_hooks` [`init` hooks][]. This + parameter has been deprecated and is ignored at runtime. Use the + `async_resource` parameter in [`napi_async_init`][] instead. * `[in] context`: Context for the async operation that is invoking the callback. This should be a value previously obtained from [`napi_async_init`][]. * `[out] result`: The newly created scope. @@ -5985,6 +5999,7 @@ This API may only be called from the main thread. [`Number.MAX_SAFE_INTEGER`]: https://tc39.github.io/ecma262/#sec-number.max_safe_integer [`Number.MIN_SAFE_INTEGER`]: https://tc39.github.io/ecma262/#sec-number.min_safe_integer [`Worker`]: worker_threads.md#worker_threads_class_worker +[`async_hooks.executionAsyncResource()`]: async_hooks.md#async_hooks_async_hooks_executionasyncresource [`global`]: globals.md#globals_global [`init` hooks]: async_hooks.md#async_hooks_init_asyncid_type_triggerasyncid_resource [`napi_add_async_cleanup_hook`]: #n_api_napi_add_async_cleanup_hook @@ -5992,6 +6007,7 @@ This API may only be called from the main thread. [`napi_add_finalizer`]: #n_api_napi_add_finalizer [`napi_async_cleanup_hook`]: #n_api_napi_async_cleanup_hook [`napi_async_complete_callback`]: #n_api_napi_async_complete_callback +[`napi_async_destroy`]: #n_api_napi_async_destroy [`napi_async_init`]: #n_api_napi_async_init [`napi_callback`]: #n_api_napi_callback [`napi_cancel_async_work`]: #n_api_napi_cancel_async_work diff --git a/src/node_api.cc b/src/node_api.cc index 12f369a809734f..4e932c19c2bf8a 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -1,12 +1,15 @@ +#include "async_wrap-inl.h" #include "env-inl.h" #define NAPI_EXPERIMENTAL #include "js_native_api_v8.h" +#include "memory_tracker-inl.h" #include "node_api.h" #include "node_binding.h" #include "node_buffer.h" #include "node_errors.h" #include "node_internals.h" #include "threadpoolwork-inl.h" +#include "tracing/traced_value.h" #include "util-inl.h" #include <memory> @@ -104,16 +107,6 @@ static inline napi_env NewEnv(v8::Local<v8::Context> context) { return result; } -static inline napi_callback_scope -JsCallbackScopeFromV8CallbackScope(node::CallbackScope* s) { - return reinterpret_cast<napi_callback_scope>(s); -} - -static inline node::CallbackScope* -V8CallbackScopeFromJsCallbackScope(napi_callback_scope s) { - return reinterpret_cast<node::CallbackScope*>(s); -} - static inline void trigger_fatal_exception( napi_env env, v8::Local<v8::Value> local_err) { v8::Local<v8::Message> local_msg = @@ -435,6 +428,111 @@ class ThreadSafeFunction : public node::AsyncResource { bool handles_closing; }; +/** + * Compared to node::AsyncResource, the resource object in AsyncContext is + * gc-able. AsyncContext holds a weak reference to the resource object. + * AsyncContext::MakeCallback doesn't implicitly set the receiver of the + * callback to the resource object. + */ +class AsyncContext { + public: + AsyncContext(node_napi_env env, + v8::Local<v8::Object> resource_object, + const v8::Local<v8::String> resource_name, + bool externally_managed_resource) + : env_(env) { + async_id_ = node_env()->new_async_id(); + trigger_async_id_ = node_env()->get_default_trigger_async_id(); + resource_.Reset(node_env()->isolate(), resource_object); + lost_reference_ = false; + if (externally_managed_resource) { + resource_.SetWeak( + this, AsyncContext::WeakCallback, v8::WeakCallbackType::kParameter); + } + + node::AsyncWrap::EmitAsyncInit(node_env(), + resource_object, + resource_name, + async_id_, + trigger_async_id_); + } + + ~AsyncContext() { + resource_.Reset(); + lost_reference_ = true; + node::AsyncWrap::EmitDestroy(node_env(), async_id_); + } + + inline v8::MaybeLocal<v8::Value> MakeCallback( + v8::Local<v8::Object> recv, + const v8::Local<v8::Function> callback, + int argc, + v8::Local<v8::Value> argv[]) { + EnsureReference(); + return node::InternalMakeCallback(node_env(), + resource(), + recv, + callback, + argc, + argv, + {async_id_, trigger_async_id_}); + } + + inline napi_callback_scope OpenCallbackScope() { + EnsureReference(); + napi_callback_scope it = + reinterpret_cast<napi_callback_scope>(new CallbackScope(this)); + env_->open_callback_scopes++; + return it; + } + + inline void EnsureReference() { + if (lost_reference_) { + const v8::HandleScope handle_scope(node_env()->isolate()); + resource_.Reset(node_env()->isolate(), + v8::Object::New(node_env()->isolate())); + lost_reference_ = false; + } + } + + inline node::Environment* node_env() { return env_->node_env(); } + inline v8::Local<v8::Object> resource() { + return resource_.Get(node_env()->isolate()); + } + inline node::async_context async_context() { + return {async_id_, trigger_async_id_}; + } + + static inline void CloseCallbackScope(node_napi_env env, + napi_callback_scope s) { + CallbackScope* callback_scope = reinterpret_cast<CallbackScope*>(s); + delete callback_scope; + env->open_callback_scopes--; + } + + static void WeakCallback(const v8::WeakCallbackInfo<AsyncContext>& data) { + AsyncContext* async_context = data.GetParameter(); + async_context->resource_.Reset(); + async_context->lost_reference_ = true; + } + + private: + class CallbackScope : public node::CallbackScope { + public: + explicit CallbackScope(AsyncContext* async_context) + : node::CallbackScope(async_context->node_env()->isolate(), + async_context->resource_.Get( + async_context->node_env()->isolate()), + async_context->async_context()) {} + }; + + node_napi_env env_; + double async_id_; + double trigger_async_id_; + v8::Global<v8::Object> resource_; + bool lost_reference_; +}; + } // end of anonymous namespace } // end of namespace v8impl @@ -627,7 +725,7 @@ NAPI_NO_RETURN void napi_fatal_error(const char* location, } napi_status napi_open_callback_scope(napi_env env, - napi_value resource_object, + napi_value /** ignored */, napi_async_context async_context_handle, napi_callback_scope* result) { // Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw @@ -635,20 +733,11 @@ napi_status napi_open_callback_scope(napi_env env, CHECK_ENV(env); CHECK_ARG(env, result); - v8::Local<v8::Context> context = env->context(); - - node::async_context* node_async_context = - reinterpret_cast<node::async_context*>(async_context_handle); - - v8::Local<v8::Object> resource; - CHECK_TO_OBJECT(env, context, resource, resource_object); + v8impl::AsyncContext* node_async_context = + reinterpret_cast<v8impl::AsyncContext*>(async_context_handle); - *result = v8impl::JsCallbackScopeFromV8CallbackScope( - new node::CallbackScope(env->isolate, - resource, - *node_async_context)); + *result = node_async_context->OpenCallbackScope(); - env->open_callback_scopes++; return napi_clear_last_error(env); } @@ -661,8 +750,9 @@ napi_status napi_close_callback_scope(napi_env env, napi_callback_scope scope) { return napi_callback_scope_mismatch; } - env->open_callback_scopes--; - delete v8impl::V8CallbackScopeFromJsCallbackScope(scope); + v8impl::AsyncContext::CloseCallbackScope(reinterpret_cast<node_napi_env>(env), + scope); + return napi_clear_last_error(env); } @@ -678,20 +768,24 @@ napi_status napi_async_init(napi_env env, v8::Local<v8::Context> context = env->context(); v8::Local<v8::Object> v8_resource; + bool externally_managed_resource; if (async_resource != nullptr) { CHECK_TO_OBJECT(env, context, v8_resource, async_resource); + externally_managed_resource = true; } else { v8_resource = v8::Object::New(isolate); + externally_managed_resource = false; } v8::Local<v8::String> v8_resource_name; CHECK_TO_STRING(env, context, v8_resource_name, async_resource_name); - // TODO(jasongin): Consider avoiding allocation here by using - // a tagged pointer with 2×31 bit fields instead. - node::async_context* async_context = new node::async_context(); + auto async_context = + new v8impl::AsyncContext(reinterpret_cast<node_napi_env>(env), + v8_resource, + v8_resource_name, + externally_managed_resource); - *async_context = node::EmitAsyncInit(isolate, v8_resource, v8_resource_name); *result = reinterpret_cast<napi_async_context>(async_context); return napi_clear_last_error(env); @@ -702,11 +796,8 @@ napi_status napi_async_destroy(napi_env env, CHECK_ENV(env); CHECK_ARG(env, async_context); - node::async_context* node_async_context = - reinterpret_cast<node::async_context*>(async_context); - node::EmitAsyncDestroy( - reinterpret_cast<node_napi_env>(env)->node_env(), - *node_async_context); + v8impl::AsyncContext* node_async_context = + reinterpret_cast<v8impl::AsyncContext*>(async_context); delete node_async_context; @@ -734,17 +825,25 @@ napi_status napi_make_callback(napi_env env, v8::Local<v8::Function> v8func; CHECK_TO_FUNCTION(env, v8func, func); - node::async_context* node_async_context = - reinterpret_cast<node::async_context*>(async_context); - if (node_async_context == nullptr) { - static node::async_context empty_context = { 0, 0 }; - node_async_context = &empty_context; - } + v8::MaybeLocal<v8::Value> callback_result; - v8::MaybeLocal<v8::Value> callback_result = node::MakeCallback( - env->isolate, v8recv, v8func, argc, - reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)), - *node_async_context); + if (async_context == nullptr) { + callback_result = node::MakeCallback( + env->isolate, + v8recv, + v8func, + argc, + reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)), + {0, 0}); + } else { + auto node_async_context = + reinterpret_cast<v8impl::AsyncContext*>(async_context); + callback_result = node_async_context->MakeCallback( + v8recv, + v8func, + argc, + reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv))); + } if (try_catch.HasCaught()) { return napi_set_last_error(env, napi_pending_exception); diff --git a/test/node-api/test_async_context/binding.c b/test/node-api/test_async_context/binding.c new file mode 100644 index 00000000000000..749bb05d2503e7 --- /dev/null +++ b/test/node-api/test_async_context/binding.c @@ -0,0 +1,128 @@ +#include <node_api.h> +#include <assert.h> +#include "../../js-native-api/common.h" + +#define MAX_ARGUMENTS 10 +#define RESERVED_ARGS 3 + +static napi_value MakeCallback(napi_env env, napi_callback_info info) { + size_t argc = MAX_ARGUMENTS; + size_t n; + napi_value args[MAX_ARGUMENTS]; + // NOLINTNEXTLINE (readability/null_usage) + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NAPI_ASSERT(env, argc > 0, "Wrong number of arguments"); + + napi_value async_context_wrap = args[0]; + napi_value recv = args[1]; + napi_value func = args[2]; + + napi_value argv[MAX_ARGUMENTS - RESERVED_ARGS]; + for (n = RESERVED_ARGS; n < argc; n += 1) { + argv[n - RESERVED_ARGS] = args[n]; + } + + napi_valuetype func_type; + NAPI_CALL(env, napi_typeof(env, func, &func_type)); + + napi_async_context context; + NAPI_CALL(env, napi_unwrap(env, async_context_wrap, &context)); + + napi_value result; + if (func_type == napi_function) { + NAPI_CALL(env, napi_make_callback( + env, context, recv, func, argc - RESERVED_ARGS, argv, &result)); + } else { + NAPI_ASSERT(env, false, "Unexpected argument type"); + } + + return result; +} + +static void AsyncDestroyCb(napi_env env, void* data, void* hint) { + napi_status status = napi_async_destroy(env, (napi_async_context)data); + // We cannot use NAPI_ASSERT_RETURN_VOID because we need to have a JS stack + // below in order to use exceptions. + assert(status == napi_ok); +} + +#define CREATE_ASYNC_RESOURCE_ARGC 2 + +static napi_value CreateAsyncResource(napi_env env, napi_callback_info info) { + napi_value async_context_wrap; + NAPI_CALL(env, napi_create_object(env, &async_context_wrap)); + + size_t argc = CREATE_ASYNC_RESOURCE_ARGC; + napi_value args[CREATE_ASYNC_RESOURCE_ARGC]; + // NOLINTNEXTLINE (readability/null_usage) + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value resource = args[0]; + napi_value js_destroy_on_finalizer = args[1]; + napi_valuetype resource_type; + NAPI_CALL(env, napi_typeof(env, resource, &resource_type)); + if (resource_type != napi_object) { + resource = NULL; + } + + napi_value resource_name; + NAPI_CALL(env, napi_create_string_utf8( + env, "test_async", NAPI_AUTO_LENGTH, &resource_name)); + + napi_async_context context; + NAPI_CALL(env, napi_async_init(env, resource, resource_name, &context)); + + bool destroy_on_finalizer = true; + if (argc == 2) { + NAPI_CALL(env, napi_get_value_bool(env, js_destroy_on_finalizer, &destroy_on_finalizer)); + } + if (resource_type == napi_object && destroy_on_finalizer) { + NAPI_CALL(env, napi_add_finalizer( + env, resource, (void*)context, AsyncDestroyCb, NULL, NULL)); + } + NAPI_CALL(env, napi_wrap(env, async_context_wrap, context, NULL, NULL, NULL)); + return async_context_wrap; +} + +#define DESTROY_ASYNC_RESOURCE_ARGC 1 + +static napi_value DestroyAsyncResource(napi_env env, napi_callback_info info) { + size_t argc = DESTROY_ASYNC_RESOURCE_ARGC; + napi_value args[DESTROY_ASYNC_RESOURCE_ARGC]; + // NOLINTNEXTLINE (readability/null_usage) + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NAPI_ASSERT(env, argc == 1, "Wrong number of arguments"); + + napi_value async_context_wrap = args[0]; + + napi_async_context async_context; + NAPI_CALL(env, napi_remove_wrap(env, async_context_wrap, &async_context)); + NAPI_CALL(env, napi_async_destroy(env, async_context)); + + return async_context_wrap; +} + +static +napi_value Init(napi_env env, napi_value exports) { + napi_value fn; + NAPI_CALL(env, napi_create_function( + // NOLINTNEXTLINE (readability/null_usage) + env, NULL, NAPI_AUTO_LENGTH, MakeCallback, NULL, &fn)); + NAPI_CALL(env, napi_set_named_property(env, exports, "makeCallback", fn)); + NAPI_CALL(env, napi_create_function( + // NOLINTNEXTLINE (readability/null_usage) + env, NULL, NAPI_AUTO_LENGTH, CreateAsyncResource, NULL, &fn)); + NAPI_CALL(env, napi_set_named_property( + env, exports, "createAsyncResource", fn)); + + NAPI_CALL(env, napi_create_function( + // NOLINTNEXTLINE (readability/null_usage) + env, NULL, NAPI_AUTO_LENGTH, DestroyAsyncResource, NULL, &fn)); + NAPI_CALL(env, napi_set_named_property( + env, exports, "destroyAsyncResource", fn)); + + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/node-api/test_async_context/binding.gyp b/test/node-api/test_async_context/binding.gyp new file mode 100644 index 00000000000000..23daf507916ff6 --- /dev/null +++ b/test/node-api/test_async_context/binding.gyp @@ -0,0 +1,9 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'defines': [ 'V8_DEPRECATION_WARNINGS=1' ], + 'sources': [ 'binding.c' ] + } + ] +} diff --git a/test/node-api/test_async_context/test-gcable-callback.js b/test/node-api/test_async_context/test-gcable-callback.js new file mode 100644 index 00000000000000..e66080bb6a3a90 --- /dev/null +++ b/test/node-api/test_async_context/test-gcable-callback.js @@ -0,0 +1,65 @@ +'use strict'; +// Flags: --gc-interval=100 --gc-global + +const common = require('../../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const { + createAsyncResource, + destroyAsyncResource, + makeCallback, +} = require(`./build/${common.buildType}/binding`); + +// Test for https://github.com/nodejs/node/issues/27218: +// napi_async_destroy() can be called during a regular garbage collection run. + +const hook_result = { + id: null, + init_called: false, + destroy_called: false, +}; + +const test_hook = async_hooks.createHook({ + init: (id, type) => { + if (type === 'test_async') { + hook_result.id = id; + hook_result.init_called = true; + } + }, + destroy: (id) => { + if (id === hook_result.id) hook_result.destroy_called = true; + }, +}); + +test_hook.enable(); +const asyncResource = createAsyncResource( + { foo: 'bar' }, + /* destroy_on_finalizer */false +); + +// Trigger GC. This does *not* use global.gc(), because what we want to verify +// is that `napi_async_destroy()` can be called when there is no JS context +// on the stack at the time of GC. +// Currently, using --gc-interval=100 + 1M elements seems to work fine for this. +const arr = new Array(1024 * 1024); +for (let i = 0; i < arr.length; i++) + arr[i] = {}; + +assert.strictEqual(hook_result.destroy_called, false); +setImmediate(() => { + assert.strictEqual(hook_result.destroy_called, false); + makeCallback(asyncResource, process, () => { + const executionAsyncResource = async_hooks.executionAsyncResource(); + // Assuming the executionAsyncResource was created for the absence of the + // initial `{ foo: 'bar' }`. + // This is the worst path of `napi_async_context` related API of + // recovering from the condition and not break the executionAsyncResource + // shape, although the executionAsyncResource might not be correct. + assert.strictEqual(typeof executionAsyncResource, 'object'); + assert.strictEqual(executionAsyncResource.foo, undefined); + destroyAsyncResource(asyncResource); + setImmediate(() => { + assert.strictEqual(hook_result.destroy_called, true); + }); + }); +}); diff --git a/test/node-api/test_make_callback/test-async-hooks-gcable.js b/test/node-api/test_async_context/test-gcable.js similarity index 95% rename from test/node-api/test_make_callback/test-async-hooks-gcable.js rename to test/node-api/test_async_context/test-gcable.js index a9b4d3d75d6040..288b7412267dcc 100644 --- a/test/node-api/test_make_callback/test-async-hooks-gcable.js +++ b/test/node-api/test_async_context/test-gcable.js @@ -17,7 +17,7 @@ const hook_result = { const test_hook = async_hooks.createHook({ init: (id, type) => { - if (type === 'test_gcable') { + if (type === 'test_async') { hook_result.id = id; hook_result.init_called = true; } @@ -28,7 +28,7 @@ const test_hook = async_hooks.createHook({ }); test_hook.enable(); -createAsyncResource(); +createAsyncResource({}); // Trigger GC. This does *not* use global.gc(), because what we want to verify // is that `napi_async_destroy()` can be called when there is no JS context diff --git a/test/node-api/test_async_context/test.js b/test/node-api/test_async_context/test.js new file mode 100644 index 00000000000000..2cf00b1ef7bb0e --- /dev/null +++ b/test/node-api/test_async_context/test.js @@ -0,0 +1,63 @@ +'use strict'; +// Flags: --gc-interval=100 --gc-global + +const common = require('../../common'); +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const { + makeCallback, + createAsyncResource, + destroyAsyncResource, +} = require(`./build/${common.buildType}/binding`); + +const hook_result = { + id: null, + resource: null, + init_called: false, + destroy_called: false, +}; + +const test_hook = async_hooks.createHook({ + init: (id, type, triggerAsyncId, resource) => { + if (type === 'test_async') { + hook_result.id = id; + hook_result.init_called = true; + hook_result.resource = resource; + } + }, + destroy: (id) => { + if (id === hook_result.id) hook_result.destroy_called = true; + }, +}); + +test_hook.enable(); +const resourceWrap = createAsyncResource( + /** + * set resource to NULL to generate a managed resource object + */ + undefined +); + +assert.strictEqual(hook_result.destroy_called, false); +const recv = {}; +makeCallback(resourceWrap, recv, function callback() { + assert.strictEqual(hook_result.destroy_called, false); + assert.strictEqual( + hook_result.resource, + async_hooks.executionAsyncResource() + ); + assert.strictEqual(this, recv); + + setImmediate(() => { + assert.strictEqual(hook_result.destroy_called, false); + assert.notStrictEqual( + hook_result.resource, + async_hooks.executionAsyncResource() + ); + + destroyAsyncResource(resourceWrap); + setImmediate(() => { + assert.strictEqual(hook_result.destroy_called, true); + }); + }); +}); diff --git a/test/node-api/test_make_callback/binding.c b/test/node-api/test_make_callback/binding.c index 782cf0f6fb8f54..214e0a4e182385 100644 --- a/test/node-api/test_make_callback/binding.c +++ b/test/node-api/test_make_callback/binding.c @@ -3,6 +3,7 @@ #include "../../js-native-api/common.h" #define MAX_ARGUMENTS 10 +#define RESERVED_ARGS 3 static napi_value MakeCallback(napi_env env, napi_callback_info info) { size_t argc = MAX_ARGUMENTS; @@ -13,12 +14,13 @@ static napi_value MakeCallback(napi_env env, napi_callback_info info) { NAPI_ASSERT(env, argc > 0, "Wrong number of arguments"); - napi_value recv = args[0]; - napi_value func = args[1]; + napi_value resource = args[0]; + napi_value recv = args[1]; + napi_value func = args[2]; - napi_value argv[MAX_ARGUMENTS - 2]; - for (n = 2; n < argc; n += 1) { - argv[n - 2] = args[n]; + napi_value argv[MAX_ARGUMENTS - RESERVED_ARGS]; + for (n = RESERVED_ARGS; n < argc; n += 1) { + argv[n - RESERVED_ARGS] = args[n]; } napi_valuetype func_type; @@ -30,12 +32,12 @@ static napi_value MakeCallback(napi_env env, napi_callback_info info) { env, "test", NAPI_AUTO_LENGTH, &resource_name)); napi_async_context context; - NAPI_CALL(env, napi_async_init(env, func, resource_name, &context)); + NAPI_CALL(env, napi_async_init(env, resource, resource_name, &context)); napi_value result; if (func_type == napi_function) { NAPI_CALL(env, napi_make_callback( - env, context, recv, func, argc - 2, argv, &result)); + env, context, recv, func, argc - RESERVED_ARGS, argv, &result)); } else { NAPI_ASSERT(env, false, "Unexpected argument type"); } @@ -45,30 +47,6 @@ static napi_value MakeCallback(napi_env env, napi_callback_info info) { return result; } -static void AsyncDestroyCb(napi_env env, void* data, void* hint) { - napi_status status = napi_async_destroy(env, (napi_async_context)data); - // We cannot use NAPI_ASSERT_RETURN_VOID because we need to have a JS stack - // below in order to use exceptions. - assert(status == napi_ok); -} - -static napi_value CreateAsyncResource(napi_env env, napi_callback_info info) { - napi_value object; - NAPI_CALL(env, napi_create_object(env, &object)); - - napi_value resource_name; - NAPI_CALL(env, napi_create_string_utf8( - env, "test_gcable", NAPI_AUTO_LENGTH, &resource_name)); - - napi_async_context context; - NAPI_CALL(env, napi_async_init(env, object, resource_name, &context)); - - NAPI_CALL(env, napi_add_finalizer( - env, object, (void*)context, AsyncDestroyCb, NULL, NULL)); - - return object; -} - static napi_value Init(napi_env env, napi_value exports) { napi_value fn; @@ -76,11 +54,6 @@ napi_value Init(napi_env env, napi_value exports) { // NOLINTNEXTLINE (readability/null_usage) env, NULL, NAPI_AUTO_LENGTH, MakeCallback, NULL, &fn)); NAPI_CALL(env, napi_set_named_property(env, exports, "makeCallback", fn)); - NAPI_CALL(env, napi_create_function( - // NOLINTNEXTLINE (readability/null_usage) - env, NULL, NAPI_AUTO_LENGTH, CreateAsyncResource, NULL, &fn)); - NAPI_CALL(env, napi_set_named_property( - env, exports, "createAsyncResource", fn)); return exports; } diff --git a/test/node-api/test_make_callback/test-async-hooks.js b/test/node-api/test_make_callback/test-async-hooks.js index 755a2389c68591..ca31ac4995ae5b 100644 --- a/test/node-api/test_make_callback/test-async-hooks.js +++ b/test/node-api/test_make_callback/test-async-hooks.js @@ -33,7 +33,16 @@ const test_hook = async_hooks.createHook({ }); test_hook.enable(); -makeCallback(process, function() {}); + +/** + * Resource should be able to be arbitrary objects without special internal + * slots. Testing with plain object here. + */ +const resource = {}; +makeCallback(resource, process, function cb() { + assert.strictEqual(this, process); + assert.strictEqual(async_hooks.executionAsyncResource(), resource); +}); assert.strictEqual(hook_result.init_called, true); assert.strictEqual(hook_result.before_called, true); diff --git a/test/node-api/test_make_callback/test.js b/test/node-api/test_make_callback/test.js index dba550a492f0a3..d0b4b22500c9f6 100644 --- a/test/node-api/test_make_callback/test.js +++ b/test/node-api/test_make_callback/test.js @@ -13,20 +13,25 @@ function myMultiArgFunc(arg1, arg2, arg3) { return 42; } -assert.strictEqual(makeCallback(process, common.mustCall(function() { +/** + * Resource should be able to be arbitrary objects without special internal + * slots. Testing with plain object here. + */ +const resource = {}; +assert.strictEqual(makeCallback(resource, process, common.mustCall(function() { assert.strictEqual(arguments.length, 0); assert.strictEqual(this, process); return 42; })), 42); -assert.strictEqual(makeCallback(process, common.mustCall(function(x) { +assert.strictEqual(makeCallback(resource, process, common.mustCall(function(x) { assert.strictEqual(arguments.length, 1); assert.strictEqual(this, process); assert.strictEqual(x, 1337); return 42; }), 1337), 42); -assert.strictEqual(makeCallback(this, +assert.strictEqual(makeCallback(resource, this, common.mustCall(myMultiArgFunc), 1, 2, 3), 42); // TODO(node-api): napi_make_callback needs to support @@ -62,7 +67,7 @@ const target = vm.runInNewContext(` return Object; }) `); -assert.notStrictEqual(makeCallback(process, target, Object), Object); +assert.notStrictEqual(makeCallback(resource, process, target, Object), Object); // Runs in inner context. const forward = vm.runInNewContext(` @@ -78,4 +83,4 @@ function endpoint($Object) { return Object; } -assert.strictEqual(makeCallback(process, forward, endpoint), Object); +assert.strictEqual(makeCallback(resource, process, forward, endpoint), Object);