Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Foreign Function Interface (FFI) implementation #46905

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

bengl
Copy link
Member

@bengl bengl commented Mar 1, 2023

Here's a first pass at implementing a foreign function interface (FFI). It's roughly based on my attempt at building an FFI library in userland. That implementation uses dyncall, which doesn't support all the platforms that Node.js does, so I've used libffi instead.

As an example, this works on my PR branch.

$ cat test.c 
int test_add(int a, int b) {
  return a + b;
}
$ clang -shared -undefined dynamic_lookup -o libtest.so test.c
$ ./node -p "require('node:ffi').getNativeFunction('./libtest.so', 'test_add', 'int', ['int', 'int'])(7, 5)"
12

Here is the new doc page for this.

Limitations

  • Structs aren't supported as return values or arguments (but pointers are, and, of course, pointers to structs).
  • Callbacks aren't supported. See Open Questions below.
  • Only the documented types are supported, even if a given type is a typedef or otherwise equivalent to a supported type.

Open Questions

  • How should libffi be updated/maintained?
  • Do callbacks need to be supported in this PR?
    • My thoughts: It's a bigger task, and I think there's plenty of utility without callbacks. I'd prefer to do this separately afterward.
  • Should pointers be obfuscated somehow? Right now they aren't. They very easily could be, though, with a Pointer class.
    • My thoughts: I don't particularly think it's necessary, considering there are certainly other ways, both in several npm modules and in this feature, to get at pointers. Also, not having raw values makes it difficult to pack them into structs inside buffers, or do any pointer arithmetic.
  • Should Buffers/ArrayBuffers/ArrayBufferViews be acceptable as pointer arguments? This might simplify userland code.
    • My thoughts: Maybe, but in a separate PR.
  • Similarly, should Strings be acceptable as char * arguments?
    • My thoughts: Maybe, but in a separate PR.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/tsc

@nodejs-github-bot nodejs-github-bot added build Issues and PRs related to build files or the CI. dependencies Pull requests that update a dependency file. meta Issues and PRs related to the general management of the project. needs-ci PRs that need a full CI run. labels Mar 1, 2023
@bengl bengl changed the title ffi: initial ffi implementation Foreign Function Interface (FFI) implementation Mar 1, 2023
Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should disable it by default when using the Permission model, right?

@bengl bengl force-pushed the bengl/ffi branch 2 times, most recently from 2a74523 to 5e33eeb Compare March 2, 2023 19:35
@benjamingr
Copy link
Member

The actual approach looks neat and the changes look like the correct approach to do this.

Still I have to ask - is this common enough to be in core and not a userland module?

@Kruithne
Copy link

Kruithne commented Mar 2, 2023

The actual approach looks neat and the changes look like the correct approach to do this.

Still I have to ask - is this common enough to be in core and not a userland module?

With desktop applications built on Chromium + Node.js frameworks (Electron, nw.js, etc) becoming the common standard, having a means to interact with native libraries without needing to reach for third party modules would be a big improvement.

Additionally, bun has shipped built in FFI support as well.

@bengl
Copy link
Member Author

bengl commented Mar 2, 2023

@benjamingr

Still I have to ask - is this common enough to be in core and not a userland module?

Calling into native code is common enough, given the myriad native modules on npm.

The primary motivation for FFI, at least from my perspective, is the ability to call into native code without the usage of an additional C++ binding layer. This comes up particularly with pre-built dynamic libraries. For example, the official Oracle DB driver requires that a closed-source dynamic library be installed. This means that (currently) an additional C++ addon must be either compiled or prebuilt. With FFI built into core, the close-source dynamic library can be used without a C++ addon.

For another fun example, libm can be used directly without an addon 😃:

$ ./node -p "require('node:ffi').getNativeFunction('/usr/lib/libm.dylib', 'atanh', 'double', ['double'])(Math.tanh(Math.PI))"
3.141592653589798

The real win here is less of a need for native addons.

@Kruithne
Copy link

Kruithne commented Mar 2, 2023

This means that (currently) an additional C++ addon must be either compiled or prebuilt.

This is a common problem integrating applications with Steamworks (the API for Steam). The library cannot be publicly distributed and the popular native addon for it is now unmaintained and broken. With FFI, this wouldn't be an issue for a lot of people.

@aapoalas
Copy link

aapoalas commented Mar 2, 2023

