Boosting Performance in Flutter with FFI: A Step-by-Step Guide (iOS example) ???
Foreign Function Interface (FFI) in Flutter allows Dart to communicate with native code like C/C++. This can dramatically improve the performance of computation-heavy tasks, making your Flutter applications more efficient. In this guide, we will walk you through how to integrate FFI into your Flutter app by using an example—matrix multiplication in both Dart and C—and compare their performance.
Why FFI Matters
Step 1: Understanding FFI
What is FFI?
FFI (Foreign Function Interface) is a mechanism by which Dart code can call functions written in other programming languages, such as C, C++, or Rust. This is especially helpful when dealing with performance-critical tasks that Dart might struggle with due to its higher-level abstractions.
Step 2: Project Setup
What We're Doing:
Setting up a new Flutter project to serve as the foundation for our performance test.
How to Do It:
Create a New Flutter Project:
Open your terminal and run:
flutter create -e ffi_performance_test
cd ffi_performance_test
Add the FFI Package:
Open your pubspec.yaml file and add the ffi package under dependencies:
dependencies:
flutter:
sdk: flutter
ffi: ^2.1.3 #add this line
Install Dependencies:
Run the following command to fetch the new dependencies:
flutter pub get
Step 3: Writing the Native C Code
What We're Doing:
Creating a simple C function for matrix multiplication. This function takes two matrices, multiplies them, and stores the result in a third matrix.
How to Do It:
Navigate to the ios directory of your Flutter project and open Runner.xcworkspace in Xcode.
Right-click on the Runner folder in the Project Navigator.
Select Add Files to "Runner"...
Choose C File from the file template options.
Name the file matrix_multiplication.c and click Create.
Paste the C Code:
Open the newly created matrix_multiplication.h file and add the following code:
#ifndef MATRIX_MULTIPLICATION_H
#define MATRIX_MULTIPLICATION_H
#ifdef __cplusplus
extern "C" {
#endif
// Export the function for FFI and prevent it from being stripped
__attribute__((visibility("default")))
__attribute__((used))
void multiply_matrices(int* A, int* B, int* result, int N);
#ifdef __cplusplus
}
#endif
#endif // MATRIX_MULTIPLICATION_H
Open the newly created matrix_multiplication.c file and add the following code:
#include "matrix_multiplication.h"
// Prevent name mangling if compiled with a C++ compiler
#ifdef __cplusplus
extern "C" {
#endif
// Ensure the function is exported and not stripped
__attribute__((visibility("default")))
__attribute__((used))
void multiply_matrices(int* A, int* B, int* result, int N) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
result[i * N + j] = 0;
for (int k = 0; k < N; k++) {
result[i * N + j] += A[i * N + k] * B[k * N + j];
}
}
}
}
#ifdef __cplusplus
}
#endif
Explanation:
Header File (matrix_multiplication.h):
extern "C" Block: Ensures that if the code is compiled with a C++ compiler, the function names are not mangled. Name mangling can prevent Dart from correctly locating the C functions.
Visibility Attributes:
Source File (matrix_multiplication.c):
#include "matrix_multiplication.h"
Ensures that the function declaration matches the implementation, maintaining consistency and preventing linkage issues.
void multiply_matrices(int* A, int* B, int* result, int N) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
result[i * N + j] = 0;
for (int k = 0; k < N; k++) {
result[i * N + j] += A[i * N + k] * B[k * N + j];
}
}
}
}
Parameters:
Functionality:
Build the C Code:
Note: Xcode automatically handles the linking of the C library when you run the project, so there's no need for manual linking steps.
Step 4: Dart Integration with FFI
领英推荐
What We're Doing:
Writing Dart code to integrate the native C function using the FFI package.
How to Do It:
Create the Dart FFI Wrapper:
Inside the lib directory, create a new file named matrix_multiplication_ffi.dart.
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart'; // Required for malloc and free
typedef MatrixMultiplyNative = ffi.Void Function(
ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Int32>, ffi.Int32);
typedef MatrixMultiplyDart = void Function(
ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Int32>, ffi.Pointer<ffi.Int32>, int);
class MatrixMultiplicationFFI {
late ffi.DynamicLibrary _lib;
late MatrixMultiplyDart _multiply;
MatrixMultiplicationFFI() {
// Load the C library
_lib = ffi.DynamicLibrary.process();
// Lookup for the matrix multiplication function
_multiply = _lib
.lookup<ffi.NativeFunction<MatrixMultiplyNative>>('multiply_matrices')
.asFunction();
}
void multiply(List<int> A, List<int> B, List<int> result, int N) {
// malloc is used to request memory from the computer's heap (a region of memory reserved for dynamic allocation).
final pointerA = malloc.allocate<ffi.Int32>(A.length * ffi.sizeOf<ffi.Int32>());
final pointerB = malloc.allocate<ffi.Int32>(B.length * ffi.sizeOf<ffi.Int32>());
final pointerResult = malloc.allocate<ffi.Int32>(result.length * ffi.sizeOf<ffi.Int32>());
// Copy Dart data into allocated memory
for (int i = 0; i < A.length; i++) {
pointerA[i] = A[i];
}
for (int i = 0; i < B.length; i++) {
pointerB[i] = B[i];
}
// Call the C function via FFI
_multiply(pointerA, pointerB, pointerResult, N);
// Copy result from native memory back to Dart list
for (int i = 0; i < result.length; i++) {
result[i] = pointerResult[i];
}
// Free allocated memory
malloc.free(pointerA);
malloc.free(pointerB);
malloc.free(pointerResult);
}
}
Typedefs:
MatrixMultiplyNative: Defines the C function signature.
MatrixMultiplyDart: Defines the Dart function signature corresponding to the C function.
MatrixMultiplicationFFI Class:
_lib: Loads the dynamic library containing the C functions.
_multiply: Maps the C multiply_matrices function to a Dart function.
multiply Method:
Allocates memory for input matrices A and B, and the result matrix.
Copies Dart list data into the allocated native memory.
Calls the C function via FFI.
Copies the result back from native memory to the Dart list.
Frees the allocated memory to prevent memory leaks.
Step 5: Writing the Flutter App for Performance Comparison
What We're Doing:
Creating a simple Flutter app that multiplies matrices using both Dart and C (via FFI) and compares the performance of both implementations.
How to Do It:
Update main.dart:
Replace the content of lib/main.dart with the following code:
import 'dart:math';
import 'package:flutter/material.dart';
import 'matrix_multiplication_ffi.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Matrix Multiplication Performance')),
body: const PerformanceComparison(),
),
);
}
}
class PerformanceComparison extends StatefulWidget {
const PerformanceComparison({super.key});
@override
State<PerformanceComparison> createState() => _PerformanceComparisonState();
}
class _PerformanceComparisonState extends State<PerformanceComparison> {
final int _matrixSize = 1000;
late List<int> _matrixA;
late List<int> _matrixB;
late List<int> _result;
late MatrixMultiplicationFFI _ffi;
int _dartTime = 0;
int _cTime = 0;
@override
void initState() {
super.initState();
_matrixA = List.generate(_matrixSize * _matrixSize, (_) => Random().nextInt(100));
_matrixB = List.generate(_matrixSize * _matrixSize, (_) => Random().nextInt(100));
_result = List.filled(_matrixSize * _matrixSize, 0);
_ffi = MatrixMultiplicationFFI();
}
void _multiplyInDart() async {
setState(() {
_dartTime = 0;
});
Stopwatch stopwatch = Stopwatch()..start();
// Matrix multiplication in Dart
for (int i = 0; i < _matrixSize; i++) {
for (int j = 0; j < _matrixSize; j++) {
_result[i * _matrixSize + j] = 0;
for (int k = 0; k < _matrixSize; k++) {
_result[i * _matrixSize + j] +=
_matrixA[i * _matrixSize + k] * _matrixB[k * _matrixSize + j];
}
}
}
stopwatch.stop();
setState(() {
_dartTime = stopwatch.elapsedMilliseconds;
});
}
void _multiplyInC() async {
setState(() {
_cTime = 0;
});
Stopwatch stopwatch = Stopwatch()..start();
_ffi.multiply(_matrixA, _matrixB, _result, _matrixSize);
stopwatch.stop();
setState(() {
_cTime = stopwatch.elapsedMilliseconds;
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Matrix size: $_matrixSize x $_matrixSize',
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
Text(
'Loop Control Overheads: ${_matrixSize * _matrixSize * _matrixSize}',
style: const TextStyle(fontSize: 16)),
],
),
),
const SizedBox(height: 30),
const Expanded(
child: Center(
child: Text(
'Let\'s multiply some matrices!',
style: TextStyle(fontSize: 20),
)),
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Dart multiplication took: ',
style: TextStyle(fontSize: 16)),
Text('${_dartTime}ms',
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('C multiplication (FFI) took: ',
style: TextStyle(fontSize: 16)),
Text('${_cTime}ms',
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _multiplyInDart,
child: const Text('Multiply in Dart'),
),
ElevatedButton(
onPressed: _multiplyInC,
child: const Text('Multiply in C (FFI)'),
),
],
),
const SizedBox(height: 30),
],
),
),
);
}
}
Explanation:
_matrixA and _matrixB are initialized with random integers.
_result is a list to store the multiplication result.
_ffi is an instance of the FFI wrapper class to interact with the native C function.
_multiplyInDart: Performs matrix multiplication purely in Dart and measures the time taken.
_multiplyInC: Calls the native C function via FFI to perform matrix multiplication and measures the time taken.
Displays the matrix size and the number of loop iterations (for context).
Shows the time taken for both Dart and C implementations.
Provides buttons to trigger each multiplication method.
Uses Stopwatch to measure the elapsed time for each multiplication method.
Updates the UI with the results using setState.
Try it:
flutter run lib/main.dart --release
Conclusion
Congratulations! You've successfully offloaded performance-critical tasks like matrix multiplication to native C code using FFI in Flutter. By integrating native code, you've demonstrated a significant performance improvement over Dart's implementation.
As illustrated in the screenshot above, the C implementation outperforms the Dart version, showcasing the efficiency gains achieved through FFI.
Note: In this example, the app's interface will freeze until the calculation completes. To enhance user experience, consider performing these operations asynchronously or offloading them to separate isolates to prevent UI blocking.
Next Steps
Did you find this tutorial helpful? Share your thoughts or ask questions below!
Happy coding!