Binding generator for FFI bindings.
Note: ffigen only supports parsing
C
headers, notC++
headers.
This bindings generator can be used to call C code -- or code in another language that compiles to C modules that follow the C calling convention -- such as Go or Rust. For more details, see: https://dart.dev/guides/libraries/c-interop
ffigen also has experimental support for calling ObjC and Swift code; for details see: https://dart.dev/guides/libraries/objective-c-interop
For some header file example.h:
int sum(int a, int b);
Add configurations to Pubspec File:
ffigen: output: 'generated_bindings.dart' headers: entry-points: - 'example.h'
Output (generated_bindings.dart).
import 'dart:ffi' as ffi; class NativeLibrary { final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) _lookup; NativeLibrary(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; NativeLibrary.fromLookup( ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) lookup) : _lookup = lookup; int sum(int a, int b) { return _sum(a, b); } late final _sumPtr = _lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Int)>>('sum'); late final _sum = _sumPtr.asFunction<int Function(int, int)>(); } }
ffigen
under dev_dependencies
in your pubspec.yaml
(run dart pub add -d ffigen
).package:ffi
under dependencies
in your pubspec.yaml
(run dart pub add ffi
).pubspec.yaml
or in a custom YAML file (see configurations).dart run ffigen
.Jump to FAQ.
package:ffigen
uses LLVM. Install LLVM (9+) in the following way.
Install libclangdev.
With apt-get: sudo apt-get install libclang-dev
.
With dnf: sudo dnf install clang-devel
.
winget install -e --id LLVM.LLVM
.xcode-select --install
.Configurations can be provided in 2 ways-
pubspec.yaml
file under the key ffigen
.dart run ffigen --config config.yaml
The following configuration options are available-
output: 'generated_bindings.dart'
or
output: bindings: 'generated_bindings.dart' ...
llvm-path: - '/usr/local/opt/llvm' - 'C:\Program Files\llvm` - '/usr/lib/llvm-11' # Specify exact path to dylib - '/usr/lib64/libclang.so'
headers: entry-points: - 'folder/**.h' - 'folder/specific_header.h' include-directives: - '**index.h' - '**/clang-c/**' - '/full/path/to/a/header.h'
name: 'SQLite'
description: 'Bindings to SQLite'
compiler-opts: - '-I/usr/lib/llvm-9/include/'
and/or via the command line -
dart run ffigen --compiler-opts "-I/headers -L 'path/to/folder name/file'"
compiler-opts-automatic: macos: include-c-standard-library: false
functions: include: # 'exclude' is also available. # Matches using regexp. - [a-z][a-zA-Z0-9]* # '.' matches any character. - prefix.* # Matches with exact name - someFuncName # Full names have higher priority. - anotherName rename: # Regexp groups based replacement. 'clang_(.*)': '$1' 'clang_dispose': 'dispose' # Removes '_' from beginning. '_(.*)': '$1' symbol-address: # Used to expose symbol address. include: - myFunc structs: rename: # Removes prefix underscores # from all structures. '_(.*)': '$1' member-rename: '.*': # Matches any struct. # Removes prefix underscores # from members. '_(.*)': '$1' enums: rename: # Regexp groups based replacement. 'CXType_(.*)': '$1' member-rename: '(.*)': # Matches any enum. # Removes '_' from beginning # enum member name. '_(.*)': '$1' # Full names have higher priority. 'CXTypeKind': # $1 keeps only the 1st # group i.e only '(.*)'. 'CXType(.*)': '$1' as-int: # These enums will be generated as Dart integers instead of Dart enums include: - MyIntegerEnum globals: exclude: - aGlobal rename: # Removes '_' from # beginning of a name. '_(.*)': '$1'
typedefs: exclude: # Typedefs starting with `p` are not generated. - 'p.*' rename: # Removes '_' from beginning of a typedef. '_(.*)': '$1'
include-unused-typedefs: true
functions: expose-typedefs: include: # Match function name. - 'myFunc' # Do this to expose types for all function. - '.*' exclude: # If you only use exclude, then everything # not excluded is generated. - 'dispose'
functions: leaf: include: # Match function name. - 'myFunc' # Do this to set isLeaf:true for all functions. - '.*' exclude: # If you only use exclude, then everything # not excluded is generated. - 'dispose'
functions: variadic-arguments: myfunc: // Native C types are supported - [int, unsigned char, long*, float**] // Common C typedefs (stddef.h) are supported too - [uint8_t, intptr_t, size_t, wchar_t*] // Structs/Unions/Typedefs from generated code or a library import can be referred too. - [MyStruct*, my_custom_lib.CustomUnion]
structs: pack: # Matches with the generated name. 'NoPackStruct': none # No packing '.*': 1 # Pack all structs with value 1
comments: style: any length: full
structs: dependency-only: opaque unions: dependency-only: opaque
sort: true
use-supported-typedefs: true
use-dart-handle: true
ignore-source-errors: true
and/or via the command line -
dart run ffigen --ignore-source-errors
silence-enum-warning: true
exclude-all-by-default: true
preamble: | // ignore_for_file: camel_case_types, non_constant_identifier_names
library-imports: custom_lib: 'package:some_pkg/some_file.dart'
type-map: 'native-types': # Targets native types. 'char': 'lib': 'pkg_ffi' # predefined import. 'c-type': 'Char' # For native-types dart-type can be be int, double or float # but same otherwise. 'dart-type': 'int' 'int': 'lib': 'custom_lib' 'c-type': 'CustomType4' 'dart-type': 'int' 'typedefs': # Targets typedefs. 'my_type1': 'lib': 'custom_lib' 'c-type': 'CustomType' 'dart-type': 'CustomType' 'structs': # Targets structs. 'my_type2': 'lib': 'custom_lib' 'c-type': 'CustomType2' 'dart-type': 'CustomType2' 'unions': # Targets unions. 'my_type3': 'lib': 'custom_lib' 'c-type': 'CustomType3' 'dart-type': 'CustomType3'
ffi-native: asset-id: 'package:some_pkg/asset' # Optional, was assetId in previous versions
language: 'objc'
output: ... objc-bindings: 'generated_bindings.m'
output: ... symbol-file: # Although file paths are supported here, prefer Package Uri's here # so that other pacakges can use them. output: 'package:some_pkg/symbols.yaml' import-path: 'package:some_pkg/base.dart'
import: symbol-files: # Both package Uri and file paths are supported here. - 'package:some_pkg/symbols.yaml' - 'path/to/some/symbol_file.yaml'
external-versions: # See https://docs.flutter.dev/reference/supported-platforms. ios: min: 12.0.0 macos: min: 10.14.0
objc-interfaces: include: # Includes a specific interface. - 'MyInterface' # Includes all interfaces starting with "NS". - 'NS.*' exclude: # Override the above NS.* inclusion, to exclude NSURL. - 'NSURL' rename: # Removes '_' prefix from interface names. '_(.*)': '$1' objc-protocols: include: # Generates bindings for a specific protocol. - MyProtocol objc-categories: include: # Generates bindings for a specific category. - MyCategory
headers: entry-points: # Generated by swiftc to wrap foo_lib.swift. - 'foo_lib-Swift.h' objc-interfaces: include: # Eg, foo_lib contains a set of classes prefixed with FL. - 'FL.*' module: # Use 'foo_lib' as the module name for all the FL.* classes. # We don't match .* here because other classes like NSString # shouldn't be given a module prefix. 'FL.*': 'foo_lib'
objc-interfaces: member-filter: MyInterface: include: - "someMethod:withArg:" # Since MyInterface has an include rule, all other methods # are excluded by default. objc-protocols: member-filter: NS.*: # Matches all protocols starting with NS. exclude: - copy.* # Remove all copy methods from these protocols. objc-categories: member-filter: MyCategory: include: - init.* # Include all init methods.
include-transitive-objc-interfaces: true include-transitive-objc-protocols: true
include-transitive-objc-categories: false
cd examples/<example_u_want_to_run>
, Run dart pub get
.dart run ffigen
.See test/README.md
Ffigen supports regexp based renaming, the regexp must be a full match, for renaming you can use regexp groups ($1
means group 1).
E.g - For renaming clang_dispose_string
to string_dispose
. We can can match it using clang_(.*)_(.*)
and rename with $2_$1
.
Here's an example of how to remove prefix underscores from any struct and its members.
structs: ... rename: '_(.*)': '$1' # Removes prefix underscores from all structures. member-rename: '.*': # Matches any struct. '_(.*)': '$1' # Removes prefix underscores from members.
The default behaviour is to include everything directly/transitively under each of the entry-points
specified.
If you only want to have declarations directly particular header you can do so using include-directives
. You can use glob matching to match header paths.
headers: entry-points: - 'path/to/my_header.h' include-directives: - '**my_header.h' # This glob pattern matches the header path.
Ffigen supports including/excluding declarations using full regexp matching.
Here's an example to filter functions using names
functions: include: - 'clang.*' # Include all functions starting with clang. exclude: - '.*dispose': # Exclude all functions ending with dispose.
This will include clang_help
. But will exclude clang_dispose
.
Note: exclude overrides include.
Ffigen treats char*
just as any other pointer,(Pointer<Int8>
). To convert these to/from String
, you can use package:ffi. Use ptr.cast<Utf8>().toDartString()
to convert char*
to dart string
and "str".toNativeUtf8()
to convert string
to char*
.
Unnamed enums are handled separately, under the key unnamed-enums
, and are generated as top level constants.
Here's an example that shows how to include/exclude/rename unnamed enums
unnamed-enums: include: - 'CX_.*' exclude: - '.*Flag' rename: 'CXType_(.*)': '$1'
Native enums are, by default, generated into Dart enums with int get value
and fromValue(int)
. This works well in the case that your enums values are known in advance and not going to change, and in return, you get the full benefits of Dart enums like exhaustiveness checking.
However, if a native library adds another possible enum value after you generate your bindings, and this new value is passed to your Dart code, this will result in an ArgumentError
at runtime. To fix this, you can regenerate the bindings on the new header file, but if you wish to avoid this issue entirely, you can tell ffigen to generate plain Dart integers for your enum instead. To do this, simply list your enum's name in the as-int
section of your ffigen config:
enums: as-int: include: - MyIntegerEnum - '*IntegerEnum' exclude: - FakeIntegerEnum
Functions that accept or return these enums will now accept or return integers instead, and it will be up to your code to map integer values to behavior and handle invalid values. But your code will be future-proof against new additions to the enums.
This happens when an excluded struct/union is a dependency to some included declaration. (A dependency means a struct is being passed/returned by a function or is member of another struct in some way)
Note: If you supply structs.dependency-only
as opaque
ffigen will generate these struct dependencies as Opaque
if they were only passed by reference(pointer).
structs: dependency-only: opaque unions: dependency-only: opaque
By default the native pointers are private, but you can use the symbol-address
subkey for functions/globals and make them public by matching with its name. The pointers are then accesible via nativeLibrary.addresses
.
Example -
functions: symbol-address: include: - 'myFunc' # Match function name. - '.*' # Do this to expose all function pointers. exclude: # If you only use exclude, then everything not excluded is generated. - 'dispose'
By default these types are inline. But you can use the expose-typedef
subkey for functions to generate them. This will expose the Native and Dart type. E.g - for a function named hello
, the generated typedefs are named as NativeHello
and DartHello
.
Example -
functions: expose-typedefs: include: - 'myFunc' # Match function name. - '.*' # Do this to expose types for all function. exclude: # If you only use exclude, then everything not excluded is generated. - 'dispose'
Named declarations use their own names even when inside another typedef. However, unnamed declarations inside typedefs take the name of the first typedef that refers to them.
The following typedefs are not generated -
ffigen
uses clang
's own compiler frontend to parse and traverse the C
header files. ffigen
expands the macros using clang
's macro expansion and then traverses the expanded code. To do this, ffigen
generates temporary files in a system tmp directory.
A custom temporary directory can be specified by setting the TEST_TMPDIR
environment variable.
Ffigen can sometimes generate a lot of logs, especially when it's parsing a lot of code.
SEVERE
logs are something you definitely need to address. They can be caused due to syntax errors, or more generally missing header files (which need to be specified using compiler-opts
in config)WARNING
logs are something you can ignore, but should probably look into. These are mostly indications of declarations ffigen couldn't generate due to limitations of dart:ffi, private declarations (which can be resolved by renaming them via ffigen config) or other minor issues in the config file itself.dart run ffigen --verbose <level>
. Level options are - [all, fine, info (default), warning, severe]
. The all
and fine
will print a ton of logs are meant for debugging purposes only.Ffigen can share type definitions using symbol files.
output.symbol-file
config.import.symbol-files
config.Checkout examples/shared_bindings
for details.
For manually reusing definitions from another package, the library-imports
and type-map
config can be used.
Methods and properties on ObjC interfaces and protocols can be filtered using the member-filter
option under objc-interfaces
and objc-protocols
. For simplicity we'll focus on interface methods, but the same rules apply to properties and protocols. There are two parts to the filtering process: matching the interface, and then filtering the method.
The syntax of member-filter
is a YAML map from a pattern to some include
/exclude
rules, and include
and exclude
are each a list of patterns.
objc-interfaces: member-filter: MyInterface: # Matches an interface. include: - "someMethod:withArg:" # Matches a method. exclude: - someOtherMethod # Matches a method.
The interface matching logic is the same as the matching logic for the member-rename
option:
_
, it is treated as a string rather than a regex.include
/exclude
rules.The method filtering logic uses the same include
/exclude
rules as the rest of the config:
include
and exclude
are a list of patterns.:
characters. This is the same name you‘ll see in ObjC’s API documentation.:
, it‘s a good idea to surround the pattern with quotes, "
. Otherwise YAML will think you’re defining a map key.include
or exclude
rules are defined, all methods are included, regardless of the top level exclude-all-by-default
rule.include
rules are defined
, all non-matching methods are excluded.exclude
rules are defined
, all non-matching methods are included.include
and exclude
rules are defined, the exclude
rules take precedence. That is, if a method name matches both an include
rule and an exclude
rule, the method is excluded. All non-matching methods are also excluded.The property filtering rules live in the same objc-interfaces.member-filter
option as the methods. There is no distinction between methods and properties in the filters. The protocol filtering rules live in objc-protocols.member-filter
.
It can be tricky to locate header files containing Apple's ObjC frameworks, and the paths can vary between computers depending on which version of Xcode you are using and where it is installed. So ffigen provides the following variable substitutions that can be used in the headers.entry-points
list:
$XCODE
: Replaced with the result of xcode-select -p
, which is the directory where Xcode's APIs are installed.$IOS_SDK
: Replaced with xcrun --show-sdk-path --sdk iphoneos
, which is the directory within $XCODE
where the iOS SDK is installed.$MACOS_SDK
: Replaced with xcrun --show-sdk-path --sdk macosx
, which is the directory within $XCODE
where the macOS SDK is installed.For example:
headers: entry-points: - '$MACOS_SDK/System/Library/Frameworks/Foundation.framework/Headers/NSDate.h'