Hey,
Thank you for getting back to me!
I'm definitely interested in implementing Externals for C callbacks both as parameters and as returns values. Returning void pointers should prove to be more difficult I guess. I can see two ways to go about it:
1. Take the same route as you mention for returning TypedArrays, where V8 will allocate a placeholder before calling the callback.
2. Simply have the C callback return the pointer and have V8 do the allocation after calling it. I presume this should be doable since the return value CType can (or perhaps "should") be trusted to speak the truth, and returning a pointer does not cause any issues calling convention wise.
My personal preference is definitely on #2, as it feels more "natural" and contains less indirection. It also has the slight benefit of not doing any unnecessary allocations for the External when the fast callback signals a need to deopt using the fallback flag.
I wonder if #2 could be used to likewise implement TypedArray returning? I can't exactly remember the System V ABI for C++ structures but I seem to recall that a structure with a size of up two two pointers worth can be returned through RAX and an extra register (that is, as long as the class does not have an non-trivial copy constructors or destructor). Other ABIs might of course differ on that. Still, if it happened to be so that all ABIs allowed returning two pointers, it would mean that a C callback could return the same TypedArray struct that is used to pass them in as parameters. (I'm skipping considerations of ownership, lifetime, copying, memory management and all that because it gets hard and ruins my idea pretty well :D )
On Strings: It turns out that as long as one keeps a pointer to the Isolate somewhere, it's already possible to support String parameters in fast calls, at least to a limited and possibly unstable degree. See this PR of mine:
https://github.com/denoland/deno/pull/16014Essentially, if a parameter is declared as v8::Value then it will happily accept a String as well, and with the Isolate pointer it is then possible to write the string data out. I'm unsure of the safety of this, I expect it should panic on roped strings as V8 flattens them but so far I've not seen clear evidence of that happening.
I personally think that a limited C string return only -kind of string support would not be a good idea. As an example, I expect that the Chrome / Blink team would find good use for returning of UTF8 strings in atob / btoa and TextDecoder APIs. (And so would Deno.) Again here I ponder on the possibility of the option #2 above.
About Deno's interest in Fast API in general: I'm not part of the Deno team, and am only contributing to the FFI and a little bit on the core ops (JS <-> Rust binding) layer so I cannot truly speak for what the team considers important and am just speaking for myself. That being said:
1. Deno's FFI API relies heavily on Fast API. Every foreign library's symbol (C function) that a user wants to use will by default use the Fast API. Only symbols that call back into V8 need to / should opt out of this using a "callback" boolean option.
Adding more supported types to Fast API directly adds to wider and better support for Deno FFI. As an example, currently returning of 64 bit integers (eg. pointers) is done via a TypedArray out pointer, where the pointer is written into. If returning of External objects was possible, this out pointer system and its (slight) performance overhead could be removed. (And most importantly, numbers-as-pointers insecurity could be eliminated.)
Returning of C strings would allow Deno FFI to have "native" support of those (currently C string extraction is done via a separate method).
2. Deno's ops layer has recently moved to using Fast API by default where possible. Deno's binding functions are written as normal Rust functions and an ops macro takes care of writing the binding logic to V8's FunctionTemplate.
Due to the near-universality of the ops macro, any Fast API binding logic needs to only be written once and the macro will take care of taking setting up the bindings for all ops that are bindable. Thus, here even more than with FFI, having more supported types leads near-automatically to faster binding layer in Deno, which is very much of interest to the Deno team.
Some examples:
* FFI might not benefit from Strings as parameters that much, since foreign APIs would only expect C strings. Deno ops however very much would like to get arbitrary (UTF8) strings in fast calls. They would also love to return arbitrary UTF8 strings.
* FFI only cares about returning pointers in some form, External being the most logical. Deno ops would very much want to return TypedArrays of varying sizes, and they would not mind being explicit about memory management either.
* ops have cases where eg. a String or TypedArray parameter might be optional. Overloads are already supported to a degree, but eg. null parameters in the middle currently are not supported directly (except as v8::Values which I'm not sure if it would ruin the "better typed" overload matching)
* (Completely impossible stretch goal): Some ops take objects of some given shape. If V8 were to match its JS object shapes to a declared parameter struct shape, now that would be impossibly cool. Also, probably too hard to feasibly do but a man can dream.
This has become a massive, meandering writeup. Sorry about that.
Back onto topic: If you can give me some pointers on where I should look to add the External<JSExternalObject> stuff for, I would much appreciate it. I would personally also prefer to write the code such that the C callback receives not the v8::External object but is directly called with the pointer that the External represents. This I expect to require some changes in the lowering code.
Thank you for your time
-Aapo Alasuutari