skip to content

In Dart FFI, how do you map a C struct and pass a Flutter image's pixel buffer to a C function taking pointers?

level: middleimportance: should knowfreq 22%

answer

  1. final class extends Struct
  2. external fields with @Int32() annotations
  3. pointer.ref for native-backed structs
  4. asTypedList to fill native bytes
  5. Uint8List.address only in leaf calls

basics

~20 s

Declare a final class extending Struct whose external fields carry native-type annotations such as @Int32(), allocate it and reach it through pointer.ref, and copy pixels into native memory via asTypedList, or pass Uint8List.address directly to a leaf @Native call.

solid answer

~40 s

A C `struct { int32_t width, height, stride; }` becomes `final class ImgzInfo extends Struct` with `@Int32() external int width;` and so on: fields must be `external` because they read and write bytes at fixed offsets, and field order must match the header. You never call a constructor: allocate with `arena<ImgzInfo>()` and fill it through `.ref`, or use `Struct.create()` (Dart 3.4) for a struct backed by Dart memory, useful for by-value arguments. Pixel data from a decoded image is a `Uint8List`; to hand C a `const uint8_t*`, allocate `arena<Uint8>(len)` and copy with `pointer.asTypedList(len).setAll(0, pixels)`. For a short **leaf** `@Native` call, Dart 3.5's `pixels.address` passes a pointer straight into the Dart list without copying, valid only as a whole argument of that call. Out-parameters are `Pointer<Pointer<Uint8>>` and `Pointer<Size>`, read back with `.value`.

code

dart · 20 lines
dart
import 'dart:ffi';

final class ImgzInfo extends Struct {
  @Int32()
  external int width;

  @Int32()
  external int height;

  @Int32()
  external int stride;
}

@Native<Int32 Function(Pointer<Uint8>, Pointer<ImgzInfo>, Int32,
    Pointer<Pointer<Uint8>>, Pointer<Size>)>()
external int imgz_compress_jpeg(Pointer<Uint8> rgba, Pointer<ImgzInfo> info,
    int quality, Pointer<Pointer<Uint8>> outData, Pointer<Size> outLen);

@Native<Void Function(Pointer<Uint8>)>()
external void imgz_free(Pointer<Uint8> data);

go deeper

for a junior

Recall that C structs become final classes extending Struct with annotated external fields, reached through pointer.ref.

for a middle

Explain layout rules, ABI-specific types like Size, out-parameters with Pointer<Pointer<T>>, and copying pixels via asTypedList.

for a senior

Weigh copying pixels into native memory against Uint8List.address in leaf calls, and debug corrupted results caused by layout mismatches.

for a principal

Decide where image buffers live, Dart heap or native heap, across a pipeline of decode, edit and compress steps, balancing copies against ownership complexity.

## Native types versus Dart types `dart:ffi` describes C data with **native types** such as `Int32`, `Uint8`, `Double`, `Size`, `Pointer<T>` and `Void`. They exist only in signatures and annotations; values that reach Dart are ordinary `int`, `double` or `Pointer` objects. ABI-specific types such as `Size`, `Long` and `Int` follow the C type's width on each platform. ## Mapping a struct For the header ```c typedef struct { int32_t width; int32_t height; int32_t stride; } ImgzInfo; ``` the Dart side is: ```dart final class ImgzInfo extends Struct { @Int32() external int width; @Int32() external int height; @Int32() external int stride; } ``` Rules from the `Struct` documentation: 1. Fields are **`external`**: their getters and setters read and write bytes at computed offsets in the backing memory, so ordinary Dart fields are not allowed. 2. Numeric fields carry an annotation naming the native type; pointer, struct and array fields use their own types. 3. **Declaration order defines layout**; it must match the C header, including nested structs and fixed-size arrays (`@Array(16)`). 4. Instances come from `Pointer<ImgzInfo>.ref` (native memory), `Struct.create()` (Dart memory, Dart 3.4), FFI return values and callback arguments, never from a generative constructor. ## Passing pixel data A decoded Flutter image arrives as bytes, for example from `ui.Image.toByteData(format: ui.ImageByteFormat.rawRgba)`, turned into a `Uint8List`. C wants a `const uint8_t*`. Three options: | Approach | Copy? | Constraint | |---|---|---| | Allocate native memory, copy with `asTypedList(len).setAll(0, pixels)` | yes, once | works with any call, leaf or not, and any thread | | `pixels.address` as an argument (Dart 3.5) | no | only as a whole argument of a **leaf** `@Native` call; the GC will not move the list during that call | | Keep the pixels in native memory from the start | no | the buffer is owned manually for its whole life | The `.address` route is attractive for large images, but a leaf call must be short and non-blocking. Compressing a 12-megapixel photo is neither, so the copy into native memory followed by a normal call, run away from the UI isolate, is the usual design. ## Out-parameters and reading results C functions often return results through pointers: - `uint8_t** out_data` becomes `Pointer<Pointer<Uint8>>`: allocate one slot, pass it, then read `outData.value` to get the buffer pointer. - `size_t* out_len` becomes `Pointer<Size>`: read `outLen.value`. - To view the result, `outData.value.asTypedList(len)` creates a `Uint8List` **view** over native memory; copy it before the buffer is freed. ## Common layout mistakes - Declaring a field as `@Int64()` where C has `int32_t`: every later field is read at the wrong offset. - Using `@Int32()` for `size_t` or `long`: correct on some ABIs, wrong on others; use `Size` or `Long`. - Forgetting a field the C header has, which shifts everything after it. - Treating `Pointer<Uint8>` as nullable: C `NULL` arrives as a pointer whose `address` is 0, compared with `nullptr`, not as Dart `null`. Generating the struct classes with `ffigen` from the same header avoids all four.

  • Why can't a Struct subclass declare an ordinary Dart field such as String label?
    A struct object is a view over a block of native or Dart-heap bytes; its external fields are compiled into reads and writes at fixed offsets. There is nowhere in that block to store a Dart object reference, so only native-typed external fields are allowed. Keep extra Dart state in a separate wrapper class.
  • When would you use Struct.create() instead of allocating a Pointer<ImgzInfo>?
    When the struct is passed or returned by value, or only needed briefly in Dart: `Struct.create()` (Dart 3.4) backs it with Dart-heap memory that the garbage collector manages, so there is nothing to free. When C needs a pointer to the struct, allocate native memory and use `.ref`.

saying these in an interview costs you the question

  • Declares struct fields as ordinary non-external Dart fields
  • Calls a generative constructor to create a Struct instance
  • Maps size_t to Int32 on every platform
  • Passes Uint8List.address to a long-running non-leaf call
  • Expects C NULL to arrive in Dart as null