Majid Hajian Majid is a Google Developer Expert, an award-winning author, Flutter, PWA, a perf enthusiast, and a passionate software developer with years of developing and architecting complex web and mobile applications.

Using Dart FFI to access native libraries in Flutter

10 min read 2996

Dart FFI Native Libraries Flutter

Dart is a feature-rich language that is well documented and easy to learn; however, it can lack some functionality when it comes to Flutter app development. For example, there may be a need for an app to link to an external binary library, or it may be beneficial to write some of the code in a lower-level language like C, C+, or Rust.

Fortunately, Flutter apps are able to use the Foreign Function Interface (FFI) through the dart:ffi library. FFI enables programs written in one language to call libraries written in other languages. For example, with FFI a Flutter app can call a C-based compiled library, such as cJSON.dylib, or call C source code, such as lib/utils.c, directly from Dart.

A core benefit of having the FFI interop mechanism in Dart is that it enables us to write code in any language compiled to the C library. Some examples are Go and Rust.

FFI also enables us to provide the same functionality across different platforms using the same code. For example, let’s say we wanted to utilize a particular open source library in all media without investing the time and effort to write the same logic in each app’s development language (Swift, Kotlin, etc.). One solution would be to implement the code in C or Rust and then expose it with FFI to a Flutter app.

Dart FFI opens up new development opportunities, particularly for projects that require sharing native codes between teams and projects or boosting app performance.

In this article, we’ll examine how to use Dart FFI to access native libraries in Flutter.

First, let’s get started with the basics and foundations.

Using Dart FFI to access a dynamic library

Let’s start by writing a basic math function in C. We’ll use it in a simple Dart application:

/// native/add.c

int add(int a, int b)
{
return a + b;
}

A native library can be linked into an app statically or dynamically. A statically linked library is embedded into the application’s executable image. It loads when the app starts. A dynamically linked library, by contrast, is distributed in a separate file or folder within the app. It loads on-demand.

We can covert our C file to the dynamic library dylib by running the following code:

We made a custom demo for .
No really. Click here to check it out.

gcc -dynamiclib add.c -o libadd.dylib

This results in the following output: add.dylib.

We’ll follow three steps to call this function in Dart:

  1. Open the dynamic library containing the function
  2. Look up the function (N.B., because types are different in C and Dart, we must specify each respectively)
  3. Call the function
/// run.dart
import 'dart:developer' as dev;
import 'package:path/path.dart';
import 'dart:ffi';void main() {
final path = absolute('native/libadd.dylib');
dev.log('path to lib $path');
final dylib = DynamicLibrary.open(path);
final add = dylib.lookupFunction('add');
dev.log('calling native function');
final result = add(40, 2);
dev.log('result is $result'); // 42
}

This example illustrates that we can employ FFI to easily use any dynamic library in a Dart application.

Now, it’s time to introduce a tool that can help generate FFI binding via code generation.

Generating FFI bindings in Dart with FFIGEN

There may be times when it would be too time consuming or tedious to write the binding code for Dart FFI. In this situation, the Foreign Function Interface GENerator (ffigen) can be very helpful. ffigen is a binding generator for FFI. It helps parse C headers and automatically generates dart code.

Let’s use this example C header file that contains basic math functions:

/// native/math.h

/** Adds 2 integers. */
int sum(int a, int b);
/** Subtracts 2 integers. */
int subtract(int *a, int b);
/** Multiplies 2 integers, returns pointer to an integer,. */
int *multiply(int a, int b);
/** Divides 2 integers, returns pointer to a float. */
float *divide(int a, int b);
/** Divides 2 floats, returns a pointer to double. */
double *dividePercision(float *a, float *b);

To generate FFI bindings in Dart, we’ll add ffigen to dev_dependencies in the pubspec.yml file:

/// pubspec.yaml 
dev_dependencies:
ffigen: ^4.1.2

ffigen requires that configurations are added as a separate config.yaml file or added under ffigen in pubspec.yaml, as shown here:

/// pubspec.yaml
....

ffigen:
name: 'MathUtilsFFI'
description: 'Written for the FFI article'
output: 'lib/ffi/generated_bindings.dart'
headers:
entry-points:
- 'native/headers/math.h'

The entry-points and the output file that should be generated are mandatory fields; however, we may also define and include a name and description.

Next, we’ll run the following code:
dart run ffigen

This results in the following output: generated_bindings.dart

Now, we can use the MathUtilsFFI class in our Dart files.

Using FFIGEN in a demo

Now that we’ve covered the basics of ffigen, let’s walk through a demo:

Generating the dynamic library

For this demo, we’ll use cJSON, which is an ultralightweight JSON parser that may be used in Flutter or Dart applications.

The entire cJSON library is comprised of one C file and one header file, so we can simply copy cJSON.c and cJSON.h to our project’s source. However, we also need to use the CMake build system. CMake is recommended for out-of-tree builds, meaning the build directory (containing the compiled files) is separate from the source directory (containing the source files). As of this writing, CMake version 2.8.5 or higher is supported.

To build cJSON with CMake on a Unix platform, we first make a build directory and then run CMake inside the directory:

cd native/cJSON // where I have copied the source files
mkdir build 
cd build
cmake ..

Here’s the output:

-- The C compiler identification is AppleClang 13.0.0.13000029
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Performing Test FLAG_SUPPORTED_fvisibilityhidden
-- Performing Test FLAG_SUPPORTED_fvisibilityhidden - Success
-- Configuring done
-- Generating done
-- Build files have been written to: ./my_app_sample/native/cJSON/build

This will create a Makefile, as well as several other files.

We use this command to compile:

make

The build progress bar will advance until it is complete:

[ 88%] Built target readme_examples
[ 91%] Building C object tests/CMakeFiles/minify_tests.dir/minify_tests.c.o
[ 93%] Linking C executable minify_tests
[ 93%] Built target minify_tests
[ 95%] Building C object fuzzing/CMakeFiles/fuzz_main.dir/fuzz_main.c.o
[ 97%] Building C object fuzzing/CMakeFiles/fuzz_main.dir/cjson_read_fuzzer.c.o
[100%] Linking C executable fuzz_main
[100%] Built target fuzz_main

The dynamic libraries are generated based on the platform. For example, Mac users will see libcjson.dylib, while Windows users may see cjson.dll, and Linux users may see libcjson.so.

Generating the Dart FFI binding file

Next, we need to generate the Dart FFI binding file. In order to demonstrate how to use separated configuration, we’ll create a new configuration file, cJSON.config.yaml, and config the cJSON library:

// cJSON.config.yaml

output: 'lib/ffi/cjson_generated_bindings.dart'
name: 'CJson'
description: 'Holds bindings to cJSON.'
headers:
entry-points:
- 'native/cJSON/cJSON.h'
include-directives:
- '**cJSON.h'
comments: false
typedef-map:
'size_t': 'IntPtr'

To generate FFI bindings. we must run dart run ffigen --config cJSON.config.yaml:

> flutter pub run ffigen --config cJSON.config.yaml
Changing current working directory to: /**/my_app_sample
Running in Directory: '/**/my_app_sample'
Input Headers: [native/cJSON/cJSON.h]
Finished, Bindings generated in /**/my_app_sample/lib/ffi/cjson_generated_bindings.dart

To use this library, we create a JSON file:

/// example.json

{
"name": "Majid Hajian",
"age": 30,
"nicknames": [
{
"name": "Mr. Majid",
"length": 9
},
{
"name": "Mr. Dart",
"length": 8
}
]
}

This example JSON file is simple, but imagine the same process with heavy JSON, which requires performant parsing.

Loading the library

First, we must ensure that we are loading the dynamic library correctly:

/// cJSON.dart
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as p;
import './lib/ffi/cjson_generated_bindings.dart' as cj;