Hello from Deno world! Nice to see FFI in Node as well: At least I've been completely taken by it and have spent significant amounts of time both thinking about and contributing to it on Deno side.

Few things that I would bring up from that experience:

  1. Be very careful of giving these keys to users. If at all possible, I would even recommend making FFI always forbidden by default and only allowed when explicitly given permission for, whether or not the user is using the new Node Permissions stuff. The possibility for misuse is just too great and too easy. Examples include opening the win32 DLL by name or by pointer address (this is somewhat static so may be liable to brute force search) which then enables exploiting basically any API one wants, or opening libc for mmap, using that to create writable, executable memory and writing one's own exploit directly into a TypedArray.
  2. Regarding pointer types, I recommend using V8's own v8::External. I've implemented support for Fast API to pass those as void pointers to and from native calls, meaning that if eventually you implement a JIT compiling trampoline on the native call side (both Bun and Deno have this) then you can leverage the V8 Fast API and get to ~2-5 nanoseconds baseline call performance. The Externals show up as either null (for null pointers) or as prototype-less and non-extendable objects on the JS side, meaning that they're already essentially opaque. Also note that V8 has some issues regarding zero-length ArrayBuffers: Certain APIs will think that a zero-length buffer is by necessity backed by a null pointer, so using buffers to represent pointers may backfire in some edge cases.
  3. Deno has chosen to have two different pointer types: "pointer" and "buffer". The first corresponds (nowadays) to an External object or null, while the latter corresponds to a TypedArray (we've chosen to always use Uint8Array as V8 Fast API requires us to choose something and that's the most reasonable one). I would love it if we could mix and match, but the V8 Fast API does not work like that, at least for the moment. Fast API does offer call type overloading, but it's limited in such a way that this particular overload wouldn't work (I think). But... this sounds like a good idea for a change!
  4. Strings as char *: While I think this is "cute" and would be nice, I don't think its necessarily a good idea. Often enough, users would probably want to optimise their string usage, reusing buffers as possible etc. Using a plain JS string would need to do a copy on each call. V8 Fast API does offer the FastOneByteString class or whatever its name was, which would offer a way to do this without copying every time, but the lifetime of said pointer is not really controllable by the FFI API user (whereas a buffer is entirely controllable) and not all JS strings would take the Fast API path (eg. TwoByte strings, roped strings, ...), meaning that the performance benefit would be "choppy".

I implemented FFI callbacks for Deno about a year ago, I think? The runtime backends are of course quite different so in that sense I doubt there's much that I could "teach" you. One thing that does spring to mind about that is, however, again regarding V8 Fast API: The Fast API has a requirement that the calls may not call back into V8 / JavaScript. This means that for FFI API declarations (if you take Fast API into use), you'll need to have some sort of flag to opt out of Fast API. For Deno that's callback: true, and it just makes it so that no Fast API binding is generated for the FFI function binding.

This opt out needs to happen in sometimes surprising places. eg. I've been playing with creating C++ lambdas from JavaScript. When passing a lambda into an API, any copies made of the lambda will callback, so APIs that pass a lambda or that may cause a lambda (created by me) to be passed on the native side need the callback: true flag.

I hope some of this may be useful to you! If you want to know more about how Deno's FFI works, I've written a bunch about it here. Feel free to ping me or send me an email or something if you have any direct questions you would want to ask!

And if I am speaking out of turn, my apologies: I absolutely mean no offense.

Copy link
Member

@Qard Qard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a good idea to make FfiSignature an ObjectWrap that could be wrapped around the function created in getNativeFunction that way the destructor could be used to clean up the mallocs for cif and argv.

