-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[vm/ffi] Allocate structs by default with TypedData #45697
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
Comments
So a couple of thoughts from my perspective:
One question on my mind still is... how will we pass TypedData-backed structs by reference through FFI? Sometimes it would be great if I could just... alloca the struct temporarily on the stack. |
I think the simplest solution would be to address #53418. final class MyStruct extends Struct {
@Int8()
external int a0;
external Pointer<Int8> a1;
external factory MyStruct.fromTypedList(Uint8List typedList);
factory MyStruct.named({
required int a0,
required Pointer<Int8> a1,
}) {
final typedList = Uint8List(sizeOf<MyStruct>());
final result = MyStruct.fromTypedList(typedList);
result.a0 = a0;
result.a1 = a1;
return result;
}
} That will require the smallest amount of magic in the Dart SDK. I'm happy to consider other designs if they serve use cases. |
Playing around with a possible API more. Currently, we disallow calling constructors for Also, the extension method on pointer gives us a type-safe If we want to design an API only around typed data, we could possibly do the following with some magic in the CFE: abstract final class NativeType {
const NativeType();
}
abstract final class _Compound extends NativeType {
@pragma("vm:entry-point")
final Object _typedDataBase;
_Compound._([TypedData? typedList]) : _typedDataBase = typedList!;
}
abstract base class Struct extends _Compound {
/// Construct a reference to the [typedList].
///
/// Use [StructPointer]'s `.ref` to gain references to native memory backed
/// structs.
Struct([TypedData? typedList]) : super._(typedList);
}
final class MyStruct extends Struct {
MyStruct([TypedData? typedList]);
@Int8()
external int a;
}
final class MyStruct2 extends Struct {
@Int8()
external int a;
}
void main() {
// Allocate a struct by allocating a `TypedData` backing the size of the struct:
MyStruct();
MyStruct2();
// Using an existing typed data:
final typedData = Uint8List(sizeOf<MyStruct>());
MyStruct(typedData);
} The CFE will then rewrite the constructors of user-defined structs/unions to: final class MyStruct extends Struct {
MyStruct([TypedData? typedList])
: super(typedList ?? Uint8List(sizeOf<MyStruct>()));
@Int8()
external int a;
}
final class MyStruct2 extends Struct {
MyStruct2() : super(Uint8List(sizeOf<MyStruct>()));
@Int8()
external int a;
} Note that I propose we don't add magic for field values but rely on writing factory constructors. (Writing field names types in the arguments would be boilerplate allready, so we might as well do the assignments as boilerplate too in that case and have no magic in the compiler. That is better for understandability.) final class MyStruct3 extends Struct {
factory MyStruct3({int? a}) {
final result = MyStruct3._();
if (a != null) {
result.a = a;
}
return result;
}
factory MyStruct3.required({
required int a,
}) {
final result = MyStruct3._();
result.a = a;
return result;
}
factory MyStruct3.fromTypedList(TypedData typedList) {
return MyStruct3._(typedList);
}
MyStruct3._([TypedData? typedList]);
@Int8()
external int a;
} If we want to be able to have the no-name factory constructor for users, then we need to take an arbitrary constructor that has an optional Maybe we should require marking the constructor as external: final class MyStruct3 extends Struct {
external MyStruct3.arbitraryName([TypedData? typedList]);
} The only requirements would be (1) Notable design decisions:
Edit: We probably also want to generate an assert that checks that typed data byte length is long enough in the constructor rewrite. |
cc @lrhn @mkustermann Any thoughts about the API? |
So the goal is to allow, but not require, a user written subclass of Such subclasses are currently not constructable. They have a default constructor which cannot be invoked - and are not allowed to declare a (non factory?) constructor. And we want to add this in a way that does not break existing code, which means that a class with no constructor declaration must remain valid. That sounds to me like or would be solved by giving external Struct([TypedData typedData, int offset = 0]); Any subclass can choose to pass an argument to that constructor. Because the parameter is not nullable, every invocation will be statically detectable as either passing a typed data or not, which might be useful. And as suggested, calling without a typed data value will allocate one just big enough to hold the size of the current struct.
If so, the struct will refer to the bytes starting at that position. The offset is there because it can be. If the argument is a Also, it seems useful to be able to do var size = sizeOfMyStruct;
var structs = [for (var i = 0; i + size < bytes.length; I += size)
MyStruct(bytes, i)]; Instead of having to allocate a view per struct. This would be a minimal change, and requires absolutely no special casing in the subclasses. They can use, or not use, the ability to pass a byte buffer to the superclass, and prevent users from instantiating by not existing a generative constructor. It does mean that any existing struct can now be allocated using the default constructor. Which shouldn't be a problem. The one thing I can't help wondering about is whether allocated structs should be extension types. Everything about this feature sounds like a view on ... something. But a view on "bytes", not an object. On a pointer, it's just that the pointer it's not a Dart object itself, so it doesn't fit extension types directly. (Also, if we put pointers into typed data to native code, I assume we're sure GC won't move the bytes.) |
We do have views already, so why introduce The code snippet would be the following: final Uint8List typedList;
final size = sizeOf<MyStruct>();
final buffer = typedList.buffer;
var structs = [for (var i = 0; i + size <buffer.length / size; i += 1)
MyStruct(Uint8List.view(buffer, typedList.offsetInBytes + i * size))]; If we think that's too verbose, shouldn't we think about adding view methods to final Uint8List bytes;
final size = sizeOf<MyStruct>();
final structs = [for (var i = 0; i + size < bytes.length; i += size)
MyStruct(bytes.view(i))];
We could theoretically make structs an extension types of TypedData and use Though, wouldn't trying to make structs extension types be a huge breaking change? How would we go about such a thing?
We would use typed data views, which are updated when the GC moves things. |
That would be nice, but extension types (aka inline classes) are pure syntactic sugar around an underlying type - the CFE lowers them to the underlying type. But our FFI struct classes can be backed either by a
The struct instances don't expose their address. One can have a |
@lrhn I'm not entirely sure I understand this. We can't have an optional positional non-nullable argument right?
I was thinking to not have users write the super invocation manually, but to have a transformation fill in the details. Invoking the super constructor manually requires the super constructor to be public, but I don't want users to write We need a CFE transformation anyway for when the user does not write the constructor (because the body needs to be filled in with allocating a typed-data of the right Did I misunderstand what you proposed @lrhn ? |
I remember someone sayiong that creating a new view is too much overhead. In some context. (Was that you, Martin?) var fooSize = sizeof<FooStruct>();
var buffer = Uint8List(500 * fooSize);
var structs = [for (var i = 0; i < 500; i++) FooStruct(buffer, i * fooSize)]; seems nicer (and more efficient than) than
We can. The parameter just need a default value, which you can't see in an abstract or external function, because that's an implementation detail. It was deliberate to make the first parameter non-nullable. If you pass a first argument at all, it has to be a buffer, you can't pass a nullable value where the receiver needs to determine whether it was ... But it might make it hard for the user to declare a constructor which forwards either zero or one argument, so it should probably be nullable. I don't know if that makes things harder later.
I'm usually in the "favore explicit over implicit" camp.
If it's an abstract type, they can't. Even if it's not an abstract class, it should be "simple" to just prohibit calling the constructors directly, so the compiler complains eagerly if someone does that. I assume most of these classes are heavily special-cased in the VM compiler.
then the user can't hold it wrong, but they also can't see what they're supposed to do. Would the transformation allow:
and there are things which cannot be done as
We can choose to support all (except the last three), some, or none of these variations with a transformation on an And since the super-constructor can be called with zero arguments, an implicit default-constructor of So, my thought, which may be naive, would be that the super-invocation, It would indeed still only remove some magic, but it moves the magic below the user-code, instead of on top of it. |
Okay, you have convinced me. Less magic is better.
We cannot supply a default value in the patch file, because it would need to be Let me give this approach a spin. @mkustermann Are you concerned about typed data views not being optimized away as said above? Edit: structs don't save their offset internally either. So calling it with offset != 0 will create a view. We should ensure such views get optimized away. |
Discussion from https://dart-review.googlesource.com/c/sdk/+/342763/13/tests/ffi/structs_typed_data_test.dart, we should consider using toplevel methods rather than super constructors to indicate that objects are also instantiated in other ways than the user defined constructor. class Point extends Struct {
factory Point() {
final result = create<Point>();
// ...
}
factory Point.fromTypedData(TypedData typedData) {
final result = create<Point>(typedData);
// ...
}
} This would absolutely make it clear that no user-constructors are run on |
Moving the discussion from cl/342763 to here. While thinking about this more, I have some reasons to think using constructors may not be ideal:
For these reasons I think the concept of generative constructors on native types is a little iffy and it may make more sense to provide a |
We have decided to go with Downsides that we find acceptable:
final class Coordinate extends Struct {
Coordinate({double? x, double? y}) {
if (x != null) this.x = x;
if (y != null) this.y = y;
}
Coordinate.fromTypedList(super.typedList);
// ...
}
// vs
final class Coordinate extends Struct {
factory Coordinate({double? x, double? y}) {
final result = Struct.create<Coordinate>();
if (x != null) result.x = x;
if (y != null) result.y = y;
return result;
}
factory Coordinate.fromTypedList(TypedData typedList) {
return Struct.create<Coordinate>(typedList);
}
// ...
} Full diff for the interested: https://dart-review.googlesource.com/c/sdk/+/342763/14..16 The upsides of (Also, if we'd wanted to explore making the structs actual extension types, in a potential |
This reverts commit c2e15cf. Reason for revert: #54754 Version skew somewhere in the analyzer/dartdoc/flutter combination. We need to land the fix inside ffi_verifier.dart first, and then reland the API docs that trigger the code path in the analyzer that throws the exception. Original change's description: > [vm/ffi] Introduce `Struct.create` and `Union.create` > > Structs and unions can now be created from an existing typed data > with the new `create` methods. > > The typed data argument to these `create` methods is optional. If > the typed data argument is omitted, a new typed data of the right > size will be allocated. > > Compound field reads and writes are unchecked. (These are > TypedDataBase loads and stores, rather than TypedData loads and stores. > And Pointers have no byte length.) Therefore the `create` method taking > existing TypedData objects check whether the length in bytes it at > least the size of the compound. > > TEST=pkg/analyzer/test/src/diagnostics/creation_of_struct_or_union_test.dart > TEST=pkg/vm/testcases/transformations/ffi/struct_typed_data.dart > TEST=tests/ffi/structs_typed_data_test.dart > TEST=tests/ffi/vmspecific_static_checks_test.dart > > Closes: #45697 > Closes: #53418 > > Change-Id: If12c56106c7ca56611bccfacbc1c680c2d4ce407 > CoreLibraryReviewExempt: FFI is a VM and WASM only feature. > Cq-Include-Trybots: luci.dart.try:vm-aot-android-release-arm64c-try,vm-aot-android-release-arm_x64-try,vm-aot-linux-debug-x64-try,vm-aot-linux-debug-x64c-try,vm-aot-mac-release-arm64-try,vm-aot-mac-release-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try,vm-aot-win-debug-arm64-try,vm-aot-win-debug-x64c-try,vm-aot-win-release-x64-try,vm-appjit-linux-debug-x64-try,vm-asan-linux-release-x64-try,vm-checked-mac-release-arm64-try,vm-eager-optimization-linux-release-ia32-try,vm-eager-optimization-linux-release-x64-try,vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64c-try,vm-ffi-qemu-linux-release-arm-try,vm-ffi-qemu-linux-release-riscv64-try,vm-fuchsia-release-x64-try,vm-kernel-linux-debug-x64-try,vm-kernel-precomp-linux-release-x64-try,vm-linux-debug-ia32-try,vm-linux-debug-x64-try,vm-linux-debug-x64c-try,vm-mac-debug-arm64-try,vm-mac-debug-x64-try,vm-msan-linux-release-x64-try,vm-reload-linux-debug-x64-try,vm-reload-rollback-linux-debug-x64-try,vm-ubsan-linux-release-x64-try,vm-win-debug-arm64-try,vm-win-debug-x64-try,vm-win-debug-x64c-try,vm-win-release-ia32-try > Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/342763 > Commit-Queue: Daco Harkes <[email protected]> > Reviewed-by: Johnni Winther <[email protected]> > Reviewed-by: Lasse Nielsen <[email protected]> > Reviewed-by: Martin Kustermann <[email protected]> Change-Id: I285dc39946b5659219b37a1d8f10de479133957e Cq-Include-Trybots: luci.dart.try:vm-aot-android-release-arm64c-try,vm-aot-android-release-arm_x64-try,vm-aot-linux-debug-x64-try,vm-aot-linux-debug-x64c-try,vm-aot-mac-release-arm64-try,vm-aot-mac-release-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try,vm-aot-win-debug-arm64-try,vm-aot-win-debug-x64c-try,vm-aot-win-release-x64-try,vm-appjit-linux-debug-x64-try,vm-asan-linux-release-x64-try,vm-checked-mac-release-arm64-try,vm-eager-optimization-linux-release-ia32-try,vm-eager-optimization-linux-release-x64-try,vm-ffi-android-debug-arm-try,vm-ffi-android-debug-arm64c-try,vm-ffi-qemu-linux-release-arm-try,vm-ffi-qemu-linux-release-riscv64-try,vm-fuchsia-release-x64-try,vm-kernel-linux-debug-x64-try,vm-kernel-precomp-linux-release-x64-try,vm-linux-debug-ia32-try,vm-linux-debug-x64-try,vm-linux-debug-x64c-try,vm-mac-debug-arm64-try,vm-mac-debug-x64-try,vm-msan-linux-release-x64-try,vm-reload-linux-debug-x64-try,vm-reload-rollback-linux-debug-x64-try,vm-ubsan-linux-release-x64-try,vm-win-debug-arm64-try,vm-win-debug-x64-try,vm-win-debug-x64c-try,vm-win-release-ia32-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/349061 Commit-Queue: Daco Harkes <[email protected]> Bot-Commit: Rubber Stamper <[email protected]> Reviewed-by: Zach Anderson <[email protected]> Reviewed-by: Martin Kustermann <[email protected]>
When working with a C API that takes structs by value users need to
malloc
structs in C memory before they can pass them to a C function call, and remember to free the afterwards.Instead, we should explore changing the API so that the default is that we create structs backed by
TypedData
rather than allocating them in C memory.The same argument can be made for
Array
s.TODO: Design an API taking the following into account:
The text was updated successfully, but these errors were encountered: