In Dart FFI, how does an @Native external function differ from binding a symbol with DynamicLibrary.open and lookupFunction, and which should new Flutter code use?
answer
- annotation vs runtime lookup
- assetId defaults to the library URI
- no platform-specific open() paths
- isLeaf for short, non-blocking calls
- ffigen ffi-native: option
basics
~20 sAn @Native external function is resolved by the VM from its annotation, by default against a code asset named after the Dart library, so no loading code is needed; lookupFunction resolves a symbol at runtime in a DynamicLibrary you opened by path. New packages built with hooks use @Native.
solid answer
~40 s`DynamicLibrary.open(path)` loads a library file by a platform-specific name (`libimgz.so` on Android, `imgz.framework/imgz` on Apple platforms, `imgz.dll` on Windows), and `lookupFunction<NativeSig, DartSig>('imgz_free')` turns a symbol into a Dart closure at runtime. `DynamicLibrary.process()` and `executable()` cover symbols already linked into the app. An `@Native<Void Function(Pointer<Uint8>)>()` **external** function instead carries its native signature in an annotation; the VM resolves the symbol from the `assetId` (by default the library's URI, or a `@DefaultAsset` on the library), then a registered resolver, then the process. With build hooks bundling a code asset under that name, the Dart code has no `Platform.isAndroid` branches. Both accept `isLeaf` for short calls that never call back into Dart. The `package_ffi` template and ffigen's `ffi-native` option generate `@Native` bindings.
code
yaml · 9 lines# ffigen.yaml (package_ffi template style)
name: ImgzBindings
output: 'lib/imgz_bindings_generated.dart'
headers:
entry-points:
- 'src/imgz.h'
include-directives:
- 'src/imgz.h'
ffi-native:go deeper
Recall the two ways to bind a C function: open a library and look up the symbol, or declare an @Native external function.
Explain the two type arguments of lookupFunction, how @Native resolves its symbol through assetId, and what isLeaf promises.
Choose the binding style per packaging model and justify isLeaf only for short non-blocking functions.
Set a migration path from legacy plugin_ffi bindings to hook-built @Native bindings across a codebase with several native libraries.
## Two ways to bind a C symbol Binding means telling Dart *which* native symbol to call and *what its C signature is*. `dart:ffi` offers two mechanisms. ### DynamicLibrary + lookupFunction (the original way) ```dart import 'dart:ffi'; import 'dart:io' show Platform; final DynamicLibrary _lib = Platform.isAndroid ? DynamicLibrary.open('libimgz.so') : Platform.isWindows ? DynamicLibrary.open('imgz.dll') : DynamicLibrary.open('imgz.framework/imgz'); final void Function(Pointer<Uint8>) imgzFree = _lib .lookupFunction<Void Function(Pointer<Uint8>), void Function(Pointer<Uint8>)>('imgz_free'); ``` - `DynamicLibrary.open(path)` behaves like `dlopen`: it loads a file once per process, even across isolates. - `DynamicLibrary.process()` resolves symbols already loaded with global visibility; `DynamicLibrary.executable()` resolves symbols in the running executable, which is how statically linked code is reached. - `lookupFunction<T, F>` takes two type arguments: **`T`** is the C signature written with native types (`Int32`, `Pointer<Uint8>`), **`F`** is the Dart signature (`int`, `Pointer<Uint8>`). - `providesSymbol` checks for a symbol; `close()` (Dart 3.1) releases the library, after which looked-up functions may become invalid. This is what the legacy `plugin_ffi` template generates, with one branch per platform. ### @Native external functions ```dart @Native<Void Function(Pointer<Uint8>)>() external void imgz_free(Pointer<Uint8> data); ``` - The **native signature** is the annotation's type argument; the Dart signature is the declaration itself. - The symbol defaults to the Dart function's name; `symbol:` overrides it. - Resolution order, from the `Native` class documentation: the **`assetId`** (by default the declaring library's URI, or the id in a `@DefaultAsset(...)` annotation on the `library` directive), then a resolver the embedder registered, then the **current process**. - A **build hook** registers the compiled library as a code asset with the same id, so the VM finds it on every platform with no `open()` path at all. - `Native.addressOf(imgz_free)` gives a function pointer when C needs a callback or a finaliser. The class documentation still labels `@Native` experimental, but it is what the current `package_ffi` template and `ffigen` produce. ## isLeaf Both `@Native(isLeaf: true)` and `lookupFunction(..., isLeaf: true)` mark a **leaf call**: a short, non-blocking function that never calls back into Dart or the VM API. The VM skips part of the normal transition, making the call cheaper. The cost: while a leaf call runs, the thread cannot cooperate with the runtime, so a slow leaf call delays operations that need every thread of the isolate group, such as garbage collection. Leaf calls also unlock passing `Uint8List.address` (Dart 3.5), a pointer into Dart-heap typed data that stays valid for the duration of the call. ## Which to choose | Situation | Binding | |---|---| | New package with a build hook (`package_ffi`) | `@Native`, generated by ffigen | | A system library found in the process | `@Native` resolved against the process, or `DynamicLibrary.process()` | | A library whose path is only known at runtime (plugins loaded by the user) | `DynamicLibrary.open` + `lookupFunction` | | Legacy `plugin_ffi` package with OS-specific build files | `DynamicLibrary.open` per platform | For a photo editor bundling an image-compression library, a `package_ffi` package with `@Native` bindings keeps the Dart side free of platform checks.
- Why does lookupFunction need two function types?The first type argument describes the C signature with `dart:ffi` native types such as `Int32` or `Size`, which fixes the calling convention and widths. The second is the Dart function type the closure will have, using `int`, `double` and `Pointer`. The Dart compiler checks that the two correspond.
- When is isLeaf: true a mistake?When the C function can block or run long (file I/O, compressing a large image) or calls back into Dart. A leaf call cannot cooperate with the runtime, so a slow one stalls garbage collection and other isolate-group-wide operations, and a callback into Dart from it is not allowed.
saying these in an interview costs you the question
- Says @Native functions still need DynamicLibrary.open for each platform
- Marks slow or blocking C functions isLeaf to make them faster
- Swaps the native and Dart type arguments of lookupFunction
- Believes DynamicLibrary.open loads a fresh copy per isolate