如何使用Dart FFI看这篇文章就够了

是什么

Dart FFI(官方地址)是可以在Dart Native平台上运行的Dart移动、命令行和服务器应用上通过Dart FFI来调用C代码的一个技术。简单来说,就是Dart与C互相调用的一种机制。Dart FFI是Dart2.12.0版本后(同时包含在 Flutter 2.0 和以后的版本里),才作为稳定版本发布。

说到底,Dart语言也是因为Flutter使用了它才火起来的,所以Dart FFI技术在Flutter应用中更能发挥它更强大的作用

解决的问题

  1. 可以同步调用C API,不像Flutter Channel一开始就是异步
  2. 调用C语言更快,不像之前需要通过Native中转(或者改Flutter引擎代码)
  3. 还可以封装替换Flutter Channel达到更快和支持同步的目地(有人做了Flutter Platform Channel和FFI通道性能测试,点这里查看)

简单使用

为了只看FFI的特性,我先不在Flutter平台上使用,仅仅用命令行Dart应用的方式来讲解。 本人工程环境:

运行环境 MacOS 12.0.1

GCC 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环境,创建librarylibrary/build文件夹

4). 创建library/sample.clibrary/sample.hlibrary/sample.defCMakeLists.txt文件

目录结构如下

|_ bin
    |_ ffi_sample.dart
|_ library
    |_ build
    |_ CMakeLists.txt
    |_ sample.c
    |_ sample.h
    |_ sample.def
|_ pubspec.yaml
复制代码

2. pubspec.yaml引入FFI

pubspec.yaml文件中的dependencies中加入ffipath

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
复制代码

3. 编译C代码

sample.h中写简单的一个函数

sample.h

void hello_world();
复制代码

sample.c中实现

sample.c

#include <stdio.h>
#include <stdlib.h>
#include "sample.h"
void hello_world()
{
    printf("Hello World\n");
}
复制代码

sample.def中简单导出

LIBRARY   sample
EXPORTS
   sample
复制代码

用于测试C代码的main文件main.cc

#include <stdio.h>
#include "sample.h"
int main()
{
    printf("测试");
    return 0;
}
复制代码

写编译使用的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)
复制代码

3. 编译C文件

现在所有文件都准备就绪,就可以编译C代码了。

1). 命令行进入到library/build文件夹下

2). 执行cmake ..生成编译所需文件

3). 执行make编译

cd library/build
cmake ..
make
复制代码

如果在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();
}
复制代码

5. 运行代码

现在,在命令行的项目根目录下运行

dart run
复制代码

如果输出

Hello World
复制代码

好,简单的Demo就跑起来了。

由于ffi部分API跟已有Framework的API名称重合,所以后面代码我所有用到ffi的地方都加了ffi前缀。

import 'dart:ffi' as ffi;
复制代码

常用属性与方法介绍

为了联通Dart与C语言,Dart FFI提供了很多方法,下面我来介绍一下主要的方法。

DynamicLibrary.open

它可以加载动态链接库

external factory DynamicLibrary.open(String path);
复制代码

此方法用于加载库文件,如上面我编译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);
复制代码

DynamicLibrary.process

external factory DynamicLibrary.process();
复制代码

它可以用于在iOS及MacOS中加载应用程序已经自动加载好的动态链接库,也可以解析静态链接到应用的二进制文件符号。需要注意的是,它不能用于windows平台

DynamicLibrary.executable

external factory DynamicLibrary.executable();
复制代码

它可用于加载静态链接库

NativeType

NativeType是在Dart中表示C语言中的数据结构(想了解有哪些NativeType可以直接跳转到『Dart FFI与C基础数据类型映射表』目录)。它不可在Dart中实例化,只能由Native返回。

Pointer

它是C语言中指针在Dart中的映射

DynamicLibrary->lookup()

external Pointer<T> lookup<T extends NativeType>(String symbolName);
复制代码

它用于在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();
复制代码

Pointer.fromAddress(int ptr)

根据内存地址获取C对象指针

例如:

// 创建一个指向NULL的Native指针
final Pointer<Never> nullptr = Pointer.fromAddress(0);
复制代码

Pointer.fromFunction

根据一个Dart函数,创建一个Native函数指针,一般用于将Dart函数传给C,使C有调用Dart函数的能力

void globalCallback(int src, int result) {
   print("globalCallback src=$src, result=$result");
}
Pointer.fromFunction(globalCallback);
复制代码

Pointer->address()

获取指针的内存地址

asFunction

将Native指针对象,转换为Dart函数

sizeOf

返回具体类型的内存占用

ffi.sizeOf<ffi.Int64>(); // 8
复制代码

malloc.allocate()

Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment});
复制代码

开辟一块大小byteCount的空间

Pointer<Uint8> bytes = malloc.allocate<Uint8>(ffi.sizeOf<ffi.Uint8>());
复制代码

malloc.free

释放内存

malloc.free(bytes);
复制代码

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;
复制代码

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()}");
复制代码

结果输出

*************** 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=修改一下
复制代码

由于我想让程序能更简单调用,我对每个函数添加了getset方法。 上面的示例基本上只展示了数字类型转换,基本上还算简单,按照上表数据结构对应转换就不会出错。

细心的朋友可能已经发现了,上面的字符串是比较特殊,需要一层转换。C语言中的char*需要用ffi.Pointer<ffi.Int8>去接收,我们可以拿到这个指针,然后转换成Utf8格式,需要说明的是Utf8ffi库下的一个类型(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()
复制代码

我们还可以通过 '这是Dart字符串'.toNativeUtf8().cast<ffi.Int8>()将Dart字符串转换成C的char*

在Dart与C的交互中,函数调用应该是最常见的场景。下面我们就来看看如何在Dart中调用C的函数,同时也能在C中调用Dart的函数。

Dart调C

无传参无返回值

我们通过一个例子,让Dart来调用C的函数,并在C的函数中输出一句话。

sample.h

void hello_world();
复制代码

sample.c

void hello_world()
{
    printf("[CPP]: Hello World");
}
复制代码

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()}');
复制代码

结果输出

[CPP]: Hello World
[Dart]: null
复制代码

有返回值

当C有返回值时,可以通过类型转换接收 sample.h

char* getName();
复制代码

sample.c

char* getName()
{
    return "My name is 大哥大";
}
复制代码

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());
复制代码

输出结果:

[Dart]: 有返回值 -> My name is 大哥大
复制代码

有传参

利用C的printf函数,实现一个Dart打印函数

sample.h

void cPrint(char *str);
复制代码

sample.c

void cPrint(char *str) 
{
    printf("[CPP]: %s", str);
    free(str);
}
复制代码

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>());
复制代码

输出

[CPP]: 我认为这个输出很有意义
复制代码

这样就实现了一个输出函数了。

C调Dart函数

我们知道了Dart如何调用C的函数,下面我们通过示例来了解一下C如何调用Dart函数。

简单示例

原理: C本身是没有提供调用Dart函数的方法的,但是我们可以在程序启动后通过Dart将函数当做参数传入C中,C中缓存起来Dart的函数指针,就可以在需要的时候实现C调用Dart。

首先,我们先在Dart上定义一个函数。需要注意的是Dart函数需要是顶级函数或者静态函数才能被调用,否则会报错.

void dartFunction() {
  debugPrint("[Dart]: Dart 函数被调用了");
}
复制代码

我们在C中定义一个注册函数 sample.h

void callDart(void (*callback)());
复制代码

sample.c

void callDart(void (*callback)()) {
    printf("[CPP]: 现在调用Dart函数");
    callback();
}
复制代码

其中的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));
复制代码

这里,我们试用结果ffi.Pointer.fromFunction方法将Dart函数转换成C函数指针的Dart映射,然后通过_callDart来调用C的callDart函数。

运行后输出:

[CPP]: 现在调用Dart函数
[Dart]: Dart 函数被调用了
复制代码

成功!

带参数的Dart函数

