| |

Dart 59 🎯 Foreign Function Interface (FFI) Bindings Auto-Generation using ffigen

⏱️ Estimated read: 15 min | |

Hand-written FFI bindings are error-prone. A single type mismatch between the Dart declaration and the native function produces undefined behavior, and the compiler cannot catch it. A library with hundreds of functions, structs, and enums would require thousands of lines of hand-written declarations, each of which must match the native header exactly. The ffigen package solves this by generating the bindings from the C header. It reads the header with libclang, extracts the declarations, and writes the Dart FFI code. The result is a binding that is correct by construction, because it is derived from the same header that the native code was compiled against.

Key point: ffigen is a code generator, not a runtime library. It runs at development time, reads the C header, and writes a .dart file. The generated file is imported by the application and used like any other Dart code. The generator is run again when the header changes, which keeps the bindings in sync.


Why ffigen exists

The manual-binding problem. A C library with a hundred functions requires a hundred lookupFunction calls, each with a native signature and a Dart signature. Every struct requires a class extending Struct with annotated fields. Every enum requires a set of constants. Writing all of this by hand is tedious, and a single mistake produces a runtime crash that is difficult to debug.

The sync problem. The C header is the source of truth. When a function’s signature changes, the Dart binding must change to match. A hand-written binding is easily forgotten, and the mismatch produces undefined behavior. The generated binding is regenerated from the header, so the two cannot drift.

The layout problem. A struct’s layout depends on the field types, the field order, and the platform’s alignment rules. Reproducing the layout by hand requires knowing the ABI details. ffigen reads the layout from libclang, which applies the same rules that the C compiler uses.

The enum problem. A C enum is a set of named integer constants. The Dart binding can generate them as constants, as a Dart enum, or as integers, depending on the configuration. ffigen handles the translation, including the as-int option for enums that must remain integers .

The callback problem. A C function that takes a function pointer requires a Pointer.fromFunction callback. ffigen generates the typedefs for the callback and the function that accepts it, which makes the callback pattern usable without manual typedefs.

The multi-platform problem. A library may have different declarations on different platforms. ffigen can generate the bindings from the headers of each platform, or from a single header that uses preprocessor guards. The generated bindings are per-platform, and the build system selects the correct one.

The @Native problem. Newer Dart versions support @Native annotations, which allow statically linked bindings that do not require DynamicLibrary.lookup. ffigen can generate both forms: the lookupFunction form and the @Native form .


a. Installing and configuring ffigen

ffigen is a development dependency. It is added to pubspec.yaml under dev_dependencies, and it requires libclang to be installed on the system.

The installation

# pubspec.yaml
dev_dependencies:
  ffigen: ^23.0.0

The package is installed with dart pub get. It requires libclang, which is installed separately: libclang-dev on Linux, LLVM on Windows and macOS .

The configuration file

The configuration is a YAML file that specifies the input header, the output file, and the generation options.

# ffigen.yaml
name: MyLibraryBindings
description: Bindings for mylib
output: 'lib/mylib_bindings.dart'
headers:
  entry-points:
    - 'src/mylib.h'

The name is the generated class name, the output is the path of the generated file, and the headers specifies the input header. The entry-points is the list of headers to parse, and the include-directives controls which headers are included transitively .

The generation command

The generator is run with dart run ffigen.

dart run ffigen --config ffigen.yaml

The command reads the configuration, parses the header, and writes the output file. The output file is a regular Dart file that can be imported .

The include-directives

The include-directives option controls which headers are included in the output. By default, all headers transitively included by the entry points are parsed. The option restricts the output to declarations from specific headers.

headers:
  entry-points:
    - 'src/mylib.h'
  include-directives:
    - '**/mylib.h'

The glob pattern matches the header path, and only the declarations from the matching headers are generated .

The declaration filters

The functions, structs, enums, and macros keys filter the declarations. The include and exclude lists accept regular expressions or exact names.

functions:
  include:
    - 'my_.*'
  exclude:
    - 'my_internal.*'

The include list specifies the declarations to generate, and the exclude list specifies the ones to skip. The exclude-all-by-default option changes the default behavior from include-everything to exclude-everything .

The rename option

The rename option changes the generated names with regular expressions.

functions:
  rename:
    'clang_(.*)': '$1'

The pattern clang_(.*) matches the original name, and the replacement $1 uses the captured group. The option is used to remove prefixes or to follow Dart naming conventions .

