What is the

Dart FFI is a technique for calling C code on Dart mobile, command line, and server applications running on the Dart Native platform. Simply put, Dart and C call each other. Dart FFI is released as a stable version after Dart2.12.0 (included with Flutter 2.0 and later versions).

After all, the Dart language became popular because of the use of Flutter, so Dart FFI technology is more powerful for Flutter applications

Problem solved

  1. C API can be called synchronously, unlike a Flutter Channel which starts asynchronously
  2. C calls are faster, unlike the previous need to transfer via Native (or change the Flutter engine code).
  3. It is also possible to encapsulate and replace the Flutter Channel to achieve faster and synchronous performance.

Simple to use

In order to see only FFI’s features, I will leave them out of the Flutter platform and just use the Dart application on the command line. My engineering environment:

Running environment MacOS 12.0.1

GCC 13.0.0

Cmake 3.20.1

Make 3.81

The dart 2.16.0

Theoretically, dart2.12 and above is no problem.

1. Create projects

Because the project structure is simple, create the project manually

1). Create pubspec.yaml file

2). Create the bin/ffi_sample.dart file

3). Create C environment, create library, library/build folder

C, library/sample.h, library/sample.def, cmakelists.txt files

The directory structure is as follows

|_ bin
    |_ ffi_sample.dart
|_ library
    |_ build
    |_ CMakeLists.txt
    |_ sample.c
    |_ sample.h
    |_ sample.def
|_ pubspec.yaml
Copy the code

2. Pubspec. yaml introduces FFI

Add the FFI and PATH libraries to dependencies in the pubspec.yaml file

pubspec.yaml

name: ffi_sample
version: 0.01.
description: Examples using FFI and FFIGen

publish_to: none

environment:
  sdk: "> = 2.12.0 < 3.0.0"

dependencies:
  path: ^ 1.7.0
  ffi: ^ 1.1.2
Copy the code

3. Compile C code

Write a simple function in sample.h

sample.h

void hello_world(a);
Copy the code

Implemented in sample.c

sample.c

#include <stdio.h>
#include <stdlib.h>
#include "sample.h"
void hello_world(a)
{
    printf("Hello World\n");
}
Copy the code

Simple export in sample.def

LIBRARY   sample
EXPORTS
   sample
Copy the code

The main file used to test C code main.cc

#include <stdio.h> #include "sample.h" int main() {printf(" test "); return 0; }Copy the code

Write and compile using cmakelists.txt file

Cmake_minimum_required (VERSION 3.7 FATAL_ERROR) project(sample VERSION 1.0.0 LANGUAGES C) add_library(sample SHARED) sample.c sample.def)Copy the code

3. Compile the C file

Now that you have all the files in place, you can compile your C code.

1). Go to the library/build folder

2). Execute cmake.. Generate the files required for compilation

3). Execute make compilation

cd library/build
cmake ..
make
Copy the code

If the libsample.dylib file is generated in the library/build folder, the compilation is successful.

4. Write Dart communication code

Call C in bin/ffi_sample.dart

import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'dart:io' show Platform, Directory;

import 'package:path/path.dart' as path;

