Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 58dc92f

Browse filesBrowse files
mcollinaaduh95
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> PR-URL: #62925 Reviewed-By: Bryan English <bryan@bryanenglish.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 528f8b2 commit 58dc92f
Copy full SHA for 58dc92f

3 files changed

+110-1Lines changed: 110 additions & 1 deletion

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/ffi.md‎

Copy file name to clipboardExpand all lines: doc/api/ffi.md
+41Lines changed: 41 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎lib/ffi.js‎

Copy file name to clipboardExpand all lines: lib/ffi.js
+10-1Lines 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;
Collapse file

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

Copy file name to clipboardExpand all lines: test/ffi/test-ffi-dynamic-library.js
+59Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.