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
25 changes: 25 additions & 0 deletions doc/api/ffi.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ const path = `libsqlite3.${suffix}`;

<!-- YAML
added: v26.1.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65909
description: Library paths inside a mounted virtual file system are now
supported.
-->

* `path` {string|null} Path to a dynamic library, or `null` to resolve symbols
Expand All @@ -221,6 +226,13 @@ Loads a dynamic library and resolves the requested function definitions.

On Windows passing `null` is not supported.

A `path` inside a mounted [virtual file system][] is supported: the
operating system's dynamic loader cannot open a virtual path, so the
library's bytes are read from the VFS and loaded from a private,
self-cleaning temporary image instead, while `lib.path` keeps reporting
the virtual path. Libraries on the real file system are unaffected and
load directly.

When `definitions` is omitted, `functions` is returned as an empty object until
symbols are resolved explicitly.

Expand Down Expand Up @@ -302,13 +314,24 @@ Represents a loaded dynamic library.

### `new DynamicLibrary(path)`

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65909
description: Library paths inside a mounted virtual file system are now
supported.
-->

* `path` {string|null} Path to a dynamic library, or `null` to resolve symbols
from the current process image.

Loads the dynamic library without resolving any functions eagerly.

On Windows passing `null` is not supported.

A `path` inside a mounted [virtual file system][] loads the same way as
with [`ffi.dlopen()`][].

```cjs
const { DynamicLibrary, suffix } = require('node:ffi');

Expand Down Expand Up @@ -798,7 +821,9 @@ and keep callback and pointer lifetimes explicit on the native side.

[Permission Model]: permissions.md#permission-model
[`--allow-ffi`]: cli.md#--allow-ffi
[`ffi.dlopen()`]: #ffidlopenpath-definitions
[`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy
[`library.functions`]: #libraryfunctions
[`using`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using
[type names]: #type-names
[virtual file system]: vfs.md
8 changes: 8 additions & 0 deletions doc/api/vfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,12 @@ addon's bytes are read from the VFS and loaded from a private, self-cleaning
temporary image instead. Addons on the real file system are unaffected and
load directly.

Shared libraries opened through [`ffi.dlopen()`][] (or
[`new ffi.DynamicLibrary()`][]) work the same way: a library path inside a
mounted VFS is detected, its bytes are read from the VFS, and the library is
loaded from a private, self-cleaning image while `library.path` keeps
reporting the virtual path. Libraries on the real file system load directly.

## Use with Single Executable Applications

When running as a [Single Executable Application][] built with
Expand Down Expand Up @@ -634,9 +640,11 @@ fields use synthetic but stable values:
[`VirtualFileSystem`]: #class-virtualfilesystem
[`VirtualProvider`]: #class-virtualprovider
[`ZipProvider`]: #class-zipprovider
[`ffi.dlopen()`]: ffi.md#ffidlopenpath-definitions
[`fs.BigIntStats`]: fs.md#class-fsstats
[`fs.Stats`]: fs.md#class-fsstats
[`import.meta.resolve()`]: esm.md#importmetaresolvespecifier
[`new ffi.DynamicLibrary()`]: ffi.md#new-dynamiclibrarypath
[`node:fs`]: fs.md
[`require()`]: modules.md#requireid
[`require.resolve()`]: modules.md#requireresolverequest-options
Expand Down
34 changes: 33 additions & 1 deletion lib/ffi.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
ObjectGetOwnPropertyDescriptor,
ObjectKeys,
ObjectPrototypeToString,
ReflectConstruct,
SafeWeakMap,
SafeWeakRef,
SymbolDispose,
Expand Down Expand Up @@ -38,7 +39,7 @@ const {
emitExperimentalWarning('FFI');

const {
DynamicLibrary,
DynamicLibrary: NativeDynamicLibrary,
getInt8,
getUint8,
getInt16,
Expand Down Expand Up @@ -119,6 +120,37 @@ function wrapFFIFunction(rawFn, owner) {
return wrapped;
}

const { getVfsLibraryReader } = require('internal/ffi/vfs');

// A thin constructor in front of the native class so that a library inside
// a mounted virtual file system loads transparently: its bytes are read
// from the VFS and handed to the native constructor, which loads them from
// a private, self-cleaning image - the same way require() handles a native
// addon in a VFS. The reader is installed by the VFS while it is mounted
// (see internal/ffi/vfs), so no VFS code is ever loaded from here. The
// wrapper shares the native prototype, so instances and instanceof behave
// as if the native class were exposed directly.
function DynamicLibrary(path) {
if (new.target === undefined) {
// Let the native constructor produce its usual error.
return FunctionPrototypeCall(NativeDynamicLibrary, this, path);
}
const readVirtualLibrary = getVfsLibraryReader();
const binary =
readVirtualLibrary === null || typeof path !== 'string' ?
undefined : readVirtualLibrary(path);
return ReflectConstruct(NativeDynamicLibrary,
binary === undefined ? [path] : [path, binary],
new.target);
}
DynamicLibrary.prototype = NativeDynamicLibrary.prototype;
ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', {
__proto__: null,
configurable: true,
value: DynamicLibrary,
writable: true,
});

const rawGetFunction = DynamicLibrary.prototype.getFunction;
const rawGetFunctions = DynamicLibrary.prototype.getFunctions;
const rawClose = DynamicLibrary.prototype.close;
Expand Down
27 changes: 27 additions & 0 deletions lib/internal/ffi/vfs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

// Seam between node:ffi and the virtual file system, mirroring the fs
// handler integration in internal/fs/utils: the VFS hook installer sets a
// library reader while at least one VFS is mounted and clears it when the
// last one unmounts, and DynamicLibrary consults it before every load. The
// dependency points from the VFS into ffi: ffi never loads any VFS code,
// and pays only a null check while no VFS is mounted.

// When reader is null, no VFS is active (zero overhead). Otherwise it is
// (path) => Buffer|undefined: the library's bytes for a path inside a
// mounted VFS, or undefined for a path the dynamic loader should open
// itself.
let vfsLibraryReader = null;

function setVfsLibraryReader(reader) {
vfsLibraryReader = reader;
}

function getVfsLibraryReader() {
return vfsLibraryReader;
}

module.exports = {
getVfsLibraryReader,
setVfsLibraryReader,
};
32 changes: 32 additions & 0 deletions lib/internal/vfs/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -968,10 +968,38 @@ function installAddonLoader() {
const { dlopenBinary } = internalBinding('process_methods');
return dlopenBinary(module, filename, flags, readFileSync(filename));
}
// Do not forward a missing flags argument as `undefined`:
// process.dlopen() coerces it to 0, which is not a valid dlopen(2)
// mode, instead of applying the default flags.
if (flags === undefined) return originalDlopen(module, filename);
return originalDlopen(module, filename, flags);
};
}

