ATA处女篇,如果对您有帮助,请狠狠的点赞
是什么
Dart FFI
(官方地址)是可以在Dart Native平台上运行的Dart移动、命令行和服务器应用上通过Dart FFI
来调用C代码的一个技术。简单来说,就是Dart与C互相调用的一种机制。Dart FFI
是Dart2.12.0版本后(同时包含在 Flutter 2.0 和以后的版本里),才作为稳定版本发布。
说到底,Dart语言也是因为Flutter使用了它才火起来的,所以Dart FFI
技术在Flutter应用中更能发挥它更强大的作用
解决的问题
- 可以同步调用C API,不像Flutter Channel一开始就是异步
- 调用C语言更快,不像之前需要通过Native中转(或者改Flutter引擎代码)
- 还可以封装替换Flutter Channel达到更快和支持同步的目地(有人做了Flutter Platform Channel和FFI通道性能测试,点这里查看)
简单使用
为了只看FFI的特性,我先不在Flutter平台上使用,仅仅用命令行Dart应用的方式来讲解。
本人工程环境:
运行环境 MacOS 12.0.1GCC 13.0.0
cmake 3.20.1
make 3.81
dart 2.16.0
理论上dart2.12以上都是没有问题的。
1. 创建项目
由于项目结构简单,直接手动创建项目
1). 创建pubspec.yaml
文件
2). 创建bin/ffi_sample.dart
文件
3). 创建C环境,创建library
、library/build
文件夹
4). 创建library/sample.c
、library/sample.h
、library/sample.def
、CMakeLists.txt
文件
目录结构如下
|_ bin
|_ ffi_sample.dart
|_ library
|_ build
|_ CMakeLists.txt
|_ sample.c
|_ sample.h
|_ sample.def
|_ pubspec.yaml
AI 代码解读
2. pubspec.yaml引入FFI
在pubspec.yaml
文件中的dependencies
中加入ffi
、path
库
pubspec.yaml
name: ffi_sample
version: 0.0.1
description: 使用ffi及ffigen的例子
publish_to: none
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
path: ^1.7.0
ffi: ^1.1.2
AI 代码解读
3. 编译C代码
在sample.h
中写简单的一个函数
sample.h
void hello_world();
AI 代码解读
在sample.c
中实现
sample.c
#include <stdio.h>
#include <stdlib.h>
#include "sample.h"
void hello_world()
{
printf("Hello World\n");
}
AI 代码解读
sample.def
中简单导出
LIBRARY sample
EXPORTS
sample
AI 代码解读
用于测试C代码的main文件main.cc
#include <stdio.h>
#include "sample.h"
int main()
{
printf("测试");
return 0;
}
AI 代码解读
写编译使用的CMakeLists.txt
文件
cmake_minimum_required(VERSION 3.7 FATAL_ERROR) project(sample VERSION 1.0.0 LANGUAGES C) add_library(sample SHARED sample.c sample.def)
AI 代码解读
3. 编译C文件
现在所有文件都准备就绪,就可以编译C代码了。
1). 命令行进入到library/build
文件夹下
2). 执行cmake ..
生成编译所需文件
3). 执行make
编译
cd library/build
cmake ..
make
AI 代码解读
如果在library/build
文件夹下生成了libsample.dylib
文件,那么说明编译成功了。
4. 写Dart通信代码
在bin/ffi_sample.dart
中调用C
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() {
// 初始化互调框架
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调用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()>();
// 调用C方法(无参)
_hello_world();
}
AI 代码解读
5. 运行代码
现在,在命令行的项目根目录下运行
dart run
AI 代码解读
如果输出
Hello World
AI 代码解读
好,简单的Demo就跑起来了。
由于ffi
部分API跟已有Framework的API名称重合,所以后面代码我所有用到ffi的地方都加了ffi前缀。
import 'dart:ffi' as ffi;
AI 代码解读
常用属性与方法介绍
为了联通Dart与C语言,Dart FFI
提供了很多方法,下面我来介绍一下主要的方法。
DynamicLibrary.open
它可以加载动态链接库
external factory DynamicLibrary.open(String path);
AI 代码解读
此方法用于加载库文件,如上面我编译C后生成的libsample.dylib
文件,我们需要使用此方法来将其加载到DartVM中。需要注意的是,多次调用此方法加载库文件也只会将库文件加载到DartVM中一次。
示例:
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);
AI 代码解读
DynamicLibrary.process
external factory DynamicLibrary.process();
AI 代码解读
它可以用于在iOS及MacOS中加载应用程序已经自动加载好的动态链接库,也可以解析静态链接到应用的二进制文件符号。需要注意的是,它不能用于windows平台
DynamicLibrary.executable
external factory DynamicLibrary.executable();
AI 代码解读
它可用于加载静态链接库
NativeType
NativeType
是在Dart中表示C语言中的数据结构(想了解有哪些NativeType
可以直接跳转到『Dart FFI与C基础数据类型映射表』目录)。它不可在Dart中实例化,只能由Native返回。
Pointer
它是C语言中指针在Dart中的映射
DynamicLibrary->lookup()
external Pointer<T> lookup<T extends NativeType>(String symbolName);
AI 代码解读
它用于在DynamicLibrary中查找到对应的符号并返回其内存地址。
Dart
使用方法:
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()>();
_hello_world();
AI 代码解读
Pointer.fromAddress(int ptr)
根据内存地址获取C对象指针
例如:
// 创建一个指向NULL的Native指针
final Pointer<Never> nullptr = Pointer.fromAddress(0);
AI 代码解读
Pointer.fromFunction
根据一个Dart函数,创建一个Native函数指针,一般用于将Dart函数传给C,使C有调用Dart函数的能力
void globalCallback(int src, int result) {
print("globalCallback src=$src, result=$result");
}
Pointer.fromFunction(globalCallback);
AI 代码解读
Pointer->address()
获取指针的内存地址
asFunction
将Native指针对象,转换为Dart函数
sizeOf
返回具体类型的内存占用
例
ffi.sizeOf<ffi.Int64>(); // 8
AI 代码解读
malloc.allocate()
Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment});
AI 代码解读
开辟一块大小byteCount
的空间
例
Pointer<Uint8> bytes = malloc.allocate<Uint8>(ffi.sizeOf<ffi.Uint8>());
AI 代码解读
malloc.free
释放内存
malloc.free(bytes);
AI 代码解读
Dart FFI与C基础数据类型映射表
Dart 中定义的NativeType | C语言中的类型 | 说明 |
---|---|---|
Opaque | opaque | 不暴露其成员类型,一般用于表示C++中的类 |
Int8 | int8_t 或 char | 有符号8位整数 |
Int16 | int16_t 或 short | 有符号16位整数 |
Int32 | int32_t 或 int | 有符号32位整数 |
Int64 | int64_t 或 long long | 有符号64位整数 |
Uint8 | uint8_t 或 unsigned char | 无符号8位整数 |
Uint16 | uint16_t 或 unsigned short | 无符号16位整数 |
Uint32 | int32_t 或 unsigned int | 无符号32位整数 |
Uint64 | uint64_t 或 unsigned long long | 无符号64位整数 |
IntPtr | int* | 整数类型指针 |
Float | float | 单精度浮点类型 |
Double | double | 双精度浮点类型 |
Void | void | void类型 |
Handle | Dart_Handle Dart | 句柄在C中的表示形式 |
NativeFunction | 函数 | 函数类型 |
Struct | struct | 结构体类型 |
Union | union | 共同体类型 |
Pointer | * | 指针类型 |
nullptr | NULL | 空指针 |
dynamic | Dart_CObject | Dart对象在C中的表现形式 |
示例
sample.c
#include <stdint.h>
// 基础数据类型
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;
AI 代码解读
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. 基础数据类型 **************\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 = "修改一下".toNativeUtf8().cast();
print("修改后:");
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()}");
AI 代码解读
结果输出
*************** 1. 基础数据类型 **************
int8=-108
int16=-16
int32=-32
int64=-64
uint8=208
uint16=16
uint32=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=修改一下
AI 代码解读
由于我想让程序能更简单调用,我对每个函数添加了get
,set
方法。 上面的示例基本上只展示了数字类型转换,基本上还算简单,按照上表数据结构对应转换就不会出错。
细心的朋友可能已经发现了,上面的字符串是比较特殊,需要一层转换。C语言中的char*
需要用ffi.Pointer<ffi.Int8>
去接收,我们可以拿到这个指针,然后转换成Utf8
格式,需要说明的是Utf8
是ffi
库下的一个类型(ffi
包含dart sdk提供的类与方法和ffi库的方法)。
Utf8
是一个UTF-8
数据的列表(Array),我们拿到Utf8
的指针后,可以通过它提供的方法toDartString
来将其转换成Dart的String类型。
late final ffi.Pointer<ffi.Pointer<ffi.Int8>> _str1 =
_lookup<ffi.Pointer<ffi.Int8>>('str1');
String value = _str1.value.cast<Utf8>().toDartString()
AI 代码解读
我们还可以通过 '这是Dart字符串'.toNativeUtf8().cast<ffi.Int8>()
将Dart字符串转换成C的char*
。
在Dart与C的交互中,函数调用应该是最常见的场景。下面我们就来看看如何在Dart中调用C的函数,同时也能在C中调用Dart的函数。
Dart调C
无传参无返回值
我们通过一个例子,让Dart来调用C的函数,并在C的函数中输出一句话。
sample.h
void hello_world();
AI 代码解读
sample.c
void hello_world()
{
printf("[CPP]: Hello World");
}
AI 代码解读
ffi_sample.dart
late final _hello_worldPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('hello_world');
late final _hello_world = _hello_worldPtr.asFunction<void Function()>();
print('[Dart]: ${_hello_world()}');
AI 代码解读
结果输出
[CPP]: Hello World
[Dart]: null
AI 代码解读
有返回值
当C有返回值时,可以通过类型转换接收
sample.h
char* getName();
AI 代码解读
sample.c
char* getName()
{
return "My name is 大哥大";
}
AI 代码解读
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()>();
print("[Dart]: 有返回值 -> "+_getName().cast<Utf8>().toDartString());
AI 代码解读
输出结果:
[Dart]: 有返回值 -> My name is 大哥大
AI 代码解读
有传参
利用C的printf
函数,实现一个Dart打印函数
sample.h
void cPrint(char *str);
AI 代码解读
sample.c
void cPrint(char *str)
{
printf("[CPP]: %s", str);
free(str);
}
AI 代码解读
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("我认为这个输出很有意义".toNativeUtf8().cast<ffi.Int8>());
AI 代码解读
输出
[CPP]: 我认为这个输出很有意义
AI 代码解读
这样就实现了一个输出函数了。
C调Dart函数
我们知道了Dart如何调用C的函数,下面我们通过示例来了解一下C如何调用Dart函数。
简单示例
原理: C本身是没有提供调用Dart函数的方法的,但是我们可以在程序启动后通过Dart将函数当做参数传入C中,C中缓存起来Dart的函数指针,就可以在需要的时候实现C调用Dart。
首先,我们先在Dart上定义一个函数。需要注意的是Dart函数需要是顶级函数或者静态函数才能被调用,否则会报错.
void dartFunction() {
debugPrint("[Dart]: Dart 函数被调用了");
}
AI 代码解读
我们在C中定义一个注册函数
sample.h
void callDart(void (*callback)());
AI 代码解读
sample.c
void callDart(void (*callback)()) {
printf("[CPP]: 现在调用Dart函数");
callback();
}
AI 代码解读
其中的callback就是接收到的Dart的函数,这里我们为了看效果,就在注册后直接调用Dart函数了。
然后我们将Dart函数转换成Pointer
类型,并通过调用C的callDart
函数传入到C中。
late final _callDartPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>>)>>(
'callDart');
late final _callDart = _callDartPtr.asFunction<
void Function(ffi.Pointer<ffi.NativeFunction<ffi.Void Function()>>)>();
_callDart(ffi.Pointer.fromFunction(dartFunction));
AI 代码解读
这里,我们试用结果ffi.Pointer.fromFunction
方法将Dart函数转换成C函数指针的Dart映射,然后通过_callDart
来调用C的callDart
函数。
运行后输出:
[CPP]: 现在调用Dart函数
[Dart]: Dart 函数被调用了
AI 代码解读
成功!
带参数的Dart函数
C如何调用带参数的Dart函数呢,我们下面来定义一个Dart函数
static void add(int num1,int num2) {
print("[Dart]: num1: ${num1}, num2: ${num2}");
}
AI 代码解读
上面函数被调用后会输出num1
、num2
的值。
然后我们改造一下callDart
函数
sample.h
void callDart(void (*callback)(), void (*add)(int, int));
AI 代码解读
sample.c
void callDart(void (*callback)(), void (*add)(int, int)) {
printf("现在调用Dart函数");
callback();
printf("调用Dart Add函数");
add(1, 2);
}
AI 代码解读
dart端
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));
AI 代码解读
返回输出
[CPP]: 现在调用Dart函数
[Dart]: Dart 方法被调用了
[CPP]: 调用Dart Add函数
[Dart]: num1: 1, num2: 2
AI 代码解读
这样,参数就从C传到Dart端了。
获取返回值
上面的示例都只是调用Dart函数,并没有从Dart端获取返回值。我们再来改造一下add
方法,让它可以返回num1
num2
相加的值。
static int add(int num1, int num2) {
return num1 + num2;
}
AI 代码解读
sample.h
void callDart(void (*callback)(), int (*add)(int, int));
AI 代码解读
sample.c
void callDart(void (*callback)(), int (*add)(int, int)) {
printf("现在调用Dart函数");
callback();
printf("调用Dart Add函数");
int result = add(1, 2);
printf("Add 结果 %d", result);
}
AI 代码解读
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));
AI 代码解读
需要注意的是,如果Dart函数有返回值,fromFunction
的第二个参数就需要传入当出错时返回的值。
输出结果
[CPP]: 现在调用Dart函数
[Dart]: Dart 方法被调用了
[CPP]: 调用Dart Add函数
[Dart]: num1: 1, num2: 2
[CPP]: Add 结果 3
AI 代码解读
好了,现在我们就学会了如何使用C调用Dart函数了。当然实际项目中,我们一般需要定义一个初始函数,把想要C调用的Dart函数传入到C的内存中缓存,C会在合适的时候调用。
结构体(Struct、Union)
在Dart1.12版本中,FFI也对C语言中的结构体进行了支持,我们可以使用ffi.Struct
来"复制"一份C语言中已经定义好的结构体
sample.h
typedef struct
{
char *name;
int age;
float score;
} Student;
AI 代码解读
bindings.dart
class Student extends ffi.Struct {
external ffi.Pointer<ffi.Int8> name;
@ffi.Int32()
external int age;
@ffi.Float()
external double score;
}
AI 代码解读
这样,我们就在Dart环境中有了C语言结构体的一个映射,不过我们在Dart中定义的这个Student
是没有构造函数的,也就是不能在Dart中去初始化。我们只能在C中定义好一个初始化函数,通过Dart调用C函数来初始化一个结构体
// C创建一个Student
Student initStudent(char *name, int age, float score)
{
Student st = {name, age, score};
return st;
}
AI 代码解读
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
AI 代码解读
// dart 初始化一个student 调用C函数初始化
var name = "幺风舞".toNativeUtf8();
var student = nativeLibrary.initStudent(name.cast<ffi.Int8>(), 25, 100);
print(
"姓名:${student.name.cast<Utf8>().toDartString()} ,年龄:${student.age} , 分数:${student.score}");
AI 代码解读
// Dart String类型转成C的Utf8类型后,需要free,不然会内存泄露
malloc.free(name);
一切准备就绪后,运行ffi_sample.dart,输出
AI 代码解读
姓名:幺风舞 ,年龄:25 , 分数:100.0
注意:
1. Struct不能在Dart中初始化
2. 如果是指针类型的结构体,`ffi`扩展了其方法,可以通过`ref`来访问结构体具体值。
3. 共同体使用跟结构体大致类似,具体就查看[示例](https://github.com/xuzhongpeng/ffi_sample/blob/master/bin/ffi_sample.dart#L123)
## 类
Dart FFI本身只能只能对接C接口,但是如果我们遇到C++的类怎么处理呢,这节我来讲解一下我自己的思路。
### 项目改造
因为之前的项目我都是使用C编译器编译的,由于这里添加了C++的类,需要使用C++来编译了,而我一直使用的`ffigen`这个库来自动根据C header生成Dart代码,这个`ffigen`底层是使用C编译器来实现的,所以对原来代码有一定改造。
1. 将sample.c重命名成sample.cc
2. 将`CMakeLists.txt`改成使用C++编译器
AI 代码解读
cmake_minimum_required(VERSION 3.7 FATAL_ERROR)
project(sample VERSION 1.0.0 LANGUAGES CXX) #这里C改成CXX
add_library(sample SHARED sample.cc sample.def) # sample.c改成sample.cc了
3. sample.h中添加能同时编译C和C++代码的条件
AI 代码解读
// 因为本测试设计到了C++的类(用的C++编译的),所以需要把函数都通过extern "C"导出让ffi识别
ifdef __cplusplus
#define EXPORT extern "C"
else
#define EXPORT // ffigen生成时,会使用C编译器,所以改成空即可
endif
其它之前定义的函数都需要使用`EXPORT`来修饰一下,如
AI 代码解读
EXPORT void hello_world();
当使用C++的风格代码时,需要使用`#ifdef __cplusplus`包裹起来,这样项目改造就完成了。 ### C++类的映射 在sample.h中添加一个简单的类
AI 代码解读
ifdef __cplusplus
class SportManType
{
const char *name; //名称
public:
void setName(const char *str)
{
name = str;
AI 代码解读
}
const char *getName()
{
return name;
AI 代码解读
}
};
endif
由于Dart FFI是获取不到C++风格的符号的,所以我们需要使用C风格函数来操作类。
AI 代码解读
EXPORT typedef void* SportMan; // 定义一个SportManType类在C中的映射类型
EXPORT SportMan createSportMan(); // 初始化SportManType类
EXPORT void setManName(SportMan self,const char *name); // 设置姓名
EXPORT const char *getManName(SportMan self); // 获取姓名
然后实现对应函数
AI 代码解读
SportMan createSportMan()
{
return new SportManType();
AI 代码解读
}
void setManName(SportMan self,const char *name)
{
SportManType* p = reinterpret_cast<SportManType*>(self);
p->setName(name);
AI 代码解读
}
const char* getManName(SportMan self) {
SportManType* p = reinterpret_cast<SportManType*>(self);
return p->getName();
AI 代码解读
}
我们可以使用`reinterpret_cast`来将传入的`SportMan`类型转成`SportManType`类型,然后直接操作类。
现在我们可以C++代码的改造就完成了,下面我们来写Dart代码。
FFI符号连接代码:
AI 代码解读
class NativeLibrary {
// ...
/// 初始化一个类
SportMan createSportMan() {
return _createSportMan();
AI 代码解读
}
late final _createSportManPtr =
_lookup<ffi.NativeFunction<SportMan Function()>>('createSportMan');
AI 代码解读
late final _createSportMan =
_createSportManPtr.asFunction<SportMan Function()>();
AI 代码解读
/// 设置姓名
void setManName(
SportMan self,
ffi.Pointer<ffi.Int8> name,
AI 代码解读
) {
return _setManName(
self,
name,
);
AI 代码解读
}
late final _setManNamePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(SportMan, ffi.Pointer<ffi.Int8>)>>('setManName');
AI 代码解读
late final _setManName = _setManNamePtr
.asFunction<void Function(SportMan, ffi.Pointer<ffi.Int8>)>();
AI 代码解读
/// 获取姓名
ffi.Pointer<ffi.Int8> getManName(
SportMan self,
AI 代码解读
) {
return _getManName(
self,
);
AI 代码解读
}
late final _getManNamePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Int8> Function(SportMan)>>(
'getManName');
AI 代码解读
late final _getManName =
_getManNamePtr.asFunction<ffi.Pointer<ffi.Int8> Function(SportMan)>();
AI 代码解读
}
然后来操作调用一下:
AI 代码解读
//...
SportMan man = nativeLibrary.createSportMan();
nativeLibrary.setManName(man, "SY".toNativeUtf8().cast());
print(
"运动员名称:" + nativeLibrary.getManName(man).cast<Utf8>().toDartString());
AI 代码解读
输出: `运动员名称:SY`
这样,我们就能通过Dart间接操作C++中的类了,可能有人说这样写太抽象,不方便使用,那我们再使用Dart类在对其包装一下。
AI 代码解读
class SportManType {
String? _name;
late NativeLibrary _lib;
late SportMan man;
SportManType(NativeLibrary library) {
_lib = library;
man = _lib.createSportMan();
AI 代码解读
}
String getName() {
return _lib.getManName(man).cast<Utf8>().toDartString();
AI 代码解读
}
void setName(String name) {
_lib.setManName(man, name.toNativeUtf8().cast());
AI 代码解读
}
}
调用方:
AI 代码解读
SportManType m = SportManType(nativeLibrary);
m.setName('SY is a dog');
print(m.getName());
输出
AI 代码解读
SY is a dog
简单的思路就是,我们先定义class,然后使用C的函数来操作这个class,然后使用Dart来操作这些函数就能达到Dart对C++类的操作。我这里还做了一些特殊的判断,主要是将sample.h做成C和C++两种编译器都可编译的代码,能兼容`ffigen`自动生成代码。
## 异步
看到ffi异步,我一下就想到一个思路,先在Dart侧建立一个函数,然后通过ffi传入C/C++侧,C/C++将其传入到线程中,然后线程完成后调用该函数,这样不就可以达到C/C++异步方法的调用吗。我去实战了一下,结果报了下面的错误:
AI 代码解读
Cannot invoke native callback outside an isolate.
熟悉Flutter isolate的人可能知道,isolate的原理就是使用C/C++线程实现的,不过多加了一个限制——无法内存共享,所以传入的在dart的线程中的callBack无法在另一个线程调用。
那么怎么办,Dart官方自然知道有这个问题,所以也出了解决方案,[#37022](https://github.com/dart-lang/sdk/issues/37022),[ffi_test_functions_vmspecific.cc](https://github1s.com/dart-lang/sdk/blob/master/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc),其原理跟isolate的SendPort是一样的,只是其也提供了C代码的封装。
我按照开发思路,讲解一下其使用的步骤。
首先我们需要引入Dart为我们准备的代码,一般位于`${Dart SDK路径}/include/`文件夹下,我们可以把这些代码复制粘贴到自己的C代码工程中。然后修改一下CMakeList.txt文件(我在C代码工程中新建了个include文件夹存放Dart API代码)
AI 代码解读
1. 在LANGUAGES后面加上C,因为Dart API代码是C写的
project(sample VERSION 1.0.0 LANGUAGES CXX C)
2. add_library添加dart_api_dl.h和dart_api_dl.c文件
add_library(sample SHARED sample.cc sample.def include/dart_api_dl.h include/dart_api_dl.c)
在`sample.c`文件中添加几个函数。
AI 代码解读
DART_EXPORT intptr_t InitDartApiDL(void *data)
{
return Dart_InitializeApiDL(data);
AI 代码解读
}
`InitDartApiDL`用于Dart API相关代码的初始化。
AI 代码解读
Dart_Port send_port_;
DART_EXPORT void registerSendPort(Dart_Port send_port)
{
localPrint("设置send port");
send_port_ = send_port;
AI 代码解读
}
`registerSendPort`用于接收Dart传过来的`Port`并存入内存
AI 代码解读
DART_EXPORT void executeCallback(VoidCallbackFunc callback) {
localPrint("执行dart返回的函数,线程: (%p)\n", pthread_self());
callback();
AI 代码解读
}
`executeCallback`函数其实一开始可能不好理解,它其实没啥用,只是Dart侧监听的`Port`接受到的值是一个C的内存地址,Dart侧无法执行,所以需要传给你C/C++来执行。 好了,现在来设置Dart相关代码 binding.dart,跟C接口层代码
AI 代码解读
class NativeLibrary {
//....
/// 初始化dart_api_dl相关数据
int InitDartApiDL(
ffi.Pointer<ffi.Void> data,
AI 代码解读
) {
return _InitDartApiDL(
data,
);
AI 代码解读
}
late final _InitDartApiDLPtr =
_lookup<ffi.NativeFunction<ffi.IntPtr Function(ffi.Pointer<ffi.Void>)>>(
'InitDartApiDL');
AI 代码解读
late final _InitDartApiDL =
_InitDartApiDLPtr.asFunction<int Function(ffi.Pointer<ffi.Void>)>();
AI 代码解读
/// 将dart send port传递到C/C++内存缓存起来
void registerSendPort(
int send_port,
AI 代码解读
) {
return _registerSendPort(
send_port,
);
AI 代码解读
}
late final _registerSendPortPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(Dart_Port)>>(
'registerSendPort');
AI 代码解读
late final _registerSendPort =
_registerSendPortPtr.asFunction<void Function(int)>();
AI 代码解读
/// 执行一个异步无返回值的异步函数
void nativeAsyncCallback(
VoidCallbackFunc callback,
AI 代码解读
) {
return _nativeAsyncCallback(
callback,
);
AI 代码解读
}
/// 执行dart传递回来的地址函数
void executeCallback(
VoidCallbackFunc callback,
AI 代码解读
) {
return _executeCallback(
callback,
);
AI 代码解读
}
late final _executeCallbackPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(VoidCallbackFunc)>>(
'executeCallback');
AI 代码解读
late final _executeCallback =
_executeCallbackPtr.asFunction<void Function(VoidCallbackFunc)>();
AI 代码解读
//...
}
ffi_sample.dart
AI 代码解读
ReceivePort _receivePort = ReceivePort();
void _handleNativeMessage(dynamic message) {
print('_handleNativeMessage $message');
final int address = message;
nativeLibrary.executeCallback(Pointer.fromAddress(address).cast());
/// 如果执行完成,需要将其close,不一定是放到这里
_receivePort.close();
}
void ensureNativeInitialized() {
var nativeInited =
nativeLibrary.InitDartApiDL(NativeApi.initializeApiDLData);
AI 代码解读
assert(nativeInited == 0, 'DART_API_DL_MAJOR_VERSION != 2');
_receivePort.listen(_handleNativeMessage);
nativeLibrary.registerSendPort(_receivePort.sendPort.nativePort);
}
`_handleNativeMessage`是`Port`监听后的回调函数,用于接收数据,其中会把收到的数据调用`executeCallback`交给C去执行,`ensureNativeInitialized`用于初始化一些必要代码,添加`Port`监听,及将`Port`的Native形式传给C层。 现在所有程序可以说是准备就绪了,其实这里简单点写是可以将所有需要传给你C层的数据用一个函数一次性传给C,我这里这样写一是可以将思路理清楚,二也是提供一个复用`Port`的思路,不需要每次设置`Port`。 我们现在来定义一个`nativeAsyncCallback`函数,用于在C语言中使用线程执行一些操作 sample.cc
AI 代码解读
DART_EXPORT void nativeAsyncCallback(VoidCallbackFunc callback)
{
localPrint("主线程: (%p)\n", pthread_self());
pthread_t callback_thread;
int ret = pthread_create(&callback_thread, NULL, thread_func, (void *)callback);
if (ret != 0)
{
localPrint("线程内部错误: error_code=%d", ret);
}
AI 代码解读
}
binding.dart
AI 代码解读
class NativeLibrary {
// ...
/// 执行一个异步无返回值的异步函数
void nativeAsyncCallback(
VoidCallbackFunc callback,
AI 代码解读
) {
return _nativeAsyncCallback(
callback,
);
AI 代码解读
}
late final _nativeAsyncCallbackPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(VoidCallbackFunc)>>(
'nativeAsyncCallback');
AI 代码解读
late final _nativeAsyncCallback =
_nativeAsyncCallbackPtr.asFunction<void Function(VoidCallbackFunc)>();
//...
AI 代码解读
}
ffi_sample.dart
AI 代码解读
void asyncCallback() {
print('asyncCallback called');
}
main() {
ensureNativeInitialized();
var asyncFunc = Pointer.fromFunction(asyncCallback);
nativeLibrary.nativeAsyncCallback(asyncFunc);
}
最后执行函数,输出
AI 代码解读
_handleNativeMessage 4450988052
asyncCallback called
## ffigen
对于某些写好的三方库,我们一个一个写dart binding函数是一件乏味而枯燥还容易出错的事情,所以这里我使用了上面提到的[`ffigen`](https://pub.dev/packages/ffigen)库来根据C/C++头文件自动生成dart binding函数。
我们需要在`pubspec.yaml`中引入该库
AI 代码解读
dev_dependencies:
ffigen: ^4.1.0
然后执行`pub get` 我们还需要在`pubspec.yaml`中配置一些信息
AI 代码解读
ffigen:
output: 'bin/bindings.dart' # 输出到bin/bindings.dart文件中
name: 'NativeLibrary' # 输出类名为NativeLibrary
description: 'demo' # 描述,随意写
headers:
entry-points: # 配置需要生成dart binding函数的头文件,可以是多个
- 'library/sample.h'
include-directives: # 保证只转换sample.h文件 不转换其包含的如stdint.h文件
- 'library/sample.h'
AI 代码解读
这样经过我们简单的配置,就可以在命令行中执行`dart run ffigen`来生成dart binding相关代码了。我们只需要简单的初始化,就可以很方便的使用了。
AI 代码解读
import 'dart:ffi' as ffi;
main() {
var libraryPath = path.join(
Directory.current.path, 'library', 'build', 'libsample.dylib');
AI 代码解读
final dylib = ffi.DynamicLibrary.open(libraryPath);
nativeLibrary = NativeLibrary(dylib);
nativeLibrary.hello_world();// 调用C++中的hello_world函数
}
注意:
1. `ffigen`只能自动生成C风格的头文件,如果你的头文件中包含了C++风格代码如class,需要使用#ifdef __cplusplus #endif包裹起来
因为dart与C/C++是两种语言,所以它们也一定会或多或少有一些兼容问题,所以对于某些复杂的库,可能还需要更多的ffigen配置才可以很好的转换。我对于ffigen目前使用还不多,大家也可以看[ffigen](https://pub.dev/packages/ffigen)文档获取更多信息。
上面代码我都提交到我的Github仓库中,[GitHub传送门](https://github.com/xuzhongpeng/ffi_sample),如果有对你帮助也请不要吝啬你的star
参考资料:
1. [使用 dart:ffi 与 C 进行交互](https://dart.cn/guides/libraries/c-interop)([英文版](https://dart.dev/guides/libraries/c-interop))
2. [Binding to native code using dart:ffi](https://docs.flutter.dev/development/platform-integration/c-interop)
3. [使用cmake构建C/C++项目和动态库](https://juejin.cn/post/6932110161469407246)
4. [C Wrappers for C++ Libraries and Interoperability](https://caiorss.github.io/C-Cpp-Notes/CwrapperToQtLibrary.html)
5. [Calling Native Libraries in Flutter with Dart FFI](https://www.raywenderlich.com/21512310-calling-native-libraries-in-flutter-with-dart-ffi)
AI 代码解读