C语言如何调用Dart语言实现交互?

99ANYc3cd6
预计阅读时长 23 分钟
位置: 首页 C语言 正文

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

C语言调用dart语言
(图片来源网络,侵删)
  1. C调用Dart:C代码可以调用Dart函数。
  2. Dart调用C:Dart代码也可以调用C函数。

下面我将详细解释这个过程,并提供一个完整的、可运行的示例。


核心概念:Dart的C API和FFI

要实现C和Dart的互操作,我们需要理解两个关键概念:

  1. Dart C API (Dart C API):这是Dart官方提供的一套C语言风格的接口,它允许你创建和操作Dart对象,例如字符串、列表、数组等,当Dart代码被编译为本地代码时,这些API就成了连接C世界的桥梁。
  2. Dart FFI (Foreign Function Interface):这是Dart语言层面的一套高级封装,用于与C语言进行交互,你不需要直接调用繁琐的C API,而是使用Dart的dart:ffi库来定义C函数的绑定,然后像调用普通Dart函数一样调用它们,FFI在底层会自动为你处理C API的调用。

工作流程总结:

  1. 编写Dart代码:定义你希望被C调用的Dart函数。
  2. 编译Dart代码:使用Dart的编译器(dart compile exe)将Dart代码编译成一个动态链接库(如.so在Linux/macOS,.dll在Windows,或.dylib在macOS)。
  3. 编写C代码:在C代码中,加载这个动态链接库,并使用dlopen, dlsym等系统函数来找到并执行Dart编译出的函数。
  4. 链接和编译:编译C代码时,需要链接Dart的C API头文件和库。

完整示例:C调用Dart

我们将创建一个简单的示例,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:ffiDynamicLibrary来加载另一个Dart编译的库,但为了演示C调用Dart,我们采用编译为共享库的方式。

C语言调用dart语言
(图片来源网络,侵删)

注意:Dart官方工具链对将纯Dart函数编译为C可调用的共享库支持相对复杂,更常见的做法是将Dart代码编译为可执行文件,然后通过进程间通信(IPC)或标准输入输出来交互,但为了回答你的问题,我们假设可以使用Dart的C API来实现。

这里我们采用一个更清晰、更符合现代实践的方法:将Dart代码编译为可执行文件,然后C代码通过system()函数调用它,这是一种更简单、更健壮的跨语言调用方式。

让我们修改一下示例,采用这种方式:

方案A:通过进程调用(推荐,简单)

  1. 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);
    }
  2. 编译Dart代码

    dart compile exe my_dart_app.dart

    这会生成一个可执行文件,例如在macOS上是my_dart_app,在Windows上是my_dart_app.exe

  3. 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;
    }
  4. 编译和运行C代码

    # gcc call_dart.c -o call_dart
    # ./call_dart

方案B:通过Dart的C API(复杂,底层)

这个方案更接近你最初的问题,但设置更复杂。

  1. 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;
    }
  2. 编译Dart代码为共享库: 这个步骤比较特殊,通常需要特定的工具链或手动操作,假设我们已经生成了libnative_lib.so

  3. 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的第二个参数)是这里最大的难点,你需要使用nmobjdump等工具来查看编译后的共享库,找到getStringLength函数对应的实际符号名,这个符号名通常会被C++编译器进行“名称修饰”(Name Mangling)。


总结与推荐

方法 优点 缺点 适用场景
通过进程调用 简单、健壮、跨平台、无需复杂的C API知识。 进程间通信有开销,不适合高频调用。 大多数场景,特别是当C和Dart作为独立服务或工具时。
通过Dart C API 性能高、内存共享、可以深度集成。 极其复杂、平台依赖性强、符号名难找、容易出错。 对性能要求极高,且需要频繁调用的底层系统组件。

给你的建议:

对于绝大多数开发者,强烈推荐使用“通过进程调用”的方案,它更易于理解、实现和维护,只有在性能是绝对瓶颈,并且你确定需要深入集成时,才去挑战Dart的C API。

Dart的FFI设计初衷主要是让Dart调用C/C++(例如调用系统原生库、图形库等),而不是反过来让C调用Dart,C调用Dart并不是一个主流或被官方重点宣传的功能。

-- 展开阅读全文 --
头像
Java转C语言,如何实现核心逻辑转换?
« 上一篇 04-23
dede图集上传错误,如何解决?
下一篇 » 04-23

相关文章

取消
微信二维码
支付宝二维码

目录[+]