CFFI 炼丹:用“点”化繁为简 🧙‍♂️

引言

CFFI,这个强大的工具,让我们在 Lisp 中调用 C 函数,如同驾驭风火轮,穿梭于两个世界。然而,使用 CFFI 的 API 编写 C 风格代码,有时却像是在泥潭中跋涉,因为你需要不断地传递类型信息,而 C 中的“点”运算符却拥有着神奇的类型推断能力,让我们可以轻松地访问结构体成员。

为了让 CFFI 也能像 C 一样优雅,我们打造了 cffi-ops 这个炼丹炉,它将 CFFI 的繁琐操作,炼化为简洁的“点”操作,让你在 Lisp 中写 C 代码,如同行云流水般流畅。

炼丹秘籍:规则与对比

cffi-ops 的炼丹秘籍,就是将 C 中的“点”运算符,映射到 Lisp 中的宏定义,让 Lisp 代码的结构与 C 代码保持一致。

C 语法cffi-ops 语法
x->y.zx->y->z(-> x y z) (注意:xyz 必须与 defcstruct 中定义的符号相同)
&x->y(& (-> x y))
*x([] x)
x[n]([] x n)
&x[n]x + n(& ([] x n))
x.y = z(setf (-> x y) z) 如果 z 是一个变量
(csetf (-> x y) z) 如果 z 是一个 CFFI 指针
A _a, *a = &_a(clet ((a (foreign-alloca '(:struct A)))) ...)
A *a = malloc(sizeof(A))(clet ((a (cffi:foreign-alloc '(:struct A)))) ...)
A _a = *b, *a = &_a(clet ((a ([] b))) ...)
A *a = b(clet ((a b)) ...)

炼丹炉的奥妙:CFFI 指针与 clet

在 Lisp 中,我们无法直接操作 C 的复合类型,因此,绑定和赋值复合类型需要借助 clet (或 clet*) 和 csetf,它们作用于 CFFI 指针,实现对 C 数据的操控。

炼丹师的助手:arrow-macros

cffi-ops 的炼丹炉,是建立在 arrow-macros 的基础上,它提供了 -> 宏,让我们可以像在 C 中一样,轻松地访问结构体成员。因此,cffi-opsarrow-macros 相辅相成,让你在 Lisp 中编写 C 代码,更加得心应手。

炼丹实例:向量加法

让我们来看一个 C 代码的例子:

#include <stdlib.h>
#include <assert.h>

typedef struct {
  float x;
  float y;
  float z;
} Vector3;

typedef struct {
  Vector3 v1;
  Vector3 v2;
  Vector3 v3;  
} Matrix3;

void Vector3Add(Vector3 *output, const Vector3 *v1, const Vector3 *v2) {
  output->x = v1->x + v2->x;
  output->y = v1->y + v2->y;
  output->z = v1->z + v2->z;
}

int main(int argc, char *argv[]) {
  Matrix3 m1[3];
  m1[0].v1.x = 1.0;
  m1[0].v1.y = 2.0;
  m1[0].v1.z = 3.0;
  Matrix3 m2 = *m1;
  Vector3 *v1 = &m2.v1;
  Vector3 *v2 = malloc(sizeof(Vector3));
  ,*v2 = *v1;
  v2->x = 3.0;
  v2->z = 1.0;
  Vector3Add(v1, v1, v2);
  assert(v1->x == 4.0);
  assert(v1->y == 4.0);
  assert(v1->z == 4.0);
  free(v2);
  return 0;
}

使用 cffi-ops,我们可以将其改写为 Lisp 代码:

(defpackage cffi-ops-example
  (:use #:cl #:cffi #:cffi-ops))

(in-package #:cffi-ops-example)

(defcstruct vector3
  (x :float)
  (y :float)
  (z :float))

(defcstruct matrix3
  (v1 (:struct vector3))
  (v2 (:struct vector3))
  (v3 (:struct vector3)))

(defun vector3-add (output v1 v2)
  (clocally
    (declare (ctype (:pointer (:struct vector3)) output v1 v2))
    (setf (-> output x) (+ (-> v1 x) (-> v2 x))
          (-> output y) (+ (-> v1 y) (-> v2 y))
          (-> output z) (+ (-> v1 z) (-> v2 z)))))

(defun main ()
  (clet ((m1 (foreign-alloca '(:array (:struct matrix3) 3))))
    (setf (-> ([] m1 0) v1 x) 1.0
          (-> ([] m1 0) v1 y) 2.0
          (-> ([] m1 0) v1 z) 3.0)
    (clet* ((m2 ([] m1))
            (v1 (& (-> m2 v1)))
            (v2 (foreign-alloc '(:struct vector3))))
      (csetf ([] v2) ([] v1))
      (setf (-> v2 x) 3.0
            (-> v2 z) 1.0)
      (vector3-add v1 v1 v2)
      (assert (= (-> v1 x) 4.0))
      (assert (= (-> v1 y) 4.0))
      (assert (= (-> v1 z) 4.0))
      (foreign-free v2))))

而没有使用 cffi-ops 的 Lisp 代码则更加冗长:

(defpackage cffi-example
  (:use #:cl #:cffi))

(in-package #:cffi-example)

(defcstruct vector3
  (x :float)
  (y :float)
  (z :float))

(defcstruct matrix3
  (v1 (:struct vector3))
  (v2 (:struct vector3))
  (v3 (:struct vector3)))

(declaim (inline memcpy))
(defcfun "memcpy" :void
  (dest :pointer)
  (src :pointer)
  (n :size))

(defun vector3-add (output v1 v2)
  (with-foreign-slots (((xout x) (yout y) (zout z)) output (:struct vector3))
    (with-foreign-slots (((x1 x) (y1 y) (z1 z)) v1 (:struct vector3))
      (with-foreign-slots (((x2 x) (y2 y) (z2 z)) v2 (:struct vector3))
        (setf xout (+ x1 x2) yout (+ y1 y2) zout (+ z1 z2))))))

(defun main ()
  (with-foreign-object (m1 '(:struct matrix3) 3)
    (with-foreign-slots ((x y z)
                         (foreign-slot-pointer
                          (mem-aptr m1 '(:struct matrix3) 0)
                          '(:struct matrix3) 'v1)
                         (:struct vector3))
      (setf x 1.0 y 2.0 z 3.0))
    (with-foreign-object (m2 '(:struct matrix3))
      (memcpy m2 m1 (foreign-type-size '(:struct matrix3)))
      (let ((v1 (foreign-slot-pointer m2 '(:struct matrix3) 'v1))
            (v2 (foreign-alloc '(:struct vector3))))
        (memcpy v2 v1 (foreign-type-size '(:struct vector3)))
        (with-foreign-slots ((x z) v2 (:struct vector3))
          (setf x 3.0 z 1.0))
        (vector3-add v1 v1 v2)
        (with-foreign-slots ((x y z) v1 (:struct vector3))
          (assert (= x 4.0))
          (assert (= y 4.0))
          (assert (= z 4.0)))
        (foreign-free v2)))))

两种代码在 SBCL 上生成几乎相同的机器码,性能也十分接近。

总结

cffi-ops 为我们提供了一种简洁高效的方式,让我们在 Lisp 中编写 C 代码,如同在 C 中一样自然流畅。它将 CFFI 的复杂操作,炼化为简洁的“点”操作,让我们可以专注于代码的逻辑,而不用被繁琐的类型信息所困扰。

参考文献

  1. cffi-ops
  2. arrow-macros
  3. CFFI
0 0 投票数
Article Rating
订阅评论
提醒
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x