void main() {
  void main() {
  // Initialize the intermodulation framework
  var libraryPath =
      path.join(Directory.current.path, 'ibrary'.'build'.'libsample.so');
  if (Platform.isMacOS) {
    libraryPath = path.join(
        Directory.current.path, 'library'.'build'.'libsample.dylib');
  }
  if (Platform.isWindows) {
    libraryPath =
        path.join(Directory.current.path, 'library'.'Debug'.'libsample.dll');
  }
  final dylib = DynamicLibrary.open(libraryPath);

  // *************** 1. Dart calls C **************
  final Pointer<T> Function<T extends NativeType>(String symbolName) _lookup = dylib.lookup;

  late final _hello_worldPtr =
      _lookup<NativeFunction<Void Function() > > ('hello_world');
  late final _hello_world = _hello_worldPtr.asFunction<void Function(a) > ();// Call the C method (no arguments)
  _hello_world();
}
Copy the code

5. Run code

Now run it from the project root on the command line

dart run
Copy the code

If the output

Hello World
Copy the code

Okay, so the simple Demo is up and running.

Since some of the FFI API names overlap with existing Framework API names, I prefixed all the places where I used FFi in the following code.

import 'dart:ffi' as ffi;
Copy the code

Common properties and methods are introduced

Dart FFI provides a number of methods for connecting Dart and C. Here are the main ones.

DynamicLibrary.open

It can load dynamic link libraries

external factory DynamicLibrary.open(String path);
Copy the code

This method is used to load library files, such as the libsample.dylib file generated after I compiled C above, which we need to use to load into DartVM. Note that calling this method multiple times to load the library file will only load the library file into DartVM once.

Example:

import 'dart:ffi' as ffi;
import 'package:path/path.dart' as path;
var libraryPath = path.join(
        Directory.current.path, 'library'.'build'.'libsample.dylib');
final dylib = ffi.DynamicLibrary.open(libraryPath);
Copy the code

DynamicLibrary.process

external factory DynamicLibrary.process();
Copy the code

It can be used to load dynamic link libraries that an application has automatically loaded in iOS and MacOS, and it can resolve binary symbols that are statically linked to an application. It is important to note that it is not available on Windows platforms

DynamicLibrary.executable

external factory DynamicLibrary.executable();
Copy the code

It can be used to load statically linked libraries

NativeType

Nativetypes represent DATA structures in C in Dart (to see which nativeTypes you can jump to the Dart FFI and C Base Data Type Mapping Table). It cannot be instantiated in Dart and can only be returned by Native.

Pointer

It is a mapping of Pointers in C to Dart

DynamicLibrary->lookup()

external Pointer<T> lookup<T extends NativeType>(String symbolName);
Copy the code

It is used to find the corresponding symbol in the DynamicLibrary and return its memory address.

Dart usage:

final dylib = DynamicLibrary.open(libraryPath);
late final _hello_worldPtr =
      dylib.lookup<NativeFunction<Void Function() > > ('hello_world');
late final _hello_world = _hello_worldPtr.asFunction<void Function(a) > (); _hello_world();Copy the code

Pointer.fromAddress(int ptr)

Gets C object Pointers based on memory addresses

Such as:

// Create a Native pointer to NULL
final Pointer<Never> nullptr = Pointer.fromAddress(0);
Copy the code

Pointer.fromFunction

From a Dart function, create a pointer to a Native function. This pointer is usually used to pass the Dart function to C so that C can call the Dart function

void globalCallback(int src, int result) {
   print("globalCallback src=$src, result=$result");
}
Pointer.fromFunction(globalCallback);
Copy the code

Pointer->address()

Gets the memory address of the pointer

asFunction

Convert Native pointer objects into Dart functions

sizeOf

Returns the specific type of memory usage

case

ffi.sizeOf<ffi.Int64>(); / / 8
Copy the code

malloc.allocate()

Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment});
Copy the code

Create a byteCount sized space

case

Pointer<Uint8> bytes = malloc.allocate<Uint8>(ffi.sizeOf<ffi.Uint8>());
Copy the code

malloc.free

Free memory

malloc.free(bytes);
Copy the code

Dart FFI and C base data type mapping table

NativeType as defined in Dart Type in C instructions
Opaque opaque Its member type is not exposed and is generally used to represent a class in C++
Int8 Int8_t or char Signed 8-bit integer
Int16 Int16_t or short Signed 16-bit integer
Int32 Int32_t or int Signed 32-bit integer
Int64 Int64_t or long long Signed 64-bit integer
Uint8 Uint8_t or unsigned char Unsigned 8-bit integer
Uint16 Uint16_t or unsigned short Unsigned 16-bit integer
Uint32 Int32_t or unsigned int An unsigned 32-bit integer
Uint64 Uint64_t or unsigned long long Unsigned 64-bit integer
IntPtr int* Integer type pointer
Float float Single-precision floating point type
Double double A double-precision floating point type
Void void Void type
Handle Dart_Handle Dart Representation of a handle in C
NativeFunction function Function types
Struct struct Structural type
Union union Community type
Pointer * Pointer types
nullptr NULL Null pointer
dynamic Dart_CObject How Dart objects are represented in C

The sample

sample.c

#include <stdint.h>

// Base data type
int8_t int8 = - 108.;
int16_t int16 = - 16;
int32_t int32 = - 32;
int64_t int64 = - 64.;
uint8_t uint8 = 208;
uint16_t uint16 = 16;
uint32_t uint32 = 32;
uint64_t uint64 = 64;
float float32 = 0.32;
double double64 = 0.64;
Copy the code

ffi_sample.dart

