| |

Dart 58 🎯 Structs, Native Memory Allocation, and Pointers in Dart FFI

⏱️ Estimated read: 19 min | |

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.

AnnotationDart typeWidth
@Int8()int1
@Uint8()int1
@Int16()int2
@Uint16()int2
@Int32()int4
@Uint32()int4
@Int64()int8
@Uint64()int8
@Float()double4
@Double()double8
@Bool()bool1
@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

AnnotationDart typeWidth
@Int8()int1
@Uint8()int1
@Int16()int2
@Uint16()int2
@Int32()int4
@Uint32()int4
@Int64()int8
@Uint64()int8
@Float()double4
@Double()double8
@Bool()bool1
@Pointer<T>()Pointer<T>pointer
@Array(n)Array<T>n × width

The Memory Functions

FunctionPurpose
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

OperationPurpose
p.valueRead or write the value
p.refAccess struct fields
p[i]Access the nth struct
p.elementAt(i)Pointer to element i
p.addressThe raw address
p.cast<T>()Reinterpret the type
Pointer.fromAddress(n)From a raw address
nullptrThe null pointer

The Struct Functions

FunctionPurpose
sizeOf<T>()The struct’s size
alignOf<T>()The struct’s alignment
@Packed(n)Control the alignment

The Common Patterns

PatternAllocationAccess
Single structcalloc<Point>()p.ref.x
Array of structscalloc<Point>(3)p[0].x
Struct with arraycalloc<Buffer>()b.ref.data[0]
Nested structcalloc<Rectangle>()r.ref.topLeft.x
Pointer fieldcalloc<Node>()n.ref.next.ref.value
Scopedarena<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

PitfallWhy It HappensFix
Layout mismatchWrong annotationMatch the native type
Size mismatchMissing @PackedAdd the annotation
Memory leakNot freedCall malloc.free
Use after freeFreed then usedFree at the end
Wrong valueWrong field orderMatch the native order
Misaligned accessCast without alignmentAlign to the type
Arena not releasedMissing releaseAllUse try/finally
Null dereferenceNo null checkCheck 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

ItemValue
StructThe 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.refAccess struct fields
p[i]Access the nth struct
p.cast<T>()Reinterpret the pointer
ArenaScoped allocation

Key takeaways:

  • A struct is a declaration, not an allocation. The Struct class describes the layout, and the memory is allocated separately with malloc or calloc. 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 sizeOf and alignOf functions verify the layout.
  • The @Packed annotation 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 @Array annotation declares an inline array. The array is part of the struct, not a pointer to a separate allocation. The Array<T> type provides indexed access.
  • Native memory is allocated and freed manually. The malloc and calloc functions allocate, and the malloc.free function releases. The garbage collector does not manage the native heap.
  • The Arena simplifies the lifetime. Allocations made through the arena are freed together with releaseAll, 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. The cast<T>() method reinterprets the pointer, and the developer is responsible for the alignment.
  • The ref property accesses struct fields. A struct pointer’s ref property 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.


Was this helpful?


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!