Description
Problem
The representation of structs in the existing API leaves much to be desired:
C:
struct Str {
int thing;
};
Dart:
@struct
class Str extends Pointer<Str> {
@IntPtr()
int thing;
}
Struct classes are defined as pointers, which conflates the concepts of the struct as a composite value and as a pointer to memory. When by-value structs are supported, there would be no (type-level) way to differentiate between value structs and pointers to structs. For example, the following two signatures in C would correspond to the same signature in Dart:
C:
void thing1(Str* x);
void thing2(Str x);
Dart:
typedef Void Function(Str) thing1Type;
typedef Void Function(Str) thing2Type;
Since struct classes are pointers, they feature the load()
and store()
operations. Unfortunately, load()
and store()
load/store a pointer recursively (since the target type of the load is Str
, which is itself a pointer), and that's incorrect. Rather, load()
should return a reference to the struct, as in C++:
Dart:
Str x = // ...
Str y = x.load(); // Loads "thing" from x as a Pointer, obviously incorrect.
C:
Str* x = //...
Str y = *x; // Loads a reference from "x" (and dereferences it to populate "y").
Pointer has a corresponding storage class in the VM defined (RawPointer
), but can be subclassed by user code. We have to prohibit (non-FFI) fields from the subclasses to ensure the memory layout is consistent, but this design works against the VM architecture and there are complications with serialization and GC.
New API
Defining structs
Struct definitions are changed as follows:
// Base class of all structs.
class Struct<S> {
final Pointer<S> addressOf;
};
// Example struct.
struct Str extends Struct<Str> {
@Int8()
int thing;
};
The type argument to Struct<S>
will be removed when we can define addressOf
as an extension method on Struct
.
Changes to Pointer
We will disallow subclassing or implementing Pointer (#35782). We will also replace load()
and store()
to reduce syntactic overhead for working with structs and distinguish between loading a value vs. a reference from pointers (#37284):
class Pointer<T extends NativeType> {
// ...
dynamic get val; // Cannot be used if T extends Struct.
void set val(dynamic _); // Cannot be used if T extends Struct.
T get ref; // Can *only* be used if T extends Struct.
// ...
}
val
and ref
will become extension methods when extension methods are supported.
Accessing fields
Instead of representing pointers, instances of the Struct
subclasses will represent references. A reference is always backed by a pointer, but exposes additional methods on top of it. load()
(ref
) against a pointer to a struct returns a reference backed by the receiving pointer. When "by-value" structs are supported, it will also be possible to store()
(val=
) an entire struct into a pointer by passing in a reference. For example:
Dart:
Pointer<Str> x = Pointer.allocate<Str>();
x.thing; // Not allowed.
x.ref.thing; // Allowed.
x.ref.thing = 1; // Allowed
Str xref = x.ref;
Str y = someFn();
x.val = y; // After #36730
x.free();
xref.thing; // Undefined behavior: backing pointer was released.
Other changes
This change in struct representation allows us to fix other warts of our API:
allocate
andfromAddress
will be constructors, since they cannot return a more precise type thanPointer<T>
.- Subclassing or implementing
Pointer
will not be allowed ([vm/ffi] Allow subclassing Pointer? #35782). - Subclassing or implementing structs or other
NativeType
s (besidesStruct
itself) will not be allowed ([vm/ffi] struct inheritance #37298). - User-defined constructors in struct (reference) classes will not be called when references are manufactured by
ref
([vm/ffi] Constructing struct classes from load()/ref in new structs API #37363). - The built-in "null" value for
Pointer
s will be a valuenullptr
separate from Dart'snull
, with an address of 0 ([vm/ffi] Pointer<Null> #37362, dart:ffi canonical representation of C null pointer in Dart #35756). - We will not generate setters for
final
fields in structs.
Migration
There are other use-cases for extending Pointer
besides structs, which will need to be handled differently in the new API. For example, the CString
class currently looks like:
class CString extends Pointer<Uint8> {
static String fromUtf8(CString str) { ... }
factory CString(String str) { ... }
}
In the new API, this might instead look like:
class Utf8 extends Struct<Utf8> {
@ffi.Uint8();
int char;
String toString();
static Pointer<Utf8> fromString(String str);
}
When extension methods are available, this could be written more gracefully as follows:
class Utf8 extends Struct<Utf8> {
@ffi.Uint8();
int char;
}
extension ConvertToUtf8 on String {
Pointer<Utf8> toUtf8();
}
extension ConvertFromUtf8 on Pointer<Utf8> {
String toString();
}