late final ffi.Pointer<ffi.Int8> _int8 = _lookup<ffi.Int8>('int8');
int get int8 => _int8.value;
set int8(int value) => _int8.value = value;
late final ffi.Pointer<ffi.Int16> _int16 = _lookup<ffi.Int16>('int16');
int get int16 => _int16.value;
set int16(int value) => _int16.value = value;
late final ffi.Pointer<ffi.Int32> _int32 = _lookup<ffi.Int32>('int32');
int get int32 => _int32.value;
set int32(int value) => _int32.value = value;
late final ffi.Pointer<ffi.Int64> _int64 = _lookup<ffi.Int64>('int64');
int get int64 => _int64.value;
set int64(int value) => _int64.value = value;
late final ffi.Pointer<ffi.Uint8> _uint8 = _lookup<ffi.Uint8>('uint8');
int get uint8 => _uint8.value;
set uint8(int value) => _uint8.value = value;
late final ffi.Pointer<ffi.Uint16> _uint16 = _lookup<ffi.Uint16>('uint16');
int get uint16 => _uint16.value;
set uint16(int value) => _uint16.value = value;
late final ffi.Pointer<ffi.Uint32> _uint32 = _lookup<ffi.Uint32>('uint32');
int get uint32 => _uint32.value;
set uint32(int value) => _uint32.value = value;
late final ffi.Pointer<ffi.Uint64> _uint64 = _lookup<ffi.Uint64>('uint64');
int get uint64 => _uint64.value;
set uint64(int value) => _uint64.value = value;
late final ffi.Pointer<ffi.Float> _float32 = _lookup<ffi.Float>('float32');
double get float32 => _float32.value;
set float32(double value) => _float32.value = value;
late final ffi.Pointer<ffi.Double> _double64 =
    _lookup<ffi.Double>('double64');
double get double64 => _double64.value;
set double64(double value) => _double64.value = value;
late final ffi.Pointer<ffi.Pointer<ffi.Int8>> _str1 =
    _lookup<ffi.Pointer<ffi.Int8>>('str1');
ffi.Pointer<ffi.Int8> get str1 => _str1.value;
set str1(ffi.Pointer<ffi.Int8> value) => _str1.value = value;


print('\n*************** 1. Basic data type **************\n');
print("int8=${nativeLibrary.int8}");
print("int16=${nativeLibrary.int16}");
print("int32=${nativeLibrary.int32}");
print("int64=${nativeLibrary.int64}");
print("uint8=${nativeLibrary.uint8}");
print("uint16=${nativeLibrary.uint16}");
print("uint32=${nativeLibrary.uint32}");
print("uint64=${nativeLibrary.uint64}");
print("float32=${nativeLibrary.float32}");
print("double64=${nativeLibrary.double64}");
print("string=${nativeLibrary.str1.cast<Utf8>().toDartString()}");

nativeLibrary.int8++;
nativeLibrary.int16++;
nativeLibrary.int32++;
nativeLibrary.int64++;
nativeLibrary.uint8++;
nativeLibrary.uint16++;
nativeLibrary.uint32++;
nativeLibrary.uint64++;
nativeLibrary.float32++;
nativeLibrary.double64++;
nativeLibrary.str1 = "Modify it.".toNativeUtf8().cast();
print("After modification :");
print("int8=${nativeLibrary.int8}");
print("int16=${nativeLibrary.int16}");
print("int32=${nativeLibrary.int32}");
print("int64=${nativeLibrary.int64}");
print("uint8=${nativeLibrary.uint8}");
print("uint16=${nativeLibrary.uint16}");
print("uint32=${nativeLibrary.uint32}");
print("uint64=${nativeLibrary.uint64}");
print("float32=${nativeLibrary.float32}");
print("double64=${nativeLibrary.double64}");
print("string=${nativeLibrary.str1.cast<Utf8>().toDartString()}");
Copy the code

Results output

* * * * * * * * * * * * * * * 1. The basic data type * * * * * * * * * * * * * * int8 int16 = = - 108-16 int32 = 32 uint8 int64 = - 64 = 208-16 uint32 uint16 = = 32 uint64 = 64 Float32 =0.11999999731779099 double64=0.64 String =Dart FFI SAMPLE Int8 =-107 INT16 =-15 INT32 =-31 INT64 =-63 UINT8 =209 UINT16 =17 uint32=33 uint64=65 FLOAT32 =1.1200000047683716 Double64 =1.6400000000000001 String = ModifyCopy the code

Because I wanted the program to be easier to call, I added get and set methods to each function. The above example basically just shows the numeric type conversion, which is basically fairly simple and does not go wrong by following the data structure in the table above.

Careful friends may have noticed that the above string is special and requires a layer of conversion. Char * in C needs to be received with ffi.pointer < ffi.int8 >. We can get the Pointer and convert it to Utf8. Utf8 is an FFI library type.

Utf8 is an Array of utf-8 data. Once we have a pointer to Utf8, we can convert it toDartString using the toDartString method.

late final ffi.Pointer<ffi.Pointer<ffi.Int8>> _str1 =
      _lookup<ffi.Pointer<ffi.Int8>>('str1');
