2.6 FFI(Foreign Function Interface)简介

基础知识

FFI(Foreign Function Interface),即外部函数接口,是 Dart 语言提供的一种机制,允许 Dart 代码直接调用用其他语言(如 C、C++)编写的动态链接库(DLLs 或 shared libraries)中的函数,以及被其他语言调用。这使得 Dart 应用程序能够利用现有的高性能原生代码库,或者在需要与操作系统底层功能交互时提供更强大的能力。

1. FFI 的核心概念

  • dart:ffi:Dart 语言内置的 FFI 库,提供了与 C 语言兼容的类型和函数,用于加载动态库、查找函数指针、调用原生函数以及在 Dart 和 C 之间传递数据。
  • 动态链接库 (Dynamic Link Libraries):FFI 主要用于调用动态链接库中的函数。这些库通常以 .dll (Windows)、.so (Linux) 或 .dylib (macOS) 文件的形式存在。
  • 类型映射 (Type Mapping):Dart 和 C 语言的数据类型需要进行映射。dart:ffi 提供了 int8_tint16_tint32_tint64_tfloatdoublePointer 等类型,它们与 C 语言中的对应类型兼容。
  • Pointer<T>:表示 C 语言中的指针。T 是指针指向的类型。例如,Pointer<Int32> 表示一个指向 32 位整数的指针。
  • NativeFunction:用于声明 C 语言函数的 Dart 类型签名。
  • DynamicLibrary:用于加载动态链接库。

2. FFI 的基本使用步骤

  1. 加载动态库:使用 DynamicLibrary.open() 加载 .dll.so.dylib 文件。
  2. 查找函数指针:使用 lookupFunction() 方法查找原生函数的指针,并将其转换为 Dart 函数。
  3. 定义 Dart 函数签名:使用 typedef 定义 C 函数的 Dart 类型签名和 Dart 函数的类型签名。
  4. 调用原生函数:直接调用转换后的 Dart 函数。

示例:调用 C 语言的加法函数

首先,我们需要一个 C 语言的动态链接库。创建一个 add.c 文件:

c
复制代码
// add.c
#include <stdio.h>

// 导出函数,Windows 下需要 __declspec(dllexport)
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif

EXPORT int add(int a, int b) {
    return a + b;
}

编译 add.c 为动态链接库:

  • Linux/macOS: gcc -shared -o libadd.so add.c (或 libadd.dylib for macOS)
  • Windows: gcc -shared -o add.dll add.c

然后,在 Dart 中调用:

dart
复制代码
import 'dart:ffi';
import 'dart:io' show Platform;

// 1. 定义 C 函数的类型签名
typedef AddFunctionC = Int32 Function(Int32 a, Int32 b);

// 2. 定义 Dart 函数的类型签名
typedef AddFunctionDart = int Function(int a, int b);

void main() {
  // 3. 加载动态库
  final DynamicLibrary nativeLib = DynamicLibrary.open(
    Platform.isWindows ? 'add.dll' : (Platform.isMacOS ? 'libadd.dylib' : 'libadd.so'),
  );

  // 4. 查找函数指针并转换为 Dart 函数
  final AddFunctionDart add = nativeLib
      .lookup<NativeFunction<AddFunctionC>>('add')
      .asFunction();

  // 5. 调用原生函数
  int result = add(10, 20);
  print('10 + 20 = $result'); // 输出: 10 + 20 = 30
}

3. 传递复杂数据结构

FFI 不仅可以传递基本数据类型,还可以传递更复杂的数据结构,如结构体(Structs)和字符串。这通常涉及到 PointerStruct 的使用。

dart
复制代码
// 假设 C 语言中有一个结构体:
// struct Point { int x; int y; };

// 在 Dart 中定义对应的结构体
import 'dart:ffi';
import 'package:ffi/ffi.dart'; // 用于内存分配

class Point extends Struct {
  @Int32() // 映射到 C 的 int
  external int x;

  @Int32()
  external int y;
}

// 假设 C 语言中有一个函数:
// EXPORT void print_point(struct Point p) { printf("Point: (%d, %d)\n", p.x, p.y); }

// 在 Dart 中定义 C 函数的类型签名
typedef PrintPointFunctionC = Void Function(Point p);

