Chap-15. Guile と C言語 の連携

Sec-1.FFI とは
Sec-2.参考にしたサイト
Sec-3.C言語でのコンパイル
Sec-4.サンプルプログラムA
Sec-5.サンプルプログラムAの別バージョン
Sec-6.Cプログラムの中で scheme の変数を定義する
Sec-7.Scheme 内部で定義された変数をCで読み出す
Sec-8.引数の数を変えてみる
Sec-9.Cの関数を guile から呼び出す

2019-09 初稿(guile-2.2.6)、2023-07 時点修正(guile-3.0.9)




Sec-1.FFI とは

 Foreigne function interface と言います。他言語との連携って意味ですね。
Scheme は言語仕様として FFI とか、実用的なプログラミングに必要なライブラリが定められていません。やむなく、各実装が独自のやり方で FFI 機能や種々のライブラリを盛り込んでいます。
C言語は UNIX 系システムの標準言語なので UNIX プログラミングインターフェイスもC言語の関数で与えられます。世の中にある実用ライブラリも多くはC言語です(最近はpython、php、ruby 等など、色々あるようですが)。
なので、Scheme でシステムの関数を使おうと思ったらC言語との連携が必要です。

深入りする気はないですが、ちょっと興味があったので調べてみました。いろいろな実装系で FFI 機能が実現されています。もちろん、guile も。
ただし、解説した書籍は無いし、解説しているサイトも少ないです。たまたま見つけても内容が古かったりします。あるいは、とびきり上級者向けすぎる難しい内容のものとか・・・もちろん、そんなのは私に理解できません。

 そこで、既に世の中にある初級者向け情報をここでちょいと自分のメモ代わりにまとめてみます。私が解説するのではなく、既にあるもののデッドコピーとか時点修正版です。

注:以下では動的なライブラリをシェアードライブラリ(so)と呼びます。以前、私はダイナミックリンクライブラリ(DLL)と呼びました。wiki で見たら後者の言い方はWindows系なんだそうです。なので、UNIX 前提としては、シェアードライブラリ(so)と呼ぶのが良さそうです。



Sec-2.参考にしたサイト

 書物で参考になるような物がないので、ネット上にあるサイトを参考にします。私が探した範囲では以下の3つが良かった。と言うか、他に適当なサイトが見つからなかった。

参考A.「Guile によるスクリプティング」
URL:https://www.ibm.com/developerworks/jp/linux/library/l-guile/
IBMのサイト。
2009年1月の版。中で使われている関数が古くて、現在は仕様変更されているので、参考プログラムのままでは動かない。
ただし、内容は初級向けにわかりやすく解説されている。
参考Bと重複する。

参考B.「How to extend C programs with Guile」
URL:http://www.lonelycactus.com/guilebook/book1.html
英語版の解説サイト。
Guile 1.6.X バージョンが前提。かなり古そう。
1.6.X というと、2002年頃。サンプルプログラムの関数は、既に仕様変更されているので時点修正しないと動かない。
ただし、内容的には初級向けに丁寧に解説されている。
参考Aと重複する。

参考C.「GNU Guile 2.2.6 Reference Manual」 「GNU Guile 3.0.9 Reference Manual」
URL:https://www.gnu.org/software/guile/manual/
そのものずばり、guile のマニュアル。英語。
2.2.6 系列及び現在の最新版 3.0.9 系列での FFI 実現の仕方がわかる。
ただし、マニュアルなのでそれほど解説風記述にはなっていない。あっさり機能説明しているだけ。


以下では、上記3つのサイトから(真似て、あるいはそのままデッドコピーして)FFI について書きます。まぜこぜに、突き合わせて書くので、どの部分がどのサイトを参考にしているとかは、説明のしようがありません。



Sec-3.C言語でのコンパイル

 いきなり、筋違いの話題から入ります。C言語で書いたプログラムで guile と連携させるときにシェアードライブラリ(以下、soとする)の扱いに要注意って話です。私はここで一回つまずきました。
そして、その時始めて自分で処理系をコンパイル・インストールするときに so の扱いに注意しないといけないことに、気が付きました。
Guile を使うために apt install するときも同じことが起こりえます。

 最初に、参考A.のサンプル・プログラムで使われている関数を今の guile-3.0.9 系仕様になおしてコンパイル・実行してみました。