The struct packing

The pack option overrides the @Packed annotation for structs.

structs:
  pack:
    'MyPackedStruct': 1
    'MyUnpackedStruct': none

The option is used when libclang cannot reliably determine the packing from the header .

The dart-bool option

The dart-bool option generates Dart bool for C99 bool in function signatures.

dart-bool: true

Without the option, the C bool is generated as a Uint8, which is the native representation .

The use-supported-typedefs option

The option maps common C typedefs to their Dart equivalents.

use-supported-typedefs: true

The uint8_t maps to Uint8, the int16_t maps to Int16, and the size_t maps to Size. Without the option, the typedefs are generated as separate types .

The ffi-native option

The ffi-native option generates @Native bindings instead of the lookupFunction form.

ffi-native:
  asset: 'myasset'

The @Native form uses compile-time symbol resolution, which is faster and avoids the DynamicLibrary.lookup call .


b. The generated bindings

The generated file contains the bindings for the functions, structs, enums, and typedefs. The code is marked with a header that indicates it is generated.

The generated header

// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
// ignore_for_file: type=lint
import 'dart:ffi' as ffi;

The header tells the developer not to edit the file, because the changes will be overwritten on the next generation .

The function bindings

A function is generated as a class with a static method that calls the native function.

class MyLibraryBindings {
  final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) _lookup;
  late final ffi.Pointer<ffi.NativeFunction<NativeAdd>> _add;
  late final NativeAdd _addPtr;

  MyLibraryBindings(this._lookup) {
    _add = _lookup<ffi.NativeFunction<NativeAdd>>('add');
    _addPtr = _add.asFunction();
  }

  int add(int a, int b) => _addPtr(a, b);
}

typedef NativeAdd = ffi.Int32 Function(ffi.Int32 a, ffi.Int32 b);

The class holds the DynamicLibrary and the looked-up function pointers. The method calls the function with Dart values .

The struct bindings

A struct is generated as a class extending ffi.Struct with annotated fields.

final class Point extends ffi.Struct {
  @ffi.Int32()
  external int x;

  @ffi.Int32()
  external int y;
}

The fields are annotated with the FFI type, and the external keyword is required .

The enum bindings

An enum is generated as a Dart enum or as a set of integer constants, depending on the configuration.

enum MyEnum {
  value1(0),
  value2(1);

  final int value;
  const MyEnum(this.value);
}

The as-int option generates integers instead, which is used when the enum’s integer type is implementation-defined .

The @Native bindings

With the ffi-native option, the function is generated with the @Native annotation instead of the lookupFunction form.

@ffi.Native<ffi.Int32 Function(ffi.Int32, ffi.Int32)>()
external int add(int a, int b);

The @Native annotation resolves the symbol at compile time, and the function is called directly .


c. The generation pipeline

ffigen operates as a three-stage pipeline: configuration, parsing, and code generation. Understanding the pipeline explains the options and the limitations.

The configuration stage

The YAML file is read and validated. The headers entry points are resolved to file paths, and the declaration filters are compiled into regular expressions .

The parsing stage

libclang parses the headers and produces an AST. ffigen traverses the AST and extracts the declarations into an internal representation. The type extraction converts each CXType into the corresponding FFI type .

The code generation stage

The Writer class generates the Dart code from the internal representation. The generated code is written to the output path. The class name, the typedefs, and the function wrappers are all generated from the configuration and the parsed declarations .

The limitations

ffigen has several limitations. It does not support C++ headers directly; it only parses C headers. It skips functions that pass or return structs by value in older versions. It removes the members of structs that contain structs in older versions . The limitations are documented, and the workarounds are provided where possible.

The array-workaround

The array-workaround option generates helper methods for fixed-size arrays in structs. The option is used when the Dart FFI does not support the array directly .

The comments option

The comments option extracts documentation comments from the header and includes them in the generated code. The option has style and length settings .

The sort option

The sort option sorts the generated declarations by name. Without the option, the declarations are generated in the order they appear in the header .

The preamble option

The preamble option adds a raw header to the generated file. The option is used to add imports or ignore directives .


Complete Example Session

# ============================================
# PART 1: THE CONFIGURATION FILE
# ============================================

# ffigen.yaml
name: ExampleBindings
description: Bindings for the example library
output: 'lib/example_bindings.dart'
headers:
  entry-points:
    - 'src/example.h'
  include-directives:
    - '**/example.h'

