Utilizing dart:ffi to Interact with C Libraries in Flutter

Flutter is renowned for its ability to create beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. While Dart provides a rich set of libraries and functionalities, sometimes you need to tap into the power and performance of existing C libraries. This is where dart:ffi comes into play. The dart:ffi library allows you to call native C code directly from your Dart code in Flutter.

What is dart:ffi?

dart:ffi is Dart’s Foreign Function Interface. It provides a mechanism for Dart code to interact with native libraries written in C. This interaction involves mapping Dart types to their corresponding C types, allowing Dart functions to call C functions, and vice versa.

Why Use dart:ffi?

  • Performance: Access highly optimized C code for computationally intensive tasks.
  • Existing Libraries: Leverage existing C libraries without rewriting them in Dart.
  • System Access: Access low-level system APIs and functionalities that are not directly available in Dart.

How to Use dart:ffi in Flutter

To utilize dart:ffi, you’ll typically follow these steps:

Step 1: Prepare Your C Library

First, you need a C library that you want to interact with. This could be a pre-existing library or one that you create yourself. Let’s create a simple C library that adds two numbers.

Create a Simple C Library (mylib.c)
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}
Compile the C Library

Compile the C library into a dynamic library (.so, .dylib, or .dll depending on your platform). For example, on Linux, you would use:

gcc -shared -o libmylib.so mylib.c

Step 2: Load the Dynamic Library in Dart

In your Flutter project, use dart:ffi to load the dynamic library.

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

// Define the dynamic library path
final String libraryPath = 'path/to/libmylib.so'; // Update this path accordingly

// Load the dynamic library
final dylib = ffi.DynamicLibrary.open(libraryPath);

Note that the libraryPath will vary depending on your platform and where you place the library file in your Flutter project. Ensure that you update this path accordingly. Common places to put the library files are in the root of your Flutter project, or within a dedicated native directory.

Step 3: Define Dart Signatures for C Functions

Create Dart signatures that match the C function signatures. You’ll need to use dart:ffi types for this.

// Define Dart type for the C function
typedef AddFunc = ffi.Int32 Function(ffi.Int32 a, ffi.Int32 b);

// Define Dart type for the C function signature
typedef Add = int Function(int a, int b);

Step 4: Retrieve C Function and Cast to Dart Function

Use the dynamic library to retrieve the C function, and then cast it to the Dart function type.

// Get a reference to the C function
final addPointer = dylib.lookupFunction<AddFunc, Add>('add');

Step 5: Call the C Function from Dart

You can now call the C function directly from your Dart code.

// Call the C function
final result = addPointer(5, 3);
print('Result from C code: $result');

Complete Example in Flutter

Here’s a complete example that combines all the steps in a Flutter application:

import 'dart:ffi' as ffi;
import 'dart:io' show Platform;
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FFI Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _result = 0;

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

  void _callCLibrary() {
    // Define the dynamic library path (adjust based on your setup)
    String libraryPath;
    if (Platform.isAndroid) {
      libraryPath = 'libmylib.so'; // Put libmylib.so in android/app/src/main/jniLibs/armeabi-v7a/
    } else if (Platform.isIOS) {
      libraryPath = 'Frameworks/libmylib.dylib'; // Use this on iOS simulator
    } else {
       libraryPath = './libmylib.so';  // For other platforms
    }
    

    // Load the dynamic library
    final dylib = ffi.DynamicLibrary.open(libraryPath);

    // Define Dart type for the C function
    typedef AddFunc = ffi.Int32 Function(ffi.Int32 a, ffi.Int32 b);

    // Define Dart type for the C function signature
    typedef Add = int Function(int a, int b);

    // Get a reference to the C function
    final addPointer = dylib.lookupFunction<AddFunc, Add>('add');

    // Call the C function
    setState(() {
      _result = addPointer(5, 3);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FFI Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Result from C code:',
            ),
            Text(
              '$_result',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
    );
  }
}

Here are a few things to remember for properly setting up your flutter project.

Step 6: Update Android build.gradle Configuration

The following setup provides you with example code of correctly loading libraries


android {
    sourceSets {
        main {
            jniLibs.srcDirs = ['src/main/jniLibs']
        }
    }
}

LOCAL_CFLAGS += "-I${CMAKE_SOURCE_DIR}/../../src/main/cpp/include"
include_directories("${CMAKE_SOURCE_DIR}/../../src/main/cpp/include")

Next make sure the C make file is in order to have everything aligned.


# Sets the C++ executable
add_library(
        native-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to the C++ source file(s)
        ../../src/main/cpp/native-lib.cpp)

Platform Considerations

  • Android: Place your .so files in the android/app/src/main/jniLibs/ABI directory, where ABI is the architecture (e.g., armeabi-v7a, arm64-v8a, x86_64).
  • iOS: Ensure the library is linked in the Xcode project and included in the “Frameworks, Libraries, and Embedded Content” section of your target. Also, make sure to specify correct path.

Tips for Working with dart:ffi

  • Error Handling: Implement robust error handling to catch exceptions and handle potential issues when calling C functions.
  • Memory Management: Be mindful of memory management when passing data between Dart and C. Use Arena for allocating memory that needs to be passed to C functions, and ensure you deallocate memory properly to prevent memory leaks.
  • Data Conversion: Pay close attention to data conversion between Dart and C types to avoid data corruption.
  • Testing: Thoroughly test your dart:ffi integrations on different platforms to ensure compatibility and stability.

Conclusion

dart:ffi is a powerful tool that allows you to leverage the performance and capabilities of C libraries in your Flutter applications. By following the steps outlined in this blog post, you can seamlessly integrate C code into your Flutter projects, opening up new possibilities for high-performance and system-level functionalities.