Technology Apr 29, 2026 · 5 min read

Dart FFI Advanced — C Library Integration, Memory Management, and Flutter Plugin

Introduction Dart FFI (Foreign Function Interface) lets you call C libraries directly from Dart and Flutter. Writing performance-critical code — image processing, cryptography, audio decoding — in Pure Dart is often impractical. FFI lets you leverage the entire C ecosystem instead. This a...

DE
DEV Community
by kanta13jp1
Dart FFI Advanced — C Library Integration, Memory Management, and Flutter Plugin

Introduction

Dart FFI (Foreign Function Interface) lets you call C libraries directly from Dart and Flutter. Writing performance-critical code — image processing, cryptography, audio decoding — in Pure Dart is often impractical. FFI lets you leverage the entire C ecosystem instead. This article covers everything from loading a shared library to packaging your FFI code as a reusable Flutter plugin.

1. Calling C Functions with dart:ffi

Start by examining the C library's function signatures:

// native/image_processor.h
int grayscale(uint8_t* pixels, int width, int height);
void free_buffer(uint8_t* buf);

Load and call them from Dart:

import 'dart:ffi';
import 'dart:io';

// Define native and Dart type aliases
typedef GrayscaleNative = Int32 Function(Pointer<Uint8>, Int32, Int32);
typedef GrayscaleDart = int Function(Pointer<Uint8>, int, int);

typedef FreeBufferNative = Void Function(Pointer<Uint8>);
typedef FreeBufferDart = void Function(Pointer<Uint8>);

class ImageProcessorFFI {
  late final DynamicLibrary _lib;
  late final GrayscaleDart _grayscale;
  late final FreeBufferDart _freeBuffer;

  ImageProcessorFFI() {
    // Load the shared library per platform
    if (Platform.isAndroid) {
      _lib = DynamicLibrary.open('libimage_processor.so');
    } else if (Platform.isIOS || Platform.isMacOS) {
      _lib = DynamicLibrary.process(); // iOS uses static linking
    } else if (Platform.isWindows) {
      _lib = DynamicLibrary.open('image_processor.dll');
    } else {
      _lib = DynamicLibrary.open('libimage_processor.so');
    }

    _grayscale = _lib
        .lookup<NativeFunction<GrayscaleNative>>('grayscale')
        .asFunction();
    _freeBuffer = _lib
        .lookup<NativeFunction<FreeBufferNative>>('free_buffer')
        .asFunction();
  }
}

2. Memory Operations with Pointer and Struct

To work with C structs from Dart, extend the Struct class:

import 'dart:ffi';
import 'package:ffi/ffi.dart';

// Dart mirror of a C struct
final class ImageInfo extends Struct {
  @Int32()
  external int width;

  @Int32()
  external int height;

  @Int32()
  external int channels;

  external Pointer<Uint8> data;
}

void processImage(Pointer<ImageInfo> imgPtr) {
  final info = imgPtr.ref;
  print('Width: ${info.width}, Height: ${info.height}');

  // Convert native pixel buffer to a Dart TypedData view
  final pixelCount = info.width * info.height * info.channels;
  final pixels = info.data.asTypedList(pixelCount);

  // In-place brightness reduction
  for (int i = 0; i < pixels.length; i++) {
    pixels[i] = (pixels[i] * 0.8).clamp(0, 255).toInt();
  }
}

asTypedList gives you a zero-copy Uint8List view into native memory — mutations affect the C buffer directly.

3. Scoped Memory Management with Arena Allocator

Manual malloc/free is error-prone. The Arena allocator from package:ffi automatically frees everything when the scope exits.

import 'package:ffi/ffi.dart';

Future<void> runWithArena() async {
  using((Arena arena) {
    // Allocate a native UTF-8 string
    final nativeStr = 'Hello FFI'.toNativeUtf8(allocator: arena);

    // Allocate a struct
    final imageInfo = arena<ImageInfo>();
    imageInfo.ref.width = 640;
    imageInfo.ref.height = 480;
    imageInfo.ref.channels = 3;

    // Allocate a pixel buffer
    final pixelBuf = arena<Uint8>(640 * 480 * 3);
    imageInfo.ref.data = pixelBuf;

    // Call C function
    processNativeImage(imageInfo, nativeStr);

    // All allocations freed automatically when `using` block exits
  });
}