/**
* Reads the bytes of a file that lives in a mounted VFS. Returns undefined
* for a path outside the reserved VFS root - the caller should open the
* path itself - and throws ENOENT for a path under the root that no
* mounted VFS serves, since no real file can exist there. Installed into
* node:ffi while hooks are installed, so DynamicLibrary can load a
* VFS-resident library from a private image, the same way the module
* loader handles a native addon in a VFS.
* @param {string} pathStr The path of the library
* @returns {Buffer|undefined} The library's bytes, or undefined
*/
function readVirtualBinary(pathStr) {
const normalized = normalizeMountedPath(pathStr);
if (!StringPrototypeStartsWith(normalized, normalizedVfsRootPrefix)) {
return undefined;
}
const layerId = getLayerIdFromPath(normalized);
const vfs = layerId === -1 ? undefined : activeVFSLayers.get(layerId);
if (vfs === undefined || !vfs.shouldHandleNormalized(normalized)) {
throw createENOENT('open', pathStr);
}
return vfs.readFileSync(normalized);
}

/**
* Install all VFS hooks: module loader overrides and fs handlers.
*/
Expand All @@ -981,6 +1009,8 @@ function installHooks() {
normalizedVfsRootPrefix = getNormalizedVfsRoot() + sep;
installModuleLoaderOverrides();
installAddonLoader();
const { setVfsLibraryReader } = require('internal/ffi/vfs');
setVfsLibraryReader(readVirtualBinary);
vfsHandlerObj = createVfsHandlers();
setVfsHandlers(vfsHandlerObj);
hooksInstalled = true;
Expand All @@ -998,6 +1028,8 @@ function uninstallHooks() {
setLoaderOverrides();
setVfsHandlers(null);
vfsHandlerObj = undefined;
const { setVfsLibraryReader } = require('internal/ffi/vfs');
setVfsLibraryReader(null);
process.dlopen = originalDlopen;
hooksInstalled = false;
}
Expand Down
Loading
Loading