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.
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).
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].
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.
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:
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.
Adding Third-party Libs
Android
- Find the desired library in the Android NDK Native APIs list in the Android docs.
- Add lib to your CMakeLists.txt
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.
Dart:ffigen
If you have a lot of native code to use in Dart, you can use ffigen to generate dart files.
- Add
ffigen
underdev_dependencies
in yourpubspec.yaml
(rundart pub add -d ffigen
) - Add
package:ffi
underdependencies
in yourpubspec.yaml
(rundart 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.