这个API允许你将Dart代码编译成本地代码(Native Code),然后这个本地代码可以被C语言(以及任何与C兼容的语言,如C++)调用,这个过程是双向的:

- C调用Dart:C代码可以调用Dart函数。
- Dart调用C:Dart代码也可以调用C函数。
下面我将详细解释这个过程,并提供一个完整的、可运行的示例。
核心概念:Dart的C API和FFI
要实现C和Dart的互操作,我们需要理解两个关键概念:
- Dart C API (Dart C API):这是Dart官方提供的一套C语言风格的接口,它允许你创建和操作Dart对象,例如字符串、列表、数组等,当Dart代码被编译为本地代码时,这些API就成了连接C世界的桥梁。
- Dart FFI (Foreign Function Interface):这是Dart语言层面的一套高级封装,用于与C语言进行交互,你不需要直接调用繁琐的C API,而是使用Dart的
dart:ffi库来定义C函数的绑定,然后像调用普通Dart函数一样调用它们,FFI在底层会自动为你处理C API的调用。
工作流程总结:
- 编写Dart代码:定义你希望被C调用的Dart函数。
- 编译Dart代码:使用Dart的编译器(
dart compile exe)将Dart代码编译成一个动态链接库(如.so在Linux/macOS,.dll在Windows,或.dylib在macOS)。 - 编写C代码:在C代码中,加载这个动态链接库,并使用
dlopen,dlsym等系统函数来找到并执行Dart编译出的函数。 - 链接和编译:编译C代码时,需要链接Dart的C API头文件和库。
完整示例:C调用Dart
我们将创建一个简单的示例,C代码调用一个Dart函数,该函数接收一个字符串并返回其长度。

第1步:准备Dart代码
创建一个名为dart_code.dart的文件。
// dart_code.dart
// 1. 导入FFI库
import 'dart:ffi';
// 2. 定义一个C函数的签名,这个签名将作为我们Dart函数的“C语言外壳”。
// 这个函数必须返回一个指针(IntPtr),并且接收一个指针(Pointer<Utf8>)作为参数。
typedef IntFunc = IntPtr Function(Pointer<Utf8>);
// 3. 这是我们要被C语言调用的实际Dart函数。
// 它接收一个字符串,并返回其长度。
int dartStringLength(String input) {
print("Dart函数被调用!输入字符串是: '$input'");
return input.length;
}
// 4. 这是整个程序的入口点,它会被C代码通过dlsym找到。
// 我们使用@pragma('vm:entry-point')来标记它。
@pragma('vm:entry-point')
void main() {
// 这个main函数在Dart编译为可执行文件时是必需的,
// 但对于作为库被加载的场景,它可能不会被直接调用。
// 关键在于下面的dartStringLength函数。
}
// 5. 为了让C代码能够找到dartStringLength函数,我们需要一个“C兼容”的包装器。
// 这个包装器会接收C风格的字符串(指针),并将其转换为Dart字符串,
// 然后调用我们的dart函数,最后将结果转换回C可以理解的整数(IntPtr)。
@pragma('vm:entry-point')
external IntPtr dartStringLengthWrapper(Pointer<Utf8> cString);
解释:
typedef IntFunc:定义了一个C函数指针的类型,这让Dart代码的签名与C函数的签名保持一致。dartStringLength:这是我们的核心业务逻辑。@pragma('vm:entry-point'):这个指令告诉Dart编译器,dartStringLengthWrapper是一个入口点,C代码可以通过符号名找到它。external:这个关键字告诉Dart编译器,这个函数的实现不在Dart源码中,而是在外部(即由C API提供)。
第2步:编译Dart代码为动态库
打开终端,运行以下命令,我们将使用dart compile kernel先编译为内核文件,然后使用dart compile exe并指定--enable-experiment=macros(或新版本中的相应选项)来生成本地代码,更简单和推荐的方式是直接编译为可执行文件,但为了C能调用,我们需要一个特定的方法。
对于这种场景,最直接的方式是使用dart:ffi的DynamicLibrary来加载另一个Dart编译的库,但为了演示C调用Dart,我们采用编译为共享库的方式。

