skip to content

FFI Bindings

dart:ffi calls C directly: DynamicLibrary or @Native bindings, Pointer types, memory from package:ffi, and ffigen-generated bindings built by hooks. Interviewers probe who frees memory.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Dart FFI, who frees native memory when Flutter code allocates with calloc or an Arena and a C library returns its own buffers?

level: seniorimportance: must knowfreq 32%

answer

  1. GC never frees native memory
  2. whoever allocates chooses the free
  3. using((arena) {...}) releases at the end
  4. library buffer goes back to the library
  5. NativeFinalizer for long-lived handles

basics

~20 s

Nobody frees it automatically: memory from calloc or malloc must be freed with the same allocator, an Arena frees everything when using() ends, and buffers the C library allocates must be released with the library's own free function.

solid answer

~40 s

Native memory lives outside the Dart heap, so the garbage collector never frees it. The rule is **the allocator that allocated it frees it**. Short-lived arguments come from `package:ffi`'s `calloc<T>(n)` or `malloc<T>(n)` and go back with `calloc.free(p)` in a `finally`, or, simpler, from an **`Arena`** inside `using((arena) { ... })`, which frees every allocation when the callback completes or throws. Strings converted with `toNativeUtf8()` are native allocations too. A buffer the **C library** allocates (the compressed JPEG from `imgz_compress_jpeg`) must be released with the library's own function (`imgz_free`), because it may use a different allocator; copy the bytes into a `Uint8List` first. For native objects that live as long as a Dart object (an encoder handle), attach a `NativeFinalizer` to the Dart wrapper and still offer an explicit `dispose`.

code

dart · 29 lines
dart
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';

Uint8List compressJpeg(Uint8List rgba, int width, int height, {int quality = 85}) {
  return using((Arena arena) {
    final Pointer<Uint8> pixels = arena<Uint8>(rgba.length);
    pixels.asTypedList(rgba.length).setAll(0, rgba);

    final Pointer<ImgzInfo> info = arena<ImgzInfo>();
    info.ref
      ..width = width
      ..height = height
      ..stride = width * 4;

    final Pointer<Pointer<Uint8>> outData = arena<Pointer<Uint8>>();
    final Pointer<Size> outLen = arena<Size>();

    final int rc = imgz_compress_jpeg(pixels, info, quality, outData, outLen);
    if (rc != 0) throw StateError('imgz_compress_jpeg failed with $rc');

    try {
      // Copy into Dart memory before the C buffer is released.
      return Uint8List.fromList(outData.value.asTypedList(outLen.value));
    } finally {
      imgz_free(outData.value); // C allocated it, C frees it.
    }
  });
}

go deeper

for a junior

Recall that native memory is not garbage-collected and that every calloc or malloc needs a matching free.

for a middle

Explain the arena pattern, out-parameters, and why a C-allocated buffer goes back through the library's own free function.

for a senior

Design ownership for long-lived native handles with dispose plus NativeFinalizer, and diagnose leaks, double frees and use-after-free in production.

for a principal

Set team conventions for native ownership at the API boundary, such as copying results, arena-only temporaries and wrapper classes, so C memory never leaks into app code.

