开发者社区> 永无岛> 正文

一起玩玩Dart FFI

简介: ATA处女篇,如果对您有帮助,请狠狠的点赞 ## 是什么 `Dart FFI`([官方地址](https://dart.dev/guides/libraries/c-interop))是可以在Dart Native平台上运行的Dart移动、命令行和服务器应用上通过`Dart FFI`来调用C代码的一个技术。简单来说,就是Dart与C互相调用的一种机制。`Dart FFI`是Dart2.12.0版
+关注继续查看

ATA处女篇,如果对您有帮助,请狠狠的点赞

是什么

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 中定义的NativeTypeC语言中的类型说明
Opaqueopaque不暴露其成员类型,一般用于表示C++中的类
Int8int8_t 或 char有符号8位整数
Int16int16_t 或 short有符号16位整数
Int32int32_t 或 int有符号32位整数
Int64int64_t 或 long long有符号64位整数
Uint8uint8_t 或 unsigned char无符号8位整数
Uint16uint16_t 或 unsigned short无符号16位整数
Uint32int32_t 或 unsigned int无符号32位整数
Uint64uint64_t 或 unsigned long long无符号64位整数
IntPtrint*整数类型指针
Floatfloat单精度浮点类型
Doubledouble双精度浮点类型
Voidvoidvoid类型
HandleDart_Handle Dart句柄在C中的表示形式
NativeFunction函数函数类型
Structstruct结构体类型
Unionunion共同体类型
Pointer*指针类型
nullptrNULL空指针
dynamicDart_CObjectDart对象在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 初始化一个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. 共同体使用跟结构体大致类似,具体就查看[示例](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++编译器

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++代码的条件

// 因为本测试设计到了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官方自然知道有这个问题,所以也出了解决方案,[#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代码)

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.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);
}

`_handleNativeMessage`是`Port`监听后的回调函数,用于接收数据,其中会把收到的数据调用`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(asyncCallback);
nativeLibrary.nativeAsyncCallback(asyncFunc);
}


最后执行函数,输出

_handleNativeMessage 4450988052

asyncCallback called

## ffigen
对于某些写好的三方库,我们一个一个写dart binding函数是一件乏味而枯燥还容易出错的事情,所以这里我使用了上面提到的[`ffigen`](https://pub.dev/packages/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](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)

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
9525 0
使用SSH远程登录阿里云ECS服务器
远程连接服务器以及配置环境
13176 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
20205 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
33173 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
18472 0
使用NAT网关轻松为单台云服务器设置多个公网IP
在应用中,有时会遇到用户询问如何使单台云服务器具备多个公网IP的问题。 具体如何操作呢,有了NAT网关这个也不是难题。
34978 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
24979 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
14700 0
阿里云服务器ECS登录用户名是什么?系统不同默认账号也不同
阿里云服务器Windows系统默认用户名administrator,Linux镜像服务器用户名root
13870 0
+关注
1
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载