注意:Dart官方工具链对将纯Dart函数编译为C可调用的共享库支持相对复杂,更常见的做法是将Dart代码编译为可执行文件,然后通过进程间通信(IPC)或标准输入输出来交互,但为了回答你的问题,我们假设可以使用Dart的C API来实现。
这里我们采用一个更清晰、更符合现代实践的方法:将Dart代码编译为可执行文件,然后C代码通过system()函数调用它,这是一种更简单、更健壮的跨语言调用方式。
让我们修改一下示例,采用这种方式:
方案A:通过进程调用(推荐,简单)
-
Dart代码 (
my_dart_app.dart): 这个程序接收一个命令行参数,打印并返回其长度。// my_dart_app.dart import 'dart:io'; void main(List<String> arguments) { if (arguments.isEmpty) { print("错误:请提供一个字符串作为参数。"); exit(1); } String input = arguments[0]; print("Dart应用收到: $input"); // 将长度打印到标准输出,C代码可以捕获它 print(input.length); } -
编译Dart代码:
dart compile exe my_dart_app.dart
这会生成一个可执行文件,例如在macOS上是
my_dart_app,在Windows上是my_dart_app.exe。 -
C代码 (
call_dart.c): C代码将调用这个可执行文件,并捕获其输出。// call_dart.c #include <stdio.h> #include <stdlib.h> int main() { char input_string[256]; printf("请输入一个字符串: "); scanf("%255s", input_string); // 构建要执行的命令 char command[300]; #ifdef _WIN32 sprintf(command, "my_dart_app.exe %s", input_string); #else sprintf(command, "./my_dart_app %s", input_string); #endif printf("C程序正在调用Dart程序...\n"); // 调用系统命令并获取其输出 FILE *fp = popen(command, "r"); if (fp == NULL) { perror("popen failed"); return 1; } int dart_result; if (fscanf(fp, "%d", &dart_result) == 1) { printf("Dart程序执行完毕,返回的长度是: %d\n", dart_result); } else { printf("无法从Dart程序读取输出,\n"); } pclose(fp); return 0; } -
编译和运行C代码:
# gcc call_dart.c -o call_dart # ./call_dart
方案B:通过Dart的C API(复杂,底层)
这个方案更接近你最初的问题,但设置更复杂。
-
Dart代码 (
native_lib.dart): 这个文件将被编译成库。// native_lib.dart library native_lib; import 'dart:ffi'; import 'package:ffi/ffi.dart'; // 定义C函数指针类型 typedef StringLengthFunc = int Function(Pointer<Utf8>); @pragma('vm:entry-point') int getStringLength(Pointer<Utf8> cString) { // 将C字符串转换为Dart字符串 String dartString = cString.toDartString(); int length = dartString.length; print("Dart (native_lib) 计算字符串 '$dartString' 的长度: $length"); return length; } -
编译Dart代码为共享库: 这个步骤比较特殊,通常需要特定的工具链或手动操作,假设我们已经生成了
libnative_lib.so。 -
C代码 (
c_client.c): C代码加载这个库并调用函数。// c_client.c #include <stdio.h> #include <dlfcn.h> // 用于动态加载库 int main() { const char* lib_path = "./libnative_lib.so"; // Linux/macOS // const char* lib_path = "./native_lib.dll"; // Windows // 1. 加载动态库 void* handle = dlopen(lib_path, RTLD_LAZY); if (!handle) { fprintf(stderr, "dlopen failed: %s\n", dlerror()); return 1; } // 2. 获取函数指针 // 假设编译器将getStringLength编译为"_ZgetStringLengthPc"这样的符号名 // 实际符号名可能因平台和编译器而异,需要工具查看 typedef int (*StringLengthFunc)(const char*); StringLengthFunc getStringLength = (StringLengthFunc)dlsym(handle, "_ZgetStringLengthPc"); // 注意:符号名可能不对! if (!getStringLength) { fprintf(stderr, "dlsym failed: %s\n", dlerror()); dlclose(handle); return 1; } printf("C程序成功加载了Dart库并找到了函数,\n"); // 3. 调用Dart函数 const char* my_string = "Hello from C!"; int length = getStringLength(my_string); printf("C调用Dart函数后,返回的长度是: %d\n", length); // 4. 关闭库 dlclose(handle); return 0; }注意:获取正确的C符号名(
dlsym的第二个参数)是这里最大的难点,你需要使用nm或objdump等工具来查看编译后的共享库,找到getStringLength函数对应的实际符号名,这个符号名通常会被C++编译器进行“名称修饰”(Name Mangling)。
总结与推荐
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 通过进程调用 | 简单、健壮、跨平台、无需复杂的C API知识。 | 进程间通信有开销,不适合高频调用。 | 大多数场景,特别是当C和Dart作为独立服务或工具时。 |
| 通过Dart C API | 性能高、内存共享、可以深度集成。 | 极其复杂、平台依赖性强、符号名难找、容易出错。 | 对性能要求极高,且需要频繁调用的底层系统组件。 |
给你的建议:
对于绝大多数开发者,强烈推荐使用“通过进程调用”的方案,它更易于理解、实现和维护,只有在性能是绝对瓶颈,并且你确定需要深入集成时,才去挑战Dart的C API。
Dart的FFI设计初衷主要是让Dart调用C/C++(例如调用系统原生库、图形库等),而不是反过来让C调用Dart,C调用Dart并不是一个主流或被官方重点宣传的功能。