String value = _str1.value.cast<Utf8>().toDartString()
Copy the code

We can also convert the Dart string to C char* by ‘this is the Dart string’.tonativeutf8 ().cast< fi.int8 >().

Function calls should be the most common scenario in Dart and C interactions. Let’s take a look at how you can call C functions from Dart and Dart functions from C.

The Dart adjustable C

No returned value

As an example, Dart calls a C function and prints a sentence in the C function.

sample.h

void hello_world(a);
Copy the code

sample.c

void hello_world(a)
{
    printf("[CPP]: Hello World");
}
Copy the code

ffi_sample.dart

late final _hello_worldPtr =
      _lookup<ffi.NativeFunction<ffi.Void Function() > > ('hello_world');
late final _hello_world = _hello_worldPtr.asFunction<void Function(a) > ();print('[Dart]: ${_hello_world()}');
Copy the code

Results output

[CPP]: Hello World
[Dart]: null
Copy the code

Returns a value

When C has a return value, sample.h can be received via type conversion

char* getName(a);
Copy the code

sample.c

char* getName(a)
{
    return "My name is Mobile phone";
}
Copy the code

ffi_sample.dart

late final _getNamePtr =
      _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Int8> Function() > > ('getName');
late final _getName =
    _getNamePtr.asFunction<ffi.Pointer<ffi.Int8> Function(a) > ();print("[Dart]: return value ->"+_getName().cast<Utf8>().toDartString());
Copy the code

Output result:

[Dart]: there is a return value -> My name is mobile phoneCopy the code

Have the arguments

Use C printf function to implement a Dart print function

sample.h

void cPrint(char *str);
Copy the code

sample.c

void cPrint(char *str) 
{
    printf("[CPP]: %s", str);
    free(str);
}
Copy the code

ffi_sample.dart

late final _cPrintPtr =
      _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Int8>)>>(
          'cPrint');
late final _cPrint =
    _cPrintPtr.asFunction<void Function(ffi.Pointer<ffi.Int8>)>();
_cPrint("I think this output makes sense.".toNativeUtf8().cast<ffi.Int8>());
Copy the code

The output

[CPP]: I think this output makes senseCopy the code

So you have an output function.

Dart function in C

Now that you know how Dart calls C’s function, let’s take a look at how C calls Dart.

A simple example

Principle: C itself does not provide a method to call the Dart function, but we can pass the Dart function as a parameter to C after the program starts, and C can cache the Dart function pointer, so that C can call the Dart function when needed.

First, let’s define a function on Dart. The Dart function must be a top-level or static function to be called, otherwise an error will be reported.

void dartFunction() {
  debugPrint("[Dart]: Dart function called");
}
Copy the code

We define a registration function sample.h in C

void callDart(void (*callback)());
Copy the code

sample.c

void callDart(void (*callback)()) {
    printf("[CPP]: Now call the Dart function");
    callback();
}
Copy the code

Callback is the received Dart function, which is called directly after registration.

We then convert the Dart function to Pointer and pass it into C by calling C’s callDart function.

late final _callDartPtr = _lookup<
          ffi.NativeFunction<
              ffi.Void Function(
                  ffi.Pointer<ffi.NativeFunction<ffi.Void Function(a) > >) > > ('callDart');
