Flutter, Google’s UI toolkit, is renowned for its ability to create natively compiled applications for mobile, web, and desktop from a single codebase. However, there are scenarios where you might need to leverage native code for performance reasons or to access platform-specific features not available through Flutter’s standard libraries. Dart FFI (Foreign Function Interface) allows Flutter apps to interact with native libraries written in languages like C, C++, or Objective-C.
What is Dart FFI?
Dart FFI (Foreign Function Interface) is a mechanism that enables Dart code to call native (C-based) libraries. It acts as a bridge between Dart and native code, allowing you to use native code within your Flutter application. This interoperability is beneficial for performance-critical tasks or when utilizing platform-specific features that aren’t available via Dart.
Why Use Dart FFI?
- Performance Optimization: Utilize native code for CPU-intensive tasks like image processing or complex algorithms.
- Access Native APIs: Access platform-specific APIs or device functionalities not exposed by Flutter’s framework.
- Reusing Existing Native Libraries: Integrate and leverage existing native libraries or SDKs.
How to Use Dart FFI in Flutter: A Step-by-Step Guide
Step 1: Set Up Your Flutter Project
Make sure you have a Flutter project set up and running. If not, create a new one using the command:
flutter create ffi_example
cd ffi_example
Step 2: Create a Native Library (C)
First, create a simple C library with a function that you want to call from Dart. For example, create a file named native_add.c:
#include <stdio.h>
int native_add(int a, int b) {
printf("Adding %d and %dn", a, b);
return a + b;
}
Step 3: Compile the C Library into a Shared Library
Compile the C code into a shared library (.so for Linux/Android, .dylib for macOS, .dll for Windows). For Linux/macOS, you can use GCC or Clang:
gcc -shared -o libnative_add.so native_add.c
For macOS, use:
clang -shared -o libnative_add.dylib native_add.c
On Windows, you will need a compiler like MinGW. The command would be:
gcc -shared -o native_add.dll native_add.c
Step 4: Configure pubspec.yaml
Add the ffigen package to your dev_dependencies in pubspec.yaml. ffigen is a tool that automatically generates Dart bindings for your C code:
dev_dependencies:
ffigen: ^8.0.0
Run flutter pub get to install the dependency.
Step 5: Create a Configuration File for ffigen
Create a configuration file named ffigen.yaml in your project root to instruct ffigen how to generate the Dart bindings:
name: NativeAdd
description: Dart FFI bindings for native_add library.
output: 'lib/native_add_bindings.dart'
headers:
entry-points:
- 'native_add.h'
include-directives:
- 'native_add.h'
compiler-opts: '-I.'
Also, create a header file native_add.h that declares the functions available in the C library:
#ifndef NATIVE_ADD_H
#define NATIVE_ADD_H
int native_add(int a, int b);
#endif
Step 6: Generate Dart Bindings
Run ffigen to generate the Dart bindings:
flutter pub run ffigen --config ffigen.yaml
This command creates a file named lib/native_add_bindings.dart with the generated bindings.
Step 7: Load the Native Library in Dart
Load the native library and access the functions in your Dart code:
import 'dart:ffi' as ffi;
import 'dart:io' show Platform;
import 'package:ffi_example/native_add_bindings.dart';
class NativeLibrary {
late final NativeAdd _nativeAdd;
NativeLibrary() {
final String libraryPath = Platform.isAndroid
? 'libnative_add.so'
: (Platform.isMacOS ? 'libnative_add.dylib' : 'native_add.dll');
_nativeAdd = NativeAdd(ffi.DynamicLibrary.open(libraryPath));
}
int add(int a, int b) {
return _nativeAdd.native_add(a, b);
}
}
Step 8: Use the Native Function in Flutter
Finally, use the NativeLibrary in your Flutter application:
import 'package:flutter/material.dart';
import 'package:ffi_example/native_library.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final nativeLibrary = NativeLibrary();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Dart FFI Example'),
),
body: Center(
child: FutureBuilder<int>(
future: Future.value(nativeLibrary.add(5, 3)),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Result: ${snapshot.data}');
} else {
return CircularProgressIndicator();
}
},
),
),
),
);
}
}
Step 9: Handle Platform-Specific Libraries
Ensure your native libraries are placed in the correct directory for each platform:
- Android: Place the
.sofiles inandroid/app/src/main/jniLibs/{architecture}/. - iOS/macOS: Libraries should be bundled with the application in Xcode.
- Windows: Place the
.dllfiles in the same directory as the executable or in a directory included in the system’s PATH.
Advanced Usage: Structures and Callbacks
Dart FFI supports more complex data types, including structures and callbacks. Here’s a simple example of passing a structure:
C Code (native_struct.c)
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int x;
int y;
} Point;
int calculate_distance(Point p1, Point p2) {
int dx = p1.x - p2.x;
int dy = p1.y - p2.y;
return dx * dx + dy * dy;
}
Corresponding Header (native_struct.h)
#ifndef NATIVE_STRUCT_H
#define NATIVE_STRUCT_H
typedef struct {
int x;
int y;
} Point;
int calculate_distance(Point p1, Point p2);
#endif
Dart Binding (ffigen.yaml adjustments)
name: NativeStruct
description: Dart FFI bindings for native_struct library.
output: 'lib/native_struct_bindings.dart'
headers:
entry-points:
- 'native_struct.h'
include-directives:
- 'native_struct.h'
compiler-opts: '-I.'
Dart Code
import 'dart:ffi' as ffi;
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart';
import 'package:ffi_example/native_struct_bindings.dart';
class NativeStructLibrary {
late final NativeStruct _nativeStruct;
NativeStructLibrary() {
final String libraryPath = Platform.isAndroid
? 'libnative_struct.so'
: (Platform.isMacOS ? 'libnative_struct.dylib' : 'native_struct.dll');
_nativeStruct = NativeStruct(ffi.DynamicLibrary.open(libraryPath));
}
int calculateDistance(int x1, int y1, int x2, int y2) {
final p1 = calloc();
final p2 = calloc();
p1.ref.x = x1;
p1.ref.y = y1;
p2.ref.x = x2;
p2.ref.y = y2;
final result = _nativeStruct.calculate_distance(p1.ref, p2.ref);
calloc.free(p1);
calloc.free(p2);
return result;
}
}
Don’t forget to compile your C code accordingly:
gcc -shared -o libnative_struct.so native_struct.c # Linux
clang -shared -o libnative_struct.dylib native_struct.c # macOS
gcc -shared -o native_struct.dll native_struct.c # Windows with MinGW
Common Pitfalls and Solutions
- Incorrect Library Paths: Ensure that the library paths are correct for each platform.
- Memory Management: Properly allocate and free memory when passing data between Dart and native code. Using
callocandfreeis essential to avoid memory leaks. - ABI Compatibility: Be aware of the architecture and ABI compatibility between your Dart and native code (e.g., 32-bit vs. 64-bit).
Conclusion
Dart FFI is a powerful tool that enables Flutter developers to harness native code for performance optimization or access to platform-specific features. While it introduces some complexity, the ability to integrate native libraries can greatly enhance the capabilities of your Flutter applications. Properly set up your build configurations, manage memory correctly, and be mindful of platform differences, to effectively leverage native code in Flutter using Dart FFI.