DEV Community

Cover image for Using FFIGen in Dart 2.18
aseem wangoo
aseem wangoo

Posted on

Using FFIGen in Dart 2.18

In case it helped :)
Pass Me A Coffee!!

We will cover briefly:

  1. What’s FFIGen
  2. What’s new in FFIGen
  3. Dart CLI App and integrate obj-c-based libraries
  4. 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:ffilibrary. 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, and dbus.

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
Enter fullscreen mode Exit fullscreen mode

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

Dart 2.18
Dart 2.18

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
Enter fullscreen mode Exit fullscreen mode

Note: There are various templates available for Dart, see below. By default, it selects console application.

Dart Templates
Dart Templates

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
Enter fullscreen mode Exit fullscreen mode

Edit your pubspec file to add the ffigen dev dependency. Next, specify the configuration under this dependency. Configurations can be provided in 2 ways-

  1. In the project’s pubspec.yaml file under the key ffigen.
  2. 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"
Enter fullscreen mode Exit fullscreen mode

Let’s see the above configuration options-

  • name The name for the class which will be generated, after we run the ffigen, this class will be called URLCacheLibrary 
  • language Must be one of `c`, or ‘objc’. Defaults to ‘c’. Since the library we select is written in Objective-C, we specify objc 
  • output Output path of the generated bindings. This file will have all the FFI bindings which take care of the functions inside Obj-C
  • headers This includes the path to the header files It includes everything from the location as specified under the entry-pointsIn our case, the header files are present inside the Foundation.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 the NSURLCache 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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This command creates a new file (url_cache_bindings.dart) as specified inside the output parameter of the url_cache_config.yamlwhich 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}');
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Library linking
Library linking

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The result is as

NSURLCache data
NSURLCache data

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.hwhich 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"
Enter fullscreen mode Exit fullscreen mode

In the above configuration, we specify the 

  • name This class will be called TimeZoneLibrary
  • language The library we select is written in Objective-C, we specify objc
  • headers The path to the header files which is present inside the Foundation.framework

For generating the bindings we run the following

dart run ffigen
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The result is as

NSTimeZone data
NSTimeZone data

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}');
  });
}
Enter fullscreen mode Exit fullscreen mode

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('));
 });
}
Enter fullscreen mode Exit fullscreen mode

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 YAMLfile, we specify the result as YamlMapYAML 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('));
Enter fullscreen mode Exit fullscreen mode

This is because once we run the test using

dart test test/ffi_2_18_test.dart
Enter fullscreen mode Exit fullscreen mode

It generates the config file during the runtime and this gets compared with the strings above.

Test passed using FFIGen
Test passed using FFIGen

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('));
 );
}
Enter fullscreen mode Exit fullscreen mode

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('));
Enter fullscreen mode Exit fullscreen mode

This is because once we run the test using

dart test test/ffi_2_18_test.dart
Enter fullscreen mode Exit fullscreen mode

It generates the config file during the runtime and this gets compared with the strings inside the test.

Test passed using FFIGen
Test passed using FFIGen

Source code

In case it helped :)
Pass Me A Coffee!!

Top comments (0)