late final _callDart = _callDartPtr.asFunction<
    void Function(ffi.Pointer<ffi.NativeFunction<ffi.Void Function(a) > > > (); _callDart(ffi.Pointer.fromFunction(dartFunction));Copy the code

Here, we use the resulting ffi.Pointer.fromFunction method to convert the Dart function into a Dart map of C function Pointers, and then call C’s callDart function via _callDart.

Output after running:

[CPP]: Dart is now calledCopy the code

Success!

Dart function with parameters

How does C call the Dart function with parameters? Let’s define a Dart function

static void add(int num1,int num2) {
    print("[Dart]: num1: ${num1}, num2: ${num2}");
}
Copy the code

The above function is called to output the value of num1 and num2.

Then we modify the callDart function sample.h

void callDart(void (*callback)(), void (*add)(int.int));
Copy the code

sample.c

void callDart(void (*callback)(), void (*add)(int.int)) {
    printf("Now call the Dart function");
    callback();

    printf(Call the Dart Add function);
    add(1.2);
}
Copy the code

The dart end

late final _callDartPtr = _lookup<
      ffi.NativeFunction<
          ffi.Void Function(
              ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>>,
              ffi.Pointer<
                  ffi.NativeFunction<
                      ffi.Void Function(ffi.Int32, ffi.Int32)>>)>>('callDart');
late final _callDart = _callDartPtr.asFunction<
    void Function(
        ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>>,
        ffi.Pointer<
            ffi.NativeFunction<ffi.Void Function(ffi.Int32, ffi.Int32)>>)>();

_callDart(ffi.Pointer.fromFunction(DartFunctions.dartFunction),ffi.Pointer.fromFunction(DartFunctions.add));
Copy the code

Returns the output

Dart function [CPP]: Dart Add function [Dart]: num1:1, num2:2Copy the code

The parameter is then passed from C to the Dart side.

Get the return value

The above examples all call the Dart function and do not get the return value from the Dart side. Let’s modify the add method so that it returns the sum of num1 and num2.

static int add(int num1, int num2) {
    return num1 + num2;
}
Copy the code

sample.h

void callDart(void (*callback)(), int (*add)(int.int));
Copy the code

sample.c

void callDart(void (*callback)(), int (*add)(int.int)) {
    printf("Now call the Dart function");
    callback();

    printf(Call the Dart Add function);
    int result = add(1.2);
    printf("Add results % d", result);
}
Copy the code

ffi_sample.dart

late final _callDartPtr = _lookup<
    ffi.NativeFunction<
        ffi.Void Function(
            ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>>,
            ffi.Pointer<
                ffi.NativeFunction<
                    ffi.Int32 Function(ffi.Int32, ffi.Int32)>>)>>('callDart');
late final _callDart = _callDartPtr.asFunction<
    void Function(
        ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>>,
        ffi.Pointer<
            ffi.NativeFunction<ffi.Int32 Function(ffi.Int32, ffi.Int32)>>)>();
_callDart(ffi.Pointer.fromFunction(DartFunctions.dartFunction),ffi.Pointer.fromFunction(DartFunctions.add, 0));
Copy the code

Note that if the Dart function has a return value, the second argument to the fromFunction needs to be passed in the value returned in case of an error.

The output

Dart function [CPP]: Dart Add function [Dart]: num1:1, num2:2 [CPP]: Add result 3Copy the code

Ok, now we have learned how to call the Dart function using C. Of course, in a real project, you would define an initial function that passes the Dart function that you want C to call into C’s in-memory cache, and C will call it when appropriate.

Struct (Struct, Union)

In Dart1.12, FFI also supports C constructs, and we can use ffi.struct to “copy” a structure that has been defined in C

sample.h

typedef struct
{
  char *name;
  int age;
  float score;
} Student;
Copy the code

bindings.dart

class Student extends ffi.Struct {
  external ffi.Pointer<ffi.Int8> name;

  @ffi.Int32()
  external int age;

  @ffi.Float()
  external double score;
}
Copy the code

Thus, we have a mapping of C constructs in the Dart environment, but the Student we define in Dart has no constructor, which means it cannot be initialized in Dart. We can only define an initialization function in C, and Dart calls the C function to initialize a structure

// C create a Student
Student initStudent(char *name, int age, float score)
{
    Student st = {name, age, score};
    return st;
}
Copy the code

bindings.dart

class NativeLibrary {
  // ...
  Student initStudent(
    ffi.Pointer<ffi.Int8> name,
    int age,
    double score,
  ) {
    return _initStudent(
      name,
      age,
      score,
    );
  }
  late final _initStudentPtr = _lookup<
      ffi.NativeFunction<
          Student Function(
              ffi.Pointer<ffi.Int8>, ffi.Int32, ffi.Float)>>('initStudent');
  late final _initStudent = _initStudentPtr
      .asFunction<Student Function(ffi.Pointer<ffi.Int8>, int.double) > (); } ffi_sample.dart ```dartDart initializes a student by calling C
var name = "Yao Feng Dance".toNativeUtf8();
var student = nativeLibrary.initStudent(name.cast<ffi.Int8>(), 25.100);
print(
    "Name:${student.name.cast<Utf8>().toDartString()}Age:${student.age}The score:${student.score}");
// After converting the Dart String type to THE Utf8 type of C, free is required to prevent memory leaks
malloc.free(name);
Copy the code

When everything is ready, run ffi_sample.dart to output

Name: Yao Fengwu, age: 25, score: 100.0Copy the code

Note:

  1. Struct cannot be initialized in Dart
  2. If the structure is of pointer type,ffiExtended its methods can be passedrefTo access structure-specific values.
  3. Community usage is similar to structure usage, see examples

class

Dart FFI itself can only connect to C interfaces, but what if we encounter C++ classes? In this section, I will explain my own ideas.

Project reform

Because I used C compiler to compile the previous project, since C++ classes were added here, IT was necessary to use C++ to compile, and I have been using the ffigen library to automatically generate THE Dart code according to THE C header. The ffigen base is implemented by using the C compiler, so there are some modifications to the original code.

  1. Rename sample.c to sample.cc
  2. willCMakeLists.txtUse the C++ compiler instead
Cmake_minimum_required (VERSION 3.7 FATAL_ERROR) project(sample VERSION 1.0.0 LANGUAGES CXX) # SHARED sample.cc sample.def) # sample.cCopy the code
  1. Add a condition to sample.h to compile both C and C++ code
// Because this test designs C++ classes (compiled in C++), I need to export functions through extern "C" for FFI to recognize
#ifdef __cplusplus
  #define EXPORT extern "C"
#else
  #define EXPORT // Ffigen will be generated using the C compiler, so change it to null
#endif
Copy the code

Other previously defined functions need to be modified with EXPORT, for example

EXPORT void hello_world();
Copy the code

When using C++ style code, it needs to be wrapped in #ifdef __cplusplus so that the project transformation is complete.

C++ class mapping

Add a simple class to sample.h

#ifdef __cplusplus 
class SportManType
{
  const char *name; / / name
public:
  void setName(const char *str)
  {
    name = str;
  }
  const char *getName(a)
  {
    returnname; }};#endif
Copy the code

Since Dart FFI does not get C++ style symbols, we need to use C-style functions to manipulate classes.

EXPORT typedef void* SportMan; // Define a mapping type of SportManType class in C

EXPORT SportMan createSportMan(a); // Initialize the SportManType class
EXPORT void setManName(SportMan self,const char *name); // Set the name
EXPORT const char *getManName(SportMan self); // Get the name
Copy the code

And then implement the corresponding function

SportMan createSportMan(a)
{
    return new SportManType(a); }void setManName(SportMan self,const char *name)
{
    SportManType* p = reinterpret_cast<SportManType*>(self);
    p->setName(name);
}
const char* getManName(SportMan self) {
    SportManType* p = reinterpret_cast<SportManType*>(self);
    return p->getName(a); }Copy the code

We can use reinterpret_cast to convert an incoming SportMan type into a SportManType, and then manipulate the class directly.

Now that we’re done with our C++ code, let’s write the Dart code.

FFI symbolic link code:

class NativeLibrary {
  // ...
  /// Initialize a class
  SportMan createSportMan() {
    return _createSportMan();
  }

  late final _createSportManPtr =
      _lookup<ffi.NativeFunction<SportMan Function() > > ('createSportMan');
  late final _createSportMan =
      _createSportManPtr.asFunction<SportMan Function(a) > ();/// Set the name
  void setManName(
    SportMan self,
    ffi.Pointer<ffi.Int8> name,
  ) {
    return _setManName(
      self,
      name,
    );
  }

  late final _setManNamePtr = _lookup<
      ffi.NativeFunction<
          ffi.Void Function(SportMan, ffi.Pointer<ffi.Int8>)>>('setManName');
  late final _setManName = _setManNamePtr
      .asFunction<void Function(SportMan, ffi.Pointer<ffi.Int8>)>();
  /// Get name
  ffi.Pointer<ffi.Int8> getManName(
    SportMan self,
  ) {
    return _getManName(
      self,
    );
  }

  late final _getManNamePtr =
      _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Int8> Function(SportMan)>>(
          'getManName');
  late final _getManName =
      _getManNamePtr.asFunction<ffi.Pointer<ffi.Int8> Function(SportMan)>();
}
Copy the code

Then call the operation:

/ /...
SportMan man = nativeLibrary.createSportMan();
nativeLibrary.setManName(man, "SY".toNativeUtf8().cast());
print(
    "Name of athlete:" + nativeLibrary.getManName(man).cast<Utf8>().toDartString());
Copy the code

Output: Athlete name: SY

This way, we can use Dart to manipulate C++ classes indirectly. Some people say that this is too abstract to use, so we can use the Dart class to wrap it.

class SportManType {
  String? _name;
  late NativeLibrary _lib;
  late SportMan man;

  SportManType(NativeLibrary library) {
    _lib = library;
    man = _lib.createSportMan();
  }

  String getName() {
    return _lib.getManName(man).cast<Utf8>().toDartString();
  }

  void setName(Stringname) { _lib.setManName(man, name.toNativeUtf8().cast()); }}Copy the code

Caller:

SportManType m = SportManType(nativeLibrary);
m.setName('SY is a dog');
print(m.getName());
Copy the code

The output

SY is a dog
Copy the code

The simple idea is that we define the class, use C functions to operate on that class, and then use Dart to operate on those functions so that Dart can operate on C++ classes. I also made some special judgments here, mainly to make sample.h into both C and C++ compilers can compile the code, compatible with ffigen automatic generation code.

asynchronous

Dart creates a function on the Dart side, and then passes it to the C/C++ side through FFI. C/C++ passes it to the thread, and then the thread calls the function when it is finished. I went to the field and reported the following error:

Cannot invoke native callback outside an isolate.
Copy the code

Those familiar with Flutter ISOLATE may know that the MECHANISM of Flutter ISOLATE is implemented using C/C++ threads, but with one additional limitation — there is no memory sharing, so callbacks passed into the DART thread cannot be called in another thread.

Dart is aware of the problem and has a solution: #37022 ffi_TEST_functions_vmspecific. Cc works the same as SendPort on ISOLATE, but it also provides C code encapsulation.

I according to the development ideas, explain the steps of its use.

First we need to import the code that Dart has prepared for us, usually in the ${Dart SDK path}/include/ folder, which we can copy and paste into our OWN C code projects. Then modify the cmakelist. TXT file (I created a new include folder in the C code project to store the Dart API code)

#1. Where are you going? Dart_api_dl. H and dart_API_DL. C files are added to project(Sample VERSION 1.0.0 LANGUAGES CXX C) #2. add_library add_library(sample SHARED sample.cc sample.def include/dart_api_dl.h include/dart_api_dl.c)Copy the code

Add a few functions to the sample.c file.

DART_EXPORT intptr_t InitDartApiDL(void *data)
{
    return Dart_InitializeApiDL(data);
}
Copy the code

InitDartApiDL is used to initialize code related to the Dart API.

Dart_Port send_port_;
DART_EXPORT void registerSendPort(Dart_Port send_port)
{
    localPrint("Set send Port");
    send_port_ = send_port;
}
Copy the code

RegisterSendPort is used to receive the Port sent by Dart and store it in memory

DART_EXPORT void executeCallback(VoidCallbackFunc callback) {
    localPrint("Execute dart return function, thread: (%p)\n", pthread_self());
    callback();
}
Copy the code

The executeCallback function might be a bit confusing at first, but it doesn’t really work, except that the Dart side listens to a C memory address for a Port, which the Dart side can’t execute, so you need to pass it to C/C++.

Ok, now set up the Dart code

Dart, and C interface layer code

class NativeLibrary {
  //....
/// Initialize the dart_api_Dl related data
  int InitDartApiDL(
    ffi.Pointer<ffi.Void> data,
  ) {
    return _InitDartApiDL(
      data,
    );
  }

  late final _InitDartApiDLPtr =
      _lookup<ffi.NativeFunction<ffi.IntPtr Function(ffi.Pointer<ffi.Void>)>>(
          'InitDartApiDL');
  late final _InitDartApiDL =
      _InitDartApiDLPtr.asFunction<int Function(ffi.Pointer<ffi.Void>)>();

  /// Dart Send Port is passed to C/C++ memory cache
  void registerSendPort(
    int send_port,
  ) {
    return _registerSendPort(
      send_port,
    );
  }

  late final _registerSendPortPtr =
      _lookup<ffi.NativeFunction<ffi.Void Function(Dart_Port)>>(
          'registerSendPort');
  late final _registerSendPort =
      _registerSendPortPtr.asFunction<void Function(int) > ();/// Executes an asynchronous function with no return value
  void nativeAsyncCallback(
    VoidCallbackFunc callback,
  ) {
    return _nativeAsyncCallback(
      callback,
    );
  }
  /// Execute the address function that dart passes back
  void executeCallback(
    VoidCallbackFunc callback,
  ) {
    return _executeCallback(
      callback,
    );
  }

  late final _executeCallbackPtr =
      _lookup<ffi.NativeFunction<ffi.Void Function(VoidCallbackFunc)>>(
          'executeCallback');
  late final _executeCallback =
      _executeCallbackPtr.asFunction<void Function(VoidCallbackFunc)>();
  / /...
}
Copy the code

ffi_sample.dart

ReceivePort _receivePort = ReceivePort();
void _handleNativeMessage(dynamic message) {
  print('_handleNativeMessage $message');
  final int address = message;
  nativeLibrary.executeCallback(Pointer<Void>.fromAddress(address).cast());
  /// If we're done, we need to close it, not necessarily put it here
  _receivePort.close();
}
void ensureNativeInitialized() {
  var nativeInited =
      nativeLibrary.InitDartApiDL(NativeApi.initializeApiDLData);
  assert(nativeInited == 0.'DART_API_DL_MAJOR_VERSION ! = 2 ');
  _receivePort.listen(_handleNativeMessage);
  nativeLibrary.registerSendPort(_receivePort.sendPort.nativePort);
}
Copy the code

_handleNativeMessage is a callback function after Port listener, used to receive data, which will call executeCallback to C to execute the received data. EnsureNativeInitialized is used to initialize the necessary code to add Port listener, And pass the Native form of Port to the C layer.

Now all programs can be said to be ready, in fact, the simple writing here is that you can pass all the data that needs to be transmitted to the C layer to C in one function at a time, I write here is to clarify the idea, and also to provide a reuse Port idea, do not need to set the Port every time.

Let’s now define a nativeAsyncCallback function that performs some operations sample.cc in C using threads

DART_EXPORT void nativeAsyncCallback(VoidCallbackFunc callback)
{
    localPrint("Main thread: (%p)\n", pthread_self());
    pthread_t callback_thread;
    int ret = pthread_create(&callback_thread, NULL, thread_func, (void *)callback);
    if(ret ! =0)
    {
        localPrint("Thread error: error_code=%d", ret); }}Copy the code

binding.dart

class NativeLibrary {
  // ...
  /// Executes an asynchronous function with no return value
  void nativeAsyncCallback(
    VoidCallbackFunc callback,
  ) {
    return _nativeAsyncCallback(
      callback,
    );
  }

  late final _nativeAsyncCallbackPtr =
      _lookup<ffi.NativeFunction<ffi.Void Function(VoidCallbackFunc)>>(
          'nativeAsyncCallback');
  late final _nativeAsyncCallback =
      _nativeAsyncCallbackPtr.asFunction<void Function(VoidCallbackFunc)>();
      / /...
}
Copy the code

ffi_sample.dart

void asyncCallback() {
  print('asyncCallback called');
}
main() {
  ensureNativeInitialized();
  var asyncFunc = Pointer.fromFunction<NativeAsyncCallbackFunc>(asyncCallback);
  nativeLibrary.nativeAsyncCallback(asyncFunc);
}
Copy the code

Finally, execute the function and output

[CPP]: initialize InitDartApiDL [CPP]: Set send Port [CPP]: main thread: (0x700008108000) [CPP]: Main thread: (0x700008108000) [CPP]: main thread: (0x700008108000) [CPP]: Asynchronous thread: (0x70000818B000) [CPP]: Asynchronous thread: (0x70000820E000) _handleNativeMessage 4450988052 [CPP]: Dart returns a function called by thread: (0x700008108000) asyncCallback calledCopy the code

ffigen

Writing the Dart Binding functions one by one is tedious and error-prone for some of the written three-party libraries, so I used the ffigen library mentioned above to automatically generate the Dart Binding function from the C/C++ header file.

We need to introduce this library in pubspec.yaml

dev_dependencies:
  ffigen: ^ 4.1.0
Copy the code

Then execute pub get

We also need to configure some information in pubspec.yaml

ffigen:
  output: 'bin/bindings.dart' Output to the bin/ binding.dart file
  name: 'NativeLibrary' The output class is called NativeLibrary
  description: 'demo' # Describe, write freely
  headers:
    entry-points: The dart Binding function header file can be multiple
      - 'library/sample.h' 
    include-directives: # ensure that only the sample.h file is converted and not the contained files such as stdint.h
      - 'library/sample.h'
Copy the code

After our simple configuration, dart Run ffigen can be executed on the command line to generate the Dart Binding code. We only need a simple initialization, can be very convenient to use.

import 'dart:ffi' as ffi;
main() {
  var libraryPath = path.join(
        Directory.current.path, 'library'.'build'.'libsample.dylib');
  final dylib = ffi.DynamicLibrary.open(libraryPath);
  nativeLibrary = NativeLibrary(dylib);
  nativeLibrary.hello_world();// call hello_world in C++
}
Copy the code

Note:

  1. ffigenOnly c-style headers can be generated automatically. If your header contains C++ style code such as class, you need to wrap it with #ifdef __cplusplus #endif

Dart and C/C++ are two languages, so there are bound to be some compatibility issues, so for some complex libraries, you may need more FFigen configuration to translate well. I don’t use Ffigen very much at the moment, but you can also see the FFigen documentation for more information.

I have submitted the above code to my Github repository, Github portal, if there is help for you, please do not skimp on your star

References:

  1. Interaction with C using DART: FFI
  2. Binding to native code using dart:ffi
  3. Build C/C++ projects and dynamic libraries using Cmake
  4. C Wrappers for C++ Libraries and Interoperability
  5. Calling Native Libraries in Flutter with Dart FFI
  6. Dart: FFI synchronous/asynchronous invocation guide