String _getPath() {
final cjsonExamplePath = Directory.current.absolute.path;
var path = p.join(cjsonExamplePath, 'native/cJSON/build/');
if (Platform.isMacOS) {
path = p.join(path, 'libcjson.dylib');
} else if (Platform.isWindows) {
path = p.join(path, 'Debug', 'cjson.dll');
} else {
path = p.join(path, 'libcjson.so');
}
return path;
}

Next, we open the dynamic library:

final cjson = cj.CJson(DynamicLibrary.open(_getPath()));

Now, we can use the generated cJSON bindings:

/// cJSON.dart

void main() {
final pathToJson = p.absolute('example.json');
final jsonString = File(pathToJson).readAsStringSync();
final cjsonParsedJson = cjson.cJSON_Parse(jsonString.toNativeUtf8().cast());
if (cjsonParsedJson == nullptr) {
print('Error parsing cjson.');
exit(1);
}
// The json is now stored in some C data structure which we need
// to iterate and convert to a dart object (map/list).
// Converting cjson object to a dart object.
final dynamic dartJson = convertCJsonToDartObj(cjsonParsedJson.cast());
// Delete the cjsonParsedJson object.
cjson.cJSON_Delete(cjsonParsedJson);
// Check if the converted json is correct
// by comparing the result with json converted by `dart:convert`.
if (dartJson.toString() == json.decode(jsonString).toString()) {
print('Parsed Json: $dartJson');
print('Json converted successfully');
} else {
print("Converted json doesn't match\n");
print('Actual:\n' + dartJson.toString() + '\n');
print('Expected:\n' + json.decode(jsonString).toString());
}
}

Next, we can use helper functions to parse (or convert) cJSON into Dart Object:

/// main.dart
dynamic convertCJsonToDartObj(Pointer<cj.cJSON> parsedcjson) {
dynamic obj;
if (cjson.cJSON_IsObject(parsedcjson.cast()) == 1) {
obj = <String, dynamic>{};
Pointer<cj.cJSON>? ptr;
ptr = parsedcjson.ref.child;
while (ptr != nullptr) {
final dynamic o = convertCJsonToDartObj(ptr!);
_addToObj(obj, o, ptr.ref.string.cast());
ptr = ptr.ref.next;
}
} else if (cjson.cJSON_IsArray(parsedcjson.cast()) == 1) {
obj = <dynamic>[];
Pointer<cj.cJSON>? ptr;
ptr = parsedcjson.ref.child;
while (ptr != nullptr) {
final dynamic o = convertCJsonToDartObj(ptr!);
_addToObj(obj, o);
ptr = ptr.ref.next;
}
} else if (cjson.cJSON_IsString(parsedcjson.cast()) == 1) {
obj = parsedcjson.ref.valuestring.cast<Utf8>().toDartString();
} else if (cjson.cJSON_IsNumber(parsedcjson.cast()) == 1) {
obj = parsedcjson.ref.valueint == parsedcjson.ref.valuedouble
? parsedcjson.ref.valueint
: parsedcjson.ref.valuedouble;
}
return obj;
}
void _addToObj(dynamic obj, dynamic o, [Pointer<Utf8>? name]) {
if (obj is Map<String, dynamic>) {
obj[name!.toDartString()] = o;
} else if (obj is List<dynamic>) {
obj.add(o);
}
}

Using FFI to pass strings from C to Dart

The [ffi] package can be used to pass strings from C to Dart. We add this package to our dependencies:

/// pubspec.yaml

dependencies:
ffi: ^1.1.2

Testing the call

Now, let’s check to see if our demo was successful!

We can see in this example, the C strings for name, age, and nicknames were successfully parsed into Dart:

> dart cJSON.dart

Parsed Json: {name: Majid Hajian, age: 30, nicknames: [{name: Mr. Majid, length: 9}, {name: Mr. Dart, length: 8}]}
Json converted successfully

Now that we’ve reviewed the essentials for FFI, let’s see how we can use them in Flutter.

Using FFI to add a dynamic library to a Flutter app

