| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
Review requested:
|
Sorry, something went wrong.
There was a problem hiding this comment.
We should disable it by default when using the Permission model, right?
Sorry, something went wrong.
|
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? |
Sorry, something went wrong.
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. |
Sorry, something went wrong.
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. |
Sorry, something went wrong.
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. |
Sorry, something went wrong.
|
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:
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. |
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
There was a problem hiding this comment.
Throw and cleanup libraries[fname]?
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
|
@aapoalas Thanks for all the info and ideas! Especially your notes on callbacks, and this set of notes.
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.
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: 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.
Hmm, it sounds like this might be because of implementation details. It might not actually apply here?
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. |
Sorry, something went wrong.
No problem, I hope it's helpful!
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.
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'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.
Ah okay, yeah I hadn't realized you're doing this in a totally different way. Deno passes the parameters "traditionally".
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!
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. |
Sorry, something went wrong.
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. |
Sorry, something went wrong.
|
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. |
Sorry, something went wrong.
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. |
Sorry, something went wrong.
|
@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? |
Sorry, something went wrong.
There was a problem hiding this comment.
I suggest including a test for the new CLI argument (--allow-ffi). See:
Sorry, something went wrong.
There was a problem hiding this comment.
| * FFI - manageable through \[`--allow-ffo`]\[] flag | |
| * FFI - manageable through \[`--allow-ffi`]\[] flag |
Sorry, something went wrong.
|
@bengl I've just seen the patch. All good. |
Sorry, something went wrong.
|
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. |
Sorry, something went wrong.
There was a problem hiding this comment.
A few suggestions to simplify things slightly, but they're minor nits. Other than that, LGTM.
Sorry, something went wrong.
There was a problem hiding this comment.
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?
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
There was a problem hiding this comment.
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.
Sorry, something went wrong.
|
related art (from Java): https://openjdk.org/projects/panama/ |
Sorry, something went wrong.
| if (lib !== null && typeof lib !== 'string') { | ||
| throw new ERR_INVALID_ARG_TYPE('library', ['string', 'null'], lib); | ||
| } |
There was a problem hiding this comment.
We can use validateString() helper
Sorry, something went wrong.
| const libCache = {}; | ||
|
|
||
| const POINTER_SIZE = sizes['char*']; | ||
| const NULL = Symbol('null'); |
There was a problem hiding this comment.
| const NULL = Symbol('null'); | |
| const kNULL = Symbol('null'); |
Sorry, something went wrong.
There was a problem hiding this comment.
Nice work! A few nits:
Sorry, something went wrong.
| setCallBuffer, | ||
| getBufferPointer: getBufferPointerInternal, | ||
| FfiSignature, | ||
| makeCall, | ||
| getSymbol, | ||
| getLibrary, | ||
| types, | ||
| sizes, | ||
| charIsSigned, |
There was a problem hiding this comment.
nit: consider sorting the entries in ASCII order
Sorry, something went wrong.
| if (type.includes('*')) return typesToFfiTypes.pointer; | ||
| if (type.endsWith('_t')) return type.replace(/_t$/, ''); | ||
| if (type.includes('long long')) { |
There was a problem hiding this comment.
| 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')) { |
Sorry, something went wrong.
| if (type.endsWith('_t')) return type.replace(/_t$/, ''); | ||
| if (type.includes('long long')) { | ||
| const size = sizes[type]; | ||
| const signed = type.includes('unsigned'); |
There was a problem hiding this comment.
| const signed = type.includes('unsigned'); | |
| const signed = StringPrototypeIncludes(type, 'unsigned'); |
Sorry, something went wrong.
| lib = NULL; | ||
| } | ||
| const getReturnVal = getReader(ret); | ||
| const argWriters = args.map(getWriter); |
There was a problem hiding this comment.
| const argWriters = args.map(getWriter); | |
| const argWriters = ArrayPrototypeMap(args, getWriter); |
Sorry, something went wrong.
| const getReturnVal = getReader(ret); | ||
| const argWriters = args.map(getWriter); | ||
| ret = normalizeTypeToFfiTypes(ret); | ||
| args = args.map(normalizeTypeToFfiTypes); |
There was a problem hiding this comment.
| args = args.map(normalizeTypeToFfiTypes); | |
| args = ArrayPrototypeMap(args, normalizeTypeToFfiTypes); |
Sorry, something went wrong.
| throw new ERR_FFI_SYMBOL_NOT_FOUND(funcName, lib === null ? 'null' : lib); | ||
| } | ||
|
|
||
| const sig = new FfiSignature(funcPtr, types[ret], args.map((n) => types[n])); |
There was a problem hiding this comment.
| const sig = new FfiSignature(funcPtr, types[ret], args.map((n) => types[n])); | |
| const sig = new FfiSignature(funcPtr, types[ret], ArrayPrototypeMap(args, (n) => types[n])); |
Sorry, something went wrong.
| This module is only available under the `node:` scheme. The following will not | ||
| work: | ||
|
|
||
| ```mjs | ||
| import ffi from 'ffi'; | ||
| ``` | ||
|
|
||
| ```cjs | ||
| const ffi = require('ffi'); | ||
| ``` |
There was a problem hiding this comment.
I don't think we need this, and we should probably remove it from the node:test docs as well.
Sorry, something went wrong.
| @@ -0,0 +1,35 @@ | |||
| 'use strict'; | |||
| const common = require('../common'); | |||
| const assert = require('assert'); | |||
There was a problem hiding this comment.
Consider using node: prefix to be consistant
| const assert = require('assert'); | |
| const assert = require('node:assert'); |
Sorry, something went wrong.
|
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. |
Sorry, something went wrong.
|
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. |
Sorry, something went wrong.
|
@aduh95 @GeoffreyBooth @jasnell did this fall off the radar? A bot closing this PR seems like the wrong thing to happen here. |
Sorry, something went wrong.
|
@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 |
Sorry, something went wrong.
|
@bengl do you think you will get back to this? If not, would you mind if someone else looked into it? |
Sorry, something went wrong.
|
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:
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. |
Sorry, something went wrong.
|
This issue was initially raised by me, and I’m willing to put in the effort to try and take over this feature. Although I have some C coding skills, I’m completely unfamiliar with Node.js's project structure and processes. However, I’ll give it a shot, and if there’s any progress, I’ll update here. I hope everyone is willing to accept me as a newcomer and not dislike me 😂. |
Sorry, something went wrong.
|
After a few days of research, I have roughly determined the following approach:
|
Sorry, something went wrong.
|
One advantage of libffi is that it doesn't require writable+executable
memory on many platforms -- a major security advantage.
…On Thu, Mar 13, 2025 at 11:01 PM 演员 ***@***.***> wrote:
After a few days of research, I have roughly determined the following
approach:
1.
*Use the dyncall library instead of libffi*. This library implements
hand-written assembly code for each platform, avoiding the need for
additional dependencies. I tested this library on the Windows x64 platform,
and it performed exceptionally well (which, of course, is the most commonly
used platform). However, testing on other platforms will require more
effort.
2.
*Supporting callback functionality is extremely important.*
3.
*I hope to fully align with Deno's implementation of FFI*, essentially
re-implementing Deno's FFI in Node.js. On one hand, this allows us to draw
on Deno's mature experience (as Deno's FFI has already moved out of the
experimental phase and is marked as stable). On the other hand, this helps
enhance consistency within the JavaScript community, reducing the
maintenance burden for library authors.
—
Reply to this email directly, view it on GitHub
<#46905 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAV7CLCYVVGIQGXJCT3ECT2UJA7NAVCNFSM6AAAAAAVMTEUAGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDOMRTGI2TKNBQHE>
.
You are receiving this because you commented.Message ID:
***@***.***>
[image: tianxiadys]*tianxiadys* left a comment (nodejs/node#46905)
<#46905 (comment)>
After a few days of research, I have roughly determined the following
approach:
1.
*Use the dyncall library instead of libffi*. This library implements
hand-written assembly code for each platform, avoiding the need for
additional dependencies. I tested this library on the Windows x64 platform,
and it performed exceptionally well (which, of course, is the most commonly
used platform). However, testing on other platforms will require more
effort.
2.
*Supporting callback functionality is extremely important.*
3.
*I hope to fully align with Deno's implementation of FFI*, essentially
re-implementing Deno's FFI in Node.js. On one hand, this allows us to draw
on Deno's mature experience (as Deno's FFI has already moved out of the
experimental phase and is marked as stable). On the other hand, this helps
enhance consistency within the JavaScript community, reducing the
maintenance burden for library authors.
—
Reply to this email directly, view it on GitHub
<#46905 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAV7CLCYVVGIQGXJCT3ECT2UJA7NAVCNFSM6AAAAAAVMTEUAGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDOMRTGI2TKNBQHE>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Sorry, something went wrong.
|
Thanks for pointing that out! I really didn’t think about this issue. GPT mentioned that dyncall avoids generating dynamic machine code in most cases, so it doesn’t need WX memory. I’ll take a closer look at the assembly code of both dyncall and libffi to see if GPT was right. That said, maybe WX memory pages aren’t such a big deal, since V8’s JIT has been doing it like this all along. |
Sorry, something went wrong.
|
I have checked, and dyncall and libffi are consistent in their use of WX memory: downcalls do not require WX memory, but upcalls do. This is because downcalls can be implemented with just a forwarding function, while upcalls are essentially closure functions, requiring at least a context pointer to be closed over. As a result, WX memory is necessary. In practice, the main function body of an upcall is still pre-encoded, but the function entry point requires at least 16 bytes of WX memory: the first 8 bytes are used to forward to the predefined function body, and the remaining 8 bytes point to the context data structure. dyncall uses the following code to allocate WX memory on the Windows platform: DCerror dcAllocWX(size_t size, void** ptr)
{
LPVOID p = VirtualAlloc(0, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (p == NULL) return -1;
*ptr = p;
return 0;
}This code has significant room for optimization because the allocation granularity of VirtualAlloc is 4096 bytes, while the actual usage might only be a few dozen bytes. The remaining memory is wasted. |
Sorry, something went wrong.
|
This is only true for non-Linux systems today. Libffi closures don't require WX memory for closures/upcalls on almost all Linux platforms thanks to work by Microsoft and others. They implemented what we call "static trampolines", and they are best described in this article: https://blog.lazym.io/2021/07/29/Cast-a-Closure-to-a-Function-Pointer-How-libffi-closure-works/ . |
Sorry, something went wrong.
|
This is indeed a very important information. I will shift my focus back from dyncall to libffi. |
Sorry, something went wrong.
|
Please goto another PR (#57761) and close this PR. |
Sorry, something went wrong.
|
#62072 has landed, so no longer need this one. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
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)" 12Here is the new doc page for this.
Limitations
Open Questions