私の場合、guile は/usr2/local/guile/ にインストールします。ちょっとクセのあるインストールの仕方。
で、ライブラリの位置は


/usr2/local/guile/lib/

インクルードするヘッダの位置は


/usr2/local/guile/include/

になります。なのでコンパイルオプションとしては以下の感じ。


$ cc sample.c -I/usr2/local/guile/include/guile/3.0/ -L/usr2/local/guile/lib/ -lguile-3.0

無事にコンパイルが終了して、以下のように実行したら、いきなりエラーが出ました。実はこのエラーは、so にパスが通っていないことが原因です。

$ ./a.out

./a.out: error while loading shared libraries: libguile-3.0.so.1: cannot open shared object file: No such file or directory

そもそも、コンパイルするときにライブラリの位置を指定しているので、てっきり so も使える状態になると思っていました。しかし、コンパイルするとき指定するパスは静的リンクのライブラリであって、動的にリンクする so は、も一回、システムに対してパスの指定が必要です。
このため、ネットで so のパスをとおす方法を調べました。

多くの Linux ディストリで、so の扱いは、/etc/ld.so.conf ってファイルにパスを書き込むか、あるいは、/etc/ld.so.conf.d/ ってディレクトリの中にパスを書いたファイルをいれるか、そのどっちかだと思います。Slackware は前者。Devuan (Debian, Ubuntu)は後者です。
so がそのプログラムでどうなっているか、読み込めるのかどうかを ldd コマンドで見てみると・・・


$ ldd a.out
    linux-vdso.so.1 (0x00007ffefd7e4000)
    libguile-2.2.so.1 => not found
    libc.so.6 => /lib64/libc.so.6 (0x00007fdfae57e000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fdfaeb49000)

 こんな具合で、libguile-3.0.so.1 が見つからない状態です。なので、これを見つけられるようにします。そのためには、上記のように、so の位置を /etc/ld.so.conf ファイルに書き込むか、あるいは、書き込んであるファイルを /etc/ld.so.conf/d ディレクトリに置きます。
で、それをやってから # ldconfig して so のパスを更新し、もう一度、ldd で soが 見つけられるかどうか試してみると

$ ldd a.out
    linux-vdso.so.1 (0x00007ffecbdf4000)
    libguile-2.2.so.1 => /usr2/local/guile-2.2.4/lib/libguile-2.2.so.1 (0x00007f24d238b000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f24d1fc2000)
    libgc.so.1 => /usr/lib64/../lib64/libgc.so.1 (0x00007f24d1c5f000)
    libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f24d1a42000)
    libffi.so.6 => /usr/lib64/../lib64/libffi.so.6 (0x00007f24d183a000)
    libunistring.so.0 => /usr/lib64/../lib64/libunistring.so.0 (0x00007f24d1525000)
    libgmp.so.10 => /usr/lib64/../lib64/libgmp.so.10 (0x00007f24d12af000)
    libltdl.so.7 => /usr/lib64/../lib64/libltdl.so.7 (0x00007f24d10a6000)
    libdl.so.2 => /lib64/libdl.so.2 (0x00007f24d0ea2000)
    libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007f24d0c6a000)
    libm.so.6 => /lib64/libm.so.6 (0x00007f24d0961000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f24d28b8000)

となって、ちゃんと全部 so が読み込める状態になります。つまり、プログラムが実行可能になります。
もしも、このチャプタに書いてあることを試してみて、上記のようなエラーが出たら、so のことを思い出してください。
apt install guile で guile を使えるようにして、それで上記の shared library error が出たら、guile だけでなく guile の開発用パッケージも、インストールする必要があるってことだと思います。
自分でコンパイルしてインストールする場合は、so ライブラリも自動的にインストールされますから、パスを通してキャッシュを更新するだけです。




Sec-4.サンプルプログラムA

 ようやくサンプルプログラムです。Cプログラムの中で、Schemeのスクリプト(関数)を実行してみます。まずはリスト( exm1.c )から。

#include <stdio.h>
#include <libguile.h>
 
int main( int argc, char **argv )
{
  SCM func;                            /** scheme 変数のfunc を宣言、関数を見つけたとき受け止められるようにしておく **/
  scm_init_guile();                      /** guile インタプリタをC実行環境にロードする **/
  scm_c_primitive_load( "exm1.scm" );     /** guile インタプリタが exm1.scm という名のリストを読む **/
  func = scm_variable_ref( scm_c_lookup( "do-hello" ) );     /** そのリスト中にある do-hello って関数を見つける  **/
  scm_call_0( func );                    /** その関数を実行する  **/
  return 0;
}

上記の exm1.c では Cの実行環境の中に guile インタプリタをロードした後、scheme ファイル exm1.scm をロードします。

;;
;; exm1.scm
;;                                                                                    
(define do-hello
  (lambda ()
    (display "Hello world.")
    (newline)
    (display "yahoo !!! i am here")
    (newline)))
;;

exm1.scm の中で実際に実行される scheme の関数の do-hello が定義されています。二行に渡って実行されたことを喋るスクリプトです。
私が guile をインストールした環境では、exm1.c を以下のようにコンパイルします。

$ cc exm1.c -I/usr2/local/guile/include/guile/3.0/ -L/usr2/local/guile/lib/ -lguile-3.0

それで、./a.out と実行しますと

$ ./a.out
Hello world.
yahoo !!! i am here
$ 

こんな感じに表示されます。成功です。
参考AやBに書かれていることは「この例で示すサンプルは、実行時にスクリプトの内容が決まる。まさにインタプリタの特質を備えている」
ってことだそうです。なので、Cプログラムをコンパイルしたあとで、scheme のスクリプトを直して、例えば下のようにすると

;;
;; exm1.scm 修正後
;;                                                                                    
(define do-hello
  (lambda ()
    (display "Hello world.")
    (newline)))
;;

当然、実行結果は

$ ./a.out
Hello world.
$ 

のように、変わります。つまり実行時に初めてスクリプトの中身が決まるわけです。schemeインタプリタの柔軟性がCの実行環境の中に持ち込まれます。
(^^)
Cプログラムの中にscheme スクリプトを持ち込むと、開発時に逐次内容を修正できて、しかもCプログラムを再コンパイルする必要がありません。

なお、関数の詳しい説明は参考C(guile マニュアル)に書いてあります。

一つ、scm_c_lookup と scm_variable_ref についてだけ、説明しておきます。前者は do-hello という scheme 内の関数を指しているポインターを見つけ出します。後者は、そのポインターが指している do-hello 関数の実体を見つけます。なので、2つの関数を使って func という scheme 空間内の変数が do-hello 関数の実体を指すようにします。
後は、引数ゼロの scheme 空間内の do-hello 関数を C から実行する・・・・というようなことが参考AとBに書いてあったと思います。




Sec-5.サンプルプログラムAの別バージョン

Sec-4 のサンプルプログラムは、guile の FFI を説明するとき、たいてい出てくるサンプルですが、同様に次のリストもよく出てきます。同じことをやる別バージョンです。

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

static void inner_main (void *closure, int argc, char **argv);

int main (int argc, char **argv)
{
  scm_boot_guile (argc, argv, inner_main, 0);

  // Never gets here                                                                                
  return(EXIT_SUCCESS);
}

static void inner_main (void *closure, int argc, char **argv)
{
  SCM func_symbol;
  SCM func;

  scm_c_primitive_load ("exm1.scm");
  func_symbol = scm_c_lookup("do-hello");
  func = scm_variable_ref(func_symbol);
  scm_call_0 (func);

  exit(EXIT_SUCCESS);
}


exm1.scm は上記と同じです。ようするに、scheme 実行環境をCプログラムの中に実現するとき、scm_init_guile 関数を使うのか、scm_boot_guile 関数を使うのか、の違いです。
参考Bの中では「後者のほうがCコンパイラの移植性が良い」と書かれています。
でも、幸い、Linux +guile であれば、前者が使えます。無理にこのセクションのようにややこしく書く必要性はないです。前者で行きましょう(^^)




Sec-6.Cプログラムの中で scheme の変数を定義する

 Sec-4.でCプログラムの中で scheme スクリプトが実行できることがわかりました。今度は、Cプログラムの中で scheme 空間の変数を定義したり、値を変えたりできる例です。

/** まずCプログラム  exm4.c **/
#include <stdio.h>
#include <stdlib.h>
#include <libguile.h>

#define AGE      20
#define HGT     180
#define WGT      80

int main()
{
  /* Start up the Guile interpeter */
  scm_init_guile();

  /* Define some Guile variables from C */
  scm_c_define("my-age",    scm_from_int(AGE));
  scm_c_define("my-height", scm_from_int(HGT));
  scm_c_define("my-weight", scm_from_double(WGT));

  /* Run a script that prints out the variables */
  scm_c_primitive_load("exm4.scm");

  return(EXIT_SUCCESS);
}


;;
;; 次に scheme スクリプト exm4.scm。関数定義していなくて、読み込まれると単純に処理を実行します。
;; called from exm4.c
;;
(display "my-age    --> ")
(display my-age)
(display " years")
(newline)
;
(display "my-height --> ")
(display my-height)
(display " cm")
(newline)
;
(display "my-weight --> ")
(display my-weight)
(display " kg")
(newline)
;;

これをさっきと同じように動かします。exm4.c をコンパイルして実行します。

$ ./a.out
my-age    --> 20 years
my-height --> 180 cm
my-weight --> 80 kg

こんな感じに出力されます。年齢・身長・体重は私の値ではありません。
狙いどおり、Cプログラムの中で scheme スクリプトが実行されていて、しかも年齢・体重・身長はCから定義しています。
変数を定義するとき、scheme の中では、(define my-age 20) とかってやりますが、それを同じことをCプログラムの中で実現しています。
(注:Cプログラムの中でCの変数を定義するのではなく、Cプログラムの中で scheme 空間の変数を定義)
なお、各関数の詳しい説明は guile のマニュアルを見てください。




Sec-7.Scheme 内部で定義された変数をCで読み出す

 今度は scheme 空間内で定義された変数をCから読み出して、利用します。まずサンプルプログラムです。

/*** サンプルプログラム exm5.c ****/
#include <stdio.h>
#include <stdlib.h>
#include <libguile.h>


int main( int argc, char **arg ){
  SCM s_symbol, s_value;
  int c_value;
  c_value = 999;

  scm_init_guile();
  scm_c_primitive_load( "exm5.scm" );

  s_symbol = scm_c_lookup("test-data");
  s_value = scm_variable_ref(s_symbol);
  if(scm_is_number(s_value)){
    /*  if(scm_number_p(s_value)){*/
    c_value = scm_to_int(s_value);
  }
  printf("c_value = %4d\n", c_value);
 
  return 0;
}

 test-data の値が数値ならその値を代入するけれど、値が数値でなければデフォルトの 999 の値をとります。
次は、ロードされる scheme スクリプト、単純に値を定義しているだけです。

;; exm5.scm
;;
(define test-data  "abc")
;;

 この例だと出力結果は 999 になります。それで test-data の値を例えば 100 とかにすると、今度はその値が収納されるので、出力結果が 100 になります。
例によって、これらは実行時に決定されるので、Cプログラム側は再コンパイルする必要、ありません。scheme スクリプトだけ修正すれば、それで良しです。




Sec-8.引数の数を変えてみる

 今度は、引数の数を変えて、Cから scheme の関数を呼ぶ例です。あんまり説明の必要がないサンプルです。

/*** exm6.c *****/
#include <stdio.h>
#include <libguile.h>

