dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.23k stars 1.57k forks source link

[vm/ffi] Make `FfiNative`s more concise #50097

Open dcharkes opened 2 years ago

dcharkes commented 2 years ago

Our current syntax for "ffi-natives" is still a bit verbose.

@FfiNative<Int64 Function(Int64, Int64)>('sum')
external int sum(int a, int b);

1. Rename FfiNative to Native

We could consider @Ffi<...> and @Native<...>.

sum is an external function. sum is a native function. sum is not an "ffi" function, the mechanism for calling sum is "ffi".

This makes it most natural to change the syntax to:

@Native<Int64 Function(Int64, Int64)>('sum')
external int sum(int a, int b);

Courtesy of @mit-mit and @mkustermann.

2. Make the symbol optional

If the symbol in C is identical to the Dart symbol, we don't need to repeat it.

@Native<Int64 Function(Int64, Int64)>()
external int sum(int a, int b);

Wish list. Don't write dart types

The native types uniquely define the Dart types, so it would be nice if we could omit the Dart types

@Native()
external Int64 sum(Int64 a, Int64 b);

@Native()
external Void foo(Pointer<Int64> a, Int64 b);

However, we would need some kind of transformation before any Dart static analysis runs to replace the native types with the corresponding Dart code. Otherwise, calling a function with Dart integers will not pass type checks.

We would likely need macros for this, if our approach for macros could already support this.

Misc

The old natives are deprecated, so we don't have to worry about naming collision (having to designate the new natives as "new native" or "ffi native"). FWIW the syntax of the deprecated old natives:

@pragma("vm:external-name", "File_OpenStdio")
external static int _openStdio(int fd);

See d8d7af15ce9e4eb78c8aac0dfa46413036747bb7.

mraleph commented 2 years ago

Wish list. Don't write dart types

I have been thinking about this and discussing this with @leafpetersen couple of times. My original thinking was that we could use views for this, though I don't think the current version of views has all necessary features.

If CFE retained typedef reference in the AST rather than performed full substitution then we could simply write:

// dart:ffi
typedef Void = void;
// ...
typedef Int32 = int;
typedef Int64 = int;
typedef Float = double;
typedef Double = double;

// ...

@Native()
external Int64 sum(Int64 a, Int64 b);

@Native()
external Void foo(Pointer<Int64> a, Int64 b);

Though unfortunately right now CFE is going the opposite way (/cc @johnniwinther @chloestefantsova) and is considering to delete TypedefType from the public AST. This node is not used for non-function type aliases anyway.

Wdestroier commented 2 years ago

@mraleph Java Native Access works with reflections to get the return and parameter types. Considering static metaprogramming is a replacement for dart:mirrors, and the equivalent of reflections in Dart is dart:mirrors, I believe static metaprogramming would be the most correct feature to help solve this problem.

mraleph commented 2 years ago

@Wdestroier I don't think static metaprogramming is going to help us much here. It can't change signatures of the existing methods it can only generate new methods. The problem we are facing here does not really exist in Java - as Java has the full zoo of numeric types. In Dart however there is just int and double.

leafpetersen commented 2 years ago

@mraleph is the primary shortcoming you see with views the inability to use literals (or in general values) of the representation type as initializers? Or are there other places it falls short?

mraleph commented 2 years ago

@leafpetersen I think if we had a way to declare conversions both ways as implicit (both to and from representation type) & CFE preserved view type, rather than fully erasing it, then I think views would do the job.

Essentially we want the following to be a valid code:

@Native()
external Int64 sum(Int64 a, Int64 b);

int useSum(List<int> v) {
  return v.reduce((a, b) => sum(a, b));
  // Maybe even the following, but probably okay if this does not type check. 
  // return v.reduce(sum);
}

In other words, we don't really need much of advanced view functionality - what we want is a typedef that survives in the AST representation until backend.

Wdestroier commented 2 years ago

Similar to allowing the overload of implicit and explicit is and as operators...

dcharkes commented 2 years ago

@leafpetersen I think if we had a way to declare conversions both ways as implicit (both to and from representation type) & CFE preserved view type, rather than fully erasing it, then I think views would do the job.

Essentially we want the following to be a valid code:

@Native()
external Int64 sum(Int64 a, Int64 b);

int useSum(List<int> v) {
  return v.reduce((a, b) => sum(a, b));
  // Maybe even the following, but probably okay if this does not type check. 
  // return v.reduce(sum);
}

In other words, we don't really need much of advanced view functionality - what we want is a typedef that survives in the AST representation until backend.

This is not really the full picture. We also use the native types (currently subtypes of NativeType) for other types of type checks. For example it is now allowed to assign a Pointer<Int8> to a Pointer<Int16>. If we were using typedefs, these assignments would not be flagged anymore by the type system.

Moreover, we'd have to change Pointer's type argument bound to be Object. Unless views can implement another type. Then we could let those views implement NativeType.

(Side note: we want to stop reifying the type argument of Pointer at runtime (https://github.com/dart-lang/sdk/issues/49935). However, we want to keep all static checks that we currently get from using native types as type arguments.)

leafpetersen commented 2 years ago

@mraleph

declare conversions both ways as implicit (both to and from representation type)

My general take is that if we want to support this, it should probably be a separate first class feature that can be used for any type.

& CFE preserved view type, rather than fully erasing it

If I understand correctly, this is more of an implementation detail around how the CFE represents these?

@dcharkes

  • Do view disable assignment to other views on the same underlying type?

Yes.

  • Can views implement another type?

Views can be made subtypes of other view types. I'm not sure if that covers your use case or not. We have considered allowing views to implement class types in the case that the underlying representation type also implements that class type. That is currently not in the proposal though.

dcharkes commented 2 years ago
  1. Rename FfiNative to Native

We need to add @Native and deprecate @FfiNative so that we don't break dart:ui in Flutter on the roll. Then after a roll, we can migrate dart:ui and then remove @FfiNative.

dcharkes commented 2 years ago

If I'm going to modify the API, I might as well take the assets into account as well. I've created https://github.com/dart-lang/sdk/issues/49803#issuecomment-1283716761 with an API design (1) and (2).

johnniwinther commented 2 years ago

View types will be part of the AST since we need them to do static reasoning on precompiled code. The implementation will be similar to the current implementation of the experimental ExtensionType:

class ViewType extends DartType {
  final View view;
  final List<DartType> typeArguments;
  final DartType representationType;

  ...
}

where representationType is the type used at runtime. For instance for

view class View<T>(T it) {}
View<int> method() => ...

the representation type of View<int> will by int.