Skip to content
Draft
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
48 changes: 40 additions & 8 deletions doc/api/ffi.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,14 @@
const path = `libsqlite3.${suffix}`;
```

## `ffi.dlopen(path[, definitions])`
## `ffi.dlopen(path[, definitions[, options]])`

<!-- YAML
added: v26.1.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/REPLACEME

Check warning on line 215 in doc/api/ffi.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: Added the `options.supportsExceptions` option.
- version: v26.10.0
pr-url: https://github.com/nodejs/node/pull/65909
description: Library paths inside a mounted virtual file system are now
Expand All @@ -220,6 +223,12 @@
* `path` {string|null} Path to a dynamic library, or `null` to resolve symbols
from the current process image.
* `definitions` {Object} Symbol definitions to resolve immediately.
* `options` {Object}
* `supportsExceptions` {boolean} Allow callbacks registered on the loaded
library to throw JavaScript exceptions that propagate to the caller of
the native function that invoked them, instead of crashing the process.
This requires the library to be built with exception unwinding support.
**Default:** `false`.
* Returns: {Object}

Loads a dynamic library and resolves the requested function definitions.
Expand Down Expand Up @@ -312,10 +321,13 @@

Represents a loaded dynamic library.

### `new DynamicLibrary(path)`
### `new DynamicLibrary(path[, options])`

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/REPLACEME

Check warning on line 329 in doc/api/ffi.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: Added the `options.supportsExceptions` option.
- version: v26.10.0
pr-url: https://github.com/nodejs/node/pull/65909
description: Library paths inside a mounted virtual file system are now
Expand All @@ -324,6 +336,12 @@

* `path` {string|null} Path to a dynamic library, or `null` to resolve symbols
from the current process image.
* `options` {Object}
* `supportsExceptions` {boolean} Allow callbacks registered on this library
to throw JavaScript exceptions that propagate to the caller of the
native function that invoked them, instead of crashing the process.
This requires the library to be built with exception unwinding support.
**Default:** `false`.

Loads the dynamic library without resolving any functions eagerly.

Expand Down Expand Up @@ -484,7 +502,7 @@
Callbacks are subject to the following restrictions:

* They must be invoked on the same system thread where they were created.
* They must not throw exceptions.
* By default, they must not throw exceptions.
* They must not return promises.
* They must return a value compatible with the declared return type.
* They must not call `library.close()` on their owning library while running.
Expand All @@ -494,13 +512,26 @@
from inside the callback is unsupported and dangerous. Doing so may crash the
process, produce incorrect output, or corrupt memory.

Callbacks may throw exceptions if the library has been loaded with
`supportsExceptions: true` (see [`ffi.dlopen()`][] and [`new DynamicLibrary()`][])
and every native function on the stack below the callback comes
from a library that provides stack unwinding support. C++ libraries typically
fulfill this requirement, but C libraries may need to be built with
explicit support for this feature (e.g. the `-funwind-tables` flag of some
compilers).

If the thread running a callback is stopped while the callback executes, for
example by `worker.terminate()`, by `process.exit()` in a Worker, or by the
main thread exiting, only that thread stops. The callback returns to native
code without a value: non-void return values are zero-initialized, so native
code receives `0`, `false`, or a null pointer. Native code that does not
handle such a value, for example by dereferencing a returned null pointer, can
crash the process.
code without a value:
* If the library does not support exceptions, non-void return values are
zero-initialized, so native code receives `0`, `false`, or a null pointer.
Native code that does not handle such a value, for example by dereferencing
a returned null pointer, can crash the process.
* If the library does support exceptions and was instantiated using
`supportsExceptions: true`, the termination will be passed down as a C++
exception. Execution will still terminate once control is returned back
from native code.

### `library.unregisterCallback(pointer)`

Expand Down Expand Up @@ -964,12 +995,13 @@
[Fast API call path]: #fast-api-call-path
[Permission Model]: permissions.md#permission-model
[`--allow-ffi`]: cli.md#--allow-ffi
[`ffi.dlopen()`]: #ffidlopenpath-definitions
[`ffi.dlopen()`]: #ffidlopenpath-definitions-options
[`ffi.toBuffer(pointer, length, copy)`]: #ffitobufferpointer-length-copy
[`library.functions`]: #libraryfunctions
[`library.getFunction()`]: #librarygetfunctionname-signature
[`library.getFunctions()`]: #librarygetfunctionsdefinitions
[`library.registerCallback()`]: #libraryregistercallbacksignature-callback
[`new DynamicLibrary()`]: #new-dynamiclibrarypath-options
[`using`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using
[call paths]: #call-paths
[generic call path]: #generic-call-path
Expand Down
12 changes: 6 additions & 6 deletions lib/ffi.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const {
TypedArrayPrototypeGetBuffer,
} = primordials;
const { Buffer } = require('buffer');
const { emitExperimentalWarning } = require('internal/util');
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
const {
isDataView,
isArrayBufferView,
Expand Down Expand Up @@ -130,17 +130,17 @@ const { getVfsLibraryReader } = require('internal/ffi/vfs');
// (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) {
function DynamicLibrary(path, options = kEmptyObject) {
if (new.target === undefined) {
// Let the native constructor produce its usual error.
return FunctionPrototypeCall(NativeDynamicLibrary, this, path);
return FunctionPrototypeCall(NativeDynamicLibrary, this, path, undefined, !!options.supportsExceptions);
}
const readVirtualLibrary = getVfsLibraryReader();
const binary =
readVirtualLibrary === null || typeof path !== 'string' ?
undefined : readVirtualLibrary(path);
return ReflectConstruct(NativeDynamicLibrary,
binary === undefined ? [path] : [path, binary],
[path, binary, !!options.supportsExceptions],
new.target);
}
DynamicLibrary.prototype = NativeDynamicLibrary.prototype;
Expand Down Expand Up @@ -234,10 +234,10 @@ function checkFFIPermission() {
'FFI');
}

function dlopen(path, definitions) {
function dlopen(path, definitions, options = kEmptyObject) {
checkFFIPermission();

const lib = new DynamicLibrary(path);
const lib = new DynamicLibrary(path, options);
try {
const functions = definitions === undefined ? ObjectFreeze({ __proto__: null }) : lib.getFunctions(definitions);
return {
Expand Down
40 changes: 40 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,36 @@
}],
],
}, # node_core_target_name
{
'target_name': 'node_cxx_exceptions',
'type': 'static_library',

'sources': [
'src/ffi/cxx_exceptions.cc',
'src/ffi/cxx_exceptions.h',
],

'include_dirs': [
'src',
],

'defines': [
'NODE_WANT_INTERNALS=1',
],
'defines!': [
'_HAS_EXCEPTIONS=0',
],
'cflags_cc!': [ '-fno-exceptions' ],
'cflags_cc': [ '-fexceptions' ],
'xcode_settings': {
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
},
'msvs_settings': {
'VCCLCompilerTool': {
'ExceptionHandling': 1,
},
},
},
{
'target_name': 'node_base',
'type': 'static_library',
Expand Down Expand Up @@ -945,6 +975,16 @@
'sources': [
'<@(node_ffi_sources)',
],
'dependencies': [
'node_cxx_exceptions',
],
# Needed for exceptions to be able to reach
# the FFI exception handling code. This does
# *not* need to be set for e.g. V8.
'cflags': [ '-funwind-tables' ],
'xcode_settings': {
'OTHER_CFLAGS': [ '-funwind-tables' ],
},
'conditions': [
[ 'node_shared_ffi=="false"', {
'dependencies': [
Expand Down
31 changes: 31 additions & 0 deletions src/ffi/cxx_exceptions.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#include "ffi/cxx_exceptions.h"

#include <exception>

namespace node::cxx_exceptions {

struct CxxException final : std::exception {
CxxExceptionInfo info;
CxxException(const CxxExceptionInfo& info) : info(info) {}
const char* what() const noexcept override { return info.message.c_str(); }
};

void CxxExceptionThrow(const CxxExceptionInfo& exception) {
throw CxxException(exception);
}

std::optional<CxxExceptionInfo> CxxExceptionCatch(void (*fn)(void*),
void* arg) noexcept {
try {
fn(arg);
return std::nullopt;
} catch (const CxxException& e) {
return e.info;
} catch (const std::exception& e) {
return CxxExceptionInfo{.message = e.what()};
} catch (...) {
return CxxExceptionInfo{.message = "Unknown exception"};
}
}

} // namespace node::cxx_exceptions
41 changes: 41 additions & 0 deletions src/ffi/cxx_exceptions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <memory>
#include <optional>
#include <string>

// Using forward declarations to avoid including V8 headers
// and inlining V8 code by accident.
namespace v8 {
template <typename T>
class Global;
class Value;
} // namespace v8

namespace node::cxx_exceptions {

// `js_exception` is set when the original error is a JS exception that should
// be rethrown as-is instead of being reported only through `message`.
struct CxxExceptionInfo {
std::string message;
std::shared_ptr<v8::Global<v8::Value>> js_exception = {};
};

// Throws `exception` as a C++ exception; must be caught via CxxExceptionCatch()
// or catch { ... }.
void CxxExceptionThrow(const CxxExceptionInfo& exception);
// Runs fn(arg), catching any C++ exception thrown through it (including ones
// thrown by CxxExceptionThrow() further down the call stack).
std::optional<CxxExceptionInfo> CxxExceptionCatch(void (*fn)(void*),
void* arg) noexcept;
template <typename T>
std::optional<CxxExceptionInfo> CxxExceptionCatch(T fn) noexcept {
return CxxExceptionCatch([](void* ptr) { (*static_cast<T*>(ptr))(); },
static_cast<void*>(&fn));
}

} // namespace node::cxx_exceptions

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
Loading
Loading