-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
dart:ffi: Support variadic C functions #38578
Comments
Package cupertino_ffi contains the necessary helpers (NSString, reference counting, etc.) for using any Objective-C library, but inability to call the variadic C function |
@terrier989 note that you can just bind to |
@mraleph |
Cool project @terrier989! /cc @sjindel-google |
It sounds like variadic functions are supported as well as possible, given that Dart does not have variadic functions (except for |
We could support variadic functions with a List as argument in the Dart signature, if we really wanted to, but I'm not sure we should. |
We can't support that actually, because the length of the list and the type of arguments in the list needs to be fixed when we compile the trampolines. |
Couldn't we make a generic trampoline like we had in DBC to accomodate for that? It's just going to be relatively slow... |
I don't see need for passing a List. This is a calling convention issue. In X64, the trampoline used by 'dart:ffi' passes an incorrect integer in AH (8 bits of RAX) to varargs C functions. But it's an implementation detail of In ARM64, the standard non-varargs ABI passes the first 8 arguments in registers X0-X7, whereas the standard varargs ABI passes all arguments in the stack. However, it turns out that Apple decided to diverge from the ARM64 standard and if I interpreted the document correctly, it's possible to call a varargs function with an ordinary trampoline. |
Fair point @terrier989 - I was under impression that varargs calling convention is the same as non-varargs, which turns out is not the case. Things would work on X64 as long as you don't need to pass any double values down to the vararg function. |
In order to distinguish the varargs calling convention from the non varargs calling convention in We have some options of doing this:
They are all equivalent in the sense that they communicate the same information for the creation of trampolines. If we think about carrying function pointers around in the program, then option 1 allows us to express which calling convention is used: Pointer<NativeVarArgsFunction<... Function(...)>> fptr = // ... (N.b. if there are more calling convention variations we want to take into account at some point, that might influence on how we deal with multiple calling conventions. Would we ever want to support a range of 32-bit calling conventions?) cc @mkustermann Edit: As discussed offline: vararg callbacks do not really make sense. |
If variadic C functions is unsupported, could inline function generation be an option? For example, #include <fcntl.h>
static inline int hidctl_open (const char *__path, int __oflag) {
return open(__path, __oflag);
} |
Why inline? Wouldn't you want to generate non-inline functions so that you can actually bind to them from Dart? Depending on how many variadic functions you're looking at, it might be worthwhile to make a PR to |
I'm working on https://github.com/Sunbreak/logic_conf.dart, a re-implementation of https://github.com/libusb/hidapi The linux implementation use Is it possible to write the wrap code manually? |
The
Can you just bind to The actually problem here is that
So if This applies to most syscalls. |
Could you shed some light on how to bind with 2 or 3 parameters? Or maybe a plain declaration just works? final int Function(Pointer<Int8> __path, int __oflag) nativeOpen2 = libfcntl
.lookup<NativeFunction<Int32 Function(Pointer<Int8>, Int32)>>("open")
.asFunction();
final int Function(Pointer<Int8> __path, int __oflag, int __ext) nativeOpen3 = libfcntl
.lookup<NativeFunction<Int32 Function(Pointer<Int8>, Int32, Int32)>>("open")
.asFunction(); |
Yes. On ia32/arm/arm64 the varargs calling convention for this signature seems to be the same. Only x64 is slightly problematic because it normally requires |
Seems to work. Thanks a lot // FIMXE https://github.com/dart-lang/sdk/issues/38578
// Need manully change name from `open2` to `open` after `ffigen`
extern int open2 (const char *__file, int __oflag) __nonnull ((1)); int open2(
ffi.Pointer<ffi.Int8> __file,
int __oflag,
) {
return (_open2 ??= _dylib.lookupFunction<_c_open2, _dart_open2>('open'))(
__file,
__oflag,
);
} |
Originally posted by @aam in #49460 (comment)
|
// int ioctl(int, unsigned long, ...);
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
typedef IOCtlNative = Int32 Function(Int32, Int64, Pointer<Void>);
typedef IOCtlDart = int Function(int, int, Pointer<Void>);
final TIOCGWINSZ = Platform.isMacOS ? 0x40087468 : 0x5413;
const STDIN_FILENO = 0;
const STDOUT_FILENO = 1;
const STDERR_FILENO = 2;
// struct winsize {
// unsigned short ws_row; /* rows, in characters */
// unsigned short ws_col; /* columns, in characters */
// unsigned short ws_xpixel; /* horizontal size, pixels */
// unsigned short ws_ypixel; /* vertical size, pixels */
// };
class WinSize extends Struct {
@Int16()
external int ws_row;
@Int16()
external int ws_col;
@Int16()
external int ws_xpixel;
@Int16()
external int ws_ypixel;
}
void main() {
final ioctl = DynamicLibrary.process().lookupFunction<IOCtlNative, IOCtlDart>('ioctl');
final winSizePointer = calloc<WinSize>();
final result = ioctl(STDOUT_FILENO, TIOCGWINSZ, winSizePointer.cast());
print('result is $result');
final winSize = winSizePointer.ref;
print('Per ioctl, this console window has ${winSize.ws_col} cols and '
'${winSize.ws_row} rows.');
calloc.free(winSizePointer);
}
|
When doing FFI calls to variadic functions we need a binding for a specific amount of arguments on the Dart side. We need to generate machine code ahead of time for the trampolines. Moreover, we need a way to signify from which argument the variadic arguments start. API design 1We could introduce a marker for native signatures as follows: /// `int printf(const char *format, ...)` with `int` and `double` as varargs.
typedef NativePrintfIntDouble =
Int Function(Pointer<Char>, VarArgs<Int>, Double); The /// Represents the start of varargs in C.
///
/// The signatures in [NativeFunction] need to specify the exact types used for
/// FFI calls.
///
/// For example take calling `printf` in C.
///
/// ```c
/// int printf(const char *format, ...);
///
/// void call_printf() {
/// int a = 4;
/// double b = 5.5;
/// const char* format = "...";
/// printf(format, a, b);
/// }
/// ```
///
/// To call `printf` directly from Dart with those two argument types, define
/// the native type as follows:
///
/// ```dart
/// /// `int printf(const char *format, ...)` with `int` and `double` as
/// /// varargs.
/// typedef NativePrintfIntDouble =
/// Int Function(Pointer<Char>, VarArgs<Int>, Double);
/// ```
///
/// Note how [VarArgs] signals where the variadic arguments start and the all
/// the arguments passed are covered.
///
/// [VarArgs] is not constructible in the Dart code and serves purely as marker
/// in type signatures.
@Since('2.19')
abstract class VarArgs<T extends NativeType> extends NativeType {} API design 2Alternatively, we could add a marker before the first variadic argument rather than "around" it: /// `int printf(const char *format, ...)` with `int` and `double` as varargs.
typedef NativePrintfIntDouble =
Int Function(Pointer<Char>, VarArgs, Int, Double); Pro:
Con:
|
@dcharkes Maybe we could consider an alternative design which outlaws direct closure calls and instead relies on static dispatch, e.g. @FfiNative<IntPtr Function(Pointer<Char>, VarArg)>
external int printf(Pointer<Char> fmt, List<Object> args);
printf("%s %d", [nativeValue1, nativeValue2]); // ok
printf("%s %d", someArray); // error If somebody needs closure calls we could provide a helper method in FFI for calling variadic functions: Pointer<NativeFunction<IntPtr Function(Pointer<Char>, VarArg)>> printfPtr;
callVariadic(printfPtr, ["%s %d", nativeValue1, nativeValue2]) If we go this route then we can use local types to lower the call-site (just like it would be in C). |
I believe this will not work, because we need the native types for the arguments at compile time to generate the correct trampoline. (I don't believe every ABI always uses only stack arguments. Some ABIs use the same registers as for regular arguments.) So with @FfiNative<Int Function(Pointer<Char>, VarArgs<Int>, Double))>
external int printf(Pointer<Char> fmt, int, double); Edit: A native signature type to signifying where var-args start works with both FfiNatives and DynamicLibrary. (I agree that we should move in the direction of more FfiNatives.) |
@dcharkes Oh right. For integers and doubles we need the right size. Maybe then: @FfiNative<IntPtr Function(Pointer<Char>, VarArg)>
external int printf<NativeType /* extends Record */>(Pointer<Char> fmt, List<Object> args); Users are expected to do: printf<(Int32, Double)>("%d %lf", [a, b]) Maybe we could go further and say that the second argument does not have to be a constant array, but rather a tuple of a compatible Dart type? printf<(Int32, Double)>("%d %lf", (a, b)); // ok
(int, double) v;
printf<(Int32, Double)>("%d %lf", v); // ok too |
That could work, records essentially give us a way to specify arbitrary types without a function type. :-) Based on the ABI logic I've seen, we would still need to compile a trampoline for each different call-site signature. (Which requires some refactoring of the current implementation.) There are still some downsides though:
Pros:
I don't understand. The arguments never have to be constants right? Only the type arguments denoting the native types have to be. The example you give is normal tuple syntax right, so they are both an expression not a constant. |
Sorry, I've chosen a wrong word. What I meant to say is that for The record based invocation has more freedom, because we know number and types of arguments from the record type itself. Though to simplify the implementation we probably still want to outlaw indirect calls to vararg functions and instead require people to use a special helper to invoke such functions indirectly. |
We can use record types: /// `int printf(const char *format, ...)` with `int` and `double` as varargs.
typedef NativePrintfIntDouble =
Int Function(Pointer<Char>, VarArgs<(Int, Double)>); |
Potentially blocked by We need at least to be able to traverse the types in the VM. /// `int printf(const char *format, ...)` with `int` and `double` as varargs.
@FfiNative<Int Function(Pointer<Char>, VarArgs<(Int, Double)>)>
external int printf(Pointer<Char>, int, double); DynamicLibrary d;
final f = d.lookupFunction<Int Function(Pointer<Char>, VarArgs<(Int, Double)>),
int Function(Pointer<Char>, int, double)>(); |
@munificent this forces us to use class VarArgs<T extends Object> ... instead of class VarArgs<T extends Record> ... Not really a big issue IMO, but it requires some more special casing in the documentation and implementation. |
Since records are supported now (if experiments flag is enabled) it seems this issue is unblocked now, right? One interesting thing to think about is how this would work for bindings generation from |
Correct, and I happen to have implemented this over the holidays: https://dart-review.googlesource.com/c/sdk/+/276921 |
We went back and forth on single-field records. They were removed at one point, but we added them back in and the feature does support them now, along with a |
Do we also support unary record types though? tests/ffi/function_callbacks_varargs_generated_test.dart:109:63: Error: Expected ')' before this.
typedef VariadicAt1Int64x2Type = Int64 Function(Int64, VarArgs<(Int64)>);
^ I do not get similar errors for nullary or multi-field records. |
Yes. To be consistent with unary record expressions (and to potentially allow parenthesized types if we later get more complex type expressions), we require unary record types to also have a trailing comma: typedef VariadicAt1Int64x2Type = Int64 Function(Int64, VarArgs<(Int64,)>); (I don't know if the implementations support it yet, but that's the correct syntax.) |
On ARM64 macos and ios, when varargs are used, the first vararg blocks all cpu and fpu registers. On Windows x64, when varargs are used, floating point arguments are passed _both_ in the integer and double register. The Windows logic requires a new kind of native location: `BothNativeLocations`, which signals that a value needs to be copied to both locations before an FFI call, and can be copied from any of the two locations when getting an FFI callback. TEST=runtime/vm/compiler/ffi/unit_tests/variadic_double/x64_win.expect Note that integer arguments already block out the corresponding xmm registers on Windows x64. On System-V, an upper bound of the number of XMM registers used must be passed in AL. (Not reflected in the unit tests here, but will be in the dependent CL.) On ARM (32 bit), using varargs forces the calling convention to be in softfp mode even on hardfp supported devices. On RISC-V, the FPU registers are blocked when using varargs. TEST=runtime/vm/compiler/ffi/native_calling_convention_test.cc Test outputs in: runtime/vm/compiler/ffi/unit_tests/variadic_* Run test with `tools/test.py ffi_unit`. Bug: #38578 Change-Id: Ic568f8156c1c28ac3d6a2144805edf8caaa0169c Cq-Include-Trybots: luci.dart.try:vm-precomp-ffi-qemu-linux-release-arm-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/278342 Reviewed-by: Ryan Macnak <rmacnak@google.com>
Bug: #38578 Change-Id: I4b98ac3d23a9e009a15af6cf165481fa80ec2d7d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/278343 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
If "dart:ffi" supported varargs calling conventions in X64/ARM64, developers could use Objective-C libraries by using C functions such as
objc_msgSend
.Related issues:
The text was updated successfully, but these errors were encountered: