|
| 1 | +// Flags: --expose-gc |
| 2 | +'use strict'; |
| 3 | +const { skipIfFFIMissing } = require('../common'); |
| 4 | +skipIfFFIMissing(); |
| 5 | + |
| 6 | +const { gcUntil } = require('../common/gc'); |
| 7 | +const test = require('node:test'); |
| 8 | +const ffi = require('node:ffi'); |
| 9 | +const { fixtureSymbols, libraryPath } = require('./ffi-test-common'); |
| 10 | + |
| 11 | +const signature = { arguments: ['i32'], return: 'i32' }; |
| 12 | + |
| 13 | +test('registered callbacks keep an unreachable library alive', async (t) => { |
| 14 | + // A separate handle provides the native function that invokes the callback. |
| 15 | + const { lib: keeper, functions: symbols } = |
| 16 | + ffi.dlopen(libraryPath, fixtureSymbols); |
| 17 | + t.after(() => keeper.close()); |
| 18 | + |
| 19 | + let lib = new ffi.DynamicLibrary(libraryPath); |
| 20 | + const ref = new WeakRef(lib); |
| 21 | + const pointer = lib.registerCallback(signature, (x) => x * 2); |
| 22 | + t.assert.strictEqual(symbols.call_int_callback(pointer, 21), 42); |
| 23 | + |
| 24 | + lib = null; |
| 25 | + for (let i = 0; i < 5; i++) { |
| 26 | + await gcUntil('library with callbacks stays alive', () => true, 1); |
| 27 | + t.assert.strictEqual(typeof ref.deref(), 'object'); |
| 28 | + t.assert.strictEqual(symbols.call_int_callback(pointer, 21), 42); |
| 29 | + } |
| 30 | + |
| 31 | + ref.deref().unregisterCallback(pointer); |
| 32 | +}); |
| 33 | + |
| 34 | +test('library becomes collectable after its callbacks are unregistered', async () => { |
| 35 | + let lib = new ffi.DynamicLibrary(libraryPath); |
| 36 | + const ref = new WeakRef(lib); |
| 37 | + const first = lib.registerCallback(signature, (x) => x); |
| 38 | + const second = lib.registerCallback(signature, (x) => x); |
| 39 | + lib.unregisterCallback(first); |
| 40 | + lib.unregisterCallback(second); |
| 41 | + lib = null; |
| 42 | + |
| 43 | + await gcUntil('library is collected after unregisterCallback', |
| 44 | + () => ref.deref() === undefined); |
| 45 | +}); |
| 46 | + |
| 47 | +test('library becomes collectable after close()', async () => { |
| 48 | + let lib = new ffi.DynamicLibrary(libraryPath); |
| 49 | + const ref = new WeakRef(lib); |
| 50 | + lib.registerCallback(signature, (x) => x); |
| 51 | + lib.close(); |
| 52 | + lib = null; |
| 53 | + |
| 54 | + await gcUntil('library is collected after close()', |
| 55 | + () => ref.deref() === undefined); |
| 56 | +}); |
| 57 | + |
| 58 | +test('unrefCallback() does not release the callback trampoline', async (t) => { |
| 59 | + const { lib: keeper, functions: symbols } = |
| 60 | + ffi.dlopen(libraryPath, fixtureSymbols); |
| 61 | + t.after(() => keeper.close()); |
| 62 | + |
| 63 | + let lib = new ffi.DynamicLibrary(libraryPath); |
| 64 | + const ref = new WeakRef(lib); |
| 65 | + const pointer = lib.registerCallback(signature, () => 1); |
| 66 | + lib.unrefCallback(pointer); |
| 67 | + lib = null; |
| 68 | + |
| 69 | + for (let i = 0; i < 5; i++) { |
| 70 | + await gcUntil('library with unref callbacks stays alive', () => true, 1); |
| 71 | + t.assert.strictEqual(typeof ref.deref(), 'object'); |
| 72 | + } |
| 73 | + // The JS function may be gone, but the trampoline must still be callable. |
| 74 | + symbols.call_int_callback(pointer, 21); |
| 75 | + |
| 76 | + ref.deref().unregisterCallback(pointer); |
| 77 | +}); |
| 78 | + |
| 79 | +test('libraries with registered callbacks do not block teardown', async (t) => { |
| 80 | + const { Worker } = require('node:worker_threads'); |
| 81 | + const { once } = require('node:events'); |
| 82 | + const worker = new Worker(` |
| 83 | + const ffi = require('node:ffi'); |
| 84 | + const lib = new ffi.DynamicLibrary(${JSON.stringify(libraryPath)}); |
| 85 | + lib.registerCallback(${JSON.stringify(signature)}, (x) => x); |
| 86 | + globalThis.lib = lib; |
| 87 | + `, { eval: true }); |
| 88 | + const [code] = await once(worker, 'exit'); |
| 89 | + t.assert.strictEqual(code, 0); |
| 90 | +}); |
0 commit comments