Dart 59 🎯 Foreign Function Interface (FFI) Bindings Auto-Generation using ffigen
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
| Key | Purpose |
|---|---|
name | The generated class name |
description | The Dart doc for the class |
output | The output file path |
headers.entry-points | The input headers |
headers.include-directives | The transitive inclusion filter |
compiler-opts | Compiler options for clang |
The Declaration Filters
| Key | Purpose |
|---|---|
functions.include | Functions to generate |
functions.exclude | Functions to skip |
functions.rename | Rename functions |
structs.include | Structs to generate |
enums.include | Enums to generate |
The Generation Options
| Option | Purpose |
|---|---|
dart-bool | Generate Dart bool for C99 bool |
use-supported-typedefs | Map uint8_t to Uint8, etc. |
array-workaround | Helpers for fixed arrays |
comments | Extract doc comments |
sort | Sort the declarations |
preamble | Raw header for the output |
The Generated Code
| Declaration | Generated Form |
|---|---|
| Function | A class method with a native wrapper |
| Struct | A class extending ffi.Struct |
| Enum | A Dart enum or integer constants |
| Typedef | A typedef in the generated file |
The Setup Requirements
| Platform | Requirement |
|---|---|
| Linux | libclang-dev |
| Windows | Visual Studio + LLVM |
| macOS | Xcode + 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
libclang not found | LLVM not installed | Install libclang-dev or LLVM |
| Header not found | Wrong entry-points path | Check the path |
| Symbol not generated | Filtered out | Check the include list |
| Wrong type | Missing use-supported-typedefs | Add the option |
| Struct layout wrong | Packing mismatch | Set the pack option |
| Generated file edited | Manual change | Regenerate |
| Output not imported | Wrong path | Check 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
| Item | Value |
|---|---|
ffigen | Generates Dart FFI bindings from C headers |
dev_dependencies | Where the package is added |
ffigen.yaml | The configuration file |
entry-points | The input headers |
include-directives | The transitive inclusion filter |
functions.include | The function filter |
rename | The name transformation |
dart-bool | Generates Dart bool for C99 bool |
use-supported-typedefs | Maps common typedefs |
ffi-native | Generates @Native bindings |
libclang | The parsing dependency |
dart run ffigen | The generation command |
Key takeaways:
ffigengenerates the bindings from the C header. It reads the header withlibclang, 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, andheadersare the required keys. Thefunctions,structs, andenumskeys filter the declarations, and therenamekey transforms the names. - The declaration filters control what is generated. The
includeandexcludelists accept regular expressions or exact names. Theexclude-all-by-defaultoption 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-booloption generates Dartboolfor C99bool. Without the option, the Cboolis generated as aUint8, which is the native representation. - The
use-supported-typedefsoption maps the common C typedefs. Theuint8_tmaps toUint8, theint16_tmaps toInt16, and thesize_tmaps toSize. - The
ffi-nativeoption generates@Nativebindings. The@Nativeform resolves the symbol at compile time and avoids theDynamicLibrary.lookupcall, which is faster. ffigenrequireslibclang. Thelibclang-devpackage 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.yamlcommand 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.
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!