int main (int argc, char ** argv){

  SCM func_symbol;
  SCM func;
  SCM ret_val;
  double subtotal, tax, shipping, total;
  double weight, length;

  scm_init_guile();

  /* Load the scheme function definitions */
  scm_c_primitive_load ("exm6.scm");

  /* Call a thunk, a procedure with no parameters */
  func_symbol = scm_c_lookup("do-hello");
  func = scm_variable_ref(func_symbol);
  scm_call_0 (func);

  subtotal = 80.00;
  weight = 10.0;
  length = 10.0;

  /* Call a procedure that takes one argument */
  func_symbol = scm_c_lookup("compute-tax");
  func = scm_variable_ref(func_symbol);
  ret_val = scm_call_1 (func, scm_from_double(subtotal));
  tax = scm_to_double(ret_val);

  /* Call a function that takes two arguments */
  func_symbol = scm_c_lookup("compute-shipping");
  func = scm_variable_ref(func_symbol);
  ret_val = scm_call_2 (func, scm_from_double(weight), scm_from_double(length));
  shipping = scm_to_double(ret_val);

  total = subtotal + tax + shipping;
  printf("Subtotal %7.2f\n", subtotal);
  printf("Tax      %7.2f\n", tax);
  printf("Shipping %7.2f\n", shipping);
  printf("-----------------\n");
  printf("Total    %7.2f\n", total);

  return(EXIT_SUCCESS);
}

 次がCから呼ばれる scheme のスクリプト、exm6.scm です。

;; exm6.scm
;;
(define (do-hello)
  (begin
    (display "Welcome to FloorMart")
    (newline)))

(define (compute-tax subtotal)
  (* subtotal 0.0875))


(define (compute-shipping weight length)

  ;; For small, light packages, charge the minimum
  (if (and (< weight 20) (< length 5))
      0.95

      ;; Otherwise for long packages, charge a lot
      (if (> length 100)
       (+ 0.95 (* weight 0.1))

      ;; Otherwise, charge the usual
       (+ 0.95 (* weight 0.05)))))
;;
;;

 呼び出す関数の引数が違うだけで、やってることはシンプルです。説明なしでもオケですね。
出力結果は下のような感じです。

$ ./a.out
Welcome to FloorMart
Subtotal   80.00
Tax         7.00
Shipping    1.45
-----------------
Total      88.45




Sec-9.Cの関数を guile から呼び出す

 今度は、Cの関数を guile から呼び出してみます。
今までは、まずCプログラムを立ち上げて、その中で guile インタープリタをロードし、さらに guile のスクリプトを実行したりしました。
今度は、まず guile スクリプトを実行し、そのスクリプトの中でCの関数を実行する方法です。
今までの説明はおおむね参考Aと参考Bが元ネタでした。このセクションは参考Cが元ネタです。

基本的な手順と考え方ですが、Cの関数をラッパーで包んで guile から呼び出せるようにしておき、これを so にします。
guile を立ち上げたら、この so をロードして使える状態にして、それから guile でC関数を実行します。
以下が、リストです。

/*** exm7.c C関数を guile で使えるように、ラップする *****/
#include <math.h>
#include <libguile.h>

/** j0_wrapper は、Cのライブラリ関数j0 をラップする関数 **/
SCM j0_wrapper (SCM x){
  return scm_from_double (j0 (scm_to_double (x)));
}


/** これは、ラッパーをscheme のサブルーチンとして、使えるようにする(初期化)**/
void init_bessel (){
  scm_c_define_gsubr ("j0-scm", 1, 0, 0, j0_wrapper);
}

ここで示している j0 って、ベッセル関数だそうです。だそうですってのは無責任な言い方ですが、サンプルプログラムがそうなっていたので、流用しています。
なので、それがなんの関数なのか、私は知りません(汗)。気にせずに先に話を進めます。
本題は guile からCの関数を呼び出すことです(キリッ!)
続いて、このコンパイルですが、so にしないといけないので、以下のようにコンパイルします。

$ gcc exm7.c $OPT -shared -o libguile-bessel.so -fPIC

OPT="-I/usr2/local/guile/include/guile/3.0/ -L/usr2/local/guile/lib/ -lguile-3.0"
(私の環境の場合です、各自の環境にあわせてください)

注:option の意味                                                                                                          
  -shared   --------------- シェアードライブラリ化する。                                                                            
  -o libguile-bessel.so --- そのライブラリの名前                                                                        
  -fPIC  もしくは  -fpic (どっちでも可)は、位置独立なコードとする。シェアードライブラリに適したコード化。

 こんな感じでコンパイルすると、libguile-bessel.so ってsoが出来ます。後は、guile からこれをロードすればオケです。