// 在 Dart 中定义 Dart 函数的类型签名
typedef PrintPointFunctionDart = void Function(Point p);

void main() {
  // ... 加载动态库 nativeLib ...

  final PrintPointFunctionDart printPoint = nativeLib
      .lookup<NativeFunction<PrintPointFunctionC>>('print_point')
      .asFunction();

  // 分配内存并创建 Point 结构体实例
  final Pointer<Point> p = calloc<Point>();
  p.ref.x = 100;
  p.ref.y = 200;

  printPoint(p.ref); // 调用原生函数,传递结构体

  calloc.free(p); // 释放内存
}

官方文档链接

Flutter 开发中的应用案例

FFI 在 Flutter 开发中是一个高级特性,通常用于以下场景:

  • 集成现有原生库:当 Flutter 应用需要使用一个已经存在的、用 C/C++ 编写的高性能库时(例如图像处理库、加密库、科学计算库)。
  • 访问操作系统底层 API:当 Dart/Flutter 提供的 API 无法满足需求,需要直接调用操作系统提供的 C 语言 API 时。
  • 性能敏感型任务:对于某些计算密集型任务,如果 Dart 的性能无法满足要求,可以考虑使用 FFI 调用 C/C++ 代码来提高性能。

案例:使用 FFI 调用原生库进行简单的字符串反转

我们将创建一个简单的 Flutter 应用,通过 FFI 调用一个 C 语言函数来反转字符串。这需要先编写 C 代码并编译成动态库,然后 Dart/Flutter 应用通过 FFI 调用它。

步骤 1: 编写 C 语言代码 (string_utils.c)

c
复制代码
// string_utils.c
#include <string.h>
#include <stdlib.h>

#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif

// 函数:反转字符串
// 注意:这里为了简单,直接修改传入的字符串。实际应用中可能需要返回新分配的字符串。
EXPORT void reverse_string(char* str) {
    int length = strlen(str);
    int i, j;
    char temp;
    for (i = 0, j = length - 1; i < j; i++, j--) {
        temp = str[i];
        str[i] = str[j];
        str[j] = temp;
    }
}

// 函数:获取字符串长度
EXPORT int get_string_length(char* str) {
    return strlen(str);
}

步骤 2: 编译 C 代码为动态库

  • Linux: gcc -shared -o libstring_utils.so string_utils.c
  • macOS: gcc -shared -o libstring_utils.dylib string_utils.c
  • Windows: gcc -shared -o string_utils.dll string_utils.c

将编译好的动态库文件放置在 Flutter 项目的合适位置,例如 lib/src/native_libs/ 目录下,或者系统路径下。

步骤 3: Flutter 应用中集成 FFI

dart
复制代码
import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:ffi/ffi.dart'; // 用于内存分配和字符串转换

// 1. 定义 C 函数的类型签名
typedef ReverseStringC = Void Function(Pointer<Utf8> str);
typedef GetStringLengthC = Int32 Function(Pointer<Utf8> str);

// 2. 定义 Dart 函数的类型签名
typedef ReverseStringDart = void Function(Pointer<Utf8> str);
typedef GetStringLengthDart = int Function(Pointer<Utf8> str);

// 获取动态库路径
String _getLibraryPath() {
  if (Platform.isWindows) {
    return 'string_utils.dll';
  } else if (Platform.isMacOS) {
    return 'libstring_utils.dylib';
  } else if (Platform.isLinux) {
    return 'libstring_utils.so';
  } else {
    throw UnsupportedError('Unknown platform');
  }
}

class FFIExampleScreen extends StatefulWidget {
  const FFIExampleScreen({super.key});

  @override
  State<FFIExampleScreen> createState() => _FFIExampleScreenState();
}

class _FFIExampleScreenState extends State<FFIExampleScreen> {
  late DynamicLibrary _nativeLib;
  late ReverseStringDart _reverseString;
  late GetStringLengthDart _getStringLength;

  final TextEditingController _inputController = TextEditingController();
  String _reversedText = '';
  int _textLength = 0;

  @override
  void initState() {
    super.initState();
    _loadNativeLibrary();
  }

