読者です 読者をやめる 読者になる 読者になる

cffi を学ぶ(5) 構造体

lisp

cffi を学ぶ(1) 変数と配列 - TIPS
cffi を学ぶ(2) 配列の続きと関数 - TIPS
cffi を学ぶ(3) Cのライブラリとコールバック - TIPS
cffi を学ぶ(4) C のライブラリを使う - TIPS
cffi を学ぶ(5) 構造体 - TIPS
cffi を学ぶ(6) 構造体の続きと translate-*-foreign - TIPS

参考
レファレンス CFFI User Manual

構造体について学んだのでまとめ

cffi では C の構造体はポインタで扱うのが基本

まず C のライブラリを書く

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

typedef struct vec
{
    float x,y,z;
} VEC; 

VEC* ret_vec()
{
    VEC *v = (VEC*)malloc(sizeof(VEC));
    v->x = 1.0; v->y = 2.0; v->z = 3.0;
    
    return v;
}

VEC* vec_plus(VEC *v, VEC *u)
{
    VEC *ans = (VEC*)malloc(sizeof(VEC));
    ans->x = v->x + u->x;
    ans->y = v->y + u->y;
    ans->z = v->z + u->z;
        
    return ans;
}

これを foo.c としてライブラリを作る

gcc -shared foo.c -o foo.so

以下を評価する

(load "~/quicklisp/setup.lisp")
(ql:quickload "cffi")

(defpackage :scratch
  (:use :cl :cffi))

(in-package :scratch)

(define-foreign-library libcffi
  (:darwin "foo.so")
  (:unix "foo.so"))

(use-foreign-library libcffi)

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

;; ライブラリの関数を定義する
(defcfun "ret_vec" (:pointer (:struct vec)))

(defcfun "vec_plus" (:pointer (:struct vec))
  (v (:pointer (:struct vec))) (u (:pointer (:struct vec))))

;; 基本はポインタ渡し、返り値もポインタ
;; 構造体のメンバー(スロット)は with-foreign-slots で束縛できる

;; ret-vec
(with-foreign-object (ptr '(:struct vec))
  (setf ptr (ret-vec))
  (with-foreign-slots ((x y z) ptr (:struct vec))
    (format t "~@{~a~^ ~}" x y z)))
;; => 1.0 2.0 3.0

;; vec-plus 
(with-foreign-objects ((u '(:struct vec)) (v '(:struct vec)) (ans '(:struct vec)))
  (with-foreign-slots ((x y z) u (:struct vec))
    (setf x 1.0 y 1.0 z 1.0)) ;; u = (x 1.0 y 1.0 z 1.0)
  (with-foreign-slots ((x y z) v (:struct vec))
    (setf x 2.0 y 2.0 z 2.0)) ;; v = (x 2.0 y 2.0 z 2.0)
  (setf ans (vec-plus u v)) ;; ans = (x 3.0 y 3.0 z 3.0)
  (with-foreign-slots ((x y z) ans (:struct vec))
    (format t "~@{~a~^ ~}" x y z)))
;; => 3.0 3.0 3.0

ライブラリによっては構造体を値渡ししたいこともある
素の cffi ではできないので lib-ffi をロードする必要がある

まず取り合えず c のライブラリを書く

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

typedef struct vec
{
    float x,y,z;
} VEC; 

VEC ret_vec()
{
    VEC v;
    v.x = 1.0; v.y = 2.0; v.z = 3.0;
    
    return v;
}

VEC vec_plus(VEC v, VEC u)
{
    VEC ans;
    ans.x = v.x + u.x;
    ans.y = v.y + u.y;
    ans.z = v.z + u.z;
        
    return ans;
}

これをコンパイル

gcc -shared bar.c -o bar.so

lisp 側では lib-ffi をロードする。
また構造体は lisp 側では plist として扱うことになる。 C のオブジェクトへの変換、逆変換については lib-ffi がやってくれる。

一度 repl を初期化してから以下を評価する

(load "~/quicklisp/setup.lisp")
(ql:quickload "cffi")
;; 構造体の値渡しには cffi-libffi をロードする
(ql:quickload "cffi-libffi")

(defpackage :scratch
  (:use :cl :cffi)) ;; ここに libffi を書く必要はないらしい

(in-package :scratch)

(define-foreign-library bar
  (:darwin "bar.so")
  (:unix "bar.so"))

(use-foreign-library bar)

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

;; ライブラリの関数を定義する
(defcfun "ret_vec" (:struct vec))

(defcfun "vec_plus" (:struct vec)
  (v (:struct vec)) (u (:struct vec)))

;; 構造体は plist として返る
(ret-vec)
;; => (Z 3.0 Y 2.0 X 1.0)

;; したがって getf でアクセスできる
(getf (ret-vec) 'z)
;; => 3.0

;; 関数の引数には plist を与える
(vec-plus '(x 1.0 y 2.0 z 3.0) '(x 10.0 y 10.0 z 10.0))
;; => (Z 13.0 Y 12.0 X 11.0)

注意
上のように構造体をタイプとして

typedef struct vec {} VEC;

のように書かずに、単に

sturct vec {};

のように書いても、上の例では問題なく動いた。その場合 C 側で関数の引数や返り値の宣言を変更する必要があるが lisp 側では変更の必要はなかった。