Most of the concepts from Dart FFI apply to Flutter too. To simplify this tutorial, we’ll focus on Android and iOS, but these methods would apply to other applications as well.

To add a dynamic library to a Flutter app using FFI, we’ll follow these steps:

Configuring the Android Studio C compiler

To configure the Android Studio C compiler, we will we’ll follow three steps:

  1. Go to: android/app
  1. Create a CMakeLists.txt
    file:cmakeminimumrequired(VERSION 3.4.1)add_library(
    cJSON
    SHARED
    ../../DART/native/cJSON/cJSON.c // path to your native code
    )
  2. Open android/app/build.gradle and add the following snippet:
    android {
    ....externalNativeBuild {
    cmake {
    path "CMakeLists.txt"
    }
    }...
    }

This code tells the Android build system to call CMake with CMakeLists.txt when building the app. It will compile the .c source file to a shared object library with an .so suffix on Android.

Configuring the Xcode C compiler

To ensure that Xcode will build our app with native C code, we’ll follow these 10 steps:

  1. Open the Xcode workspace by running:
open< ios/Runner.xcworkspace
  1. From the Targets dropdown in the top navigation bar, select Runner
  2. From the row of tabs, select Build Phases
  3. Expand the Compile Sources tab, and click the + key.
  4. From the popup window, click Add Other
  5. Navigate to where the C files are stored, for example, FLUTTER_PROJCT_ROOT/DART/native/cJSON/cJSON.c, and add both the cJSON.c and cJSON.h files
  6. Expand the Compile Sources tab and click the + key
  7. On the popup window, click Add Other
  8. Navigate to where the r .c file stored, for example, FLUTTER_PROJCT_ROOT/DART/native/cJSON/cJSON.c
  9. Select Copy Items If Needed and click Finish

Now, we’re ready to add the generated Dart binding code to the Flutter app, load the library, and call the functions.

Generating the FFI binding code

We’ll use ffigen to generate binding code. First, we’ll add ffigen to the Flutter app:

/// pubspec.yaml for my Flutter project
...
dependencies:
ffigen: ^4.1.2
...

ffigen:
output: 'lib/ffi/cjson_generated_bindings.dart'
name: 'CJson'
description: 'Holds bindings to cJSON.'
headers:
entry-points:
- 'DART/native/cJSON/cJSON.h'
include-directives:
- '**cJSON.h'
comments: false
typedef-map:
'size_t': 'IntPtr'

Next, we’ll run ffigen:

flutter pub run ffigen

We’ll need to ensure that the example.json file is added under assets:

/// pubspec.yaml
...
flutter:
uses-material-design: true
assets:
- example.json
...

Loading the dynamic library

Just as a statically linked library can be embedded to load when an app starts, symbols from a statically linked library can be loaded using DynamicLibrary.executable or DynamicLibrary.process.

On Android, a dynamically linked library is distributed as a set of .so (ELF) files, one for each architecture. On iOS, a dynamically linked library is distributed as a .framework folder.

A dynamically linked library may be loaded into Dart via the DynamicLibrary.open command.

We’ll use the following code to load the library:

/// lib/ffi_loader.dart

import 'dart:convert';
import 'dart:developer' as dev_tools;
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:my_app_sample/ffi/cjson_generated_bindings.dart' as cj;