> (load-extension "./libguile-bessel" "init_bessel") 

実行してみます。

> (j0-scm 2)                                                                                        
$1 = 0.22389077914123567                                                                                               

ベッセル関数で引数を2にすると、値は $1 = 0.22389077914123567 となるそうです(^^;;。くりかえしますが、ベッセル関数とは何かなんて難しいことは考えないことにします。



 今の例は、ベッセル関数などという名を見ただけでやる気のなくなる例でした。
なので、今度は誰もが知ってる関数を使って、もう一度サンプルやってみます。3つの関数を guile から使えるようにします。単純な関数です。
下のリストを見てください。

#include <stdio.h>
#include <libguile.h>

void disp1_wrapper (void){
  printf("abcde");
  putchar('\n');
  return;
}

void disp2_wrapper (void){
  printf("012345");
  putchar('\n');
  return;
}

void disp3_wrapper(void){
  printf("hello world");
  putchar('\n');
  return;
}

void init_disp123(void){
  scm_c_define_gsubr ("disp1-scm", 0, 0, 0, disp1_wrapper);
  scm_c_define_gsubr ("disp2-scm", 0, 0, 0, disp2_wrapper);
  scm_c_define_gsubr ("disp3-scm", 0, 0, 0, disp3_wrapper);
}

先ほどと同じようにコンパイルして so にします。guile を起動して > (load-extension "./libguile-hogehoge" "init_disp123") とかってやります。hoge は適当にライブラリネームを決めてください。それで以下のように実行できます。

scheme@(guile-user)> (disp1-scm)
abcde
$1 = 2
scheme@(guile-user)> (disp2-scm)
012345
$2 = 2
scheme@(guile-user)> (disp3-scm)
hello world
$3 = 2
scheme@(guile-user)>

以上、ただ単に3つの出力プログラムですが、guile とC言語の連携イメージはつかめると思います。

あと少し、補足しておきます。
第一の補足
scm_c_hogehoge とか scm_from_hogehoge ってのが出てきます。
いずれも、guile とCの間のデータやり取りの関数です。
覚えておくのは以下のような感じです。

いずれも、guile の世界からみて from と to を使っていると思えば、覚えられる・・・でしょ?

第二の補足 (guile-2.2.x 対応)
printf の中で "abcde" と出力して、その後で putchar('\n');ってやってます。なぜ "abcde\n" って出力しないか?ってことです。
そうやると、何故か Segmentation fault ってエラーが出ます。
で、なぜだか \n を別途 putchar で出力すると、そのエラーが出ません。ワケワカランです。
あんまり深く考えないことにしますが、出力関数をラップするときは要注意ってことです。


第二の補足の (guile-3.0.9 対応)
最初の関数は "abcde\n" でも出力出来るようになっています。試したのは Slackware-current(kernel 6.1.x 系列)+ guile-3.0.9 です。
しかし依然として第二の関数と第三の関数はおかしい。segmentetion fault みたいにはなりませんが、適正出力ではありません。
この部分のバグらしきものは 2023-07 時点、kernel-6.1.x + guile-3.0.9 でもなおっていないようです。
ただし、こんな風にして C の関数を scheme から呼び出す際に「単に出力するだけの関数」なんて使うはずはありません。このため実用上は(多分・・)問題ないと思います。



いかがでしょうか? 参考AからCまでの主だった例を実行してみました。どれも、何となく役に立ちそうな気がしますが、私が使いこなせる訳ではありません、念の為。
もしも、この例をみてCの関数を so 化して使ってみようと考えたら、まず guile のマニュアルをよく見ることをおすすめします。随分とたくさんの関数が予め組み込まれています。自分でラッピングしなくても既ににたくさんあります。

なお、最後の scheme スクリプトをメインにして C 言語の関数を呼び出す部分です。
時間的に急がないなら system 関数で OS 側のコマンドを実行すれば済む場合が大半だと思います。どうしても直接 C のライブラリ関数やシステムコール関数を呼ぶのは、時間的にゆっくりやれないような時だろうと思います。私は現時点で(2023-07) C の関数を scheme から呼び出す必要性は感じていません。こんなセクションを書いておいてなんなんですけど、、、笑。