Skip to content

Commit a49e8e4

Browse files
committed
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.
1 parent a5b3d76 commit a49e8e4

3 files changed

Lines changed: 116 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: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
'use strict';
22

33
const {
4+
ObjectDefineProperty,
45
ObjectFreeze,
56
ObjectPrototypeToString,
7+
SymbolDispose,
68
} = primordials;
79
const { Buffer } = require('buffer');
810
const { emitExperimentalWarning } = require('internal/util');
@@ -53,6 +55,15 @@ const {
5355
toArrayBuffer,
5456
} = internalBinding('ffi');
5557

58+
ObjectDefineProperty(DynamicLibrary.prototype, SymbolDispose, {
59+
__proto__: null,
60+
configurable: true,
61+
writable: true,
62+
value: function dispose() {
63+
this.close();
64+
},
65+
});
66+
5667
function checkFFIPermission() {
5768
if (!permission.isEnabled() || permission.has('ffi')) {
5869
return;
@@ -69,7 +80,11 @@ function dlopen(path, definitions) {
6980
const lib = new DynamicLibrary(path);
7081
try {
7182
const functions = definitions === undefined ? ObjectFreeze({ __proto__: null }) : lib.getFunctions(definitions);
72-
return { lib, functions };
83+
return {
84+
lib,
85+
functions,
86+
[SymbolDispose]() { lib.close(); },
87+
};
7388
} catch (error) {
7489
lib.close();
7590
throw error;

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

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

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

0 commit comments

Comments
 (0)