# ============================================
# PART 2: THE C HEADER
# ============================================

// src/example.h
#include <stdint.h>

typedef struct {
  int32_t x;
  int32_t y;
} Point;

typedef enum {
  STATUS_OK = 0,
  STATUS_ERROR = 1,
} Status;

int32_t add(int32_t a, int32_t b);
Point make_point(int32_t x, int32_t y);
Status do_work(void);

# ============================================
# PART 3: THE DECLARATION FILTERS
# ============================================

functions:
  include:
    - 'add'
    - 'make_point'
    - 'do_work'

structs:
  include:
    - 'Point'

enums:
  include:
    - 'Status'

# ============================================
# PART 4: THE RENAME OPTION
# ============================================

functions:
  rename:
    'do_work': 'performWork'
    'make_point': 'createPoint'

# ============================================
# PART 5: THE DART-BOOL OPTION
# ============================================

dart-bool: true

# ============================================
# PART 6: THE USE-SUPPORTED-TYPEDEFS OPTION
# ============================================

use-supported-typedefs: true

# ============================================
# PART 7: THE PACK OPTION
# ============================================

structs:
  pack:
    'Point': none  # No packing

# ============================================
# PART 8: THE GENERATION COMMAND
# ============================================

# dart run ffigen --config ffigen.yaml

# ============================================
# PART 9: THE GENERATED BINDINGS
# ============================================

// lib/example_bindings.dart
// AUTO GENERATED FILE, DO NOT EDIT.
import 'dart:ffi' as ffi;

class ExampleBindings {
  final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) _lookup;
  late final ffi.Pointer<ffi.NativeFunction<NativeAdd>> _add;
  late final NativeAdd _addPtr;
  late final ffi.Pointer<ffi.NativeFunction<NativeCreatePoint>> _createPoint;
  late final NativeCreatePoint _createPointPtr;
  late final ffi.Pointer<ffi.NativeFunction<NativePerformWork>> _performWork;
  late final NativePerformWork _performWorkPtr;

  ExampleBindings(this._lookup) {
    _add = _lookup<ffi.NativeFunction<NativeAdd>>('add');
    _addPtr = _add.asFunction();
    _createPoint = _lookup<ffi.NativeFunction<NativeCreatePoint>>('make_point');
    _createPointPtr = _createPoint.asFunction();
    _performWork = _lookup<ffi.NativeFunction<NativePerformWork>>('do_work');
    _performWorkPtr = _performWork.asFunction();
  }

  int add(int a, int b) => _addPtr(a, b);
  Point createPoint(int x, int y) => _createPointPtr(x, y);
  int performWork() => _performWorkPtr();
}

typedef NativeAdd = ffi.Int32 Function(ffi.Int32 a, ffi.Int32 b);
typedef NativeCreatePoint = Point Function(ffi.Int32 x, ffi.Int32 y);
typedef NativePerformWork = ffi.Int32 Function();

final class Point extends ffi.Struct {
  @ffi.Int32()
  external int x;

  @ffi.Int32()
  external int y;
}

enum Status {
  statusOk(0),
  statusError(1);

  final int value;
  const Status(this.value);
}

// ============================================
# PART 10: THE USAGE
# ============================================

// main.dart
import 'dart:ffi';
import 'example_bindings.dart';

void main() {
  final lib = DynamicLibrary.open('libexample.so');
  final bindings = ExampleBindings(lib.lookup);

  print(bindings.add(2, 3));  // 5
  final point = bindings.createPoint(10, 20);
  print('${point.x}, ${point.y}');  // 10, 20
}

The ten parts showed the configuration file, the C header, the declaration filters, the rename option, the dart-bool option, the use-supported-typedefs option, the pack option, the generation command, the generated bindings, and the usage.


Quick Reference

The Configuration Keys

KeyPurpose
nameThe generated class name
descriptionThe Dart doc for the class
outputThe output file path
headers.entry-pointsThe input headers
headers.include-directivesThe transitive inclusion filter
compiler-optsCompiler options for clang

The Declaration Filters

KeyPurpose
functions.includeFunctions to generate
functions.excludeFunctions to skip
functions.renameRename functions
structs.includeStructs to generate
enums.includeEnums to generate

The Generation Options