Use calloc/malloc directly only when you need allocations to outlive a scope. In that case, always pair with an explicit calloc.free() call in a finally block or a Finalizer.

4. NativeCallable — Passing Dart Callbacks to C

When a C library requires a progress callback, use NativeCallable to pass a Dart function as a C function pointer:

typedef ProgressCallbackNative = Void Function(Int32 current, Int32 total);
typedef EncodeWithProgressNative = Void Function(
    Pointer<Uint8> data,
    Int32 len,
    Pointer<NativeFunction<ProgressCallbackNative>>);

void encodeWithProgress(Uint8List data) {
  // Wrap a Dart closure as a C function pointer
  final callback = NativeCallable<ProgressCallbackNative>.listener(
    (int current, int total) {
      final percent = (current / total * 100).toStringAsFixed(1);
      print('Encoding: $percent%');
    },
  );

  using((arena) {
    final nativeData = arena<Uint8>(data.length);
    nativeData.asTypedList(data.length).setAll(0, data);

    final encodeWithProgressFn = _lib
        .lookup<NativeFunction<EncodeWithProgressNative>>('encode_with_progress')
        .asFunction<
            void Function(Pointer<Uint8>, int,
                Pointer<NativeFunction<ProgressCallbackNative>>)>();

    encodeWithProgressFn(nativeData, data.length, callback.nativeFunction);
  });

  // Always close to release the underlying port
  callback.close();
}

NativeCallable.listener dispatches the callback on the Dart isolate, making it thread-safe for use with multithreaded C libraries.

5. Packaging FFI Code as a Flutter Plugin

To share your FFI wrapper across projects, package it as a Flutter plugin with ffiPlugin: true:

# pubspec.yaml (plugin side)
name: image_processor_ffi
description: Flutter plugin wrapping libimage_processor via dart:ffi

flutter:
  plugin:
    platforms:
      android:
        ffiPlugin: true
      ios:
        ffiPlugin: true
      macos:
        ffiPlugin: true
      windows:
        ffiPlugin: true
      linux:
        ffiPlugin: true

With ffiPlugin: true, Flutter's build system automatically handles CMakeLists.txt on Android/Windows/Linux and Podspec on iOS/macOS. Place your C source in src/ and it gets compiled automatically.

image_processor_ffi/
  lib/
    image_processor_ffi.dart   # Public Dart API
  src/
    image_processor.c          # C implementation
    image_processor.h
  android/
    CMakeLists.txt
  ios/
    Classes/
  windows/
    CMakeLists.txt

Practical Example: OpenSSL AES Encryption in Flutter

// AES-256-CBC encryption via OpenSSL FFI wrapper
Uint8List aesEncrypt(Uint8List plaintext, Uint8List key, Uint8List iv) {
  return using((arena) {
    final ptPtr = arena<Uint8>(plaintext.length);
    ptPtr.asTypedList(plaintext.length).setAll(0, plaintext);

    final keyPtr = arena<Uint8>(32); // 256-bit key
    keyPtr.asTypedList(32).setAll(0, key);

    final ivPtr = arena<Uint8>(16);
    ivPtr.asTypedList(16).setAll(0, iv);

    // Extra 16 bytes for PKCS7 padding
    final ctPtr = arena<Uint8>(plaintext.length + 16);
    final ctLen = arena<Int32>();

    _opensslAesEncrypt(ptPtr, plaintext.length, keyPtr, ivPtr, ctPtr, ctLen);

    return Uint8List.fromList(ctPtr.asTypedList(ctLen.value));
  });
}

Summary

Feature Purpose
DynamicLibrary.open Load a shared library at runtime
Pointer<T> / Struct Manipulate C memory from Dart
Arena allocator Scoped, exception-safe memory management
NativeCallable Pass Dart closures as C function pointers
ffiPlugin: true Package FFI code as a distributable Flutter plugin

Dart FFI dramatically expands what you can do with Flutter. Performance-sensitive workloads that would be impractical in Pure Dart — image processing, audio codecs, cryptography — become straightforward by leveraging the proven C ecosystem. Start with Arena for memory safety and NativeCallable for callbacks, and you'll avoid most of the pitfalls that make FFI feel intimidating.

DE
Source

This article was originally published by DEV Community and written by kanta13jp1.

Read original article on DEV Community
Back to Discover

Reading List