Skip to content

Commit a0f4a8a

Browse files
mcollinaShogunPanda
authored andcommitted
ffi: support Symbol.dispose on DynamicLibrary
Install [Symbol.dispose]() on DynamicLibrary.prototype (calling close()) and on the object returned from ffi.dlopen(), so both can be used with the `using` declaration for automatic cleanup. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 8385efc commit a0f4a8a

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

doc/api/ffi.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,21 @@ The returned object contains:
182182
* `lib` {DynamicLibrary} The loaded library handle.
183183
* `functions` {Object} Callable wrappers for the requested symbols.
184184

185+
The returned object also implements the explicit resource management protocol,
186+
so it can be used with the [`using`][] declaration. Disposing the returned
187+
object closes the library handle.
188+
189+
```mjs
190+
import { dlopen } from 'node:ffi';
191+
192+
{
193+
using handle = dlopen('./mylib.so', {
194+
add_i32: { parameters: ['i32', 'i32'], result: 'i32' },
195+
});
196+
console.log(handle.functions.add_i32(20, 22));
197+
} // handle.lib.close() is invoked automatically here.
198+
```
199+
185200
```mjs
186201
import { dlopen } from 'node:ffi';
187202

@@ -275,6 +290,21 @@ An object containing previously resolved symbol addresses as `bigint` values.
275290

276291
Closes the library handle.
277292

293+
`DynamicLibrary` implements the explicit resource management protocol, so a
294+
library instance can be managed with the [`using`][] declaration. Leaving the
295+
enclosing scope invokes `library.close()` automatically.
296+
297+
```mjs
298+
import { DynamicLibrary } from 'node:ffi';
299+
300+
{
301+
using lib = new DynamicLibrary('./mylib.so');
302+
// Use `lib` here; `lib.close()` is called when the block exits.
303+
}
304+
```
305+
306+
Calling `library.close()` (or disposing the library) more than once is a no-op.
307+
278308
After a library has been closed:
279309

280310
* Resolved function wrappers become invalid.
@@ -295,6 +325,16 @@ Calling `library.close()` from one of the library's active callbacks is
295325
unsupported and dangerous. The callback must return before the library is
296326
closed.
297327

328+
### `library[Symbol.dispose]()`
329+
330+
<!-- YAML
331+
added: REPLACEME
332+
-->
333+
334+
Calls `library.close()`. This allows `DynamicLibrary` instances to be used with
335+
the [`using`][] declaration for automatic cleanup when the enclosing scope
336+
exits. It is a no-op on a library that has already been closed.
337+
298338
### `library.getFunction(name, signature)`
299339

300340
* `name` {string}
@@ -684,3 +724,4 @@ and keep callback and pointer lifetimes explicit on the native side.
684724
[Permission Model]: permissions.md#permission-model
685725
[`--allow-ffi`]: cli.md#--allow-ffi
686726
[`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy
727+
[`using`]: https://tc39.es/proposal-explicit-resource-management/#sec-using-declarations

lib/ffi.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const {
44
ObjectFreeze,
55
ObjectPrototypeToString,
6+
SymbolDispose,
67
} = primordials;
78
const { Buffer } = require('buffer');
89
const { emitExperimentalWarning } = require('internal/util');
@@ -55,6 +56,10 @@ const {
5556

5657
require('internal/ffi-shared-buffer');
5758

59+
DynamicLibrary.prototype[SymbolDispose] = function() {
60+
this.close();
61+
};
62+
5863
function checkFFIPermission() {
5964
if (!permission.isEnabled() || permission.has('ffi')) {
6065
return;
@@ -71,7 +76,11 @@ function dlopen(path, definitions) {
7176
const lib = new DynamicLibrary(path);
7277
try {
7378
const functions = definitions === undefined ? ObjectFreeze({ __proto__: null }) : lib.getFunctions(definitions);
74-
return { lib, functions };
79+
return {
80+
lib,
81+
functions,
82+
[SymbolDispose]() { lib.close(); },
83+
};
7584
} catch (error) {
7685
lib.close();
7786
throw error;

test/ffi/test-ffi-dynamic-library.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,65 @@ test('closed libraries reject subsequent operations', () => {
129129
assert.throws(() => lib.getSymbol('add_i32'), /Library is closed/);
130130
});
131131

132+
test('DynamicLibrary supports Symbol.dispose', () => {
133+
const lib = new ffi.DynamicLibrary(libraryPath);
134+
const addI32 = lib.getFunction('add_i32', fixtureSymbols.add_i32);
135+
136+
assert.strictEqual(typeof lib[Symbol.dispose], 'function');
137+
assert.strictEqual(addI32(20, 22), 42);
138+
139+
lib[Symbol.dispose]();
140+
141+
assert.throws(() => addI32(1, 2), /Library is closed/);
142+
assert.throws(() => lib.getSymbol('add_i32'), /Library is closed/);
143+
144+
// Disposing twice is a no-op.
145+
lib[Symbol.dispose]();
146+
lib.close();
147+
});
148+
149+
test('using declaration closes DynamicLibrary on scope exit', () => {
150+
let captured;
151+
{
152+
using lib = new ffi.DynamicLibrary(libraryPath);
153+
captured = lib.getFunction('add_i32', fixtureSymbols.add_i32);
154+
assert.strictEqual(captured(20, 22), 42);
155+
}
156+
157+
assert.throws(() => captured(1, 2), /Library is closed/);
158+
});
159+
160+
test('dlopen return value is disposable', () => {
161+
let capturedLib;
162+
let capturedFn;
163+
{
164+
using handle = ffi.dlopen(libraryPath, {
165+
add_i32: fixtureSymbols.add_i32,
166+
});
167+
assert.strictEqual(typeof handle[Symbol.dispose], 'function');
168+
capturedLib = handle.lib;
169+
capturedFn = handle.functions.add_i32;
170+
assert.strictEqual(capturedFn(20, 22), 42);
171+
}
172+
173+
assert.throws(() => capturedFn(1, 2), /Library is closed/);
174+
assert.throws(() => capturedLib.getSymbol('add_i32'), /Library is closed/);
175+
});
176+
177+
test('using still disposes DynamicLibrary when block throws', () => {
178+
const sentinel = new Error('boom');
179+
let captured;
180+
181+
assert.throws(() => {
182+
using lib = new ffi.DynamicLibrary(libraryPath);
183+
captured = lib.getFunction('add_i32', fixtureSymbols.add_i32);
184+
assert.strictEqual(captured(20, 22), 42);
185+
throw sentinel;
186+
}, sentinel);
187+
188+
assert.throws(() => captured(1, 2), /Library is closed/);
189+
});
190+
132191
test('dynamic library APIs validate failures and bad signatures', () => {
133192
assert.throws(() => {
134193
ffi.dlopen('missing-library-for-ffi-tests.so');

0 commit comments

Comments
 (0)