OptionPurpose
dart-boolGenerate Dart bool for C99 bool
use-supported-typedefsMap uint8_t to Uint8, etc.
array-workaroundHelpers for fixed arrays
commentsExtract doc comments
sortSort the declarations
preambleRaw header for the output

The Generated Code

DeclarationGenerated Form
FunctionA class method with a native wrapper
StructA class extending ffi.Struct
EnumA Dart enum or integer constants
TypedefA typedef in the generated file

The Setup Requirements

PlatformRequirement
Linuxlibclang-dev
WindowsVisual Studio + LLVM
macOSXcode + LLVM

Best Practices

✅ Do This:

# Add ffigen to dev_dependencies.
dev_dependencies:
  ffigen: ^23.0.0  # ✅
# Specify the entry point and the output.
headers:
  entry-points:
    - 'src/mylib.h'
output: 'lib/mylib_bindings.dart'  # ✅
# Filter the declarations.
functions:
  include:
    - 'my_.*'  # ✅
# Use supported typedefs.
use-supported-typedefs: true  # ✅
# Rename the functions.
functions:
  rename:
    'clang_(.*)': '$1'  # ✅
# Run the generator after changing the header.
dart run ffigen --config ffigen.yaml  # ✅

❌ Don’t Do This:

# Don't edit the generated file.
// AUTO GENERATED FILE, DO NOT EDIT.  # ✅ The file says this
# Don't forget the LLVM installation.
# ffigen needs libclang.  # ❌
# Don't omit the include-directives when using transitive headers.
# The output may include unwanted declarations.  # ❌
# Don't use the wrong type mapping.
# Match the native types.  # ❌
# Don't forget to regenerate after a header change.
# The bindings will be stale.  # ❌

Common Pitfalls

PitfallWhy It HappensFix
libclang not foundLLVM not installedInstall libclang-dev or LLVM
Header not foundWrong entry-points pathCheck the path
Symbol not generatedFiltered outCheck the include list
Wrong typeMissing use-supported-typedefsAdd the option
Struct layout wrongPacking mismatchSet the pack option
Generated file editedManual changeRegenerate
Output not importedWrong pathCheck the output key

Real-World Examples

1. Basic Configuration

name: MyBindings
output: 'lib/bindings.dart'
headers:
  entry-points:
    - 'src/mylib.h'

2. Include Filter

headers:
  include-directives:
    - '**/mylib.h'

3. Function Filter

functions:
  include:
    - 'my_.*'

4. Rename

functions:
  rename:
    'clang_(.*)': '$1'

5. Dart Bool

dart-bool: true

6. Supported Typedefs

use-supported-typedefs: true

7. Pack Override

structs:
  pack:
    'MyStruct': 1

8. Array Workaround

array-workaround: true

9. FFI Native

ffi-native:
  asset: 'myasset'

10. Generate

dart run ffigen --config ffigen.yaml

Visual

The ffigen Pipeline

┌────────────────────────────────────────────────────────────┐
│  THE ffigen PIPELINE                                       │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. CONFIGURATION                                 │     │
│  │    ffigen.yaml                                    │     │
│  │    - name, output, headers                        │     │
│  │    - functions, structs, enums                    │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  2. PARSING                                       │     │
│  │    libclang reads the header                      │     │
│  │    Builds an AST                                  │     │
│  │    Extracts the declarations                      │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  3. CODE GENERATION                               │     │
│  │    The Writer generates the Dart code             │     │
│  │    Writes the output file                         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The generated file is imported and used.                   │
└────────────────────────────────────────────────────────────┘

The Generated Bindings

┌────────────────────────────────────────────────────────────┐
│  THE GENERATED BINDINGS                                    │
│                                                            │
│  C HEADER                    DART BINDINGS                 │
│  ┌──────────────────┐        ┌──────────────────────────┐ │
│  │ int add(int, int)│        │ int add(int a, int b)    │ │
│  └──────────────────┘        └──────────────────────────┘ │
│                                                            │
│  ┌──────────────────┐        ┌──────────────────────────┐ │
│  │ typedef struct { │        │ final class Point extends│ │
│  │   int x;         │        │   ffi.Struct {           │ │
│  │   int y;         │        │   @ffi.Int32()           │ │
│  │ } Point;         │        │   external int x;        │ │
│  └──────────────────┘        │ }                        │ │
│                              └──────────────────────────┘ │
│                                                            │
│  ┌──────────────────┐        ┌──────────────────────────┐ │
│  │ typedef enum {   │        │ enum Status {            │ │
│  │   OK, ERROR      │        │   ok(0), error(1);       │ │
│  │ } Status;        │        │ }                        │ │
│  └──────────────────┘        └──────────────────────────┘ │
└────────────────────────────────────────────────────────────┘

