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.
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:
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:
/// 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.
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.
Now that we’ve covered the basics of ffigen
, let’s walk through a demo:
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
.
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.
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); } }
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
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.
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:
To configure the Android Studio C compiler, we will we’ll follow three steps:
android/app
CMakeLists.txt
file:cmakeminimumrequired(VERSION 3.4.1)add_library( cJSON SHARED ../../DART/native/cJSON/cJSON.c // path to your native code )
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.
To ensure that Xcode will build our app with native C code, we’ll follow these 10 steps:
open< ios/Runner.xcworkspace
FLUTTER_PROJCT_ROOT/DART/native/cJSON/cJSON.c
, and add both the cJSON.c
and cJSON.h
files.c
file stored, for example, FLUTTER_PROJCT_ROOT/DART/native/cJSON/cJSON.c
Now, we’re ready to add the generated Dart binding code to the Flutter app, load the library, and call the functions.
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 ...
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.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
One Reply to "Using Dart FFI to access native libraries in Flutter"
Can I use dart FFI for C++, Qt project?
If I have more than one .dll files, how do I deal with them? Do I need to compile the C file firstly?