Using Dart FFI for Native Interoperability in Flutter

Flutter is renowned for its ability to create beautiful, high-performance applications for multiple platforms using a single codebase. However, sometimes you need to tap into platform-specific capabilities or leverage existing native libraries for performance reasons or access to functionalities not available through Flutter packages. This is where Dart Foreign Function Interface (FFI) comes into play. Dart FFI allows you to call native libraries written in languages like C, C++, and others directly from your Dart code.

What is Dart FFI?

Dart Foreign Function Interface (FFI) is a mechanism that enables Dart code (and consequently Flutter code) to interact with native libraries written in other languages. This capability is essential for tasks such as accessing system APIs, integrating with existing native codebases, or improving performance-critical sections of an application by utilizing native implementations.

Why Use Dart FFI?

  • Performance Optimization: Delegate performance-critical tasks to highly optimized native code.
  • Access Native APIs: Utilize platform-specific features and APIs not available in Flutter.
  • Integrate Legacy Code: Incorporate existing C/C++ libraries and codebases.

How to Use Dart FFI in Flutter

To demonstrate how to use Dart FFI, let’s create a simple example where we call a native C function to add two numbers from Flutter.

Step 1: Set Up the Native C Library

First, create a C file (e.g., native_add.c) with the function you want to call:


// native_add.c
#include <stdio.h>

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

Step 2: Compile the C Library into a Shared Library

Compile the C file into a shared library (.so for Linux/Android, .dylib for macOS, and .dll for Windows). Here’s how to do it using GCC:

For Linux:

gcc -shared -o libnative_add.so native_add.c
For macOS:

gcc -shared -o libnative_add.dylib native_add.c
For Windows (using MinGW):

gcc -shared -o native_add.dll native_add.c

Ensure that the compiled library is placed in a location accessible to your Flutter project. A common practice is to place the libraries under a directory such as /lib in your Flutter project.

Step 3: Configure Your Flutter Project

Update your pubspec.yaml file to include the ffi package:


dependencies:
  flutter:
    sdk: flutter
  ffi: ^2.0.0 # Use the latest version

Run flutter pub get to install the package.

Step 4: Create Dart Code to Interface with the Native Library

Create a Dart file (e.g., native_add.dart) to handle the FFI calls. This involves:

  1. Loading the native library.
  2. Defining the function signature using typedef.
  3. Looking up the function in the native library.
  4. Calling the native function.

import 'dart:ffi' as ffi;
import 'dart:io' show Platform;

// 1. Define the function signature
typedef NativeAddFunc = ffi.Int32 Function(ffi.Int32, ffi.Int32);
typedef NativeAdd = int Function(int, int);

class NativeAddWrapper {
  // 2. Load the native library
  late ffi.DynamicLibrary nativeLib;

  NativeAddWrapper() {
    String libraryPath = _getLibraryPath();
    nativeLib = ffi.DynamicLibrary.open(libraryPath);
  }

  // Determine the correct library path based on the platform
  String _getLibraryPath() {
    if (Platform.isMacOS) {
      return 'libnative_add.dylib';
    } else if (Platform.isLinux) {
      return 'libnative_add.so';
    } else if (Platform.isWindows) {
      return 'native_add.dll';
    } else {
      throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');
    }
  }

  // 3. Lookup the native function
  late NativeAdd add;

  int nativeAdd(int a, int b) {
    final nativeAddFuncPtr = nativeLib.lookupFunction<NativeAddFunc, NativeAdd>('native_add');
    return nativeAddFuncPtr(a, b);
  }
}

Step 5: Use the Native Function in Flutter

In your Flutter code, create an instance of the NativeAddWrapper and call the nativeAdd function:


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

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _result = 0;
  final NativeAddWrapper _nativeAddWrapper = NativeAddWrapper();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Dart FFI Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Result: $_result'),
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    _result = _nativeAddWrapper.nativeAdd(5, 3);
                  });
                },
                child: const Text('Add Numbers'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Here’s a breakdown of the Flutter code:

  • An instance of NativeAddWrapper is created to interact with the native library.
  • When the button is pressed, the nativeAdd function is called with arguments 5 and 3, and the result is displayed.

Important Considerations:

  • Platform-Specific Libraries:
    Ensure you compile the native library for each target platform and load the correct library based on the operating system.
  • Error Handling: Implement proper error handling to catch exceptions when loading the library or calling functions.
  • Memory Management: Be cautious with memory management when passing complex data structures between Dart and native code.

Conclusion

Dart FFI opens up a world of possibilities for Flutter developers by allowing seamless integration with native code. Whether you need to optimize performance-critical sections, access platform-specific features, or integrate with existing native libraries, Dart FFI provides the necessary tools to bridge the gap between Dart and native code. By following the steps outlined in this guide, you can effectively leverage Dart FFI to enhance the capabilities of your Flutter applications.