The Configuration Filters

┌────────────────────────────────────────────────────────────┐
│  THE CONFIGURATION FILTERS                                 │
│                                                            │
│  C HEADER (all declarations)                               │
│  ┌──────────────────────────────────────────────────┐     │
│  │  my_public_func                                   │     │
│  │  my_internal_func                                 │     │
│  │  helper_func                                      │     │
│  │  MyStruct                                         │     │
│  │  MyInternalStruct                                 │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           │  functions.include: ['my_public.*']│
│                           │  structs.include: ['MyStruct']   │
│                           v                                │
│  DART BINDINGS (filtered)                                  │
│  ┌──────────────────────────────────────────────────┐     │
│  │  my_public_func                                   │     │
│  │  MyStruct                                         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The filters control what is generated.                    │
└────────────────────────────────────────────────────────────┘

The @Native vs lookupFunction

┌────────────────────────────────────────────────────────────┐
│  TWO BINDING STYLES                                        │
│                                                            │
│  lookupFunction (default)                                  │
│  ┌──────────────────────────────────────────────────┐     │
│  │  class MyBindings {                               │     │
│  │    final _lookup;                                 │     │
│  │    late final _add;                               │     │
│  │    MyBindings(this._lookup) {                     │     │
│  │      _add = _lookup('add');                       │     │
│  │    }                                              │     │
│  │    int add(int a, int b) => _add(a, b);           │     │
│  │  }                                                │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  @Native (ffi-native: true)                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  @ffi.Native<ffi.Int32 Function(ffi.Int32,        │     │
│  │    ffi.Int32)>()                                  │     │
│  │  external int add(int a, int b);                  │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The @Native form avoids the lookup.                       │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
ffigenGenerates Dart FFI bindings from C headers
dev_dependenciesWhere the package is added
ffigen.yamlThe configuration file
entry-pointsThe input headers
include-directivesThe transitive inclusion filter
functions.includeThe function filter
renameThe name transformation
dart-boolGenerates Dart bool for C99 bool
use-supported-typedefsMaps common typedefs
ffi-nativeGenerates @Native bindings
libclangThe parsing dependency
dart run ffigenThe generation command

Key takeaways:

  • ffigen generates the bindings from the C header. It reads the header with libclang, extracts the declarations, and writes a Dart file. The bindings are correct by construction, because they are derived from the same header the native code was compiled against.
  • The configuration is a YAML file. The name, output, and headers are the required keys. The functions, structs, and enums keys filter the declarations, and the rename key transforms the names.
  • The declaration filters control what is generated. The include and exclude lists accept regular expressions or exact names. The exclude-all-by-default option changes the default behavior from include-everything to exclude-everything.
  • The generated bindings are not edited by hand. The header says AUTO GENERATED FILE, DO NOT EDIT. The file is regenerated from the header, and any manual change is overwritten.
  • The dart-bool option generates Dart bool for C99 bool. Without the option, the C bool is generated as a Uint8, which is the native representation.
  • The use-supported-typedefs option maps the common C typedefs. The uint8_t maps to Uint8, the int16_t maps to Int16, and the size_t maps to Size.
  • The ffi-native option generates @Native bindings. The @Native form resolves the symbol at compile time and avoids the DynamicLibrary.lookup call, which is faster.
  • ffigen requires libclang. The libclang-dev package on Linux, LLVM on Windows and macOS, and Xcode on macOS. The tool cannot run without it.
  • The bindings are regenerated when the header changes. The dart run ffigen --config ffigen.yaml command is run again, and the output file is overwritten. The process is part of the development workflow.

Remember: ffigen is the tool that generates Dart FFI bindings from C headers. It reads the header with libclang, extracts the declarations, and writes a Dart file. The generated file contains the function wrappers, the struct classes, and the enum definitions. The configuration file controls the input, the output, and the filters. The bindings are correct by construction, because they are derived from the same header the native code was compiled against. The tool is run at development time, and the generated file is imported by the application. The result is a binding that is in sync with the native library, without the manual work and the risk of a type mismatch.


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!