Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions doc/api/ffi.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,21 @@ The returned object contains:
* `lib` {DynamicLibrary} The loaded library handle.
* `functions` {Object} Callable wrappers for the requested symbols.

The returned object also implements the explicit resource management protocol,
so it can be used with the [`using`][] declaration. Disposing the returned
object closes the library handle.

```mjs
import { dlopen } from 'node:ffi';

{
using handle = dlopen('./mylib.so', {
add_i32: { parameters: ['i32', 'i32'], result: 'i32' },
});
console.log(handle.functions.add_i32(20, 22));
} // handle.lib.close() is invoked automatically here.
```

```mjs
import { dlopen } from 'node:ffi';

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

Closes the library handle.

`DynamicLibrary` implements the explicit resource management protocol, so a
library instance can be managed with the [`using`][] declaration. Leaving the
enclosing scope invokes `library.close()` automatically.

```mjs
import { DynamicLibrary } from 'node:ffi';

{
using lib = new DynamicLibrary('./mylib.so');
// Use `lib` here; `lib.close()` is called when the block exits.
}
```

Calling `library.close()` (or disposing the library) more than once is a no-op.

After a library has been closed:

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

### `library[Symbol.dispose]()`

<!-- YAML
added: REPLACEME
-->

Calls `library.close()`. This allows `DynamicLibrary` instances to be used with
the [`using`][] declaration for automatic cleanup when the enclosing scope
exits. It is a no-op on a library that has already been closed.

### `library.getFunction(name, signature)`

* `name` {string}
Expand Down Expand Up @@ -684,3 +724,4 @@ and keep callback and pointer lifetimes explicit on the native side.
[Permission Model]: permissions.md#permission-model
[`--allow-ffi`]: cli.md#--allow-ffi
[`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy
[`using`]: https://tc39.es/proposal-explicit-resource-management/#sec-using-declarations
11 changes: 10 additions & 1 deletion lib/ffi.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const {
ObjectFreeze,
ObjectPrototypeToString,
SymbolDispose,
} = primordials;
const { Buffer } = require('buffer');
const { emitExperimentalWarning } = require('internal/util');
Expand Down Expand Up @@ -53,6 +54,10 @@ const {
toArrayBuffer,
} = internalBinding('ffi');

DynamicLibrary.prototype[SymbolDispose] = function() {
this.close();
};

function checkFFIPermission() {
if (!permission.isEnabled() || permission.has('ffi')) {
return;
Expand All @@ -69,7 +74,11 @@ function dlopen(path, definitions) {
const lib = new DynamicLibrary(path);
try {
const functions = definitions === undefined ? ObjectFreeze({ __proto__: null }) : lib.getFunctions(definitions);
return { lib, functions };
return {
lib,
functions,
[SymbolDispose]() { lib.close(); },
};
} catch (error) {
lib.close();
throw error;
Expand Down
59 changes: 59 additions & 0 deletions test/ffi/test-ffi-dynamic-library.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,65 @@ test('closed libraries reject subsequent operations', () => {
assert.throws(() => lib.getSymbol('add_i32'), /Library is closed/);
});

test('DynamicLibrary supports Symbol.dispose', () => {
const lib = new ffi.DynamicLibrary(libraryPath);
const addI32 = lib.getFunction('add_i32', fixtureSymbols.add_i32);

assert.strictEqual(typeof lib[Symbol.dispose], 'function');
assert.strictEqual(addI32(20, 22), 42);

lib[Symbol.dispose]();

assert.throws(() => addI32(1, 2), /Library is closed/);
assert.throws(() => lib.getSymbol('add_i32'), /Library is closed/);

// Disposing twice is a no-op.
lib[Symbol.dispose]();
lib.close();
});

test('using declaration closes DynamicLibrary on scope exit', () => {
let captured;
{
using lib = new ffi.DynamicLibrary(libraryPath);
captured = lib.getFunction('add_i32', fixtureSymbols.add_i32);
assert.strictEqual(captured(20, 22), 42);
}

assert.throws(() => captured(1, 2), /Library is closed/);
});

test('dlopen return value is disposable', () => {
let capturedLib;
let capturedFn;
{
using handle = ffi.dlopen(libraryPath, {
add_i32: fixtureSymbols.add_i32,
});
assert.strictEqual(typeof handle[Symbol.dispose], 'function');
capturedLib = handle.lib;
capturedFn = handle.functions.add_i32;
assert.strictEqual(capturedFn(20, 22), 42);
}

assert.throws(() => capturedFn(1, 2), /Library is closed/);
assert.throws(() => capturedLib.getSymbol('add_i32'), /Library is closed/);
});

test('using still disposes DynamicLibrary when block throws', () => {
const sentinel = new Error('boom');
let captured;

assert.throws(() => {
using lib = new ffi.DynamicLibrary(libraryPath);
captured = lib.getFunction('add_i32', fixtureSymbols.add_i32);
assert.strictEqual(captured(20, 22), 42);
throw sentinel;
}, sentinel);

assert.throws(() => captured(1, 2), /Library is closed/);
});

test('dynamic library APIs validate failures and bad signatures', () => {
assert.throws(() => {
ffi.dlopen('missing-library-for-ffi-tests.so');
Expand Down
Loading