Dynamic vs Static Linking

A native library can be linked into an app either dynamically or statically. A statically linked library is embedded into the app’s executable image and is loaded when the app starts.

We are going to use this repository.

GitHub - flutterwtf/Flutter-FFI-Demo
Contribute to flutterwtf/Flutter-FFI-Demo development by creating an account on GitHub.

Symbols from a statically linked library can be loaded using DynamicLibrary.executable or DynamicLibrary.process.

A dynamically linked library, by contrast, is distributed in a separate file or folder within the app and loaded on-demand. On Android, a dynamically linked library is distributed as a set of .so (ELF) files, one for each architecture. On iOS, the dynamically linked library is distributed as a .framework folder.

A dynamically linked library can be loaded into Dart via DynamicLibrary.open.

Adding C/C++ Sources

Dart:ffi can only build C sources. That's why you need to mark C++ symbols with "extern C”

In order to bind to native code, you must ensure that the native code is loaded, and its symbols are visible to Dart.

Here is the code for complex math operations (like pi calculation or calculation the area under the sinusoid).

dart meme

Add your .c sources. For example:

example.c

#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>
#include "example.h"

double calc_pi(int32_t precision) {
    double pi = 0;
    int8_t sign = 1;

    for (int32_t i = 1; i < precision * 2; i+=2) {
        pi = pi + sign * 4.0/i;
        sign *= -1;
    }

    return pi;
}

double calc_area_under_sin(int32_t precision, int32_t pi_precision)
{
    double sum = 0;
    double pi = calc_pi(pi_precision);
    double rectArea = pi * 1.0;

    srand(time(NULL));
    for (int i = 0; i < precision; ++i) {
        double x = ((float)rand())/RAND_MAX * pi;
        double y = ((float)rand())/RAND_MAX * 1.0;

        if (y < sin(x)) {
            sum++;
        }
    }
    return sum / precision * rectArea;
}

Then you have to create a header file that declares function prototypes and marks external sources.

Mark files that you want to be visible in Dart using:

extern "C" __attribute__((visibility("default"))) __attribute__((used))

example.h

#ifndef FFI_TEST_EXAMPLE_H
#define FFI_TEST_EXAMPLE_H

extern "C" __attribute__((visibility("default"))) __attribute__((used))
double calc_pi(int32_t precision);

extern "C" __attribute__((visibility("default"))) __attribute__((used))
double calc_area_under_sin(int32_t precision, int32_t pi_precision);

#endif //FFI_TEST_EXAMPLE_H

After adding source code to your project, you need to inform Android build system about the code.

Android

Create a file named CMakeLists.txt under android/app/ path.

Then fill this file with data. For example:

cmake_minimum_required(VERSION 3.4.1)  # for example

add_library( example_lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        example.cpp)

then update your android/app/build.gradle:

android {
  // ...
  externalNativeBuild {
    // Encapsulates your CMake build configurations.
    cmake {
      // Provides a relative path to your CMake build script.
      path "CMakeLists.txt"
    }
  }
  // ...
}

IOS

On iOS, you need to tell Xcode to statically link the file:

  • In Xcode, open Runner.xcworkspace
  • Add the C/C++/Objective-C/Swift source files to the Xcode project.

Loading the Code

In order to load C code at first you need to load a library. Follow the example below to do that. Remember that .so file name is 'lib' + lib name that you've put in CMakeLists.txt file. In our case, we have 'libexample_lib.so'.

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

final DynamicLibrary nativeAddLib = Platform.isAndroid
    ? DynamicLibrary.open('libexample_lib.so')
    : DynamicLibrary.process();

After dealing with library, you can get your native functions from here.

typedef CalcPiC = Double Function(Int32 precision);

CalcPiDart calcPi =  nativeAddLib.lookup<NativeFunction<CalcPiC>>('calc_pi').asFunction();

Then you can call the function:

calcPi(10000); // returns 3.14....

Comparing Dart and Native Call Performance

Let's look at the performance of Dart and Native calls in calculating area of complex shapes with the random points method.

We will calculate area under sin(x) function where x belongs to [0, pi].

Comparing Dart and Native Call Performance

Let's pick a rectangle that has height 1 and width pi and contains our sin(x) curve part. We know that the area of this rect is 1 * pi. Random points method tells us that if we place a random point in our rect and then count the number of points in the area that we want to calculate and then divide this number by all points amount and multiply it by the rect area, we will get an approximate area of our shape. And the bigger the number of points is, the more accurate area we will have.

In our example we calculate pi at first using infinite series and then calculate our shape area.

Let's pick low precisions of calculating pi and area at first. That means that we will have a little calculation.

precisions of calculating pi and area

As you can see the difference is not so big and C is even slower in calculating pi. That is because of call to native function time. But when we have a lot more calculations, things are getting different:

a lot more calculations

As you can see here, C is nearly 2 times faster than dart at calculating the approximate area of our shape.

⚠️ But remember to call nativeLib.lookup<NativeFunction<CalcPiC>>('calc_pi').asFunction(); only once when initialize and store function in your program, because calling it a lot of times may produce HUGE performance pitfall.

0:00
/

Adding Third-party Libs

Android

find_library( # Defines the name of the path variable that stores the
        # location of the NDK library.
        log

        # Specifies the name of the NDK library that
        # CMake needs to locate.
        log )
  • Import your lib
DynamicLibrary.open('liblog.so');

IOS

  • In Xcode, open Runner.xcworkspace.
  • Add the C/C++/Objective-C/Swift source files to the Xcode project.
  • Add the following prefix to the exported symbol declarations to ensure they are visible to Dart:
extern "C" /* <= C++ only */ __attribute__((visibility("default"))) __attribute__((used))

Working with Structs and Pointers

Defining a structure in a struct_example.h:

extern"C"
struct __attribute__((visibility("default"))) Person
{
	int age;
	char*name;
};

And in Dart:

class Person extends Struct {
  @Int32()
	external int age;

	external Pointer<Utf8> name;
}

Let’s define some functions to init Person struct. One with int and char*, and the second one with int* and char*. Then:

final createPerson = nativeLib.lookupFunction<CreatePersonC, CreatePersonDart>('create_person');
Person p1 = createPerson(18, 'John'.toNativeUtf8());

To work with pointers, we need to allocate memory at first and init a value. Then feel free to use!

final agePtr = calloc<Int32>();
agePtr.value = 22;
Person p2 = createPersonPointer(agePtr, 'Mike'.toNativeUtf8());

To get derefered value you can simply use value field of the pointer.

⚠️ Don’t forget to free memory
calloc.free(agePtr);

☝ Remember that you can get access to native allocated memory. This, for example, can be used to work with images.


memory leak meme

Dart:ffigen

If you have a lot of native code to use in Dart, you can use ffigen to generate dart files.

  • Add ffigenunder dev_dependenciesin your pubspec.yaml(run dart pub add -d ffigen)
  • Add package:ffi under dependencies in your pubspec.yaml (run dart pub add ffi).
  • Install LLVM (see Installing LLVM).
  • Configurations must be provided in pubspec.yaml.
  • Run the tool- dart run ffigen.

Provide all the configs to pubspec.yaml under ffigen tag:

ffigen:
	name: ExampleLibGen
	description: Bindings to structs_example.
	output:'lib/example_gen.dart'
	headers:
		entry-points:
      -'android/app/calc_example/example.cpp'
	llvm-path:
    -'D:\\LLVM'

Then feel free to use generated files! For more details, please visit Dart:ffigen.

GitHub - flutterwtf/Flutter-FFI-Demo
Contribute to flutterwtf/Flutter-FFI-Demo development by creating an account on GitHub.
Share this post