src/node_ffi.cc Outdated
libraries[fname] = lib;
bool success = lib->Open();
if (!success) {
// TODO(bengl) what now?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw and cleanup libraries[fname]?

src/node_ffi.cc Outdated Show resolved Hide resolved
src/node_ffi.cc Outdated
Comment on lines 70 to 82
void GetBufferPointer(const v8::FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsArrayBuffer());
void* data = args[0].As<ArrayBuffer>()->Data();

ReturnBigInt(args.GetIsolate(), data);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this not happen purely on the JS side? Seems to me like it would be faster to avoid the native barrier hop here if possible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the Buffer types have pointer addresses available on the JS side.

We could add them to Buffer, and special case those? Also, there's definitely an opportunity to cache the address per ArrayBuffer, so that calls to other ArrayBuffer views on the same underlying ArrayBuffer can be handled in JS.

@bengl
Copy link
Member Author

bengl commented Mar 3, 2023

@aapoalas Thanks for all the info and ideas! Especially your notes on callbacks, and this set of notes.

Be very careful of giving these keys to users.

Yes, before I take this PR out of Draft mode, I'll be ensuring that it's covered by the permission model. That being said, I don't see this as being any more dangerous (from a security perspective) than being able to load native addons.

Regarding pointer types, I recommend using V8's own v8::External.

One of the goals here is to enable pointer math. I'd prefer not having to require multiple buffers in order to pass multiple pointers into a function. For example, in a test file, I have this:

const size = ffi.sizeof(type);
const buf = Buffer.alloc(size * 3);
const retPtr = ffi.getBufferPointer(buf);
const rwName = getRWName(type, size);
buf[`write${rwName}`](three, size);
buf[`write${rwName}`](four, 2 * size);
ptrFn(retPtr, retPtr + BigInt(size), retPtr + BigInt(2 * size));

I could enable this with a wrapper class, and get a new External when calling add() on the wrapper class or something like that, but I have a strong preference for being able to do this entirely in JavaScript. As before, I don't think this is any more of a security issue than native addon.

The biggest issue with this, right now, is that my implementation stuffs all arguments and a slot for the return value into a Buffer (which is re-used for each call). I don't know how I would do that if pointers were External objects instead of bigints or numbers.

Deno has chosen to have two different pointer types: "pointer" and "buffer".

Hmm, it sounds like this might be because of implementation details. It might not actually apply here?

Strings as char *: While I think this is "cute" and would be nice, I don't think its necessarily a good idea. Often enough, users would probably want to optimise their string usage, reusing buffers as possible etc. Using a plain JS string would need to do a copy on each call.

I don't think it would make sense to only support strings. Pointers (or Buffers, if taking that approach) would stuff need to be supported. Strings could also be cached.

@aapoalas
Copy link

aapoalas commented Mar 3, 2023

@aapoalas Thanks for all the info and ideas! Especially your notes on callbacks, and this set of notes.

No problem, I hope it's helpful!

Be very careful of giving these keys to users.

Yes, before I take this PR out of Draft mode, I'll be ensuring that it's covered by the permission model. That being said, I don't see this as being any more dangerous (from a security perspective) than being able to load native addons.

Yeah, it's not opening more doors really, but it is much easier to do nasty stuff with FFI since it becomes possible to keep most of the work inside JS.

Regarding pointer types, I recommend using V8's own v8::External.

One of the goals here is to enable pointer math. I'd prefer not having to require multiple buffers in order to pass multiple pointers into a function. For example, in a test file, I have this:

Deno only in the most recent minor version update changed from pointer numbers to pointer objects. With that change I added a few APIs: Creating a pointer from a number (dangerous!), offsetting a pointer, and getting the numeric value of a pointer. These of course exist to enable pointer arithmetic when needed.

Only today I changed one private repo to use the new API and admittedly assigning pointers to buffers became harder, one might even say tedious. Still, it wasn't a terribly common use case, at least in that code base.

In essence, I'd argue that the performance that Fast API offers is more important :) but that can of course be construed to be "sour grapes" :D

I could enable this with a wrapper class, and get a new External when calling add() on the wrapper class or something like that, but I have a strong preference for being able to do this entirely in JavaScript. As before, I don't think this is any more of a security issue than native addon.

I'm no security expert, but isn't pointer spoofing one relatively common attack? Any number always being a pointer does feel a bit scary to me in that sense :) From an ergonomics point of view it's also a bit unfortunate that there is no difference between a pointer and a number.

Side note: ARM might have pointer cryptography turned on, in which case pointer arithmetics would need to prepare for number OR BigInt values.

The biggest issue with this, right now, is that my implementation stuffs all arguments and a slot for the return value into a Buffer (which is re-used for each call). I don't know how I would do that if pointers were External objects instead of bigints or numbers.

Ah okay, yeah I hadn't realized you're doing this in a totally different way. Deno passes the parameters "traditionally".

Deno has chosen to have two different pointer types: "pointer" and "buffer".

Hmm, it sounds like this might be because of implementation details. It might not actually apply here?

