GeezerVison is showing github.com — ffi: support Symbol.dispose on DynamicLibrary · nodejs/node@58dc92f
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file 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 number | Diff line number | Diff line change |
|---|
@@ -182,6 +182,21 @@ The returned object contains: | 182 | 182 | * `lib` {DynamicLibrary} The loaded library handle. | 183 | 183 | * `functions` {Object} Callable wrappers for the requested symbols. | 184 | 184 |
| | 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 | + | 185 | 200 | ```mjs | 186 | 201 | import { dlopen } from 'node:ffi'; | 187 | 202 |
| @@ -275,6 +290,21 @@ An object containing previously resolved symbol addresses as `bigint` values. | 275 | 290 |
| 276 | 291 | Closes the library handle. | 277 | 292 |
| | 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 | + | 278 | 308 | After a library has been closed: | 279 | 309 |
| 280 | 310 | * Resolved function wrappers become invalid. | @@ -295,6 +325,16 @@ Calling `library.close()` from one of the library's active callbacks is | 295 | 325 | unsupported and dangerous. The callback must return before the library is | 296 | 326 | closed. | 297 | 327 |
| | 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 | + | 298 | 338 | ### `library.getFunction(name, signature)` | 299 | 339 |
| 300 | 340 | * `name` {string} | @@ -684,3 +724,4 @@ and keep callback and pointer lifetimes explicit on the native side. | 684 | 724 | [Permission Model]: permissions.md#permission-model | 685 | 725 | [`--allow-ffi`]: cli.md#--allow-ffi | 686 | 726 | [`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy | | 727 | +[`using`]: https://tc39.es/proposal-explicit-resource-management/#sec-using-declarations |
Collapse file Copy file name to clipboardExpand all lines: lib/ffi.js +10-1Lines changed: 10 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 | const { | 4 | 4 | ObjectFreeze, | 5 | 5 | ObjectPrototypeToString, | | 6 | + SymbolDispose, | 6 | 7 | } = primordials; | 7 | 8 | const { Buffer } = require('buffer'); | 8 | 9 | const { emitExperimentalWarning } = require('internal/util'); | @@ -55,6 +56,10 @@ const { | 55 | 56 |
| 56 | 57 | require('internal/ffi-shared-buffer'); | 57 | 58 |
| | 59 | +DynamicLibrary.prototype[SymbolDispose] = function() { | | 60 | + this.close(); | | 61 | +}; | | 62 | + | 58 | 63 | function checkFFIPermission() { | 59 | 64 | if (!permission.isEnabled() || permission.has('ffi')) { | 60 | 65 | return; | @@ -71,7 +76,11 @@ function dlopen(path, definitions) { | 71 | 76 | const lib = new DynamicLibrary(path); | 72 | 77 | try { | 73 | 78 | 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 | + }; | 75 | 84 | } catch (error) { | 76 | 85 | lib.close(); | 77 | 86 | throw error; | |
Collapse file Copy file name to clipboardExpand all lines: test/ffi/test-ffi-dynamic-library.js +59Lines changed: 59 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -129,6 +129,65 @@ test('closed libraries reject subsequent operations', () => { | 129 | 129 | assert.throws(() => lib.getSymbol('add_i32'), /Library is closed/); | 130 | 130 | }); | 131 | 131 |
| | 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 | + | 132 | 191 | test('dynamic library APIs validate failures and bad signatures', () => { | 133 | 192 | assert.throws(() => { | 134 | 193 | ffi.dlopen('missing-library-for-ffi-tests.so'); | |
|