class MyNativeCJson {
MyNativeCJson({
required this.pathToJson,
}) {
final cJSONNative = Platform.isAndroid
? DynamicLibrary.open('libcjson.so')
: DynamicLibrary.process();
cjson = cj.CJson(cJSONNative);
}
late cj.CJson cjson;
final String pathToJson;
Future<void> load() async {
final jsonString = await rootBundle.loadString('assets/$pathToJson');
final cjsonParsedJson = cjson.cJSON_Parse(jsonString.toNativeUtf8().cast());
if (cjsonParsedJson == nullptr) {
dev_tools.log('Error parsing cjson.');
}
final dynamic dartJson = convertCJsonToDartObj(cjsonParsedJson.cast());
cjson.cJSON_Delete(cjsonParsedJson);
if (dartJson.toString() == json.decode(jsonString).toString()) {
dev_tools.log('Parsed Json: $dartJson');
dev_tools.log('Json converted successfully');
} else {
dev_tools.log("Converted json doesn't match\n");
dev_tools.log('Actual:\n$dartJson\n');
dev_tools.log('Expected:\n${json.decode(jsonString)}');
}
}
dynamic convertCJsonToDartObj(Pointer<cj.cJSON> parsedcjson) {
dynamic obj;
if (cjson.cJSON_IsObject(parsedcjson.cast()) == 1) {
obj = <String, dynamic>{};
Pointer<cj.cJSON>? ptr;
ptr = parsedcjson.ref.child;
while (ptr != nullptr) {
final dynamic o = convertCJsonToDartObj(ptr!);
_addToObj(obj, o, ptr.ref.string.cast());
ptr = ptr.ref.next;
}
} else if (cjson.cJSON_IsArray(parsedcjson.cast()) == 1) {
obj = <dynamic>[];
Pointer<cj.cJSON>? ptr;
ptr = parsedcjson.ref.child;
while (ptr != nullptr) {
final dynamic o = convertCJsonToDartObj(ptr!);
_addToObj(obj, o);
ptr = ptr.ref.next;
}
} else if (cjson.cJSON_IsString(parsedcjson.cast()) == 1) {
obj = parsedcjson.ref.valuestring.cast<Utf8>().toDartString();
} else if (cjson.cJSON_IsNumber(parsedcjson.cast()) == 1) {
obj = parsedcjson.ref.valueint == parsedcjson.ref.valuedouble
? parsedcjson.ref.valueint
: parsedcjson.ref.valuedouble;
}
return obj;
}
void _addToObj(dynamic obj, dynamic o, [Pointer<Utf8>? name]) {
if (obj is Map<String, dynamic>) {
obj[name!.toDartString()] = o;
} else if (obj is List<dynamic>) {
obj.add(o);
}
}
}

For Android, we call DynamicLibrary to find and open the libcjson.so shared library:

final cJSONNative = Platform.isAndroid
? DynamicLibrary.open('libcJSON.so')
: DynamicLibrary.process();

cjson = cj.CJson(cJSONNative);

There is no need for this particular step in iOS, since all linked symbols map when an iOS app runs.

Testing the call in Flutter

To demonstrate that the native call is working in Flutter, we add usage to the main.dart file:

// main.dart

import 'package:flutter/material.dart';
import 'ffi_loader.dart';

void main() {
runApp(const MyApp());

final cJson = MyNativeCJson(pathToJson: 'example.json');
await cJson.load();
}

Next, we run the app: flutter run

Voilà! We’ve successfully called the native library from our Flutter app.

We can view the logs from the native calls in the console:

Launching lib/main_development.dart on iPhone 13 in debug mode...
lib/main_development.dart:1
Xcode build done. 16.5s
Connecting to VM Service at ws://127.0.0.1:53265/9P2HdUg5_Ak=/ws
[log] Parsed Json: {name: Majid Hajian, age: 30, nicknames: [{name: Mr. Majid, length: 9}, {name: Mr. Dart, length: 8}]}
[log] Json converted successfully

Going forward, we can use this library in our Flutter app in different widgets and services.

Conclusion

Dart FFI offers an easy solution for integrating native libraries into Dart and Flutter applications. In this article, we’ve demonstrated how to call the C function in Dart using Dart FFI and integrate a C library into a Flutter application.

You may want to experiment further with Dart FFI, using code written in other languages. I am especially interested in experimenting with Go and Rust as these languages are memory managed. Rust is particularly interesting, is it is a memory-safe language and fairly performant.

All the examples used in this article may be found on GitHub.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Majid Hajian Majid is a Google Developer Expert, an award-winning author, Flutter, PWA, a perf enthusiast, and a passionate software developer with years of developing and architecting complex web and mobile applications.

Leave a Reply