dart-setup-ffi-assets

dart-setup-ffi-assets

热门

指导智能体使用 Dart 的 Native Assets 钩子系统(通过 hook/build.dart 和 hook/link.dart,利用 package:hooks 和 package:native_toolchain_c)将 C/C++ 源代码编译并打包为动态或静态库(Code Assets)。当用户要求“设置原生资产”、“编译 C/C++ 源代码”、“打包动态库”、“构建原生 C 代码”、“链接原生资产”、“实现 build.dart 或 link.dart 钩子”或“在 Dart/Flutter 中集成 C/C++ 互操作”时使用。帮助智能体避免手动工具链编排,并配置安全的哈希验证二进制下载或使用 package:record_use 映射的高级链接器树摇优化。

406Star
26Fork
更新于 2026/7/20
SKILL.md
readonly只读
name
dart-setup-ffi-assets
description

指导智能体使用 Dart 的 Native Assets 钩子系统(通过 hook/build.dart 和 hook/link.dart,利用 package:hooks 和 package:native_toolchain_c)将 C/C++ 源代码编译并打包为动态或静态库(Code Assets)。当用户要求“设置原生资产”、“编译 C/C++ 源代码”、“打包动态库”、“构建原生 C 代码”、“链接原生资产”、“实现 build.dart 或 link.dart 钩子”或“在 Dart/Flutter 中集成 C/C++ 互操作”时使用。帮助智能体避免手动工具链编排,并配置安全的哈希验证二进制下载或使用 package:record_use 映射的高级链接器树摇优化。

使用原生资产钩子将 C 代码编译为 Code Assets

集成并自动化原生 C/C++ 源代码的编译和打包,通过构建和链接钩子将其转换为 Dart 整体 Native Assets 功能下的 Code Assets

目录


简介

在 Dart 的 Native Assets 功能下,包可以将原生代码(如 C/C++ 库)打包为 Code Assets,并在标准开发周期(例如 dart rundart testdart buildflutter run)中自动捆绑。Code Assets 的打包由放置在包 hook/ 文件夹中的两个程序化钩子脚本驱动:

  1. hook/build.dart:将本地 C 源代码编译为机器码,或将预构建的原生二进制文件作为代码资产打包到特定主机/目标架构。
  2. hook/link.dart:链接已构建的代码资产,应用高级树摇优化以剥离未使用的原生符号并压缩运行时二进制大小。

约束

[!IMPORTANT]
保持所有文件解析与平台无关。切勿硬编码绝对目标路径、shell 脚本或系统命令变量。始终使用 Platform.script.resolve() 或基于 Uri 的解析,以确保脚本完全可移植。

  • 钩子位置:编译和打包钩子必须严格位于包根目录的 hook/ 目录内:
    • hook/build.dart(构建执行阶段)
    • hook/link.dart(可选打包/链接/树摇阶段)
  • 编译工具链标准:使用 package:native_toolchain_c 的程序化 API(例如 CBuilderCLibrary)来运行编译工具链。切勿通过 shell 命令直接调用原始的 gccclangmsvc
  • 前言与许可头:每个手工编写和生成的源文件(包括绑定、辅助函数和钩子)必须严格包含目标包的版权和许可头。
  • 树摇映射:如果使用编译器树摇,必须使用 FFIgen 生成的记录使用映射,将目标 Dart 方法名(例如 Method.name)映射回原始的原生 C 符号名。映射文件必须位于 lib/src/third_party/ 下,并严格使用 .g.dart 扩展名(例如 sqlite3.record_use_mapping.g.dart)。
  • 预编译库的完整性保护:如果采用动态下载模式:
    • 加密验证:下载的预构建二进制文件必须根据预配置的查找表(包含 MD5 或 SHA-256 哈希)进行检查,以确保二进制完整性并防止篡改。
    • 优雅恢复:通过提供回退机制(例如通过 local_build 等标志启用本地编译器执行)支持离线开发者。

原生互操作包

用于 Code Assets 的程序化构建和链接钩子利用三个专门的原生互操作包:

依赖项 用途 关键 API 抽象
package:hooks 定义执行边界的主要编排器。 build(args, callback)link(args, callback)
package:native_toolchain_c 检测本地编译器(MSVC、Xcode/Clang、GCC)并执行构建工具链。 CLibraryCBuilderLinkerOptions.treeshake
package:code_assets 对传递给动态加载器的代码元数据记录进行建模。 CodeAssetDynamicLoadingBundled

逐步工作流

步骤 1:添加依赖项

将 Code Assets 钩子和工具链依赖项添加到您的包中。您必须直接从 pub.dev 获取这些依赖项。

您可以使用 CLI 自动添加:

dart pub add code_assets hooks native_toolchain_c record_use dev:ffigen

或者手动在目标包的 pubspec.yaml 中声明它们:

dependencies:
  code_assets: ^1.0.0
  hooks: ^0.1.0
  native_toolchain_c: ^0.1.0
  record_use: ^0.6.0

dev_dependencies:
  ffigen: ^20.1.1

步骤 2:定义 C 规范

lib/src/c_library.dart 中定义目标 C 库编译元数据。这使构建和链接钩子可以共享资产、名称和源代码的单一事实来源。

步骤 3:实现构建和链接钩子脚本

hook/build.dart 中编写编译编排脚本,在 hook/link.dart 中编写死代码消除逻辑。

步骤 4:运行钩子周期

运行标准测试套件会在后台动态启动构建和链接钩子生命周期:

dart test

选择集成方法

在 Dart 中集成和交付 C/C++ 原生资产主要有两种方法。选择符合您项目需求的方法:

方面 方法 1:本地编译与树摇 方法 2:预编译下载
主要用例 当 C/C++ 源代码直接包含在包中,并且您希望最大化大小优化时。 当本地编译缓慢/复杂,或希望避免开发者主机工具链要求时。
主机工具链要求 需要预安装平台 C 编译器(Xcode 工具、MSVC、GCC)。 开发者/用户机器上无需编译器设置。
二进制优化 高级。未使用的符号被完全树摇,减小库大小。 标准。标准编译的二进制文件按原样提供。
离线设置 完全合规。完全离线工作。 需要网络访问以下载库,并提供离线回退。

方法 1:本地编译与链接器树摇(推荐)

在这种方法中,构建钩子调用本地工具链(GCC、Clang、MSVC)直接编译源文件。链接钩子随后利用编译器选项过滤输出符号,仅保留用户代码中调用的目标方法。这是 pkgs/code_assets/example/sqlite 下标准、健壮的 SQLite 模式。

前提条件:主机编译器工具链

由于 package:native_toolchain_c 将实际的动态编译委托给主机操作系统的默认工具链,开发机器必须预安装以下编译器包之一:

  • macOS:Xcode 命令行工具。通过以下命令安装:
    xcode-select --install
    
  • Linux:GCC 或 Clang。通过以下命令安装:
    sudo apt install build-essential
    
  • Windows:MSVC(Microsoft Visual C++)。安装 Visual Studio Installer 并选择 使用 C++ 的桌面开发 工作负载。

注意:如果在主机路径上未发现兼容的工具链,构建钩子脚本将抛出编译执行异常。如果无法保证工具链,请确保指定编译器约束或采用方法 2。

C 源代码和绑定设置

假设一个 C 源文件在 third_party/sqlite/sqlite3.c 中定义了简单的数学函数,其入口点头文件位于 third_party/sqlite/sqlite3.h

#ifndef SQLITE3_H_
#define SQLITE3_H_

const char *sqlite3_libversion(void);

#endif // SQLITE3_H_

我们使用程序化的 FFIgen 脚本(tool/ffigen.dart)在 lib/src/third_party/sqlite3.g.dart 中创建 FFI 绑定,启用记录的使用跟踪,并在 lib/src/third_party/sqlite3.record_use_mapping.g.dart 中生成查找元数据映射:

// 自动生成文件 - 请勿修改。
// 通过 ffigen 生成。

const recordUseMapping = {
  'sqlite3_libversion': 'sqlite3_libversion',
};

定义 C 库构建规范

