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
- C API can be called synchronously, unlike a Flutter Channel which starts asynchronously
- C calls are faster, unlike the previous need to transfer via Native (or change the Flutter engine code).
- 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:
- Struct cannot be initialized in Dart
- If the structure is of pointer type,
ffi
Extended its methods can be passedref
To access structure-specific values. - 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.
- Rename sample.c to sample.cc
- will
CMakeLists.txt
Use 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
- 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:
ffigen
Only 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:
- Interaction with C using DART: FFI
- Binding to native code using dart:ffi
- Build C/C++ projects and dynamic libraries using Cmake
- C Wrappers for C++ Libraries and Interoperability
- Calling Native Libraries in Flutter with Dart FFI
- Dart: FFI synchronous/asynchronous invocation guide