Yeah, since you're just passing a buffer, we're on very different paths :) It's a very intriguing choice. Due to Deno's reliance on V8 Fast API it wouldn't make sense to us, but without that and JIT compiling the native side glue code it definitely makes sense. Nice stuff!

Strings as char *: While I think this is "cute" and would be nice, I don't think its necessarily a good idea. Often enough, users would probably want to optimise their string usage, reusing buffers as possible etc. Using a plain JS string would need to do a copy on each call.

I don't think it would make sense to only support strings. Pointers (or Buffers, if taking that approach) would stuff need to be supported. Strings could also be cached.

I'm not sure how strings could be cached in a reasonable way, since you'd want to avoid leaking memory and thus would need to bind the encoded string's buffer into the lifetime of the string itself, somehow, but strings cannot be used as keys in WeakMaps. So, it would then make sense to just encode a string once and return the buffer to the user, after which they can manage the lifetime. This then just becomes essentially the same as TextDecoder, I think.

@bengl
Copy link
Member Author

bengl commented Mar 3, 2023

@RafaelGSS

We should disable it by default when using the Permission model, right?

Actually, how should this work? Should it just be enabled or disabled, like with worker threads or child processes? Since the filename parameter to dlopen() doesn't necessarily correspond to an actual file on disk (e.g. /usr/lib/libm.dylib is not a real file on MacOS, but it can be loaded and it works), I don't think it quite makes sense to apply the fs permissions here.

@cjihrig
Copy link
Contributor

cjihrig commented Mar 3, 2023

I think this should be a separate permission (similar to child process and worker) with its own flag. When the permission system is enabled, access to FFI should be restricted unless the --allow-ffi flag is provided.

@RafaelGSS
Copy link
Member

RafaelGSS commented Mar 4, 2023

I think this should be a separate permission (similar to child process and worker) with its own flag. When the permission system is enabled, access to FFI should be restricted unless the --allow-ffi flag is provided.

Exactly. The safest option is to disable it by default when using --experimental-permission. Happy to help implement it. Feel free to ping me on OpenJS slack.

@bengl
Copy link
Member Author

bengl commented Mar 4, 2023

