How to use Dart FFI for writing C code

Harish KunchalaHarish Kunchala
5 min read

What is FFI?

  • Foreign Function Interface (FFI) allows a program written in one language to call functions or services written in another language.

  • In Dart FFI is used to call native C functions and libraries, thus enhancing the app's capabilities by utilizing existing code or libraries.

  • Dart FFI can be used with any language that can expose a C-compatible API like C, C++, Rust and to some extent even Python3.

Use Cases:

  • Reuse existing libraries.

  • Performing tasks that require high performance.

  • Accessing platform-specific features not exposed by Flutter.

Let's create a basic FFI.

Source Code:

You can find the source code for here: ffi_factorial

Dart FFI Factorial App:

We'll create two basic Dart Apps that runs on Windows, Linux and Mac as a CLI app to get factorial.

  • The first app will have give us factorial of a static number like 5 or 10.

  • The second app will provide us a factorial based on the user input.

Also you can find the source code for the app here: ffi_factorial

Step 1. Create the Dart App:

Let's create a new dart project:

dart create ffi_factorial
cd ffi_factorial/bin/

Now if we look at the files inside the Dart project we get this:

ls
ffi_factorial.dart

Step 2. Create factorial.c inside our bin/

Let's write our C code (e.g., factorial.c ) containing the functions you want to access from Dart. Remember to write inside our bin/ folder for easy access.

#include <stdio.h>
// factorial.c using memoization to get the time complexity to O(n)
// Array to store memoized results (adjust size as needed)
int memo[100];

int factorial_memo(int n) {
    if (n == 0) {
        return 1;
    } else if (memo[n] != 0) {
        return memo[n]; // Return memoized result
    } else {
        memo[n] = n * factorial_memo(n - 1);
        return memo[n];
    }
}

Step 3. Create Dynamic library.

Now if we want to access our C Code inside our Dart function. We need to create a dynamic library.

Now since each operating system is different we'll have to create separate dynamic library files for each OS.

Operating System and Library Name:

  • macOS: The library file will be named libfactorial.dylib.

  • Linux: The library file will be named libfactorial.so.

  • Windows: The library file will be named factorial.dll.

Compile your C code into a dynamic library:

  • Linux: gcc -shared -o libfactorial.so factorial.c -fPIC

  • Windows: gcc -shared -o factorial.dll factorial.c

  • macOS: gcc -dynamiclib -o libfactorial.dylib factorial.c

Note: When I say different OS, I mean target Operating Systems, not where you are compiling the code from.

For instance although I am writing all of this code in macOS if I want it to run on all 3 platforms, then I'll have to create all 3 files.

So I am running all three commands:

gcc -dynamiclib -o libfactorial.dylib factorial.c
gcc -shared -o factorial.dll factorial.c
gcc -shared -o libfactorial.so factorial.c -fPIC

Now here are the current files:

ls
factorial.c        factorial.dll        ffi_factorial.dart    libfactorial.dylib    libfactorial.so

Step 4. Call Native Functions from our ffi_factorial.dart:

Open bin/my_ffi_project.dart and add the following imports:

import 'dart:ffi';
import 'dart:io';
  1. Now within the main() load the dynamic libraries:

As you can see we are ensuring that the correct file is opened in correct os

// 1. Load the Dynamic Library (dylib)
// Ensure to use the correct extension for your platform (.dll for Windows, .dylib for macOS, .so for Linux)
  final dylib = Platform.isWindows
      ? DynamicLibrary.open('libfactorial.dll')
      : Platform.isMacOS
          ? DynamicLibrary.open('libfactorial.dylib')
          : DynamicLibrary.open('libfactorial.so'); // Linux
  1. Create typedefs to represent the the C function signatures in Dart:

Create them on top of our main().

// 2. Define FFI Function Signature
typedef NativeFactorialMemoFunc = Int32 Function(Int32 n);
typedef DartFactorialMemoFunc = int Function(int n);
  1. Use the lookup method to get a reference to the C function within the loaded library:
  // 3. Look Up the Native Function
  final DartFactorialMemoFunc nativeFactorialMemo =
      dylib.lookupFunction<NativeFactorialMemoFunc, DartFactorialMemoFunc>('factorial_memo');
  1. Finally call the native function
// 4. Call the Native Function (from Dart)
  final result = nativeFactorialMemo(5);
  print('Factorial of 5 is: $result'); // Output: 120

Here's the entire code:

import 'dart:ffi';
import 'dart:io';

// 2. Define FFI Function Signature
typedef NativeFactorialMemoFunc = Int32 Function(Int32 n);
typedef DartFactorialMemoFunc = int Function(int n);

void main(List<String> arguments) {

  // 1. Load the Dynamic Library (dylib)
  // Ensure to use the correct extension for your platform (.dll for Windows, .dylib for macOS, .so for Linux)
  final dylib = Platform.isWindows
      ? DynamicLibrary.open('libfactorial.dll')
      : Platform.isMacOS
          ? DynamicLibrary.open('libfactorial.dylib')
          : DynamicLibrary.open('libfactorial.so'); // Linux

  // 3. Look Up the Native Function
  final DartFactorialMemoFunc nativeFactorialMemo =
      dylib.lookupFunction<NativeFactorialMemoFunc, DartFactorialMemoFunc>('factorial_memo');

  // 4. Call the Native Function (from Dart)
  final result = nativeFactorialMemo(5);
  print('Factorial of 5 is: $result'); // Output: 120
}

So if we run the app. We get the following output:

dart run ffi_factorial.dart
# Factorial of 5 is: 120

Factorial Using User Input:

Let's create a new file named factorial_user_input.dart inside our bin/

code factorial_user_input.dart

Go ahead and copy the code from our existing ffi_factorial.dart.

Now since we want to get the value from user terminal: We'll use stdin.readLineSync(). Here is our new code:

import 'dart:ffi';
import 'dart:io';

// 2. Define FFI Function Signature
typedef NativeFactorialMemoFunc = Int32 Function(Int32 n);
typedef DartFactorialMemoFunc = int Function(int n);

void main(List<String> arguments) {

  // 1. Load the Dynamic Library (dylib)
  // Ensure to use the correct extension for your platform (.dll for Windows, .dylib for macOS, .so for Linux)
  final dylib = Platform.isWindows
      ? DynamicLibrary.open('libfactorial.dll')
      : Platform.isMacOS
          ? DynamicLibrary.open('libfactorial.dylib')
          : DynamicLibrary.open('libfactorial.so'); // Linux

  // 3. Look Up the Native Function
  final DartFactorialMemoFunc nativeFactorialMemo =
      dylib.lookupFunction<NativeFactorialMemoFunc, DartFactorialMemoFunc>('factorial_memo');

  // Get user input
  print('Enter a number to calculate its factorial: ');
  final input = stdin.readLineSync();

  if (input != null) {
    final number = int.tryParse(input);
    if (number != null) {
      // 4. Call the Native Function (from Dart)
      final result = nativeFactorialMemo(number);
      print('Factorial of $number is: $result'); // Output: 120 (for 5)
    } else {
      print('Please enter a valid integer.');
    }
  } else {
    print('Input cannot be null.');
  }
}

Here's the output:

dart run factorial_user_input.dart
# Enter a number to calculate its factorial:
# 23
# Factorial of 23 is: 862453760

Perfect. So this covers our introduction to FFI. I'll keep writing more about FFI and platform channels as these are some of the most interesting aspects of the flutter app. So please subscribe to my blog for more interesting Flutter posts.

And Happy Coding 🐦

0
Subscribe to my newsletter

Read articles from Harish Kunchala directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harish Kunchala
Harish Kunchala