标签: CommonLisp

  • 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
人生梦想 - 关注前沿的计算机技术 acejoy.com