@RafaelGSS In the most recent rev, I have it behaving exactly as the worker and child process permissions do (that is, it's only disabled if an fs permission is enabled). It's easy enough to change it so that it's disabled even when only --experimental-permission is provided. That's what, you're suggesting, right?

Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest including a test for the new CLI argument (--allow-ffi). See:

  • test-permission-deny-worker-threads.js
  • test-permission-deny-worker-threads-cli.js
  • test-permission-deny-allow-worker-cli.js

doc/api/cli.md Outdated
@@ -547,6 +589,7 @@ following permissions are restricted:
\[`--allow-fs-read`]\[],\[`allow-fs-write`]\[] flags
* Child Process - manageable through \[`--allow-child-process`]\[] flag
* Worker Threads - manageable through \[`--allow-worker`]\[] flag
* FFI - manageable through \[`--allow-ffo`]\[] flag
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* FFI - manageable through \[`--allow-ffo`]\[] flag
* FFI - manageable through \[`--allow-ffi`]\[] flag

@RafaelGSS
Copy link
Member

@bengl I've just seen the patch. All good.

@bnoordhuis
Copy link
Member

More of a practical matter: I suggest removing unused big files from libffi (like the ChangeLog files) to avoid bloating the git tree and the source tarballs.

@bengl bengl marked this pull request as ready for review March 5, 2023 16:33
Copy link
Member

@Qard Qard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few suggestions to simplify things slightly, but they're minor nits. Other than that, LGTM.

src/node_ffi.cc Outdated

CHECK(args[0]->IsBigInt());
binding::DLib* lib =
(binding::DLib*)args[0].As<BigInt>()->Uint64Value(&lossless);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need the lossless argument if you aren't going to read it. Perhaps we should be reading it though and throwing if it's lossy?

Copy link
Member Author

@bengl bengl Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all the cases here where we're calling Uint8Value(), it's always with a pointer retrieved from this binding (not even exposed to the user). I think we're probably fine just getting rid of that argument everywhere in this file.

src/node_ffi.cc Outdated
static bool lossless = false;

#define ReturnBigInt(isolate, ptr) \
args.GetReturnValue().Set(BigInt::NewFromUnsigned(isolate, (uint64_t)ptr))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calls to this seem to use args.GetIsolate() and env->isolate() interchangeably, but it always needs args present anyway. Could probably just drop the isolate parameter and make it always use args.GetIsolate() internally.

src/node_ffi.cc Outdated
Comment on lines 35 to 41
bool isNull = true;
std::string fname = "";
if (!args[0]->IsNull()) {
CHECK(args[0]->IsString());
node::Utf8Value filename(env->isolate(), args[0]);
fname = filename.ToString();
isNull = false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could simplify that to something like:

  std::string fname = "";
  CHECK(args[0]->IsNull() || args[0]->IsString());
  if (args[0]->IsString()) {
    fname = *node::Utf8Value(env->isolate(), args[0]);
  }

Then you can check fname.empty() rather than needing isNull.

@gireeshpunathil
Copy link
Member

related art (from Java): https://openjdk.org/projects/panama/

Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we target Node.js >= 21?

@nodejs-github-bot
Copy link
Collaborator

@mcollina
Copy link
Member

I don't see why this should be a semver major change. Backporting to v20 would be a great win.

```

```cjs
const ffi = require('ffi');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be node:ffi, and we need to ensure that the node: prefix is required so that this can be backported.


<!--introduced_in=REPLACEME-->

> Stability: 1 - Experimental
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> Stability: 1 - Experimental
> Stability: 1.1 - Active development

https://nodejs.org/api/documentation.html#stability-index

@atgreen
Copy link

atgreen commented Oct 20, 2023

Looks like this didn't make 21. Let me know if there's anything I can help with from the libffi side. I expect to make a new release in the next few weeks.

@jasnell jasnell added the semver-minor PRs that contain new features and should be released in the next minor version. label Oct 28, 2023
} = require('internal/errors').codes;

// [ sigPtr | fnPtr | rvalue | avaluesPointers ]
const callBuffer = new ArrayBuffer(4096);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking nit: likely worth making 4096 a constant.

'signed long int': 'long',
'unsigned long': 'ulong',
'unsigned long int': 'ulong',
// 'long long': ,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here about why these are commented out would be good

xan105 added a commit to xan105/node-ffi that referenced this pull request Nov 5, 2023
Comment on lines +170 to +172
if (lib !== null && typeof lib !== 'string') {
throw new ERR_INVALID_ARG_TYPE('library', ['string', 'null'], lib);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use validateString() helper

const libCache = {};

const POINTER_SIZE = sizes['char*'];
const NULL = Symbol('null');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const NULL = Symbol('null');
const kNULL = Symbol('null');

Copy link
Contributor

@aduh95 aduh95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! A few nits:

Comment on lines +10 to +18
setCallBuffer,
getBufferPointer: getBufferPointerInternal,
FfiSignature,
makeCall,
getSymbol,
getLibrary,
types,
sizes,
charIsSigned,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider sorting the entries in ASCII order

Comment on lines +83 to +85
if (type.includes('*')) return typesToFfiTypes.pointer;
if (type.endsWith('_t')) return type.replace(/_t$/, '');
if (type.includes('long long')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (type.includes('*')) return typesToFfiTypes.pointer;
if (type.endsWith('_t')) return type.replace(/_t$/, '');
if (type.includes('long long')) {
if (StringPrototypeIncludes(type, '*')) return typesToFfiTypes.pointer;
if (StringPrototypeEndsWith(type, '_t')) return StringPrototypeReplaceAll(type, '_t', '');
if (StringPrototypeIncludes(type, 'long long')) {

if (type.endsWith('_t')) return type.replace(/_t$/, '');
if (type.includes('long long')) {
const size = sizes[type];
const signed = type.includes('unsigned');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const signed = type.includes('unsigned');
const signed = StringPrototypeIncludes(type, 'unsigned');

lib = NULL;
}
const getReturnVal = getReader(ret);
const argWriters = args.map(getWriter);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const argWriters = args.map(getWriter);
const argWriters = ArrayPrototypeMap(args, getWriter);

const getReturnVal = getReader(ret);
const argWriters = args.map(getWriter);
ret = normalizeTypeToFfiTypes(ret);
args = args.map(normalizeTypeToFfiTypes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
args = args.map(normalizeTypeToFfiTypes);
args = ArrayPrototypeMap(args, normalizeTypeToFfiTypes);

throw new ERR_FFI_SYMBOL_NOT_FOUND(funcName, lib === null ? 'null' : lib);
}

const sig = new FfiSignature(funcPtr, types[ret], args.map((n) => types[n]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const sig = new FfiSignature(funcPtr, types[ret], args.map((n) => types[n]));
const sig = new FfiSignature(funcPtr, types[ret], ArrayPrototypeMap(args, (n) => types[n]));

Comment on lines +21 to +30
This module is only available under the `node:` scheme. The following will not
work:

```mjs
import ffi from 'ffi';
```

```cjs
const ffi = require('ffi');
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this, and we should probably remove it from the node:test docs as well.

@@ -0,0 +1,35 @@
'use strict';
const common = require('../common');
const assert = require('assert');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using node: prefix to be consistant

Suggested change
const assert = require('assert');
const assert = require('node:assert');

@aduh95 aduh95 added the stalled Issues and PRs that are stalled. label May 11, 2024
Copy link
Contributor

This issue/PR was marked as stalled, it will be automatically closed in 30 days. If it should remain open, please leave a comment explaining why it should remain open.

Copy link
Contributor

Closing this because it has stalled. Feel free to reopen if this issue/PR is still relevant, or to ping the collaborator who labelled it stalled if you have any questions.

@github-actions github-actions bot closed this Jun 12, 2024
@Pomax
Copy link

Pomax commented Jul 25, 2024

@aduh95 @GeoffreyBooth @jasnell did this fall off the radar? A bot closing this PR seems like the wrong thing to happen here.

@targos targos reopened this Aug 7, 2024
@targos targos added never-stale Mark issue so that it is never considered stale and removed stalled Issues and PRs that are stalled. labels Aug 7, 2024
@marco-ippolito
Copy link
Member

@bengl we can add this behind a compile flag so it can be released as initial implementation and iterated on. It would be amazing to see it land

@cjihrig
Copy link
Contributor

cjihrig commented Jan 5, 2025

@bengl do you think you will get back to this? If not, would you mind if someone else looked into it?

@bengl
Copy link
Member Author

bengl commented Jan 8, 2025

Hey folks! Yes, it's been almost 2 years now, sorry about that. Life happened.

I don't think I'll reasonably have time to tackle this one further. If others want to give it a go, please feel free! I'm happy to get on a Zoom call to discuss this with anyone, at length.

For those wondering, the primary thing blocking this (apart from my time capacity) is that it depends on libffi, which generates a bunch of stuff _per platform with autotools/m4. Historically, there's been desire to not have autotools as a build dependency, and so what's typically done for native dependencies that need this is that any generated code is committed, so that it's not needed in build infra.

That's fine, but for the fact that libffi's scripts generate very different code depending on what platform you're on. This makes sense, because each platform's native code generated by libffi is going to be different. Therefore, code for all the platforms Node.js supports needs to be generated. I tried off-and-on for a few weeks to get things cross-compiled/generated, but always ran into failures that were difficult to diagnose without having the platform in question handy to test on.

Recently at NodeConfEU 2024, @marco-ippolito suggested (as indicated in the comment on Nov 2, 2024) to me that this could land behind a flag on just a few major platforms as experimental. I think that might work with the following caveats:

  1. That would require a huge rebase (it's almost 2 years out of date now!), for starters.
  2. It's probably best if it's updated to the latest libffi.
  3. It's unclear to me how sustainable it would be to maintain all that generated code, e.g. when there are new libffi versions. Maybe it's fine if it's just a few platforms?
  4. It's unclear to me that there would be a reasonable path forward to take it out of experimental status.
  5. As is, the PR doesn't support callbacks, and a few other things, so it would be limited in its current state. That's probably fine for an initial implementation, though!
  6. I probably won't have time to do this, so as I said before, if others want to try, please do! I wholeheartedly encourage this! I'm happy to walk through implementation details or anything else on a Zoom call.

And all that said, it's totally possible that the approach I took in this PR is not the best. I modelled it after my attempt at a fast userland FFI library. The are other angles that could be taken here, like embedding a compiler, pre-compiling a bunch of common function signatures, using a different FFI library, or even (mad science hat on) leveraging V8's JIT compilation somehow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
build Issues and PRs related to build files or the CI. dependencies Pull requests that update a dependency file. meta Issues and PRs related to the general management of the project. needs-ci PRs that need a full CI run. never-stale Mark issue so that it is never considered stale semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.