lib/src/c_library.dart 中定义集中式库规范:

import 'package:native_toolchain_c/native_toolchain_c.dart';

/// sqlite 库的 C 构建规范。
final cLibrary = CLibrary(
  name: 'sqlite3',
  assetName: 'src/third_party/sqlite3.g.dart',
  sources: ['third_party/sqlite/sqlite3.c'],
);

实现 hook/build.dart

使用 CLibrary.build 实现 hook/build.dart。这将库构建为动态库(例如 .so.dylib.dll)并放置在钩子的目标目录中:

import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:sqlite/src/c_library.dart';

void main(List<String> args) async {
  await build(args, (input, output) async {
    if (input.config.buildCodeAssets) {
      await cLibrary.build(
        input: input,
        output: output,
        defines: {
          if (input.config.code.targetOS == OS.windows)
            // 确保 C 函数在 Windows DLL 中显式导出
            'SQLITE_API': '__declspec(dllexport)',
        },
      );
    }
  });
}

实现 hook/link.dart

hook/link.dart 中实现链接优化阶段。这利用编译器树摇选项(LinkerOptions.treeshake)根据符号使用记录编译最小化、死代码消除的二进制文件:

import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:record_use/record_use.dart';
import 'package:sqlite/src/c_library.dart';
import 'package:sqlite/src/third_party/sqlite3.record_use_mapping.g.dart';

void main(List<String> arguments) async {
  await link(arguments, (input, output) async {
    await cLibrary.link(
      input: input,
      output: output,
      linkerOptions: LinkerOptions.treeshake(
        // 将 Dart 方法引用映射回原始 C 符号名
        symbolsToKeep: input.recordedUses?.calls.keys.cast<Method>().map(
          (e) => recordUseMapping[e.name]!,
        ),
      ),
    );
  });
}

方法 2:下载预编译动态库

另一种方法是在中央构建机器上预先编译二进制文件,将其归档,并在构建钩子执行期间下载目标二进制文件。这与 download_asset 钩子包中演示的模式相匹配。

为什么下载预编译二进制文件?

  • 主机约束:本地编译大型 C/C++ 库需要完整的编译器设置(GCC、Xcode/SDK、Visual Studio),而最终开发者的主机可能不具备。
  • 编译速度:预编译下载只需毫秒级时间,而编译过程可能需要数分钟。
  • 平台桥接:如果主机架构有限,可以避免交叉编译约束。

实现预编译动态下载

我们配置构建钩子以检测本地编译器标志(例如 local_build)。如果未指定,钩子使用 HttpClient 拉取平台特定的库,计算 MD5 哈希以对照配置的哈希查找表确认下载安全性,并将二进制文件注册为 CodeAsset

