Open ds84182 opened 1 year ago
Dart's FFI has been intentionally designed in a way that allows us to optimize the code to make it as efficient as in C (haven't done all optimizations yet, but we can do them).
More specifically, it means code has to specify the concrete (non-generic) type it's operating on: for reading memory, writing memory, calling sizeOf<...>()
- so those can be directly translated into memory loads/stores (or constants in case of sizeOf<...>()
).
What you're trying to do, making a generic growable array/vector implementation over arbitrary native types, would not be possible in C. (It would be possible in C++ when utilizing templates - which a C++ compiler will just code-duplicate for the template parameters. Though Dart generics work very differently from C++ templates - they don't use code duplication, so they cannot be used.)
So with the current FFI, we can think how would we implement a generic container implementation in C and then translate that to Dart. C code may look like this
// Define `vector_t` as our vector type
vector_t* vector_new(intptr_t element_size);
void vector_push(void* value);
void* vector_get(intptr_t index);
void vector_set(intptr_t index, void* value);
void vector_free(vector_t*);
// Use as this
struct MyStruct { ... }
vector_t* v = vector_new(sizeof(MyStruct));
MyStruct value;
vector_push(&value);
MyStruct* value2 = (MyStruct*)vector_get(0);
...
vector_free(v);
One could translate this into Dart
class Vector<T> {
final int elementSize;
// Dynamically allocated & grown.
Pointer<Void> memory;
int length;
int allocatedSize;
Vector(this.elementSize) { ... }
Pointer<T> operator[](int index) => Pointer<T>.fromAddress(memory.address + elementSize*index);
void operator[](Pointer<T> value) {
memcpy(memory.address + elementSize*index, value.address, elementSize);
}
}
main() {
final vector = Vector<MyStruct>(sizeOf<MyStruct>());
vector.push(...);
vector[0];
}
The downside is that it will take pointers to elements and return pointers to elements rather than have value/copy-semantics. That makes in particular insertions less ideal as one would need to e.g. have a global Pointer<MyStruct>
that one can initialize and then give to Vector
:
final box = allocate<MyStruct>(sizeOf<MyStruct>());
main() {
final vector = Vector<MyStruct>(sizeOf<MyStruct>());
initialize(vector);
read(vector);
}
void foo(Vector<MyStruct> vector) {
box.ref
..fieldA = ...
..fieldB = ...;
vector.push(box);
}
void read(Vector<MyStruct> vector) {
MyStruct struct = vector[0].ref;
...
}
To avoid this for built-in types / primitives one could make specialized versions e.g. VectorInt8
instead of Vector<Int8>
.
Would that work for you (even it's a little less convenient than in C++)?
While it works, I'd really like to avoid exposing the underlying pointer to elements in the container. The part I'm struggling with is making WinRT & Win2D bindings appear as the high-level API it already is without accruing heaps of simple objects in memory. In C# the runtime's array of structs are C-compatible which avoids this dreaded overhead for otherwise simple types like Vector2.
For example, take CanvasDrawingSession.DrawGlyphRun. CanvasGlyphs has 3 float fields and 1 int field. In an ideal world I'd be able to wrap this with a List interface that optimizes down to writing struct fields without actually exposing a pointer to native memory. But to even get something close to this, I have to generate multiple classes & extensions per struct. And even then, the ergonomics still aren't great.
At its root, I'm struggling to make myListOfVector2[i].X += 3.0
feel like you're interacting with a Dart class without also incurring the overhead of a List<Vector2>
. And without generating tons of top-level members & exposing raw pointers so mixins can do the translation between native and Dart.
After spending hours trying and failing to implement an array type across thousands of auto-generated structs, I give up! Instead I'm writing yet-another-feature-request (sorry)
This is designed to avoid exposing the underlying pointer, which may be dangerous to do. It also separates indexing and mutability.
As an example, here's a vector type.
It's certainly better than nothing. Because right now, pain.