## Why this is the question interviewers ask `dart:ffi` gives Dart code raw `Pointer`s into **native memory**, memory the C allocator manages, outside the Dart heap. The Dart garbage collector does not know what a `Pointer` points at and never frees it. Every allocation therefore needs an explicit owner. Getting it wrong produces leaks (never freed), crashes (freed twice, or freed by the wrong allocator) or corruption (used after free). ## Allocators on the Dart side `dart:ffi` defines the `Allocator` interface (`allocate` and `free`) and the call syntax `allocator<T>(count)`, which allocates `sizeOf<T>() * count` bytes. `package:ffi` supplies the implementations: | Allocator | Frees with | Notes | |---|---|---| | `calloc` | `calloc.free(p)` | memory zero-initialised | | `malloc` | `malloc.free(p)` | memory uninitialised | | `Arena` via `using(...)` | automatically when `using` completes | groups many temporaries under one owner | The pairing rule: free with the same allocator family that allocated. Mixing them may happen to work on one platform and break on another. ## The arena pattern For a single call that needs several temporary buffers, an **arena** removes the bookkeeping: ```dart final Uint8List jpeg = using((Arena arena) { final Pointer<Uint8> pixels = arena<Uint8>(rgba.length); pixels.asTypedList(rgba.length).setAll(0, rgba); final Pointer<Pointer<Uint8>> outData = arena<Pointer<Uint8>>(); final Pointer<Size> outLen = arena<Size>(); // ...call C... return Uint8List.fromList(outData.value.asTypedList(outLen.value)); }); ``` Every `arena<T>()` allocation is freed when the callback returns, and also when it throws, so an early `throw` does not leak. ## Memory the C library owns Many C APIs allocate result buffers themselves and document a matching release function. For `imgz_compress_jpeg(..., uint8_t** out_data, size_t* out_len)`: 1. Dart allocates the **out-parameters** (`Pointer<Pointer<Uint8>>`, `Pointer<Size>`) from the arena. 2. C writes the address of a buffer it allocated into `*out_data`. 3. Dart **copies** the bytes into Dart memory with `asTypedList(len)` plus `Uint8List.fromList`, because `asTypedList` is only a view over native memory. 4. Dart calls **`imgz_free(outData.value)`**, the library's own function, never `calloc.free`, because the library may use its own allocator or a different C runtime. ## Long-lived native objects An encoder handle that lives as long as a screen should be owned by a Dart object: - Give the wrapper an explicit **`dispose()`** that calls the C destroy function and sets the pointer to `nullptr`, guarding against double frees. - As a safety net, have the wrapper implement **`Finalizable`** and attach a **`NativeFinalizer`** built from a pointer to the C destroy function (`Native.addressOf(...)` gives one). When the wrapper becomes unreachable, the runtime calls the native function with the token, possibly on another thread; detach it in `dispose()`. - Pass `externalSize` to `attach` for large native allocations so the garbage collector's heuristics know how much memory the object really holds. ## Common failures, mapped to causes - **Steady native memory growth**: `toNativeUtf8()` or `calloc` in a loop without a matching free. - **Crash in free**: releasing a C-owned buffer with `calloc.free`, or freeing twice. - **Garbage image data**: returning `asTypedList` views after the buffer was freed. - **Crash long after a screen closed**: a finaliser and a manual `dispose` both freed the handle.

  • Why copy the result with Uint8List.fromList instead of returning asTypedList directly?
    `asTypedList` creates a view over the native buffer without copying. Once `imgz_free` releases that buffer, the view points at freed memory. Copying into a Dart-heap `Uint8List` first makes the result independent of the C allocation.
  • Is a NativeFinalizer enough on its own to release an encoder handle?
    It is a safety net, not a schedule: it runs only after the wrapper becomes unreachable and the collector notices, which may be much later. Heavy native resources should still be released promptly with an explicit `dispose()`, which also detaches the finaliser to avoid a double free.

saying these in an interview costs you the question

  • Says the Dart garbage collector frees memory from calloc
  • Frees a library-allocated buffer with calloc.free
  • Returns an asTypedList view after freeing the native buffer
  • Relies on a finalizer as the only way to release large native objects
  • Forgets that toNativeUtf8 allocates native memory
open as a page

In Flutter, what does dart:ffi let you do, and when would you call a C library through it instead of a MethodChannel?

level: juniorimportance: should knowfreq 35%

basics

~20 s

dart:ffi calls C-ABI functions and reads or writes native memory directly from Dart, synchronously and without a codec. Use it for a C library such as an image compressor; use a MethodChannel for Kotlin or Swift platform APIs.

open as a page

In Dart FFI, how does an @Native external function differ from binding a symbol with DynamicLibrary.open and lookupFunction, and which should new Flutter code use?

level: middleimportance: should knowfreq 24%

basics

~20 s

An @Native external function is resolved by the VM from its annotation, by default against a code asset named after the Dart library, so no loading code is needed; lookupFunction resolves a symbol at runtime in a DynamicLibrary you opened by path. New packages built with hooks use @Native.

open as a page

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%

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.

open as a page

In Flutter, what does flutter create --template=package_ffi set up, and how do its build hook and ffigen config deliver a C library to the app?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

package_ffi creates a package with C sources, a hook/build.dart that compiles them into a code asset with native_toolchain_c, and an ffigen.yaml that generates @Native bindings resolved against that asset, with no Gradle, CMake or podspec files.

open as a page