Flutter ffi实践录

6,611 阅读5分钟

最近琢磨着要给自己的 APP 接一个日志收集的 SDK 备用。考虑到一个问题,目前大多数开源的日志库,例如美团的 Logan 和腾讯的 XLog ,日志的存取都选择了使用 mmap 建立内存文件映射来提升读写效率和日志防丢。如果直接封装 plugin 调用 Android、iOS平台代码的话,就会出现 Flutter -> Platform -> Native 的情况。很显然,这种调用是没有必要的。那可以直接 Dart 调用 C/C++ 吗?答案是可以的。

实践了一下 Flutter 通过 ffi 包调用 native C/C++ 代码,ffi 代表 Foreign function interface (外部函数接口),入门实践 可以在 Flutter 的官方文档中找到。
我们使用 DynamicLibrary 来加载 C/C++ 编写的动态库。在 iOS 中,可以直接在源代码目录写,在Android 中则需要在 Gradle 中配置 CMakeList 。

接下来我们以接入 Logan 的 C 代码为例来实践一下,关于 Logan ,可以参考它的 github 。


image.png


基于双端不一致的考虑,我们把 C 代码放在plugin 工程中的 ios/Classes/logan  里面, 在 Android的 cmakelist 里面,会声明这个路径:

project(clogger)

add_library( logan

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
        ../ios/Classes/cloger_logan.c)
set(EXTERN_DIR ../ios/Classes/logan)

# 下面部分是动态链接 cloagn 本身的代码:
    
add_subdirectory(${EXTERN_DIR} clogan.out)

include_directories(${EXTERN_DIR} logg)
link_directories(clogan.out)

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log )

target_link_libraries(logan ${log-lib} z clogan)


而C 的最终实现则放在 lib/clogan 下面。

关于C代码的编写和Cmakelist的构建,建议使用 Clion 这个IDE,非常的好用


接着在 Dart 端,可以加载我们的动态库:

在 Android 中最终是以 so 库的形式来动态链接的。所以加载native 功能的实现需要区分一下平台:

static final DynamicLibrary nativeLogLib = Platform.isAndroid
      ? DynamicLibrary.open("liblogan.so")
      : DynamicLibrary.process();


这里如果能正常编译运行通过,基本就没有什么问题。说明 C/C++ 编写的库也能正常加载到。我们继续实践一下:

CLogan 在读写方面最终在 C 层暴露了下面几个函数,在 clogan_core.h 里面:


int
clogan_init(const char *cache_dirs, const char *path_dirs, int max_file, const char *encrypt_key16,
            const char *encrypt_iv16);

int clogan_open(const char *pathname);

int
clogan_write(int flag, char *log, long long local_time, char *thread_name, long long thread_id,
             int is_main);

int clogan_flush(void);


可以看到这里分别对应了 mmap 的建立,写入和配置。我们在 Dart 层来做一份对应的实现。

先介绍一下 dart 是如何实现对应的 c函数调用的, DynamicLibrary  中提供了 lookup 方法来查找原生类型符号并返回它在内存中的地址。我们先看一个简单的示例,2个int类型相加:

image.png


这里最后会把 lookup 的结果转换成一个 Function,通过 Function的执行,来调用C里面的逻辑得到最终结果。注意这里 Function 里面定义的类型是:

NativeFunction<Int32 Function(Int32, Int32) 

这里的 NativeFunction 和 Int32 是什么呢?我们进 ffi 的源码可以看到:

image.png


image.png


原来 ffi 里面定义了 NativeType 来表示 C/C++ native 层的类型。看一下它的继承结构:

image.png


这里提供的全部都是基础类型。指针和结构体在 Dart 层也有封装:

class Pointer<T extends NativeType> extends NativeType {
	external int get address;
}


abstract class Struct extends NativeType {
  final Pointer<Struct> _addressOf;
  Struct() : _addressOf = nullptr;
  Struct._fromPointer(this._addressOf);
}

// 这个文件里面同时也定义了 sizeof 这个方法,对应C的sizeOf
external int sizeOf<T extends NativeType>();


回到 Logan 的调用我们就会发现,int类型参数好指定 ,String 类型则不是很好指定了,如果我们直接传 uin8的point类型,需要解决2个问题:

  1. Pointer 的内存分配,毕竟到了C层,它是个指针
  2. String如何转成 Point


官方实现了一个包来替代。但是直接在 pubspec 引入会报错,所以我直接 copy 了他的代码。

封装一个 Utf8 来表示 String :

class Utf8 extends Struct {
}


把Dart的字符串转成指针:

static Pointer<Utf8> toUtf8(String string) {
    final units = utf8.encode(string);
    final Pointer<Uint8> result = allocate<Uint8>(count: units.length + 1);
    final Uint8List nativeString = result.asTypedList(units.length + 1);
    nativeString.setAll(0, units);
    nativeString[units.length] = 0;
    return result.cast();
  }


我们分析一下这段代码的实现思路

  1. 先把字符串encode成 uint8的数组
  2. 根据数据长度来分配指针的内存大小,需要分配 length + 1,因为c的字符串必须是 \0 结尾
  3. 把指针转成对应dart类型的list,然后全部赋值为0
  4. 把char* 重新cast成 Pointer<Utf8>


这里其实就是通常c代码实现放在了 Dart 层来控制。对应的C伪代码:

char *str;
str = molloac(len+1);
memset(str, 0, len+1);

allocate 里面其实也是调用了 C 的 molloac 函数:

image.png


image.png



在Dart的调用中,我们声明 Function的类型:

typedef WriteLogDart = void Function(int,Pointer<Utf8>,int,Pointer<Utf8>,int,int);
typedef WriteLog = Void Function(Int32,Pointer<Utf8>,Int64,Pointer<Utf8>,Int64,Int64);

实现 write 方法:

static void write(int flag,String log,int time,String threadName,int threadId,bool isMainThread) {
    final writeLogan = nativeLogLib.lookup<NativeFunction<WriteLog>>('writeLog');
    final write = writeLogan.asFunction<WriteLogDart>();
    write(flag, Utf8.toUtf8(log), time, Utf8.toUtf8(threadName), 
      threadId, isMainThread?0:1);
  }


我们在调用的时候,例如 String log ,也需要先转成 Utf8 在使用,否则语法并不能检测出来 String 和 Pointer<Uint8> 其实到了C层是一个东西。
相比于 Android有封装好的 JNI, ffi  相对来说还是比较麻烦的。需要自己提供内存分配和类型转换的实现。

总结


到这里 ffi 的实践就介绍完了。示例的代码我放在了自己的 github , 需要阅读的朋友可以自己去clone下来。

另外也欢迎大家关注我的公众号: 半行代码 :