Using FFIGen in Dart 2.18
aseem wangoo
Posted on December 22, 2022
We will cover briefly:
- What’s FFIGen
- What’s new in FFIGen
- Dart CLI App and integrate obj-c-based libraries
- Testing FFIGen
Wait, what’s FFIGen?
Before explaining the answer to this question, the reader needs to know about FFI
(Foreign Function Interface)
What’s FFI
FFI enables programs written in one language to call libraries written in other languages. The term FFI
comes from CommonLisp, however, it’s applicable to any language. Some languages such as Java, use FFI in their ecosystem and call it JavaNativeInterface.
If we refer to the low-level language as the “host” language and the high-level language as the “guest” language, below are the ways to communicate between them.
- The host is expected to bridge the gap with the guest. We write host-language functions specifically to be called by the guest. An API is offered for the host language to communicate with guests.
- The gap is bridged by some kind of tool that does not belong strictly to either the host or guest languages.
- The guest is also expected to bridge the gap with the host. Guest language can call any host-language function, but it needs to support many low-level features, in order to communicate with the host language effectively.
According to Wikipedia, these are the things to consider for FFI:
- If one language supports garbage collection (GC) and the other does not; care must be taken that the non-GC language code does nothing to cause GC in the other to fail.
- Complicated or non-trivial objects or datatypes may be difficult to map from one environment to another.
- One or both languages may be running on a virtual machine (VM); moreover, if both are, these will probably be different VMs.
Fortunately, we are able to use FFI in Dart through the dart:ffi
library. With Dart v2.12 onwards, Dart FFI is available on the stable channel. Dart FFI allows you to use the existing code in C libraries. By using FFI we can avail the benefits of both portability and integration of highly tuned C code for performance-intensive tasks. We are not limited to C
, in fact, we can write the code in any language that is compiled to the C library, for instance Go, Rust
Another use case for using Dart FFI can be there are times when the Flutter app needs to have greater control over memory management and garbage collection, for instance, an app using tensor flow.
Dart FFI can be used to read, write, allocate and deallocate the native memory. There are some packages that already used this feature:
file_picker
,printing
,win32
,objectbox
,realm
,isar
,tflite_flutter
, anddbus
.
Ways of using Dart FFI
There are times when you want to create your own fresh library, but the maximum number of times, the library would already exist (created by some other team) and you simply want to use it. In either of the cases, we have the following choices
- Manually creating the FFI bindings
- Automatically generating the FFI bindings
If you like automation, you probably chose the second option, and as a result, we have package:ffigen
The idea behind the package ffigen
is: For large APIs, it can be very time-consuming to write the Dart bindings which allow the integration with the C code. Hence, the Dart team came up with a binding generator (ffigen) that automatically creates the FFI wrappers out of the C header files.
Under the hood, this package uses LLVM and LibClang
to parse C header files. For installing LLVM inside macOS
brew install llvm
There are multiple types provided by dart:ffi
for representing the types in C. However, they broadly are classified in
- Instantiable Native Types
- Purely marker Native Types
Instantiable Native Types: They or their subtypes can be instantiated in the Dart Code. For instance, Array
Pointer
Struct
Union
Purely marker Native Types: They are platform agnostic and cannot be instantiated in the Dart Code. For instance, Bool
Double
Int64
Int32
etc
There are also ABI marker types that extend AbiSpecificInteger
For instance Size
Short
etc
Until now, we have covered what’s FFI and what’s ffigen, let’s explore what’s new inside ffigen from Dart 2.18
What’s new in FFIGen
The Dart team wants Dart to support interoperability with all the primary languages on the platforms where Dart runs.
As of Dart 2.18 the Dart
code can now call the Objective-C and Swift code since these are used for writing APIs for macOS and iOS. This interop mechanism is supported across all types of apps (for instance, CLI app to backend app to Flutter code)
This feature is not limited to command-line apps. Even the Dart mobile, and server apps running on the Dart Native platform, on macOS or iOS, can use dart:ffi
This unlocks the possibilities since before 2.18 it was only possible to call the C/C++
based libraries.
According to the official blog,
This new mechanism utilizes the fact that Objective-C and Swift code can be exposed as C code based on API bindings. The Dart API wrapper generation tool,
ffigen
, can create these bindings from API headers
This support for Objective-C and Swift is marked as experimental starting from Dart 2.18
In case someone experiences any problems, they can comment on the feedback issue on GitHub.
Dart CLI App with Objective-C-based libraries
In this section, we create a Dart-based command line application that demonstrates how to call an Objective-C-based library using the new functionalities from ffigen
We will choose any Objective-C library present inside the macOS, and integrate it inside the Dart CLI App.
One such library is
NSURLCache
macOS has an API for querying URL cache information exposed by the NSURLCache
class.
The NSURLCache
implements the caching of responses to URL load requests, by mapping NSURLRequest
objects to NSCachedURLResponse
objects. It provides a composite in-memory and on-disk cache, and lets you manipulate the sizes of both the in-memory and on-disk portions.
We will be integrating the NSURLCache
inside Dart
and call some of its functions:
-
currentDiskUsage
: The current size of the on-disk cache, in bytes. -
diskCapacity
: The capacity of the on-disk cache, in bytes. -
memoryCapacity
: The capacity of the in-memory cache, in bytes.
Create Dart CLI App
We start by creating the Dart CLI App using the below command. Also, upgrade to the latest Dart version 2.18
dart create ffi_2_18
## ffi_2_18 is the name of the project which will be created
Note: There are various templates available for Dart, see below. By default, it selects console application.
This gives us a basic template with all the necessary files, for instance, pubspec
or linter
Open the pubspec
file to check the dependencies which come bundled with this template.
dev_dependencies:
lints: ^2.0.0
test: ^1.16.0
Edit your pubspec
file to add the ffigen
dev dependency. Next, specify the configuration under this dependency. Configurations can be provided in 2 ways-
- In the project’s
pubspec.yaml
file under the keyffigen
. - Via a custom YAML file, then specify this file while running —
dart run ffigen --config config.yaml
We will see the option 2 first. Separate config files for the libraries
Create a file called url_cache_config.yaml
and put the below contents inside it.
name: URLCacheLibrary
language: objc
output: "url_cache_bindings.dart"
exclude-all-by-default: true
objc-interfaces:
include:
- "NSURLCache"
headers:
entry-points:
- "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSURLCache.h"
Let’s see the above configuration options-
-
name
The name for the class which will be generated, after we run theffigen
, this class will be calledURLCacheLibrary
-
language
Must be one of `c`, or ‘objc’. Defaults to ‘c’. Since the library we select is written in Objective-C, we specifyobjc
-
output
Output path of the generated bindings. This file will have all the FFI bindings which take care of the functions insideObj-C
-
headers
This includes the path to theheader files
It includes everything from the location as specified under theentry-points
In our case, the header files are present inside theFoundation.framework
-
exclude-all-by-default
When a declaration filter (eg functions or structs:) is empty, it defaults to including everything. If this flag is enabled, the default behavior is to exclude everything instead.
Objective-C config options
-
objc-interfaces
This filters for the interface declarations. In our case, we specify theNSURLCache
interface
objc-interfaces:
include:
# Includes a specific interface.
- 'NSURLCache'
# Includes all interfaces starting with "NS".
- 'NS.*'
exclude:
# Override the above NS.* inclusion, to exclude NSURL.
- 'NSURL'
rename:
# Removes '_' prefix from interface names.
'_(.*)': '$1'
Generate Bindings
To generate the bindings, run the following:
dart run ffigen --config url_cache_config.yaml
## url_cache_config is the file which we created above
This command creates a new file (url_cache_bindings.dart) as specified inside the output
parameter of the url_cache_config.yaml
which contains a bunch of generated API bindings. Using this binding file, we can write our Dart main
method.
Integrate into Dart
We generated the bindings using the ffigen
in the above step. Let’s see how to integrate it inside the Dart
We create a new dart file called url_cache.dart
Inside this file, we would be loading and interacting with the generated library.
void main() {
const dylibPath = '/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation';
final lib = URLCacheLibrary(DynamicLibrary.open(dylibPath));
final urlCache = NSURLCache.getSharedURLCache(lib);
if (urlCache != null) {
print('currentDiskUsage: ${urlCache.currentDiskUsage}');
print('currentMemoryUsage: ${urlCache.currentMemoryUsage}');
print('diskCapacity: ${urlCache.diskCapacity}');
print('memoryCapacity: ${urlCache.memoryCapacity}');
}
}
We mention the path of the library in the first step. Since, the library we are using is an internal library, the dylib
points to the macOS’s framework dylib
We can consider this library to be dynamically linked.
Note: We can also use our own library or a static library (linked inside our app)
Dynamic Linking: In this type, the external libraries are placed inside the final executable, however, the actual linking happens at the run time. In dynamic linking, only one copy of the shared library is kept inside the memory which reduces the program size, memory, and disk space. Since the libraries are shared, dynamic linking programs are slower in comparison to static linking programs.
A dynamically linked library is distributed in a separate file or folder within the app and loaded on demand. A dynamically linked library can be loaded into Dart via DynamicLibrary.open
.
Static Linking: In this type, the modules are copied inside the program before creating the final executable. Since these programs include libraries, they are large in size. However, because of the libraries already compiled, these programs are faster than dynamically linked programs.
A statically linked library is embedded into the app’s executable image and is loaded when the app starts. Symbols from a statically linked library can be loaded using DynamicLibrary.executable
or DynamicLibrary.process
.
Next, we construct the URLCacheLibrary
by using the constructor which needs the dylib
path. For this, we call the DynamicLibrary.open
This loads the library file and provides the access to its symbols.
Note: This process loads the library into the DartVM only once, regardless of the function calls.
Once the library gets initialized, we can call the different methods present inside it (which were generated).
We are looking for a NSURLCache
class. This class implements the caching of responses to URL load requests, by mapping NSURLRequest
objects to NSCachedURLResponse
objects. For getting an instance of this class, we call sharedURLCache
final urlCache = NSURLCache.getSharedURLCache(lib)
Since we have the instance of URLCache
we can access the different methods currentDiskUsage
currentMemoryUsage
diskCapacity
memoryCapacity
. Let’s run the dart code using
dart run bin/url_cache.dart
The result is as
Using configuration inside pubspec
In the above section, we saw how to use the configuration specified inside a separate config file, let’s see how to use the configuration inside the pubspec
We will choose another Objective-C library present inside the macOS.
One such library is
NSTimeZone
This API is used for querying the time zones along with the standard time policies of a region. These time zones can have identifiers such as America/Los_Angeles
and can also be identified by abbreviations such as PST
for Pacific Standard Time.
The header for this library is present inside the NSTimeZone.h
which can be found inside the Apple Foundation library. Let’s include the configuration inside the pubspec:
dev_dependencies:
ffigen:
name: TimeZoneLibrary
language: objc
output: "timezone_bindings.dart"
exclude-all-by-default: true
objc-interfaces:
include:
- "NSTimeZone"
headers:
entry-points:
- "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSTimeZone.h"
In the above configuration, we specify the
-
name
This class will be calledTimeZoneLibrary
-
language
The library we select is written in Objective-C, we specifyobjc
-
headers
The path to theheader files
which is present inside theFoundation.framework
For generating the bindings we run the following
dart run ffigen
This command creates a new file (timezone_bindings.dart) as specified inside the output parameter that contains a bunch of generated API bindings. Using this binding file, we can write our Dart main
method.
We create a new dart file called timezones.dart
Inside this file, we load and interact with the generated library.
const dylibPath='/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation';
final lib = TimeZoneLibrary(DynamicLibrary.open(dylibPath));
final timeZone = NSTimeZone.getLocalTimeZone(lib);
if (timeZone != null) {
print('Timezone name: ${timeZone.name}');
print('Offset: ${timeZone.secondsFromGMT / 60 / 60} hours');
}
We construct the TimeZoneLibrary
by using the constructor which needs the dylib
path. Once the library gets initialized, we call the different methods present inside it.
We will be integrating the NSTimeZone
inside Dart
and call some of its functions:
-
name
: The geopolitical region ID that identifies the receiver. -
secondsFromGMT
: The current difference in seconds between the receiver and Greenwich Mean Time.
For getting an instance of this class, we call localTimeZone
final timeZone = NSTimeZone.getLocalTimeZone(lib)
Since we have the instance of NSTimeZone
we can access the different methods name
secondsFromGMT
. Let’s run the dart code using
dart run bin/timezones.dart
The result is as
Garbage Collection
Objective-C uses reference counting for memory management, but on the Dart side, memory management is handled automatically. The Dart wrapper object retains a reference to the Objective-C object, and when the Dart object is garbage collected, the generated code automatically releases that reference using a NativeFinalizer
.
Limitations of Objective-C Interop
The issues with the multithreading currently are a limitation to Dart’s experimental support for Objective-C
interop. However, these limitations are not intentional, but due to the relationship between the Dart isolates and OS threads, and also how Apple handles the multithreading.
- While
ffigen
supports converting Dart functions to Objective-C blocks, but most Apple APIs don’t guarantee on which thread a callback will run. - Dart isolates are not the same as threads. Isolates run on threads but aren’t guaranteed to run on any particular thread. The VM can change which thread an isolate is running on without warning.
- Apple APIs are not thread-safe.
Since the VM can change the thread in which an isolate can run, this means a callback created in one isolate might be invoked on a different or no isolate. However, there are some tweaks around this, as implemented in the cupertino:http
Testing FFIGen
Till now, we saw how to generate the bindings, and consume them from Dart CLI. In this section, we will see how to test the generated bindings.
We install the dependencies yaml
and logging
and create a file called ffi_2_18_test
Note: The tests should follow
<name>_test.dart
pattern
The yaml
dependency helps in the parsing of a YAML
file. Whereas logging
provides us with the APIs useful for logging (based on the configuration as specified).
Setup Logging
We configure the logging level and add a handler for the log messages. The level is set to Level.SEVERE
and next, we listen on the onRecord
stream for LogRecord
events.
void logWarnings([Level level = Level.WARNING]) {
Logger.root.level = level;
Logger.root.onRecord.listen((record) {
print('${record.level.name.padRight(8)}: ${record.message}');
});
}
This function logWarnings
is called inside the setUpAll
The function registered under setUpAll
will be run once before all the tests.
Test for NSURLCache
test('url_cache', () {
final pubspecFile = File('url_cache_config.yaml');
final pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
final config = Config.fromYaml(pubspecYaml);
final output = parse(config).generate();
expect(output, contains('class URLCacheLibrary{'));
expect(output, contains('static NSURLCache?getSharedURLCache('));
});
}
We begin writing a test using the test
method. The first thing we do is create the url_cache_config.yaml
using a file object.
Next, we use the loadYaml
the function which loads a single document from the YAML string. Since this method expects the parameter to be a string, we use the readAsStringSync
to convert the file contents into string synchronously.
The return value is mostly normal Dart objects. Since we are using the YAML
file, we specify the result as YamlMap
YAML mappings support some key types that the default Dart map implementation doesn’t have.
Next, we use the Config
from the ffigen
to create the configuration required for testing from the above yaml
map. Finally, we use the parse
to generate the bindings.
The output from the above step is compared against the strings, for instance
expect(output, contains('class URLCacheLibrary{'));
expect(output, contains('static NSURLCache? getSharedURLCache('));
This is because once we run the test using
dart test test/ffi_2_18_test.dart
It generates the config file during the runtime and this gets compared with the strings above.
Test for NSTimeZone
test('timezones', () {
final pubspecFile = File('pubspec.yaml');
final pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
final config = Config.fromYaml(pubspecYaml['ffigen'] as YamlMap);
final output = parse(config).generate();
expect(output, contains('class TimeZoneLibrary{'));
expect(output, contains('class NSString extends _ObjCWrapper {'));
expect(output, contains('static NSTimeZone? getLocalTimeZone('));
);
}
We create a file object using the pubspec.yaml
file. Next, we use the loadYaml
which loads the file from the YAML string.
Next, we use the Config
from the ffigen
to create the configuration required for testing from the above yaml
map. Since the pubspec file has the property ffigen
defined inside it, we straight away refer to that and specify the output type to be YamlMap
Note: For the NSTimeZone, we specified the ffigen configuration inside the pubspec.yaml
Finally, we use the parse
to generate the bindings. The output from this step is compared against the strings, for instance
expect(output, contains('class TimeZoneLibrary{'));
expect(output, contains('static NSTimeZone? getLocalTimeZone('));
This is because once we run the test using
dart test test/ffi_2_18_test.dart
It generates the config file during the runtime and this gets compared with the strings inside the test.
Posted on December 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.