From 0b985bf9c0cf65f75643b4f6368dc4bb79e097cf Mon Sep 17 00:00:00 2001 From: toyobayashi Date: Mon, 26 Dec 2022 18:51:26 +0800 Subject: [PATCH] refactor: generalize finalizer second pass callback (nodejs/node#44141) --- packages/emnapi/src/emnapi.c | 9 +- packages/emnapi/src/env.ts | 4 +- packages/emnapi/src/internal.ts | 11 ++- packages/emnapi/src/life.ts | 4 +- packages/emnapi/src/simple-async-operation.ts | 32 ++++--- packages/emnapi/src/thread-safe-function.ts | 17 +--- packages/emnapi/src/value/create.ts | 2 +- packages/runtime/src/Finalizer.ts | 31 +++---- packages/runtime/src/Global.ts | 77 ++++++++++++++++ packages/runtime/src/RefBase.ts | 53 +++++------ packages/runtime/src/RefTracker.ts | 4 +- packages/runtime/src/Reference.ts | 92 ++++++++----------- packages/runtime/src/env.ts | 35 ++++++- packages/runtime/src/index.ts | 8 +- packages/runtime/src/util.ts | 13 +++ packages/test/objwrap/myobject.cc | 61 ++++++++++++ packages/test/objwrap/objwrapref.test.js | 16 ++++ 17 files changed, 322 insertions(+), 147 deletions(-) create mode 100644 packages/runtime/src/Global.ts create mode 100644 packages/test/objwrap/objwrapref.test.js diff --git a/packages/emnapi/src/emnapi.c b/packages/emnapi/src/emnapi.c index 804beb8f..e66ab22a 100644 --- a/packages/emnapi/src/emnapi.c +++ b/packages/emnapi/src/emnapi.c @@ -363,6 +363,8 @@ napi_status napi_cancel_async_work(napi_env env, napi_async_work work) { #if NAPI_VERSION >= 4 && defined(__EMSCRIPTEN_PTHREADS__) +extern void _emnapi_call_finalizer(napi_env env, napi_finalize cb, void* data, void* hint); + static const unsigned char kDispatchIdle = 0; static const unsigned char kDispatchRunning = 1 << 0; static const unsigned char kDispatchPending = 1 << 1; @@ -538,16 +540,11 @@ static void _emnapi_tsfn_empty_queue_and_delete(napi_threadsafe_function func) { _emnapi_tsfn_destroy(func); } -static void _emnapi_tsfn_call_finalize_cb(napi_env env, void* args) { - napi_threadsafe_function func = (napi_threadsafe_function) args; - func->finalize_cb(env, func->finalize_data, func->context); -} - static void _emnapi_tsfn_finalize(napi_threadsafe_function func) { napi_handle_scope scope; napi_open_handle_scope(func->env, &scope); if (func->finalize_cb) { - _emnapi_call_into_module(func->env, _emnapi_tsfn_call_finalize_cb, func); + _emnapi_call_finalizer(func->env, func->finalize_cb, func->finalize_data, func->context); } _emnapi_tsfn_empty_queue_and_delete(func); napi_close_handle_scope(func->env, scope); diff --git a/packages/emnapi/src/env.ts b/packages/emnapi/src/env.ts index 5126db91..e5bbd7c0 100644 --- a/packages/emnapi/src/env.ts +++ b/packages/emnapi/src/env.ts @@ -1,9 +1,9 @@ function napi_set_instance_data (env: napi_env, data: void_p, finalize_cb: napi_finalize, finalize_hint: void_p): napi_status { return emnapiCtx.checkEnv(env, (envObject) => { if (envObject.instanceData) { - emnapiRt.RefBase.doDelete(envObject.instanceData) + envObject.instanceData.dispose() } - envObject.instanceData = new emnapiRt.RefBase(envObject, 0, true, finalize_cb, data, finalize_hint) + envObject.instanceData = new emnapiRt.RefBase(envObject, 0, emnapiRt.Ownership.kRuntime, finalize_cb, data, finalize_hint) return envObject.clearLastError() }) } diff --git a/packages/emnapi/src/internal.ts b/packages/emnapi/src/internal.ts index 8e3b206a..4dd9fd69 100644 --- a/packages/emnapi/src/internal.ts +++ b/packages/emnapi/src/internal.ts @@ -158,12 +158,12 @@ function $emnapiWrap (type: WrapType, env: napi_env, js_object: napi_value, nati let reference: emnapi.Reference if (result) { if (!finalize_cb) return envObject.setLastError(napi_status.napi_invalid_arg) - reference = emnapiRt.Reference.create(envObject, value.id, 0, false, finalize_cb, native_object, finalize_hint) + reference = emnapiRt.Reference.create(envObject, value.id, 0, emnapiRt.Ownership.kUserland, finalize_cb, native_object, finalize_hint) $from64('result') $makeSetValue('result', 0, 'reference.id', '*') } else { - reference = emnapiRt.Reference.create(envObject, value.id, 0, true, finalize_cb, native_object, !finalize_cb ? finalize_cb : finalize_hint) + reference = emnapiRt.Reference.create(envObject, value.id, 0, emnapiRt.Ownership.kRuntime, finalize_cb, native_object, !finalize_cb ? finalize_cb : finalize_hint) } if (type === WrapType.retrievable) { @@ -199,7 +199,12 @@ function $emnapiUnwrap (env: napi_env, js_object: napi_value, result: void_pp, a } if (action === UnwrapAction.RemoveWrap) { value.wrapped = 0 - emnapiRt.Reference.doDelete(ref) + if (ref.ownership() === emnapiRt.Ownership.kUserland) { + // When the wrap is been removed, the finalizer should be reset. + ref.resetFinalizer() + } else { + ref.dispose() + } } return envObject.getReturnStatus() }) diff --git a/packages/emnapi/src/life.ts b/packages/emnapi/src/life.ts index d2b342d1..b5d66099 100644 --- a/packages/emnapi/src/life.ts +++ b/packages/emnapi/src/life.ts @@ -90,7 +90,7 @@ function napi_create_reference ( } // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unused-vars - const ref = emnapiRt.Reference.create(envObject, handle.id, initial_refcount >>> 0, false) + const ref = emnapiRt.Reference.create(envObject, handle.id, initial_refcount >>> 0, emnapiRt.Ownership.kUserland) $from64('result') $makeSetValue('result', 0, 'ref.id', '*') return envObject.clearLastError() @@ -104,7 +104,7 @@ function napi_delete_reference ( ): napi_status { return emnapiCtx.checkEnv(env, (envObject) => { return emnapiCtx.checkArgs(envObject, [ref], () => { - emnapiRt.Reference.doDelete(emnapiCtx.refStore.get(ref)!) + emnapiCtx.refStore.get(ref)!.dispose() return envObject.clearLastError() }) }) diff --git a/packages/emnapi/src/simple-async-operation.ts b/packages/emnapi/src/simple-async-operation.ts index ecdc16a0..d471bd22 100644 --- a/packages/emnapi/src/simple-async-operation.ts +++ b/packages/emnapi/src/simple-async-operation.ts @@ -6,23 +6,12 @@ // declare function __emnapi_async_send_js (callback: number, data: number): void declare function __emnapi_set_immediate (callback: number, data: number): void declare function __emnapi_next_tick (callback: number, data: number): void -declare function emnapiSetImmediate (callback: () => any): any mergeInto(LibraryManager.library, { - $emnapiSetImmediate: 'typeof setImmediate === "function" ? setImmediate : function (f) {' + - 'var channel = new MessageChannel();' + - 'channel.port1.onmessage = function () {' + - 'channel.port1.onmessage = null;' + - 'channel = undefined;' + - 'f();' + - '};' + - 'channel.port2.postMessage(null);' + - '};', - _emnapi_set_immediate__sig: 'vpp', - _emnapi_set_immediate__deps: ['$emnapiGetDynamicCalls', '$emnapiSetImmediate'], + _emnapi_set_immediate__deps: ['$emnapiGetDynamicCalls', '$emnapiInit', '$emnapiRt'], _emnapi_set_immediate: function (callback: number, data: number): void { - emnapiSetImmediate(() => { + emnapiRt._setImmediate(() => { $from64('callback') emnapiGetDynamicCalls.call_vp(callback, data) }) @@ -99,3 +88,20 @@ mergeInto(LibraryManager.library, { 'return r;' + '};' }) + +function _emnapi_call_into_module (env: napi_env, callback: number, data: number): void { + const envObject = emnapiCtx.envStore.get(env)! + const scope = emnapiCtx.openScope(envObject, emnapiRt.HandleScope) + try { + envObject.callIntoModule((_envObject) => { + $from64('callback') + emnapiGetDynamicCalls.call_vpp(callback, env, data) + }) + } catch (err) { + emnapiCtx.closeScope(envObject, scope) + throw err + } + emnapiCtx.closeScope(envObject, scope) +} + +emnapiImplement('_emnapi_call_into_module', 'vppp', _emnapi_call_into_module, ['$emnapiGetDynamicCalls']) diff --git a/packages/emnapi/src/thread-safe-function.ts b/packages/emnapi/src/thread-safe-function.ts index 33144b89..4df92638 100644 --- a/packages/emnapi/src/thread-safe-function.ts +++ b/packages/emnapi/src/thread-safe-function.ts @@ -1,16 +1,7 @@ -function _emnapi_call_into_module (env: napi_env, callback: number, data: number): void { +function _emnapi_call_finalizer (env: napi_env, callback: number, data: number, hint: number): void { const envObject = emnapiCtx.envStore.get(env)! - const scope = emnapiCtx.openScope(envObject, emnapiRt.HandleScope) - try { - envObject.callIntoModule((_envObject) => { - $from64('callback') - emnapiGetDynamicCalls.call_vpp(callback, env, data) - }) - } catch (err) { - emnapiCtx.closeScope(envObject, scope) - throw err - } - emnapiCtx.closeScope(envObject, scope) + $from64('callback') + envObject.callFinalizer(callback, data, hint) } function _emnapi_tsfn_dispatch_one_js (env: number, ref: number, call_js_cb: number, context: number, data: number): void { @@ -31,5 +22,5 @@ function _emnapi_tsfn_dispatch_one_js (env: number, ref: number, call_js_cb: num emnapiCtx.closeScope(envObject, scope) } -emnapiImplement('_emnapi_call_into_module', 'vpp', _emnapi_call_into_module, ['$emnapiGetDynamicCalls']) +emnapiImplement('_emnapi_call_finalizer', 'vpppp', _emnapi_call_finalizer) emnapiImplement('_emnapi_tsfn_dispatch_one_js', 'vppppp', _emnapi_tsfn_dispatch_one_js, ['$emnapiGetDynamicCalls']) diff --git a/packages/emnapi/src/value/create.ts b/packages/emnapi/src/value/create.ts index 3765e0f5..4ea208ac 100644 --- a/packages/emnapi/src/value/create.ts +++ b/packages/emnapi/src/value/create.ts @@ -67,7 +67,7 @@ function napi_create_external (env: napi_env, data: void_p, finalize_cb: napi_fi const externalHandle = emnapiRt.ExternalHandle.createExternal(envObject, data) emnapiCtx.getCurrentScope()!.addHandle(externalHandle) if (finalize_cb) { - emnapiRt.Reference.create(envObject, externalHandle.id, 0, true, finalize_cb, data, finalize_hint) + emnapiRt.Reference.create(envObject, externalHandle.id, 0, emnapiRt.Ownership.kRuntime, finalize_cb, data, finalize_hint) } $from64('result') $makeSetValue('result', 0, 'externalHandle.id', '*') diff --git a/packages/runtime/src/Finalizer.ts b/packages/runtime/src/Finalizer.ts index dfd1893a..518563d0 100644 --- a/packages/runtime/src/Finalizer.ts +++ b/packages/runtime/src/Finalizer.ts @@ -1,34 +1,25 @@ import type { Env } from './env' -/** @internal */ -export enum EnvReferenceMode { - kNoEnvReference, - kKeepEnvReference -} - /** @internal */ export class Finalizer { - protected _hasEnvReference: boolean - protected _finalizeRan: boolean - public constructor ( protected envObject: Env, protected _finalizeCallback: napi_finalize = 0, protected _finalizeData: void_p = 0, - protected _finalizeHint: void_p = 0, - refmode: EnvReferenceMode = EnvReferenceMode.kNoEnvReference - ) { - this._finalizeRan = false - this._hasEnvReference = refmode === EnvReferenceMode.kKeepEnvReference - if (this._hasEnvReference) { - envObject.ref() - } + protected _finalizeHint: void_p = 0 + ) {} + + public callback (): napi_finalize { return this._finalizeCallback } + public data (): void_p { return this._finalizeData } + public hint (): void_p { return this._finalizeHint } + + public resetFinalizer (): void { + this._finalizeCallback = 0 + this._finalizeData = 0 + this._finalizeHint = 0 } public dispose (): void { - if (this._hasEnvReference) { - this.envObject.unref() - } this.envObject = undefined! } } diff --git a/packages/runtime/src/Global.ts b/packages/runtime/src/Global.ts new file mode 100644 index 00000000..fbdc9b08 --- /dev/null +++ b/packages/runtime/src/Global.ts @@ -0,0 +1,77 @@ +import { supportFinalizer } from './util' + +/** @public */ +export class Global { + private _ref: T | WeakRef | null + private _param: any + private _callback: ((param: any) => void) | undefined + + private static readonly _registry = supportFinalizer + ? new FinalizationRegistry((value: Global) => { + value._ref = null + if (typeof value._callback === 'function') { + value._callback(value._param) + } + }) + : undefined! + + constructor (value: T) { + this._ref = value + } + + setWeak

(param: any, callback: (param: P) => void): void { + if (this._ref === null) return + if (!supportFinalizer) return + if (this._ref instanceof WeakRef) return + this._param = param + this._callback = callback + Global._registry.register(this._ref, this, this) + this._ref = new WeakRef(this._ref) + } + + clearWeak (): void { + if (this._ref === null) return + if (!supportFinalizer) return + if (this._ref instanceof WeakRef) { + try { + Global._registry.unregister(this) + } catch (_) {} + this._param = undefined + this._callback = undefined + this._ref = this._ref.deref() as T + } + } + + reset (other?: T | WeakRef): void { + if (supportFinalizer) { + try { + Global._registry.unregister(this) + } catch (_) {} + } + this._param = undefined + this._callback = undefined + if (other) { + this._ref = other + } else { + this._ref = null + } + } + + isEmpty (): boolean { + return this._ref === null + } + + deref (): T | undefined { + if (!supportFinalizer) { + return (this._ref as T | null) ?? undefined + } + + if (this._ref === null) return undefined + + if (this._ref instanceof WeakRef) { + return this._ref.deref() + } + + return this._ref + } +} diff --git a/packages/runtime/src/RefBase.ts b/packages/runtime/src/RefBase.ts index 904faf1b..607cff4b 100644 --- a/packages/runtime/src/RefBase.ts +++ b/packages/runtime/src/RefBase.ts @@ -1,11 +1,16 @@ import type { Env } from './env' import { Finalizer } from './Finalizer' import { RefTracker } from './RefTracker' -import { supportFinalizer } from './util' /** @internal */ export interface RefBase extends Finalizer, RefTracker {} +/** @public */ +export enum Ownership { + kRuntime, + kUserland +} + /** @internal */ export class RefBase extends Finalizer { public static finalizeAll (list: RefTracker): void { @@ -21,12 +26,12 @@ export class RefBase extends Finalizer { } private _refcount: uint32_t - private _deleteSelf: boolean + private readonly _ownership: Ownership constructor ( envObject: Env, initial_refcount: uint32_t, - delete_self: boolean, + ownership: Ownership, finalize_callback: napi_finalize, finalize_data: void_p, finalize_hint: void_p @@ -35,13 +40,14 @@ export class RefBase extends Finalizer { ;(this as any)._next = null ;(this as any)._prev = null this._refcount = initial_refcount - this._deleteSelf = delete_self + this._ownership = ownership this.link(!finalize_callback ? envObject.reflist : envObject.finalizing_reflist) } public dispose (): void { this.unlink() + this.envObject.dequeueFinalizer(this) super.dispose() } @@ -64,39 +70,34 @@ export class RefBase extends Finalizer { return this._refcount } - public static doDelete (reference: RefBase): void { - if ((reference.refCount() !== 0) || (reference._deleteSelf) || - (reference._finalizeRan) || !supportFinalizer) { - reference.dispose() - } else { - // defer until finalizer runs as - // it may already be queued - reference._deleteSelf = true - } + public ownership (): Ownership { + return this._ownership } - protected finalize (isEnvTeardown = false): void { - if (isEnvTeardown && this.refCount() > 0) this._refcount = 0 + public finalize (): void { + const ownership = this._ownership + // Swap out the field finalize_callback so that it can not be accidentally + // called more than once. + const finalize_callback = this._finalizeCallback + const finalize_data = this._finalizeData + const finalize_hint = this._finalizeHint + this.resetFinalizer() + + this.unlink() let error: any let caught = false - if (this._finalizeCallback) { - const fini = Number(this._finalizeCallback) - this._finalizeCallback = 0 + if (finalize_callback) { + const fini = Number(finalize_callback) try { - this.envObject.callFinalizer(fini, this._finalizeData, this._finalizeHint) + this.envObject.callFinalizer(fini, finalize_data, finalize_hint) } catch (err) { caught = true error = err } } - if (this._deleteSelf || isEnvTeardown) { - RefBase.doDelete(this) - } else { - this._finalizeRan = true - // leak if this is a non-self-delete weak reference - // should call napi_delete_referece manually - // Reference.doDelete(this) + if (ownership === Ownership.kRuntime) { + this.dispose() } if (caught) { throw error diff --git a/packages/runtime/src/RefTracker.ts b/packages/runtime/src/RefTracker.ts index 6ad55d10..f067155d 100644 --- a/packages/runtime/src/RefTracker.ts +++ b/packages/runtime/src/RefTracker.ts @@ -1,7 +1,7 @@ /** @internal */ export class RefTracker { /** @virtual */ - protected finalize (_isEnvTeardown: boolean): void {} + public finalize (): void {} private _next: RefTracker | null = null private _prev: RefTracker | null = null @@ -28,7 +28,7 @@ export class RefTracker { public static finalizeAll (list: RefTracker): void { while (list._next !== null) { - list._next.finalize(true) + list._next.finalize() } } } diff --git a/packages/runtime/src/Reference.ts b/packages/runtime/src/Reference.ts index 3051829e..1a91ce55 100644 --- a/packages/runtime/src/Reference.ts +++ b/packages/runtime/src/Reference.ts @@ -2,70 +2,71 @@ import type { IStoreValue } from './Store' import { supportFinalizer, isReferenceType } from './util' import type { Env } from './env' import type { Handle } from './Handle' -import { RefBase } from './RefBase' +import { Ownership, RefBase } from './RefBase' +import { Global } from './Global' + +function weakCallback (ref: Reference): void { + ref.persistent!.reset() + ref.envObject.enqueueFinalizer(ref) +} /** @internal */ export class Reference extends RefBase implements IStoreValue { public id: number - private finalizerRegistered: boolean = false - - public static finalizationGroup: FinalizationRegistry | null = - supportFinalizer - ? new FinalizationRegistry((ref: Reference) => { - ref.finalize(false) - }) - : null - public static create ( envObject: Env, handle_id: napi_value, initialRefcount: uint32_t, - deleteSelf: boolean, + ownership: Ownership, finalize_callback: napi_finalize = 0, finalize_data: void_p = 0, finalize_hint: void_p = 0 ): Reference { const handle = envObject.ctx.handleStore.get(handle_id)! - const ref = new Reference(envObject, handle, initialRefcount, deleteSelf, finalize_callback, finalize_data, finalize_hint) + const ref = new Reference(envObject, handle, initialRefcount, ownership, finalize_callback, finalize_data, finalize_hint) envObject.ctx.refStore.add(ref) handle.addRef(ref) if (supportFinalizer && isReferenceType(handle.value)) { - ref.objWeakRef = new WeakRef(handle.value) + ref.persistent = new Global(handle.value) } else { - ref.objWeakRef = null + ref.persistent = null } if (initialRefcount === 0) { - ref._setWeak(handle.value) + ref._setWeak() } return ref } - public objWeakRef!: WeakRef | null + public persistent!: Global | null private constructor ( public envObject: Env, public handle: Handle, initialRefcount: uint32_t, - deleteSelf: boolean, + ownership: Ownership, finalize_callback: napi_finalize = 0, finalize_data: void_p = 0, finalize_hint: void_p = 0 ) { - super(envObject, initialRefcount >>> 0, deleteSelf, finalize_callback, finalize_data, finalize_hint) + super(envObject, initialRefcount >>> 0, ownership, finalize_callback, finalize_data, finalize_hint) this.id = 0 } public ref (): number { + if (this.persistent?.isEmpty()) { + return 0 + } + const count = super.ref() - if (count === 1 && this.objWeakRef) { - const obj = this.objWeakRef.deref() + if (count === 1 && this.persistent) { + const obj = this.persistent.deref() if (obj) { const handle = this.envObject.ensureHandle(obj) handle.addRef(this) - this._clearWeak() + this.persistent.clearWeak() if (handle !== this.handle) { this.handle.removeRef(this) this.handle = handle @@ -77,13 +78,17 @@ export class Reference extends RefBase implements IStoreValue { } public unref (): number { + if (this.persistent?.isEmpty()) { + return 0 + } + const oldRefcount = this.refCount() const refcount = super.unref() if (oldRefcount === 1 && refcount === 0) { - if (this.objWeakRef) { - const obj = this.objWeakRef.deref() + if (this.persistent) { + const obj = this.persistent.deref() if (obj) { - this._setWeak(obj) + this._setWeak() } } this.handle.tryDispose() @@ -92,8 +97,8 @@ export class Reference extends RefBase implements IStoreValue { } public get (): napi_value { - if (this.objWeakRef) { - const obj = this.objWeakRef.deref() + if (this.persistent) { + const obj = this.persistent.deref() if (obj) { const handle = this.envObject.ensureHandle(obj) handle.addRef(this) @@ -111,43 +116,20 @@ export class Reference extends RefBase implements IStoreValue { return 0 } - private _setWeak (value: object): void { - if (!supportFinalizer || this.finalizerRegistered) return - Reference.finalizationGroup!.register(value, this, this) - this.finalizerRegistered = true - } - - private _clearWeak (): void { - if (!supportFinalizer || !this.finalizerRegistered) return - try { - this.finalizerRegistered = false - Reference.finalizationGroup!.unregister(this) - } catch (_) {} + private _setWeak (): void { + this.persistent?.setWeak(this, weakCallback) } - /* public queueFinalizer (value?: object): void { - if (!Reference.finalizationGroup) return - if (this.finalizerRegistered) return - if (!value) { - value = this.objWeakRef!.deref()! - } - Reference.finalizationGroup.register(value, this, this) - this.finalizerRegistered = true - } */ - - public override finalize (isEnvTeardown = false): void { - if (isEnvTeardown) { - this._clearWeak() - } - - super.finalize(isEnvTeardown) + public override finalize (): void { + this.persistent?.reset() + super.finalize() } public override dispose (): void { if (this.id === 0) return + this.persistent?.reset() this.envObject.ctx.refStore.remove(this.id) this.handle.removeRef(this) - this._clearWeak() this.handle = undefined! super.dispose() this.id = 0 diff --git a/packages/runtime/src/env.ts b/packages/runtime/src/env.ts index 4c0f7e13..e9428b77 100644 --- a/packages/runtime/src/env.ts +++ b/packages/runtime/src/env.ts @@ -1,7 +1,7 @@ import type { Handle } from './Handle' import type { Context } from './Context' import { IStoreValue, Store } from './Store' -import { TryCatch, isReferenceType } from './util' +import { TryCatch, isReferenceType, _setImmediate } from './util' import { RefTracker } from './RefTracker' import { HandleScope } from './HandleScope' import { RefBase } from './RefBase' @@ -30,6 +30,9 @@ export class Env implements IStoreValue { public reflist = new RefTracker() public finalizing_reflist = new RefTracker() + public finalizationScheduled: boolean = false + public pendingFinalizers: RefTracker[] = [] + public static create ( ctx: Context, emnapiGetDynamicCalls: IDynamicCalls, @@ -129,7 +132,37 @@ export class Env implements IStoreValue { this.ctx.closeScope(this, scope) } + public enqueueFinalizer (finalizer: RefTracker): void { + if (this.pendingFinalizers.indexOf(finalizer) === -1) { + this.pendingFinalizers.push(finalizer) + } + if (!this.finalizationScheduled) { + this.finalizationScheduled = true + this.ref() + _setImmediate(() => { + this.finalizationScheduled = false + this.unref() + this.drainFinalizerQueue() + }) + } + } + + public dequeueFinalizer (finalizer: RefTracker): void { + const index = this.pendingFinalizers.indexOf(finalizer) + if (index !== -1) { + this.pendingFinalizers.splice(index, 1) + } + } + + public drainFinalizerQueue (): void { + while (this.pendingFinalizers.length > 0) { + const refTracker = this.pendingFinalizers.shift()! + refTracker.finalize() + } + } + public dispose (): void { + this.drainFinalizerQueue() // this.scopeList.clear() RefBase.finalizeAll(this.finalizing_reflist) RefBase.finalizeAll(this.reflist) diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 22f7dafb..e51ddd19 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -10,10 +10,11 @@ export { DeferredStore } from './DeferredStore' export { ILastError, Env } from './env' export { EnvStore } from './EnvStore' export { EmnapiError, NotSupportWeakRefError, NotSupportBigIntError } from './errors' -export { EnvReferenceMode, Finalizer } from './Finalizer' +export { Finalizer } from './Finalizer' export { Handle, ExternalHandle, HandleStore } from './Handle' export { IHandleScope, HandleScope, EscapableHandleScope } from './HandleScope' -export { RefBase } from './RefBase' +export { RefBase, Ownership } from './RefBase' +export { Global } from './Global' export { Reference } from './Reference' export { RefStore } from './RefStore' export { RefTracker } from './RefTracker' @@ -27,7 +28,8 @@ export { supportNewFunction, canSetFunctionName, isReferenceType, - TryCatch + TryCatch, + _setImmediate } from './util' declare const __VERSION__: string diff --git a/packages/runtime/src/util.ts b/packages/runtime/src/util.ts index 4bcb47db..1667a187 100644 --- a/packages/runtime/src/util.ts +++ b/packages/runtime/src/util.ts @@ -74,3 +74,16 @@ export const supportBigInt = typeof BigInt !== 'undefined' export function isReferenceType (v: any): v is object { return (typeof v === 'object' && v !== null) || typeof v === 'function' } + +export const _setImmediate = typeof setImmediate === 'function' + ? setImmediate + : function (f: () => void): void { + if (typeof f !== 'function') return + let channel = new MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + channel = undefined! + f() + } + channel.port2.postMessage(null) + } diff --git a/packages/test/objwrap/myobject.cc b/packages/test/objwrap/myobject.cc index 1baafcc4..521cf147 100644 --- a/packages/test/objwrap/myobject.cc +++ b/packages/test/objwrap/myobject.cc @@ -1,3 +1,4 @@ +#include #include "myobject.h" #include "../common.h" @@ -152,9 +153,69 @@ napi_value MyObject::Multiply(napi_env env, napi_callback_info info) { return instance; } +// This finalizer should never be invoked. +void ObjectWrapDanglingReferenceFinalizer(napi_env env, + void* finalize_data, + void* finalize_hint) { + assert(0 && "unreachable"); +} + +napi_ref dangling_ref; +napi_value ObjectWrapDanglingReference(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NAPI_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + // Create a napi_wrap and remove it immediately, whilst leaving the out-param + // ref dangling (not deleted). + NAPI_CALL(env, + napi_wrap(env, + args[0], + nullptr, + ObjectWrapDanglingReferenceFinalizer, + nullptr, + &dangling_ref)); + NAPI_CALL(env, napi_remove_wrap(env, args[0], nullptr)); + + return args[0]; +} + +napi_value ObjectWrapDanglingReferenceTest(napi_env env, + napi_callback_info info) { + napi_value out; + napi_value ret; + NAPI_CALL(env, napi_get_reference_value(env, dangling_ref, &out)); + + if (out == nullptr) { + // If the napi_ref has been invalidated, delete it. + NAPI_CALL(env, napi_delete_reference(env, dangling_ref)); + NAPI_CALL(env, napi_get_boolean(env, true, &ret)); + } else { + // The dangling napi_ref is still valid. + NAPI_CALL(env, napi_get_boolean(env, false, &ret)); + } + return ret; +} + EXTERN_C_START napi_value Init(napi_env env, napi_value exports) { MyObject::Init(env, exports); + + napi_property_descriptor descriptors[] = { + DECLARE_NAPI_PROPERTY("objectWrapDanglingReference", + ObjectWrapDanglingReference), + DECLARE_NAPI_PROPERTY("objectWrapDanglingReferenceTest", + ObjectWrapDanglingReferenceTest), + }; + + NAPI_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + return exports; } EXTERN_C_END diff --git a/packages/test/objwrap/objwrapref.test.js b/packages/test/objwrap/objwrapref.test.js new file mode 100644 index 00000000..f9cf2135 --- /dev/null +++ b/packages/test/objwrap/objwrapref.test.js @@ -0,0 +1,16 @@ +/* eslint-disable symbol-description */ +/* eslint-disable camelcase */ +'use strict' +const { load } = require('../util') +const common = require('../common') + +const p = load('objwrap') +module.exports = p.then(addon => { + (function scope () { + addon.objectWrapDanglingReference({}) + })() + + return common.gcUntil('object-wrap-ref', () => { + return addon.objectWrapDanglingReferenceTest() + }) +})