使用 dart:ffi 在 macOS 中调用动态库代码

1,323 阅读2分钟

准备macos的动态库

  • 使用Xcode创建动态库
    • File - New - Project - macOS - Library - Tpye: Dynamic
    • 输入动态库名字 点击下一步即可
  • 使用C语言编写,不能有OC的任何代码
  • 动态库如下 test 是你输入的名字,前面的lib是系统自动加的。

image.png

编写c语言代码

  • 头文件代码

image.png

  • c代码

image.png

编译动态库

  • 使用Xcode编译动态库代码
  • 然后导出动态库文件准备着
  • Copy动态库文件到/Users/apple/Library/Containers/com.example.xxx/Data/
  • apple换成你电脑的用户名
  • com.example.xxx是你应用的包名(可以打开macos的工程去查看)
  • 最终的文件路径为/Users/apple/Library/Containers/com.example.xxx/Data/libtest.dylib
  • 为什么把动态库Copy到这个目录?
  • 因为你的Mac程序在不做任何配置的情况下,能访问到这个路径。放到电脑的其他路径可能没有运行权限。

准备flutter项目

  • 创建Flutter项目
    • flutter create demo
  • 我的Flutter版本为Flutter 3.3.5 Tools • Dart 2.18.2 • DevTools 2.15.0

编写dart代码

  • 打开动态库
import 'dart:ffi' as ffi;

var libraryPath = "/Users/apple/Library/Containers/com.example.xxx/Data/libtest.dylib";
final dylib = ffi.DynamicLibrary.open(libraryPath);
  • 为 C 函数的 FFI 类型签名的定义一个类型 和 调用 C 函数的变量定义一个类型
// 同步函数
typedef SumFunc = Int32 Function(Int32 a, Int32 b);
typedef Sum = int Function(int a, int b);

// 异步函数 带有callback回调
typedef DartCallback = ffi.Void Function(ffi.Int,ffi.Pointer<ffi.Char>);
typedef CallbackNative = Int Function(Pointer<NativeFunction<DartCallback>>);
typedef CallbackSandbox = int Function(Pointer<NativeFunction<DartCallback>>);

参考资料:dart.cn/guides/libr…

  • 调用动态库的代码

同步函数调用

final sumPointer = dylib.lookup<NativeFunction<SumFunc>>('sum'); // 这里的sum是C语言动态库的函数名字
final sum = sumPointer.asFunction<Sum>();
print('3 + 5 = ${sum(3, 5)}');

异步函数调用

CallbackSandbox callback = dylib.lookupFunction<CallbackNative, CallbackSandbox>('hellCallback');
final value = callback(Pointer.fromFunction(_callback)); // 回调函数看下面的说明
print("value = $value");

回调函数定义

  • 要求回调函数为静态函数或者全局函数
  • 不可以直接定义到class
void _callback(int a, Pointer<ffi.Char> msg) {
  print("--------");
  print("$a - $b - ${msg.toDartString()}");
  ;
}
  • 整体的代码
import 'dart:ffi' as ffi;
import 'dart:ffi';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as path;

typedef SumFunc = Int32 Function(Int32 a, Int32 b);
typedef Sum = int Function(int a, int b);

typedef DartCallback = ffi.Void Function(ffi.Int, ffi.Int, ffi.Pointer<ffi.Char>);
typedef CallbackNative = Int Function(Pointer<NativeFunction<DartCallback>>);
typedef CallbackSandbox = int Function(Pointer<NativeFunction<DartCallback>>);

// 静态函数或者全局函数
void _callback(int a, int b, Pointer<ffi.Char> msg) {
  print("--------");
  print("$a - $b - ${msg.toDartString()}");
  ;
}

class Setting extends StatefulWidget {
  Setting({Key? key}) : super(key: key);

  @override
  _SettingState createState() => _SettingState();
}

class _SettingState extends State<Setting> {
  void _ffiButtonClick() {
    if (Platform.isMacOS) {
      var libraryPath = path.join(Directory.current.path, 'libMacBoxSdk.dylib');
      final dylib = ffi.DynamicLibrary.open(libraryPath);
      final sumPointer = dylib.lookup<NativeFunction<SumFunc>>('sum');
      final sum = sumPointer.asFunction<Sum>();
      print('3 + 5 = ${sum(3, 5)}');
    }
  }

