About dart-native-interop-ffi
A guide to using Dart's `dart:ffi` to call native C APIs and manage native memory. It is for Dart Native developers, and covers type mapping, generated bindings with `ffigen`, library loading, and resource cleanup.
Key features
- 01Calling native C APIs with FFI
- 02Native memory allocation and cleanup
- 03Cross-platform C type mapping
- 04Binding generation with `ffigen`
- 05Dynamic library loading workflow
Use cases
- 01Connecting a Dart app to a C library
- 02Generating bindings for complex headers
- 03Managing native resources across object lifecycles
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ dart-lang/ skills --skill dart-native-interop-ffiAfter installing, type/dart-native-interop-ffiin Claude Code to confirm it loaded.
Details
From the project READMEInteroperating with C using dart:ffi
Contents
- Core Concepts
- Memory Management & Finalization
- Cross-Platform Type Mapping
- Workflow: Generating Bindings with ffigen
- Workflow: Loading and Calling Native Functions
- Examples
Core Concepts
Use the dart:ffi library to call native C APIs and manage native memory in Dart applications running on the Dart Native platform.
Related Skills: Refer to dart-concurrency-isolates when executing heavy FFI workloads to avoid blocking the main thread.
Memory Management & Finalization
Manage native memory explicitly to prevent leaks.
- DO manually allocate and free memory using
callocormallocfrompackage:ffiwhen passing dynamically sized data to C. - DO use
FinalizableandNativeFinalizerto ensure native resources are automatically cleaned up when the Dart object is garbage collected. - NEVER rely solely on manual
free()calls for objects that have a lifecycle managed by Dart's garbage collector. Attach aNativeFinalizer.
Cross-Platform Type Mapping
Map C types to Dart FFI types accurately to prevent memory corruption and segmentation faults.
- PREFER
AbiSpecificIntegersubtypes (e.g.,Int,Long,Size,WChar) for cross-platform type mapping where the C type size varies by architecture (e.g.,int,long,size_t). - Use fixed-size markers (
Int8,Int32,Float,Double) only when the C header explicitly defines fixed-width types (e.g.,int32_t). - Use
Pointer<T>to represent pointers into native C memory. - Use
StructandUnionas supertypes for complex C structures.
Workflow: Generating Bindings with ffigen
DO use package:ffigen to automate the generation of FFI bindings for large API surfaces. Avoid writing manual bindings for complex headers.
Task Progress
- Add
ffigenas a dev dependency:dart pub add --dev ffigen. - Add
ffias a standard dependency:dart pub add ffi. - Create an
ffigen.yamlconfiguration file (or add topubspec.yaml) specifying the header files and output path. - Run the generator:
dart run ffigen --config ffigen.yaml. - Feedback Loop: Run
dart analyze-> review type mismatch or missing definition errors -> adjustffigen.yaml(e.g., adding compiler options or include paths) -> regenerate.
Workflow: Loading and Calling Native Functions
Implement conditional logic to load the correct dynamic library based on the host operating system.
Task Progress
- Determine the library path using
Platformfromdart:io.- If macOS: Load
.dylib. - If Windows: Load
.dll. - If Linux: Load
.so.
- If macOS: Load
- Open the library using
DynamicLibrary.open(libraryPath). - Define the C function signature using FFI types (e.g.,
Void Function(Int32)). - Define the Dart function signature (e.g.,
void Function(int)). - Lookup the function:
dylib.lookup<NativeFunction<CFunc>>('symbol_name').asFunction<DartFunc>(). - Invoke the resulting Dart function.
Examples
Example: Memory Allocation and Finalization
Demonstrates allocating memory, passing it to C, and ensuring cleanup using NativeFinalizer.
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// Assume this is imported from ffigen bindings
// typedef FreeFunc = Void Function(Pointer<Void>);
// final void Function(Pointer<Void>) nativeFree = ...;
final NativeFinalizer _finalizer = NativeFinalizer(nativeFreePtr);
final Pointer<NativeFunction<Void Function(Pointer<Void>)>> nativeFreePtr = DynamicLibrary.process().lookup('free');
class NativeResource implements Finalizable {
late final Pointer<Uint8> _data;
NativeResource(int size) {
// Manually allocate memory
_data = calloc<Uint8>(size);
// Attach the finalizer to ensure cleanup when NativeResource is GC'd
_finalizer.attach(this, _data.cast<Void>(), detach: this);
}
void dispose() {
// Optional manual cleanup before GC
_finalizer.detach(this);
calloc.free(_data);
}
}
Example: Loading a Library and Calling a Function
Demonstrates the conditional loading workflow and ABI-specific integer usage.
import 'dart:ffi' as ffi;
import 'dart:io' show Platform;
import 'package:path/path.dart' as path;
// C signature: void hello_world(size_t count);
typedef hello_world_func = ffi.Void Function(ffi.Size count);
// Dart signature
typedef HelloWorld = void Function(int count);
void main() {
final String libraryName;
if (Platform.isMacOS) {
libraryName = 'libhello.dylib';
} else if (Platform.isWindows) {
libraryName = 'hello.dll';
} else {
libraryName = 'libhello.so';
}
final libraryPath = path.join(Directory.current.path, 'hello_library', libraryName);
final dylib = ffi.DynamicLibrary.open(libraryPath);
final HelloWorld hello = dylib
.lookup<ffi.NativeFunction<hello_world_func>>('hello_world')
.asFunction();
hello(5);
}