1. 定义目标哈希(lib/src/hook_helpers/hashes.dart

在包源代码中为每个平台文件定义目标 MD5 哈希检查:

const assetHashes = {
  'libnative_add_macos_arm64.dylib': '4a88f50438a98402db2dbd47b59eb412',
  'libnative_add_linux_x64.so': '9f5e15043aa98402dcdbbd47b59ea520',
  'native_add_windows_x64.dll': 'a881e5043ba98402acdebd47b59fa321',
};
2. 钩子下载辅助函数(lib/src/hook_helpers/download.dart

使用动态目标文件名匹配实现下载和完整性检查逻辑:

import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:crypto/crypto.dart';

const version = '1.0.0';

Uri downloadUri(String target) => Uri.parse(
  'https://github.com/my-org/my-native-repo/releases/download/$version/$target',
);

Future<File> downloadAsset(
  OS targetOS,
  Architecture targetArchitecture,
  Directory outputDir,
) async {
  final fileName = targetOS.dylibFileName('native_add_${targetOS.name}_${targetArchitecture.name}');
  final uri = downloadUri(fileName);
  
  final client = HttpClient()..findProxy = HttpClient.findProxyFromEnvironment;
  final request = await client.getUrl(uri);
  final response = await request.close();
  
  if (response.statusCode != 200) {
    throw ArgumentError('下载目标 $uri 失败:状态码 ${response.statusCode}');
  }
  
  final targetFile = File.fromUri(outputDir.uri.resolve(fileName));
  await targetFile.create(recursive: true);
  await response.pipe(targetFile.openWrite());
  
  return targetFile;
}

Future<String> hashAsset(File file) async {
  return md5.convert(await file.readAsBytes()).toString();
}
3. 实现 hook/build.dart

编写最终的下载构建钩子,包含本地编译回退:

import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:my_download_package/src/hook_helpers/hashes.dart';
import 'package:my_download_package/src/hook_helpers/download.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';

void main(List<String> args) async {
  await build(args, (input, output) async {
    final localBuild = input.userDefines['local_build'] as bool? ?? false;

    if (localBuild) {
      final name = 'native_add_${input.config.code.targetOS.name}_${input.config.code.targetArchitecture.name}';
      final builder = CBuilder.library(
        name: name,
        assetName: 'native_add.dart',
        sources: ['src/native_add.c'],
      );
      await builder.run(input: input, output: output);
    } else {
      final targetOS = input.config.code.targetOS;
      final targetArch = input.config.code.targetArchitecture;
      final outputDir = Directory.fromUri(input.outputDirectory);

      final file = await downloadAsset(targetOS, targetArch, outputDir);

      final fileHash = await hashAsset(file);
      final expectedFileName = targetOS.dylibFileName('native_add_${targetOS.name}_${targetArch.name}');
      final expectedHash = assetHashes[expectedFileName];

      if (fileHash != expectedHash) {
        throw Exception(
          '安全不匹配:文件 $expectedFileName 哈希验证失败!'
          '找到的哈希:$fileHash,期望的哈希:$expectedHash。'
        );
      }

      output.assets.code.add(
        CodeAsset(
          package: input.packageName,
          name: 'native_add.dart',
          linkMode: DynamicLoadingBundled(),
          file: file.uri,
        ),
      );
    }
  });
}

验证清单

在声明构建或链接钩子实现完成之前,始终执行以下检查:

1. 本地执行沙箱

运行单元测试并确认原生资产编译/链接过程完成,无运行时或构建工具异常:

dart test

2. 验证目标输出

导航到包目标目录并验证为主机系统创建了动态二进制资产:

  • macOS:验证 .dart_tool/resources/ 或目标目录包含 .dylib 文件。
  • Linux:验证 .dart_tool/resources/ 或目标目录包含 .so 文件。
  • Windows:验证 .dart_tool/resources/ 或目标目录包含 .dll 文件。

3. 验证树摇剥离

为确保链接钩子实际剥离未使用的原生符号并压缩二进制打包,执行以下验证:

  1. 编译 CLI/应用的生产包:
    dart build cli bin/main.dart
    
  2. 导航到包含动态库的编译构建目录。
  3. 查询导出的动态符号表:
    • macOS
      nm -gU build/cli/lib/libsqlite3.dylib
      
    • Linux
      nm -D build/cli/lib/libsqlite3.so
      
    • Windows(使用 MSVC 开发者命令提示符):
      dumpbin /EXPORTS build\cli\lib\sqlite3.dll
      
  4. 确认目标导出:验证命令输出包含显式保留的入口点函数(例如 sqlite3_libversion),并且不输出任何未引用/剥离的符号。
  5. 无包场景:如果应用程序未导入或调用原生库的任何方法:
    • 验证链接钩子记录:Skipping linking as no symbols are to be kept.
    • 验证生产包中未构建/放置任何库(未生成 .dylib/.so/.dll 文件,节省包大小)。

4. 验证离线合规性(用户定义)

确认离线合规性完全激活,并且下载回退在离线状态下完美执行:

  1. 在包的 pubspec.yaml(或工作区根目录 pubspec.yaml)中为包配置 local_build: true 定义:
    hooks:
      user_defines:
        <your_package_name>:
          local_build: true
    
  2. 禁用机器的网络适配器或在沙盒离线 shell 中运行。
  3. 启动单元测试:
    dart test
    
  4. 验证测试套件成功使用主机编译器编译本地源文件,无编译错误,并且从未尝试网络下载请求。