C如何调用带参数的Dart函数呢,我们下面来定义一个Dart函数

static void add(int num1,int num2) {
    print("[Dart]: num1: ${num1}, num2: ${num2}");
}
复制代码

上面函数被调用后会输出num1num2的值。

然后我们改造一下callDart函数 sample.h

void callDart(void (*callback)(), void (*add)(int, int));
复制代码

sample.c

void callDart(void (*callback)(), void (*add)(int, int)) {
    printf("现在调用Dart函数");
    callback();

    printf("调用Dart Add函数");
    add(1, 2);
}
复制代码

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));
复制代码

返回输出

[CPP]: 现在调用Dart函数
[Dart]: Dart 方法被调用了
[CPP]: 调用Dart Add函数
[Dart]: num1: 1, num2: 2
复制代码

这样,参数就从C传到Dart端了。

获取返回值

上面的示例都只是调用Dart函数,并没有从Dart端获取返回值。我们再来改造一下add方法,让它可以返回num1 num2相加的值。

static int add(int num1, int num2) {
    return num1 + num2;
}
复制代码

sample.h

void callDart(void (*callback)(), int (*add)(int, int));
复制代码

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);
}
复制代码

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));
复制代码

需要注意的是,如果Dart函数有返回值,fromFunction的第二个参数就需要传入当出错时返回的值。

输出结果

[CPP]: 现在调用Dart函数
[Dart]: Dart 方法被调用了
[CPP]: 调用Dart Add函数
[Dart]: num1: 1, num2: 2
[CPP]: Add 结果 3
复制代码

好了,现在我们就学会了如何使用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;
复制代码

bindings.dart

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

  @ffi.Int32()
  external int age;

  @ffi.Float()
  external double score;
}
复制代码

