编程 Babashka 1.13.220 新增 FFI:Clojure 脚本直接调用 C 库

2026-09-06 07:16:18

Babashka 1.13.220 新增 FFI:Clojure 脚本直接调用 C 库

Clojure 开发者 Michiel Borkent 在个人博客宣布,Babashka 1.13.220 正式发布,新版本引入了 babashka.ffi 命名空间,允许从 Babashka 脚本中直接调用 C 库。同时,babashka.ffi 库也可以作为独立库用于 JVM Clojure,因此在常规的 Clojure 项目中也可以使用。需要注意的是,该 API 目前仍是实验性的,虽然目前没有计划进行更改,但需要更多的曝光和用户反馈。

背景:Babashka 是什么

Babashka 简介

Babashka 是一个用于快速启动 Clojure 脚本的工具:

  • 快速启动:使用 GraalVM native-image 编译,启动时间在毫秒级
  • Clojure 语法:使用完整的 Clojure 语言语法
  • 内置库:内置了常用的 Clojure 库(clojure.core、clojure.string、clojure.java.io 等)
  • 脚本友好:适合编写 shell 脚本、自动化任务、开发工具
  • 跨平台:支持 Linux、macOS、Windows

为什么需要 FFI

在 Babashka 中调用 C 库之前,开发者面临几个问题:

  1. 性能瓶颈:某些计算密集型任务用 Clojure 实现较慢,需要调用 C 实现
  2. 系统交互:需要调用系统级 C API(如文件系统、网络、进程管理)
  3. 现有库复用:有大量成熟的 C 库可以复用(如 zlib、openssl、sqlite)
  4. 原生功能:某些功能只有 C 库提供(如特定硬件的驱动接口)
  5. JVM 已有方案:JVM Clojure 可以通过 JNA/JNI 调用 C 库,但 Babashka 之前没有对应方案

babashka.ffi 入门

加载系统库

第一个示例加载系统中的 libz(zlib 压缩库)并获取版本:

(require '[babashka.ffi :as ffi :refer [defcfn]])

;; 加载系统库 zlib
(def zlib (ffi/load-system-library "z"))

;; 定义 C 函数 zlibVersion,返回字符串,无参数
(defcfn zlib-version zlib "zlibVersion" String [])

;; 调用
(println (zlib-version))
;; 输出: 1.3.1

加载自定义库

除了系统库,也可以加载自定义的 C 库:

;; 加载指定路径的库
(def mylib (ffi/load-library "/path/to/libmylib.so"))

;; macOS 上的 .dylib
(def mylib-mac (ffi/load-library "/path/to/libmylib.dylib"))

;; Windows 上的 .dll
(def mylib-win (ffi/load-library "C:/path/to/mylib.dll"))

核心 API

load-system-library

加载系统库,根据操作系统自动查找库文件:

(ffi/load-system-library "z")
;; Linux: libz.so
;; macOS: libz.dylib
;; Windows: z.dll

参数:

  • lib-name:库名称(不需要前缀 lib 和后缀)

返回:库对象,用于后续的函数定义。

load-library

加载指定路径的库:

(ffi/load-library "/absolute/path/to/libfoo.so")
(ffi/load-library "relative/path/libfoo.so")

参数:

  • path:库文件的路径(绝对或相对)

返回:库对象。

defcfn

定义 C 函数,这是最核心的宏:

(defcfn function-name library "c_function_name" ReturnType [ArgType1 ArgType2 ...])

参数说明:

  • function-name:Clojure 中的函数名
  • library:库对象(由 load-library 或 load-system-library 返回)
  • "c_function_name":C 函数的名称
  • ReturnType:返回值类型
  • [ArgType1 ArgType2 ...]:参数类型列表

支持的类型

Clojure 类型C 类型说明
Stringconst char*字符串(以 null 结尾)
Integer / Longint / long整数
Double / Floatdouble / float浮点数
Booleanint(0/1)布尔值
Voidvoid无返回值
Pointervoid*通用指针
ByteBuffervoid*字节缓冲区
数组类型type*数组指针

更多示例

示例 1:调用 libc 的函数

(require '[babashka.ffi :as ffi :refer [defcfn]])

;; 加载 libc
(def libc (ffi/load-system-library "c"))

;; getpid: 返回进程 ID
(defcfn getpid libc "getpid" Integer [])
(println "进程 ID:" (getpid))

;; getenv: 获取环境变量
(defcfn getenv libc "getenv" String [String])
(println "HOME:" (getenv "HOME"))

;; system: 执行 shell 命令
(defcfn system libc "system" Integer [String])
(system "echo Hello from C")

示例 2:使用 zlib 压缩

(require '[babashka.ffi :as ffi :refer [defcfn]])
(require '[clojure.java.io :as io])

(def zlib (ffi/load-system-library "z"))

;; compress2: 压缩数据
;; int compress2(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen, int level)
(defcfn compress2 zlib "compress2" Integer [Pointer Pointer Pointer Integer Integer])

;; uncompress: 解压数据
(defcfn uncompress zlib "uncompress" Integer [Pointer Pointer Pointer Integer])

(defn compress-bytes [data level]
  (let [src (byte-array data)
        dest-len (+ (alength src) 12)
        dest (byte-array dest-len)
        dest-len-ptr (int-array [dest-len])]
    (compress2 dest dest-len-ptr src (alength src) level)
    (java.util.Arrays/copyOf dest (aget dest-len-ptr 0))))

(defn uncompress-bytes [data original-len]
  (let [src (byte-array data)
        dest (byte-array original-len)
        dest-len-ptr (int-array [original-len])]
    (uncompress dest dest-len-ptr src (alength src))
    dest))

示例 3:调用 SQLite

(require '[babashka.ffi :as ffi :refer [defcfn]])

(def sqlite (ffi/load-system-library "sqlite3"))

;; 打开数据库
(defcfn sqlite3-open sqlite "sqlite3_open" Integer [String Pointer])
;; 执行 SQL
(defcfn sqlite3-exec sqlite "sqlite3_exec" Integer [Pointer String Pointer Pointer Pointer])
;; 关闭数据库
(defcfn sqlite3-close sqlite "sqlite3_close" Integer [Pointer])

(defn query [db-path sql]
  (let [db-ptr (make-array Pointer 1)
        _ (sqlite3-open db-path db-ptr)
        db (aget db-ptr 0)]
    (try
      (sqlite3-exec db sql nil nil nil)
      (finally
        (sqlite3-close db)))))

示例 4:JVM Clojure 中使用

babashka.ffi 也可以作为独立库在 JVM Clojure 中使用:

;; 在 deps.edn 中添加依赖
{:deps {babashka/ffi {:mvn/version "0.1.0"}}}

;; 在 JVM Clojure 中使用
(require '[babashka.ffi :as ffi :refer [defcfn]])

(def libc (ffi/load-system-library "c"))
(defcfn getpid libc "getpid" Integer [])
(println (getpid))

这意味着同一份代码可以在 Babashka 和 JVM Clojure 中运行,提高了可移植性。

工作原理

基于 GraalVM 的 FFI

Babashka 使用 GraalVM native-image 编译,因此 babashka.ffi 底层基于 GraalVM 的 FFI 机制:

  • GraalVM Native Image FFI:GraalVM 提供了调用原生库的机制
  • JNI 兼容:在 JVM 模式下,可能使用 JNA 或 JNI
  • 类型映射:Clojure 类型与 C 类型的自动映射
  • 内存管理:自动处理指针和内存分配

函数调用流程

Clojure 代码调用 (my-c-func arg1 arg2)
        │
        ▼
babashka.ffi 类型检查和转换
        │
        ▼
GraalVM FFI / JNA 调用 C 函数
        │
        ▼
C 函数执行
        │
        ▼
返回值转换为 Clojure 类型
        │
        ▼
返回给 Clojure 代码

注意事项和限制

1. API 仍是实验性的

  • 可能变更:API 可能在未来版本中发生变更
  • 需要反馈:作者鼓励用户提供反馈和使用案例
  • 生产环境谨慎:在生产环境中使用时需要评估稳定性

2. 类型安全

  • 运行时检查:类型错误在运行时才会被发现
  • 崩溃风险:错误的类型映射可能导致程序崩溃(segfault)
  • 建议:仔细核对 C 函数签名,确保类型映射正确

3. 内存管理

  • 手动管理:某些情况下需要手动管理内存
  • 指针有效性:确保指针在使用时仍然有效
  • 内存泄漏:不当使用可能导致内存泄漏
  • 建议:对于复杂的内存管理,考虑使用 Clojure 的 with-open 或类似模式

4. 线程安全

  • C 库可能不是线程安全的:某些 C 库使用全局状态,不支持多线程并发调用
  • 建议:查阅 C 库的文档,了解其线程安全特性
  • 同步:必要时使用 Clojure 的锁或原子变量进行同步

5. 平台差异

  • 库名称差异:不同操作系统的库名称和路径可能不同
  • 函数签名差异:某些 C 函数在不同平台上的签名可能不同
  • 建议:使用 load-system-library 自动处理平台差异,或使用条件编译

6. 错误处理

  • C 错误码:C 函数通常通过返回错误码表示失败
  • 不会自动抛出异常:babashka.ffi 不会自动将 C 错误码转换为 Clojure 异常
  • 建议:在 Clojure 代码中检查返回值,手动处理错误

性能考虑

优势

  • 原生性能:C 函数的执行速度通常比 Clojure 快
  • 低开销:FFI 调用的开销相对较小
  • 适合计算密集型:适合调用计算密集型的 C 库(如压缩、加密、数值计算)

开销

  • 类型转换:Clojure 类型与 C 类型之间的转换有一定开销
  • 函数调用:跨语言函数调用有一定开销
  • 不适合频繁小调用:对于频繁的小函数调用,FFI 开销可能占比较大
  • 建议:尽量批量处理,减少 FFI 调用次数

与其他方案的对比

维度babashka.ffiJNAJNIClojure interop (Java)
适用环境Babashka + JVMJVMJVMJVM
易用性高(宏定义)低(需要 C 代码)
性能
类型安全运行时运行时编译期编译期
跨平台支持支持需要编译支持
学习曲线
适用场景Babashka 脚本JVM 通用高性能需求Java 库可用时

实际应用场景

1. 系统管理脚本

  • 调用系统 C API 获取系统信息(CPU、内存、磁盘)
  • 调用 libc 进行进程管理
  • 调用系统库进行网络操作

2. 数据处理

  • 调用 zlib 进行压缩/解压
  • 调用 openssl 进行加密/解密
  • 调用 sqlite 进行嵌入式数据库操作
  • 调用 libxml2 进行 XML 解析

3. 科学计算

  • 调用 BLAS/LAPACK 进行矩阵运算
  • 调用 FFTW 进行快速傅里叶变换
  • 调用 GSL(GNU Scientific Library)进行科学计算

4. 开发工具

  • 调用 libgit2 进行 Git 操作
  • 调用 libcurl 进行 HTTP 请求
  • 调用 imagemagick 进行图像处理

5. 硬件交互

  • 调用串口库进行串口通信
  • 调用 GPIO 库进行树莓派等硬件的 GPIO 操作
  • 调用 USB 库进行 USB 设备交互

未来展望

短期

  • API 稳定:根据用户反馈稳定 API
  • 更多类型支持:支持更多 C 类型(结构体、联合体、回调函数)
  • 更好的文档:完善文档和示例
  • 错误处理改进:可能添加自动错误处理机制

中期

  • 结构体支持:支持 C 结构体的定义和操作
  • 回调函数:支持将 Clojure 函数作为 C 回调函数
  • 内存管理工具:提供更好的内存管理工具
  • 调试工具:提供 FFI 调用的调试工具

长期

  • 自动绑定生成:自动从 C 头文件生成 Clojure 绑定
  • 类型安全增强:编译期类型检查
  • 跨语言互操作:支持更多语言(C++、Rust、Go)的互操作
  • 标准化:成为 Clojure 生态中 FFI 的标准方案

总结

Babashka 1.13.220 引入的 babashka.ffi 命名空间是一个重要的新功能,它让 Babashka 脚本可以直接调用 C 库,大大扩展了 Babashka 的能力边界。

核心要点:

  1. 背景:Babashka 是快速启动的 Clojure 脚本工具,之前缺少调用 C 库的能力
  2. 核心 APIload-system-library(加载系统库)、load-library(加载指定路径库)、defcfn(定义 C 函数)
  3. 支持类型:String、Integer、Long、Double、Float、Boolean、Void、Pointer、ByteBuffer、数组
  4. 双平台支持:同时支持 Babashka 和 JVM Clojure,提高可移植性
  5. 应用场景:系统管理、数据处理、科学计算、开发工具、硬件交互
  6. 注意事项:API 实验性、类型安全、内存管理、线程安全、平台差异、错误处理
  7. 性能:原生性能优势,但有类型转换和函数调用开销,适合计算密集型任务

对于 Babashka 用户和 Clojure 开发者来说,babashka.ffi 提供了一个强大的工具,可以在保持 Clojure 开发效率的同时,利用丰富的 C 库生态。这使得 Babashka 不仅适合编写简单的脚本,也能够处理需要原生性能和系统级交互的复杂任务。

正如作者所说,API 仍是实验性的,需要更多的曝光和用户反馈。对于有 FFI 需求的开发者来说,现在是尝试并提供反馈的好时机。

原文链接:https://blog.michielborkent.nl/babashka-ffi.html

推荐文章

程序员茄子在线接单