  void _ffiAsyncButtonClick() {
    var libraryPath = path.join(Directory.current.path, 'libMacBoxSdk.dylib');
    final dylib = ffi.DynamicLibrary.open(libraryPath);
    CallbackSandbox callback = dylib.lookupFunction<CallbackNative, CallbackSandbox>('hellCallback');
    final value = callback(Pointer.fromFunction(_callback));
    print("value = $value");
  }

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 10,
      children: [
        ElevatedButton(
          child: Text('ffi同步通信'),
          onPressed: _ffiButtonClick,
        ),
        ElevatedButton(
          child: Text('ffi异步通信'),
          onPressed: _ffiAsyncButtonClick,
        )
      ],
    );
  }
}

进阶

动态库位置优化

  • 按照下图操作

image.png

参考链接:flutter.cn/docs/develo…

动态库位置更改后的加载方式

  • 先open整个生命周期可以只执行一次
final dylib = ffi.DynamicLibrary.open('libMacBoxSdk.dylib');
  • 再调用 open 调用之后下面调用才会生效
print('5 + 5 = ${mboxSdk.sum(5, 5)}');

ffigen 使用

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0
  ffigen: ^7.2.0

ffigen:
  output: 'libtest_generated_bindings.dart' // 输出路径
  name: 'test'
  description: 'Holds bindings to test.'
  headers:
    entry-points:
      - 'lib/library/include/test.h' // 输入路径
    include-directives:
      - '**test.h'

参考链接:pub.dev/packages/ff…

ffigen 代码

import 'utf8.dart';
import 'libtest_generated_bindings.dart' as sdk;

void _ffigenButtonClick() {
  var libraryPath = path.join(Directory.current.path, 'libtest.dylib');
  final test = sdk.test(ffi.DynamicLibrary.open(libraryPath));
  print('5 + 5 = ${test.sum(5, 5)}');
}

void _ffigenAsyncButtonClick() {
  var libraryPath = path.join(Directory.current.path, 'libtest.dylib');
  final mboxSdk = sdk.MacBoxSdk(DynamicLibrary.open(libraryPath));
  final value = mboxSdk.hellCallback(Pointer.fromFunction(_callback));
  print('value = $value');
}

// 静态函数或者全局函数
void _callback(int a, int b, Pointer<ffi.Char> msg) {
  print("--------");
  print("$a - $b - ${msg.toDartString()}"); // 这里的toDartString()参考utf8.dart核心代码
  ;
}

utf8.dart核心代码

  • 主要处理 toDartString() 函数
  • C语言char *指针转Dart字符串
import 'dart:convert';
import 'dart:ffi';
import 'dart:typed_data';

import 'package:ffi/ffi.dart';

extension Utf8Pointer on Pointer<Char> {
  int get length {
    _ensureNotNullptr('length');
    final codeUnits = cast<Uint8>();
    return _length(codeUnits);
  }
  String toDartString({int? length}) {
    _ensureNotNullptr('toDartString');
    final codeUnits = cast<Uint8>();
    if (length != null) {
      RangeError.checkNotNegative(length, 'length');
    } else {
      length = _length(codeUnits);
    }
    return utf8.decode(codeUnits.asTypedList(length));
  }

  static int _length(Pointer<Uint8> codeUnits) {
    var length = 0;
    while (codeUnits[length] != 0) {
      length++;
    }
    return length;
  }

  void _ensureNotNullptr(String operation) {
    if (this == nullptr) {
      throw UnsupportedError("Operation '$operation' not allowed on a 'nullptr'.");
    }
  }
}

extension StringUtf8Pointer on String {
  Pointer<Utf8> toNativeUtf8({Allocator allocator = malloc}) {
    final units = utf8.encode(this);
    final Pointer<Uint8> result = allocator<Uint8>(units.length + 1);
    final Uint8List nativeString = result.asTypedList(units.length + 1);
    nativeString.setAll(0, units);
    nativeString[units.length] = 0;
    return result.cast();
  }
}