Dart 58 🎯 Structs, Native Memory Allocation, and Pointers in Dart FFI
The previous chapter introduced dart:ffi as the bridge between Dart and native code. It covered loading a library, looking up a function, and calling it with simple types. This chapter goes deeper into the three mechanisms that make FFI practical for real libraries: structs, which describe composite native types; native memory allocation, which provides the buffers that native functions read and write; and pointers, which are the addresses that connect the Dart side to the native side. These three are inseparable. A struct without memory is a declaration; memory without a pointer is unreachable; a pointer without a type is unsafe. Together they are what lets a Dart application pass a complex data structure to a C function and read the result back.
Key point: A Struct in Dart FFI is a declaration, not an allocation. The declaration describes the layout; the allocation provides the bytes; the pointer connects them. A struct instance is always accessed through a pointer, and the pointer’s type determines how the bytes are interpreted.
Why structs, memory, and pointers are inseparable
The composite-type problem. A C function rarely takes only integers and doubles. It takes a struct with multiple fields: a configuration object, a message header, a pixel with color channels. The Dart side must describe the same layout, byte for byte, or the native function reads garbage.
The layout problem. A C struct’s layout is determined by the field types, their order, and the platform’s alignment rules. A struct with an int8_t followed by an int32_t is not five bytes; it is eight, because the int32_t is aligned to a four-byte boundary. The Dart declaration must match the native layout exactly, and the @Packed annotation controls the alignment when the native code uses a packed struct.
The allocation problem. The Dart runtime manages its own memory, and the native code does not know about it. A struct must be allocated on the native heap with malloc or calloc, and the memory must be freed with malloc.free. The garbage collector does not see the native heap, so the developer is responsible for the lifetime.
The pointer problem. A struct is always accessed through a Pointer<Struct>. The ref property dereferences the pointer and returns the struct’s fields. The elementAt method returns a pointer to the nth struct in an array. The address getter returns the raw address, which is what the native function expects.
The array problem. A struct often contains an array field: a fixed-size buffer, a list of points, a table of entries. The @Array annotation declares the array, and the Array<T> type provides indexed access.
The alignment problem. The alignment of a struct is determined by its largest field. A struct with a double is aligned to eight bytes; a struct with an int32_t is aligned to four. The @Packed annotation overrides this, and the mismatch with the native code is a common source of bugs.
The ownership problem. A pointer does not carry ownership information. The developer must know whether the pointer is owned by the Dart side (and must be freed), owned by the native side (and must not be freed), or borrowed for the duration of a call. The convention varies by library, and the documentation is the only guide.
a. Declaring structs
A Dart FFI struct is a class that extends Struct. The fields are declared as external with an FFI type annotation.
The basic struct
import 'dart:ffi';
final class Point extends Struct {
@Int32()
external int x;
@Int32()
external int y;
}
The class extends Struct, and the fields are declared with the external keyword and the @Int32() annotation. The layout is two 32-bit integers, eight bytes total.
The field annotations
Every field has an FFI type annotation that describes its native type.
| Annotation | Dart type | Width |
|---|---|---|
@Int8() | int | 1 |
@Uint8() | int | 1 |
@Int16() | int | 2 |
@Uint16() | int | 2 |
@Int32() | int | 4 |
@Uint32() | int | 4 |
@Int64() | int | 8 |
@Uint64() | int | 8 |
@Float() | double | 4 |
@Double() | double | 8 |
@Bool() | bool | 1 |
@Pointer<T>() | Pointer<T> | pointer |
The annotation is not optional. Without it, the field has no native type, and the struct cannot be used with FFI.
The struct with a pointer
A struct field can be a pointer to another type.
final class Node extends Struct {
@Int32()
external int value;
@Pointer<Node>()
external Pointer<Node> next;
}
The next field is a pointer to another Node, which is how a linked list is represented in C.
The struct with an array
An array field is declared with @Array(n) and the type is Array<T>.
final class Buffer extends Struct {
@Uint32()
external int length;
@Array(256)
external Array<Uint8> data;
}
The struct is 260 bytes: four for the length and 256 for the array. The array is inlined into the struct, not a pointer to a separate allocation.
The nested struct
A struct field can be another struct.
final class Point extends Struct {
@Int32()
external int x;
@Int32()
external int y;
}
final class Rectangle extends Struct {
@Int32()
external int id;
external Point topLeft;
external Point bottomRight;
}
The Rectangle struct contains two Point structs inlined. The total size is four bytes for the id plus eight for each point, which is twenty bytes.
The packed struct
The @Packed annotation controls the struct’s alignment. A packed struct has no padding between fields.
@Packed(1)
final class PackedStruct extends Struct {
@Uint8()
external int a;
@Uint32()
external int b;
}
Without @Packed, the struct would be eight bytes because the Uint32 would be aligned to a four-byte boundary. With @Packed(1), the struct is five bytes.
The size and alignment
The sizeOf and alignOf functions return the struct’s size and alignment.
print(sizeOf<Point>()); // 8
print(alignOf<Point>()); // 4
print(sizeOf<PackedStruct>()); // 5
The functions are used to verify that the Dart struct matches the native one.
The struct layout verification
The Struct class provides a sizeOf check at runtime. The VM verifies the layout when the struct is first used, and a mismatch produces an error.
// The VM checks the layout.
final p = calloc<Point>();
The check catches the common mistakes: a missing annotation, a wrong field order, a wrong type.
b. Allocating native memory
Native memory is allocated with malloc or calloc from package:ffi. The functions return a Pointer<T>, and the memory is freed with malloc.free.
The malloc
The malloc<T>() function allocates uninitialized memory for a single value.
import 'package:ffi/ffi.dart';
final p = malloc<Int32>();
p.value = 42;
print(p.value); // 42
malloc.free(p);
The memory is not zeroed. The value is whatever the heap contained before.
The calloc
The calloc<T>() function allocates zero-initialized memory.
final p = calloc<Int32>();
print(p.value); // 0
calloc.free(p);
The memory is zeroed before it is returned. It is slightly slower than malloc but safer.
The array allocation
Both functions accept a count to allocate an array.
final arr = malloc<Int32>(10);
final zeros = calloc<Int32>(10);
The array is a contiguous block of count * sizeOf<T>() bytes.
The struct allocation
A struct is allocated the same way.
final p = calloc<Point>();
p.ref.x = 10;
p.ref.y = 20;
calloc.free(p);
The ref property dereferences the pointer and returns the struct’s fields. The fields are read and written directly.
The array of structs
An array of structs is allocated with a count.
final points = calloc<Point>(3);
points[0].x = 1;
points[1].x = 2;
points[2].x = 3;
calloc.free(points);
The [] operator on a Pointer<Struct> returns a Struct reference at the given index.
The free
The memory is freed with the same allocator’s free method.
malloc.free(p);
calloc.free(points);
The free method must match the allocator. Mixing malloc and calloc.free is undefined behavior.
The asTypedList
The asTypedList method creates a typed data list view over the native memory.
final arr = malloc<Uint8>(1024);
final list = arr.asTypedList(1024);
list[0] = 42;
malloc.free(arr);
The list shares the native memory, so writing through the list modifies the native buffer. The list must not outlive the native allocation.
The elementAt
The elementAt method returns a pointer to the element at an index.
final arr = malloc<Int32>(10);
final third = arr.elementAt(3);
third.value = 42;
malloc.free(arr);
The method is equivalent to Pointer.fromAddress(arr.address + 3 * sizeOf<Int32>()).
The memory lifetime
Native memory does not have a garbage collector. The developer must free it when it is no longer needed. A missed free is a leak; a double free or a use after free is undefined behavior.
The arena pattern
The Arena class from package:ffi provides a scope-based allocation. All allocations made through the arena are freed together.
import 'package:ffi/ffi.dart';
void process() {
final arena = Arena();
try {
final p1 = arena<Int32>();
final p2 = arena<Point>();
// Use p1 and p2.
} finally {
arena.releaseAll();
}
}
The arena simplifies the lifetime management. Every allocation made through the arena is freed when releaseAll is called.
c. Working with pointers
A Pointer<T> is the address of a native value. The type parameter determines how the bytes are interpreted.
The value getter and setter
The value property reads and writes the pointed-to value.
final p = malloc<Int32>();
p.value = 42;
print(p.value); // 42
The property is available on pointers to primitive types. On a Pointer<Struct>, the ref property is used instead.
The ref property
The ref property dereferences a struct pointer and returns the struct’s fields.
final p = calloc<Point>();
p.ref.x = 10;
print(p.ref.x); // 10
The property is the primary way to access a struct’s fields.
The address getter
The address getter returns the raw address as an integer.
final p = malloc<Int32>();
print(p.address.toRadixString(16));
The getter is used when the address must be passed to a native function as an integer.
The fromAddress constructor
The Pointer.fromAddress constructor creates a pointer from a raw address.
final p = Pointer<Int32>.fromAddress(0x12345678);
The constructor is used when a native function returns an address as an integer.
The cast
The cast<T>() method reinterprets the pointer as a different type.
final p = malloc<Uint8>(1024);
final intPtr = p.cast<Int32>();
intPtr.value = 42;
The cast does not change the address; it changes the type. The developer is responsible for the alignment and the size.
The nullptr
The nullptr constant is a pointer with address zero.
Pointer<Int32>? p = nullptr;
The constant is used to represent a null pointer, and it is the value returned by native functions that fail.
The Pointer.fromFunction
The Pointer.fromFunction method creates a native function pointer from a Dart function.
typedef NativeCallback = Void Function(Int32);
typedef DartCallback = void Function(int);
void onEvent(int value) {
print('Event: $value');
}
final callback = Pointer.fromFunction<NativeCallback>(onEvent, nullptr);
The Dart function must be top-level or static. The second argument is the value returned if the Dart function throws.
The array access
The [] operator on a struct pointer returns a struct reference at the given index.
final points = calloc<Point>(3);
points[0].x = 1;
points[1].x = 2;
points[2].x = 3;
The operator is available on Pointer<T extends Struct> and on pointers to primitive types.
The string pointer
A Pointer<Utf8> is the native string type. The toDartString method reads it.
final name = 'World'.toNativeUtf8();
try {
final p = greet(name);
print(p.toDartString());
} finally {
malloc.free(name);
}
The toNativeUtf8 method allocates the native string, and the toDartString method reads it back. The native string must be freed.
The pointer to a pointer
A pointer to a pointer is used when a native function returns a pointer through an out parameter.
final pp = malloc<Pointer<Int32>>();
try {
nativeFunction(pp);
final p = pp.value;
print(p.value);
} finally {
malloc.free(pp);
}
The pp.value reads the pointer that the native function stored.
The null check
A pointer returned by a native function may be null. The check is done by comparing the address to zero.
final p = nativeFunction();
if (p.address == 0) {
throw Exception('Native function returned null');
}
The nullptr constant can also be used, but the address comparison is the explicit form.
Complete Example Session
// ============================================
// PART 1: THE BASIC STRUCT
// ============================================
import 'dart:ffi';
import 'package:ffi/ffi.dart';
final class Point extends Struct {
@Int32()
external int x;
@Int32()
external int y;
}
void main() {
final p = calloc<Point>();
p.ref.x = 10;
p.ref.y = 20;
print('${p.ref.x}, ${p.ref.y}'); // 10, 20
calloc.free(p);
}
// ============================================
// PART 2: THE SIZE AND ALIGNMENT
// ============================================
void main() {
print(sizeOf<Point>()); // 8
print(alignOf<Point>()); // 4
}
// ============================================
// PART 3: THE STRUCT WITH AN ARRAY
// ============================================
final class Buffer extends Struct {
@Uint32()
external int length;
@Array(256)
external Array<Uint8> data;
}
void main() {
final b = calloc<Buffer>();
b.ref.length = 4;
b.ref.data[0] = 0xDE;
b.ref.data[1] = 0xAD;
b.ref.data[2] = 0xBE;
b.ref.data[3] = 0xEF;
print(b.ref.data[0].toRadixString(16)); // de
calloc.free(b);
}
// ============================================
// PART 4: THE NESTED STRUCT
// ============================================
final class Rectangle extends Struct {
@Int32()
external int id;
external Point topLeft;
external Point bottomRight;
}
void main() {
final r = calloc<Rectangle>();
r.ref.id = 1;
r.ref.topLeft.x = 0;
r.ref.topLeft.y = 0;
r.ref.bottomRight.x = 100;
r.ref.bottomRight.y = 50;
print('${r.ref.bottomRight.x}, ${r.ref.bottomRight.y}'); // 100, 50
calloc.free(r);
}
// ============================================
// PART 5: THE PACKED STRUCT
// ============================================
@Packed(1)
final class Packed extends Struct {
@Uint8()
external int a;
@Uint32()
external int b;
}
void main() {
print(sizeOf<Packed>()); // 5
}
// ============================================
// PART 6: THE ARRAY OF STRUCTS
// ============================================
void main() {
final points = calloc<Point>(3);
try {
points[0].x = 1;
points[1].x = 2;
points[2].x = 3;
for (var i = 0; i < 3; i++) {
print(points[i].x);
}
} finally {
calloc.free(points);
}
}
// ============================================
// PART 7: THE POINTER TO A STRUCT
// ============================================
final class Node extends Struct {
@Int32()
external int value;
@Pointer<Node>()
external Pointer<Node> next;
}
void main() {
final first = calloc<Node>();
final second = calloc<Node>();
try {
first.ref.value = 1;
first.ref.next = second;
second.ref.value = 2;
second.ref.next = nullptr;
print(first.ref.value); // 1
print(first.ref.next.ref.value); // 2
} finally {
calloc.free(first);
calloc.free(second);
}
}
// ============================================
// PART 8: THE CAST
// ============================================
void main() {
final bytes = malloc<Uint8>(4);
try {
final intPtr = bytes.cast<Int32>();
intPtr.value = 0x12345678;
print(bytes[0].toRadixString(16)); // 78 (little-endian)
} finally {
malloc.free(bytes);
}
}
// ============================================
// PART 9: THE ARENA
// ============================================
void main() {
final arena = Arena();
try {
final p1 = arena<Point>();
final p2 = arena<Point>();
p1.ref.x = 10;
p2.ref.x = 20;
print('${p1.ref.x}, ${p2.ref.x}'); // 10, 20
} finally {
arena.releaseAll();
}
}
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. Declare the struct with the Struct class.
// 2. Annotate every field with its native type.
// 3. Allocate with calloc for zeroed memory.
// 4. Access the fields through the ref property.
// 5. Use the [] operator for arrays of structs.
// 6. Cast pointers with the cast<T>() method.
// 7. Free the memory with the matching free.
// 8. Use the Arena for scoped allocations.
// 9. Verify the size with sizeOf<T>().
// 10. Match the native struct layout exactly.
The ten parts showed the basic struct, the size and alignment, the struct with an array, the nested struct, the packed struct, the array of structs, the pointer to a struct, the cast, the arena, and the complete pattern.
Quick Reference
The Struct Annotations
| Annotation | Dart type | Width |
|---|---|---|
@Int8() | int | 1 |
@Uint8() | int | 1 |
@Int16() | int | 2 |
@Uint16() | int | 2 |
@Int32() | int | 4 |
@Uint32() | int | 4 |
@Int64() | int | 8 |
@Uint64() | int | 8 |
@Float() | double | 4 |
@Double() | double | 8 |
@Bool() | bool | 1 |
@Pointer<T>() | Pointer<T> | pointer |
@Array(n) | Array<T> | n × width |
The Memory Functions
| Function | Purpose |
|---|---|
malloc<T>() | Allocate uninitialized |
calloc<T>() | Allocate zero-initialized |
malloc<T>(n) | Allocate an array |
malloc.free(p) | Free the memory |
arena<T>() | Scoped allocation |
arena.releaseAll() | Free all arena allocations |
The Pointer Operations
| Operation | Purpose |
|---|---|
p.value | Read or write the value |
p.ref | Access struct fields |
p[i] | Access the nth struct |
p.elementAt(i) | Pointer to element i |
p.address | The raw address |
p.cast<T>() | Reinterpret the type |
Pointer.fromAddress(n) | From a raw address |
nullptr | The null pointer |
The Struct Functions
| Function | Purpose |
|---|---|
sizeOf<T>() | The struct’s size |
alignOf<T>() | The struct’s alignment |
@Packed(n) | Control the alignment |
The Common Patterns
| Pattern | Allocation | Access |
|---|---|---|
| Single struct | calloc<Point>() | p.ref.x |
| Array of structs | calloc<Point>(3) | p[0].x |
| Struct with array | calloc<Buffer>() | b.ref.data[0] |
| Nested struct | calloc<Rectangle>() | r.ref.topLeft.x |
| Pointer field | calloc<Node>() | n.ref.next.ref.value |
| Scoped | arena<Point>() | p.ref.x |
Best Practices
✅ Do This:
// Annotate every field with its native type.
final class Point extends Struct {
@Int32() external int x;
@Int32() external int y;
} // ✅
// Use calloc for zero-initialized memory.
final p = calloc<Point>(); // ✅
// Access fields through the ref property.
p.ref.x = 10; // ✅
// Free the memory when done.
calloc.free(p); // ✅
// Use sizeOf to verify the layout.
print(sizeOf<Point>()); // ✅
// Use @Packed for packed structs.
@Packed(1) // ✅
// Use the Arena for scoped allocations.
final arena = Arena(); // ✅
// Check for nullptr after a native call.
if (p.address == 0) throw Exception(); // ✅
❌ Don’t Do This:
// Don't forget the annotation.
final class Point extends Struct {
external int x; // ❌ No native type
}
// Don't mix allocators.
final p = malloc<Int32>();
calloc.free(p); // ❌ Wrong free
// Don't use a freed pointer.
malloc.free(p);
p.value = 42; // ❌ Use after free
// Don't assume the layout.
// Use sizeOf to verify. // ❌
// Don't cast without alignment.
final p = malloc<Uint8>(4);
p.cast<Double>().value = 1.0; // ❌ Misaligned
// Don't forget @Packed when the native struct is packed.
// The layout will not match. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Layout mismatch | Wrong annotation | Match the native type |
| Size mismatch | Missing @Packed | Add the annotation |
| Memory leak | Not freed | Call malloc.free |
| Use after free | Freed then used | Free at the end |
| Wrong value | Wrong field order | Match the native order |
| Misaligned access | Cast without alignment | Align to the type |
| Arena not released | Missing releaseAll | Use try/finally |
| Null dereference | No null check | Check the address |
Real-World Examples
1. Basic Struct
final class Point extends Struct {
@Int32() external int x;
@Int32() external int y;
}
2. Allocate a Struct
final p = calloc<Point>();
3. Access Fields
p.ref.x = 10;
4. Free Memory
calloc.free(p);
5. Struct with Array
final class Buffer extends Struct {
@Uint32() external int length;
@Array(256) external Array<Uint8> data;
}
6. Array of Structs
final points = calloc<Point>(3);
7. Nested Struct
final class Rectangle extends Struct {
external Point topLeft;
external Point bottomRight;
}
8. Pointer Field
final class Node extends Struct {
@Pointer<Node>() external Pointer<Node> next;
}
9. The Arena
final arena = Arena();
final p = arena<Point>();
arena.releaseAll();
10. Verify Size
print(sizeOf<Point>());
Visual
The Struct Declaration and Allocation
┌────────────────────────────────────────────────────────────┐
│ STRUCT DECLARATION vs ALLOCATION │
│ │
│ DECLARATION │
│ ┌──────────────────────────────────────────────────┐ │
│ │ final class Point extends Struct { │ │
│ │ @Int32() external int x; │ │
│ │ @Int32() external int y; │ │
│ │ } │ │
│ │ │ │
│ │ This describes a layout. No memory is allocated. │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ │ calloc<Point>() │
│ v │
│ ALLOCATION │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ┌───────────────┬───────────────┐ │ │
│ │ │ x (4 bytes) │ y (4 bytes) │ │ │
│ │ └───────────────┴───────────────┘ │ │
│ │ A native heap block of 8 bytes. │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ │ p.ref.x = 10 │
│ v │
│ ACCESS │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ┌───────────────┬───────────────┐ │ │
│ │ │ 10 │ 0 │ │ │
│ │ └───────────────┴───────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Struct Layout and Alignment
┌────────────────────────────────────────────────────────────┐
│ STRUCT LAYOUT AND ALIGNMENT │
│ │
│ WITHOUT @Packed: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ class { @Uint8() int a; @Uint32() int b; } │ │
│ │ │ │
│ │ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ │
│ │ │ a │pad│pad│pad│ b │ b │ b │ b │ │ │
│ │ └───┴───┴───┴───┴───┴───┴───┴───┘ │ │
│ │ Size: 8 bytes. The Uint32 is aligned to 4. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ WITH @Packed(1): │
│ ┌──────────────────────────────────────────────────┐ │
│ │ @Packed(1) class { @Uint8() int a; @Uint32() int b; }│ │
│ │ │ │
│ │ ┌───┬───┬───┬───┬───┐ │ │
│ │ │ a │ b │ b │ b │ b │ │ │
│ │ └───┴───┴───┴───┴───┘ │ │
│ │ Size: 5 bytes. No padding. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The native code's layout must match the Dart declaration. │
└────────────────────────────────────────────────────────────┘
The Pointer and Ref
┌────────────────────────────────────────────────────────────┐
│ POINTER AND REF │
│ │
│ final p = calloc<Point>(); │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ DART STACK │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ p: Pointer<Point> │ │ │
│ │ │ address: 0x7f... │ │ │
│ │ └──────────┬───────────┘ │ │
│ └─────────────┼──────────────────────────────────────┘ │
│ │ │
│ │ points to │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ NATIVE HEAP │ │
│ │ ┌───────────────┬───────────────┐ │ │
│ │ │ x (4 bytes) │ y (4 bytes) │ │ │
│ │ └───────────────┴───────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ p.ref.x = 10; │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The ref property dereferences the pointer and │ │
│ │ returns the struct's fields. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Array of Structs
┌────────────────────────────────────────────────────────────┐
│ ARRAY OF STRUCTS │
│ │
│ final points = calloc<Point>(3); │
│ │
│ NATIVE HEAP: │
│ ┌─────────────────┬─────────────────┬─────────────────┐ │
│ │ Point 0 │ Point 1 │ Point 2 │ │
│ │ x=1, y=2 │ x=3, y=4 │ x=5, y=6 │ │
│ └─────────────────┴─────────────────┴─────────────────┘ │
│ offset 0 offset 8 offset 16 │
│ │
│ ACCESS: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ points[0].x = 1 │ │
│ │ points[1].x = 3 │ │
│ │ points[2].x = 5 │ │
│ │ │ │
│ │ The [] operator returns the struct at the index. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
Struct | The base class for native structs |
@Int32() etc. | The field type annotations |
@Array(n) | A fixed-size array field |
@Packed(n) | Controls the struct’s alignment |
sizeOf<T>() | The struct’s size in bytes |
alignOf<T>() | The struct’s alignment |
malloc<T>() | Allocate uninitialized memory |
calloc<T>() | Allocate zero-initialized memory |
malloc.free(p) | Free the memory |
p.ref | Access struct fields |
p[i] | Access the nth struct |
p.cast<T>() | Reinterpret the pointer |
Arena | Scoped allocation |
Key takeaways:
- A struct is a declaration, not an allocation. The
Structclass describes the layout, and the memory is allocated separately withmallocorcalloc. The pointer connects the two. - Every field needs a type annotation. The
@Int32(),@Uint8(), and the rest describe the native type. A missing annotation means the field has no native layout, and the struct cannot be used with FFI. - The struct layout must match the native layout exactly. The field types, the field order, and the alignment all matter. The
sizeOfandalignOffunctions verify the layout. - The
@Packedannotation controls the alignment. A packed struct has no padding between fields, and the annotation is required when the native code uses a packed struct. - The
@Arrayannotation declares an inline array. The array is part of the struct, not a pointer to a separate allocation. TheArray<T>type provides indexed access. - Native memory is allocated and freed manually. The
mallocandcallocfunctions allocate, and themalloc.freefunction releases. The garbage collector does not manage the native heap. - The
Arenasimplifies the lifetime. Allocations made through the arena are freed together withreleaseAll, which avoids the individual frees. - A pointer’s type determines the interpretation. The
Pointer<T>type parameter tells the Dart side how to read the bytes. Thecast<T>()method reinterprets the pointer, and the developer is responsible for the alignment. - The
refproperty accesses struct fields. A struct pointer’srefproperty returns the fields, and the[]operator on a pointer to a struct returns the struct at the index.
Remember: Structs, native memory, and pointers are the three mechanisms that make FFI practical. A Struct declaration describes the layout, the malloc or calloc allocation provides the bytes, and the Pointer<T> connects the Dart side to the native side. Every field is annotated with its native type, the layout must match the native code exactly, and the @Packed annotation controls the alignment. The @Array annotation declares an inline array, and the ref property accesses the fields. The memory is freed manually, and the Arena simplifies the lifetime. The result is a Dart application that can pass a complex data structure to a C function and read the result back, byte for byte.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!