Common Lisp CFFI 入门: 与 C 语言交互及内存管理
Common Lisp 是一门功能强大的编程语言, 但有时我们可能需要利用 C 语言编写的现有库, 或者需要与底层系统进行更紧密的交互. 这时, CFFI (C Foreign Function Interface) 就派上了用场. CFFI 与其说是一门编程语言, 不如说它是一个协议, 让高级语言能够调用 C 库暴露的函数.
对于大多数 Common Lisp 实现(如 SBCL, CCL, ECL 等)来说, 都内置了某种形式的 FFI. 然而, FFI 并非 Common Lisp 标准的一部分. 从 Lisp 的角度看, 底层系统仿佛不存在, 但有时与它" 对话" 却非常有用. CFFI 包是 Common Lisp 中使用 FFI 的事实标准, 它统一了不同 Lisp 实现的 FFI 用法.
本文将重点介绍 CFFI 的工作方式, 特别是内存管理方面, 并以 SBCL 为例进行演示(但 CFFI 的接口在各实现中是统一的).
CFFI 如何工作? —— 绑定 (Binding)
要从像 Lisp 这样的高级语言调用一个暴露了 C 函数的代码, 我们需要一个绑定(binding). 这本质上是一些"胶水代码(glue code), 它告诉高级语言如何向 C 函数传递数据, 以及如何读取 C 函数的返回结果.
如果用过其他语言的 FFI, 比如 Java 的 JNI, 你可能知道通常需要在 C 端编写一些胶水代码. Java 通过提供一个 C 的 SDK 来允许你手动管理 Java 对象, 从而控制数据如何传递给 C 函数.
而在 Common Lisp 中, 我们直接在 Lisp 端编写绑定. 因此, 理解如何在 Lisp 中管理 C 内存是第一步.
内存管理
C 语言没有垃圾回收器 (garbage collector). 因此, 每当我们在堆 (heap) 上分配一些内存后, 必须记住在不再需要时释放它.
CFFI 同样遵循这个原则.
1. 加载 CFFI 和创建包
首先, 我们需要加载 CFFI, 并创建一个包来存放我们的代码:
(ql:quickload :cffi) ; 使用 Quicklisp 加载 CFFI
(defpackage :cffi-intro
(:use :cl :cffi)
(:export #:run-examples))
(in-package :cffi-intro)
2. 基本内存操作
CFFI 提供了几个核心函数来处理内存:
foreign-alloc type &key count initial-element initial-contents null-terminated-p: 分配一块能容纳count个type类型元素的内存. 返回一个指向这块内存的外部指针 (foreign pointer).foreign-free ptr: 释放由foreign-alloc分配的内存.foreign-type-size type: 返回type类型在 C 中的大小(字节数).
;; 查看 :int 类型的大小
(print (foreign-type-size :int))
;; 在 SBCL x86-64 上通常输出 4 (字节)
分配一个整数大小的内存, 并尝试对其进行读写:
(let ((ptr nil))
(unwind-protect
(progn
(setf ptr (cffi:foreign-alloc :int))
(format t "分配的指针: ~A~%" ptr)
;; 读取未初始化的内存是未定义行为, 可能会得到垃圾值
(format t "写入前读取 (未定义行为): ~A~%" (cffi:mem-ref ptr :int))
;; 写入数据
(setf (cffi:mem-ref ptr :int) 42)
(format t "写入 42 后读取: ~A~%" (cffi:mem-ref ptr :int)))
(when ptr
(format t "释放指针: ~A~%" ptr)
(cffi:foreign-free ptr))))
;; 输出:
;; 分配的指针: #<SB-SYS:INT-SAP #X7Fxxxxxxxxxx>
;; 写入前读取 (未定义行为): -259990735 (或其它垃圾值)
;; 写入 42 后读取: 42
;; 释放指针: #<SB-SYS:INT-SAP #X7Fxxxxxxxxxx>
注意: mem-ref ptr type &optional offset 用于读取指针 ptr 指向的
type 类型的数据. offset 是可选的, 用于数组访问.
setf (mem-ref ptr type &optional offset) value 用于向指针 ptr
指向的内存写入数据. 指针不携带类型信息. 类型信息是在使用 mem-ref
等操作时提供的. 必须确保分配和释放的内存类型一致,
读写时提供的类型也应与分配时一致.
例如, 即使 :int 和 :float 在某些系统上大小相同,
混用它们也会导致问题:
(print (cffi:foreign-type-size :float))
;; 在 SBCL x86-64 上通常输出 4 (字节)
(let ((ptr (cffi:foreign-alloc :int)))
(unwind-protect
(progn
(setf (cffi:mem-ref ptr :int) 42)
(format t "作为 :int 读取: ~A~%" (cffi:mem-ref ptr :int))
;; 将整数 42 的位模式解释为浮点数, 结果通常无意义
(format t "作为 :float 读取: ~A~%" (cffi:mem-ref ptr :float)))
(cffi:foreign-free ptr)))
;; 输出:
;; 作为 :int 读取: 42
;; 作为 :float 读取: 5.8854536E-44 (或其它无意义的浮点数)
3. 安全的内存管理: with-foreign-pointer 和 with-foreign-object
手动跟踪动态内存分配和释放在 C 中就很困难, 在带有异常处理的 Lisp 中更甚. 如果分配内存后, 释放前发生错误, 内存就可能泄露.
解决方案是使用 with-foreign-pointer (或其包装器
with-foreign-object).
这些宏会在其动态作用域内绑定一个变量到新分配的外部指针,
并确保在退出作用域时(无论是正常退出还是通过 unwind-protect
异常退出)自动释放内存.
with-foreign-object (var type &optional count) 它会分配足够存储
count 个 type 类型对象的内存, 并将指针绑定到 var.
;; 使用 with-foreign-object 创建和操作一个包含10个整数的数组
(cffi:with-foreign-object (my-array :int 10)
;; 初始化数组元素
(dotimes (i 10)
(setf (cffi:mem-ref my-array :int i) (random 100)))
;; 收集并打印数组元素
(let ((lisp-list (loop for i below 10
collect (cffi:mem-ref my-array :int i))))
(format t "数组内容: ~A~%" lisp-list)))
;; my-array 指向的内存在此宏退出后自动释放
在 with-foreign-object 的内部实现中(至少在 SBCL 中), 如果大小是常量,
它可能会尝试在栈上分配内存以提高效率.
4. 分配数组
如上例所示, with-foreign-object 可以通过第三个参数 count 来分配数组.
foreign-alloc 也可以通过 :count 关键字参数分配连续的内存块(数组):
(let ((arr-ptr nil))
(unwind-protect
(progn
;; 分配一个包含 10 个整数的数组
(setf arr-ptr (cffi:foreign-alloc :int :count 10))
(format t "数组指针: ~A~%" arr-ptr)
;; 设置第三个元素 (索引为 2, 因为索引从0开始)
(setf (cffi:mem-ref arr-ptr :int 2) 99)
(format t "数组第三个元素: ~A~%" (cffi:mem-ref arr-ptr :int 2))
;; 错误示例: 如果忘记提供类型 (mem-aref 是旧的, 现在推荐 mem-ref)
;; (setf (cffi:mem-aref arr-ptr 2) 42) ; 这会报错, 需要类型
)
(when arr-ptr
(format t "释放数组指针: ~A~%" arr-ptr)
(cffi:foreign-free arr-ptr))))
重要: 只能读写你已经分配的内存. 指针本身不携带类型信息.
类型信息是在解引用指针时(例如通过 mem-ref)提供的. 在分配,
读取和写入时, 必须保持类型的一致性.
结论
与 C 语言打交道总是有些棘手, 尤其是在内存管理方面. 但 CFFI 为 Common Lisp 提供了一个强大的工具. 有许多知名且经过测试的 C 库可以通过 CFFI 在 Lisp 程序中使用.
通过 foreign-alloc, foreign-free, mem-ref 以及更安全的
with-foreign-object, 我们可以有效地在 Lisp 中管理 C 内存.
关键在于细心和一致性.