这样,我们就在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;
}
复制代码

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
```dart
// 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}");
// Dart String类型转成C的Utf8类型后,需要free,不然会内存泄露
malloc.free(name);
复制代码

一切准备就绪后,运行ffi_sample.dart,输出

姓名:幺风舞 ,年龄:25 , 分数:100.0
复制代码

注意:

  1. Struct不能在Dart中初始化
  2. 如果是指针类型的结构体,ffi扩展了其方法,可以通过ref来访问结构体具体值。
  3. 共同体使用跟结构体大致类似,具体就查看示例

Dart FFI本身只能只能对接C接口,但是如果我们遇到C++的类怎么处理呢,这节我来讲解一下我自己的思路。

项目改造

因为之前的项目我都是使用C编译器编译的,由于这里添加了C++的类,需要使用C++来编译了,而我一直使用的ffigen这个库来自动根据C header生成Dart代码,这个ffigen底层是使用C编译器来实现的,所以对原来代码有一定改造。

  1. 将sample.c重命名成sample.cc
  2. CMakeLists.txt改成使用C++编译器
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了
复制代码
  1. sample.h中添加能同时编译C和C++代码的条件
// 因为本测试设计到了C++的类(用的C++编译的),所以需要把函数都通过extern "C"导出让ffi识别
#ifdef __cplusplus
  #define EXPORT extern "C"
#else
  #define EXPORT // ffigen生成时,会使用C编译器,所以改成空即可
#endif
复制代码

其它之前定义的函数都需要使用EXPORT来修饰一下,如

EXPORT void hello_world();
复制代码

当使用C++的风格代码时,需要使用#ifdef __cplusplus包裹起来,这样项目改造就完成了。

C++类的映射

在sample.h中添加一个简单的类

#ifdef __cplusplus 
class SportManType
{
  const char *name; //名称
public:
  void setName(const char *str)
  {
    name = str;
  }
  const char *getName()
  {
    return name;
  }
};
#endif
复制代码

由于Dart FFI是获取不到C++风格的符号的,所以我们需要使用C风格函数来操作类。

EXPORT typedef void* SportMan; // 定义一个SportManType类在C中的映射类型

EXPORT SportMan createSportMan(); // 初始化SportManType类
EXPORT void setManName(SportMan self,const char *name); // 设置姓名
EXPORT const char *getManName(SportMan self); // 获取姓名
复制代码

然后实现对应函数

SportMan createSportMan()
{
    return new SportManType();
}
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();
}
复制代码

我们可以使用reinterpret_cast来将传入的SportMan类型转成SportManType类型,然后直接操作类。

现在我们可以C++代码的改造就完成了,下面我们来写Dart代码。

FFI符号连接代码:

class NativeLibrary {
  // ...
  /// 初始化一个类
  SportMan createSportMan() {
    return _createSportMan();
  }

  late final _createSportManPtr =
      _lookup<ffi.NativeFunction<SportMan Function()>>('createSportMan');
  late final _createSportMan =
      _createSportManPtr.asFunction<SportMan Function()>();
  /// 设置姓名
  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>)>();
  /// 获取姓名
  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)>();
}
复制代码

然后来操作调用一下:

//...
SportMan man = nativeLibrary.createSportMan();
nativeLibrary.setManName(man, "SY".toNativeUtf8().cast());
print(
    "运动员名称:" + nativeLibrary.getManName(man).cast<Utf8>().toDartString());
复制代码

输出: 运动员名称:SY

这样,我们就能通过Dart间接操作C++中的类了,可能有人说这样写太抽象,不方便使用,那我们再使用Dart类在对其包装一下。

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(String name) {
    _lib.setManName(man, name.toNativeUtf8().cast());
  }
}
复制代码

调用方:

SportManType m = SportManType(nativeLibrary);
m.setName('SY is a dog');
print(m.getName());
复制代码

输出

SY is a dog
复制代码

简单的思路就是,我们先定义class,然后使用C的函数来操作这个class,然后使用Dart来操作这些函数就能达到Dart对C++类的操作。我这里还做了一些特殊的判断,主要是将sample.h做成C和C++两种编译器都可编译的代码,能兼容ffigen自动生成代码。

异步

看到ffi异步,我一下就想到一个思路,先在Dart侧建立一个函数,然后通过ffi传入C/C++侧,C/C++将其传入到线程中,然后线程完成后调用该函数,这样不就可以达到C/C++异步方法的调用吗。我去实战了一下,结果报了下面的错误:

Cannot invoke native callback outside an isolate.
复制代码

熟悉Flutter isolate的人可能知道,isolate的原理就是使用C/C++线程实现的,不过多加了一个限制——无法内存共享,所以传入的在dart的线程中的callBack无法在另一个线程调用。

那么怎么办,Dart官方自然知道有这个问题,所以也出了解决方案,#37022ffi_test_functions_vmspecific.cc,其原理跟isolate的SendPort是一样的,只是其也提供了C代码的封装。

我按照开发思路,讲解一下其使用的步骤。

首先我们需要引入Dart为我们准备的代码,一般位于${Dart SDK路径}/include/文件夹下,我们可以把这些代码复制粘贴到自己的C代码工程中。然后修改一下CMakeList.txt文件(我在C代码工程中新建了个include文件夹存放Dart API代码)

#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文件中添加几个函数。

DART_EXPORT intptr_t InitDartApiDL(void *data)
{
    return Dart_InitializeApiDL(data);
}
复制代码

InitDartApiDL用于Dart API相关代码的初始化。

Dart_Port send_port_;
DART_EXPORT void registerSendPort(Dart_Port send_port)
{
    localPrint("设置send port");
    send_port_ = send_port;
}
复制代码

registerSendPort用于接收Dart传过来的Port并存入内存

DART_EXPORT void executeCallback(VoidCallbackFunc callback) {
    localPrint("执行dart返回的函数,线程: (%p)\n", pthread_self());
    callback();
}
复制代码

executeCallback函数其实一开始可能不好理解,它其实没啥用,只是Dart侧监听的Port接受到的值是一个C的内存地址,Dart侧无法执行,所以需要传给你C/C++来执行。

好了,现在来设置Dart相关代码

binding.dart,跟C接口层代码

class NativeLibrary {
  //....
/// 初始化dart_api_dl相关数据
  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传递到C/C++内存缓存起来
  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)>();

  /// 执行一个异步无返回值的异步函数
  void nativeAsyncCallback(
    VoidCallbackFunc callback,
  ) {
    return _nativeAsyncCallback(
      callback,
    );
  }
  /// 执行dart传递回来的地址函数
  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)>();
  //...
}
复制代码

ffi_sample.dart

ReceivePort _receivePort = ReceivePort();
void _handleNativeMessage(dynamic message) {
  print('_handleNativeMessage $message');
  final int address = message;
  nativeLibrary.executeCallback(Pointer<Void>.fromAddress(address).cast());
  /// 如果执行完成,需要将其close,不一定是放到这里
  _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);
}
复制代码

_handleNativeMessagePort监听后的回调函数,用于接收数据,其中会把收到的数据调用executeCallback交给C去执行,ensureNativeInitialized用于初始化一些必要代码,添加Port监听,及将Port的Native形式传给C层。

现在所有程序可以说是准备就绪了,其实这里简单点写是可以将所有需要传给你C层的数据用一个函数一次性传给C,我这里这样写一是可以将思路理清楚,二也是提供一个复用Port的思路,不需要每次设置Port

我们现在来定义一个nativeAsyncCallback函数,用于在C语言中使用线程执行一些操作 sample.cc

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);
    }
}
复制代码

binding.dart

class NativeLibrary {
  // ...
  /// 执行一个异步无返回值的异步函数
  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)>();
      //...
}
复制代码

ffi_sample.dart

void asyncCallback() {
  print('asyncCallback called');
}
main() {
  ensureNativeInitialized();
  var asyncFunc = Pointer.fromFunction<NativeAsyncCallbackFunc>(asyncCallback);
  nativeLibrary.nativeAsyncCallback(asyncFunc);
}
复制代码

最后执行函数,输出

[CPP]: 初始化InitDartApiDL
[CPP]: 设置send port
[CPP]: 主线程: (0x700008108000)
[CPP]: 主线程: (0x700008108000)
[CPP]: 异步线程: (0x70000818b000)
[CPP]: 异步线程: (0x70000820e000)
_handleNativeMessage 4450988052
[CPP]: 执行dart返回的函数,线程: (0x700008108000)
asyncCallback called
复制代码

ffigen

对于某些写好的三方库,我们一个一个写dart binding函数是一件乏味而枯燥还容易出错的事情,所以这里我使用了上面提到的ffigen库来根据C/C++头文件自动生成dart binding函数。

我们需要在pubspec.yaml中引入该库

dev_dependencies:
  ffigen: ^4.1.0
复制代码

然后执行pub get

我们还需要在pubspec.yaml中配置一些信息

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'
复制代码

这样经过我们简单的配置,就可以在命令行中执行dart run ffigen来生成dart binding相关代码了。我们只需要简单的初始化,就可以很方便的使用了。

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();// 调用C++中的hello_world函数 
}
复制代码

注意:

  1. ffigen只能自动生成C风格的头文件,如果你的头文件中包含了C++风格代码如class,需要使用#ifdef __cplusplus #endif包裹起来

因为dart与C/C++是两种语言,所以它们也一定会或多或少有一些兼容问题,所以对于某些复杂的库,可能还需要更多的ffigen配置才可以很好的转换。我对于ffigen目前使用还不多,大家也可以看ffigen文档获取更多信息。

上面代码我都提交到我的Github仓库中,GitHub传送门,如果有对你帮助也请不要吝啬你的star

参考资料:

  1. 使用 dart:ffi 与 C 进行交互(英文版)
  2. Binding to native code using dart:ffi
  3. 使用cmake构建C/C++项目和动态库
  4. C Wrappers for C++ Libraries and Interoperability
  5. Calling Native Libraries in Flutter with Dart FFI
  6. dart:ffi同步/异步调用指南

猜你喜欢

转载自juejin.im/post/7055306930507497485