  void _loadNativeLibrary() {
    try {
      _nativeLib = DynamicLibrary.open(_getLibraryPath());
      _reverseString = _nativeLib
          .lookup<NativeFunction<ReverseStringC>>('reverse_string')
          .asFunction();
      _getStringLength = _nativeLib
          .lookup<NativeFunction<GetStringLengthC>>('get_string_length')
          .asFunction();
    } catch (e) {
      print('Error loading native library: $e');
      // 在实际应用中,这里应该给用户提示
      setState(() {
        _reversedText = 'Error: Could not load native library.';
      });
    }
  }

  void _processString() {
    final String originalText = _inputController.text;
    if (originalText.isEmpty) {
      setState(() {
        _reversedText = '';
        _textLength = 0;
      });
      return;
    }

    // 将 Dart 字符串转换为 C 字符串 (UTF-8 编码)
    final Pointer<Utf8> cString = originalText.toNativeUtf8();

    try {
      // 调用 C 函数反转字符串
      _reverseString(cString);
      // 调用 C 函数获取字符串长度
      _textLength = _getStringLength(cString);

      // 将 C 字符串转换回 Dart 字符串
      setState(() {
        _reversedText = cString.toDartString();
      });
    } catch (e) {
      print('Error calling native function: $e');
      setState(() {
        _reversedText = 'Error processing string.';
      });
    } finally {
      // 释放 C 字符串内存
      calloc.free(cString);
    }
  }

  @override
  void dispose() {
    _inputController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FFI 字符串处理'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: <Widget>[
            TextField(
              controller: _inputController,
              decoration: const InputDecoration(
                labelText: '输入文本',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _processString,
              child: const Text('处理字符串'),
            ),
            const SizedBox(height: 24),
            Text(
              '反转后的文本: $_reversedText',
              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              '文本长度 (通过 C 函数获取): $_textLength',
              style: const TextStyle(fontSize: 16, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: FFIExampleScreen()));
}

案例分析:

  • dart:ffipackage:ffi/ffi.dart:这两个库是使用 FFI 的核心。dart:ffi 提供了 FFI 的基本功能,而 package:ffi/ffi.dart 提供了方便的内存分配(calloc)和 Dart 字符串与 C 字符串(Utf8)之间的转换方法。
  • typedef ReverseStringC = Void Function(Pointer<Utf8> str);:这里定义了 C 语言中 reverse_string 函数的 Dart 类型签名。Void 对应 C 的 void 返回类型,Pointer<Utf8> 对应 C 的 char*
  • typedef ReverseStringDart = void Function(Pointer<Utf8> str);:这是 Dart 中调用该 C 函数时使用的 Dart 函数类型签名。
  • DynamicLibrary.open(_getLibraryPath()):根据当前平台加载对应的动态链接库。这是 FFI 的第一步。
  • _nativeLib.lookup<NativeFunction<ReverseStringC>>('reverse_string').asFunction():通过 lookup 方法查找 C 函数的指针,然后使用 asFunction() 将其转换为可直接调用的 Dart 函数。
  • originalText.toNativeUtf8():将 Dart 的 String 转换为 C 语言兼容的 UTF-8 编码的字符串指针。注意: 这会分配原生内存,因此在使用完毕后必须通过 calloc.free() 释放。
  • cString.toDartString():将 C 语言的字符串指针转换回 Dart 的 String
  • 错误处理:在加载库和调用原生函数时,都使用了 try-catch 块来捕获可能发生的异常,以提高应用的健壮性。

这个案例展示了如何在 Flutter 应用中使用 Dart FFI 调用原生 C 语言函数。虽然 FFI 提供了强大的能力,但它也增加了开发的复杂性,包括需要管理原生内存、处理不同平台下的库路径以及进行类型映射。因此,FFI 通常只在性能关键或需要与特定原生功能深度集成时才考虑使用。

注意: 运行此案例需要:

  1. 安装 C 编译器(如 GCC)。

  2. string_utils.c 编译成对应平台的动态链接库。

  3. pubspec.yaml 文件中添加 ffi 依赖:

    yaml
    复制代码
    dependencies:
      flutter:
        sdk: flutter
      ffi: ^2.1.2 # 添加此行
  4. 运行 flutter pub get 获取依赖。

  5. 确保编译好的动态库文件位于 Flutter 应用可以访问的路径。在调试时,通常将其放在项目根目录或 build 目录下,或者将其添加到系统路径中。