Chap-15. スクリプト系言語 と C言語 の連携

2019-09 初稿(guile-2.2.6)、2023-07 時点修正(guile-3.0.9)、2024-05 時点修正、2024-10 lua 追加



 前段は Scheme(guile) で後段は lua です。

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 から呼び出す

Sec-11.Lua のインストール
Sec-12.C言語の中で lua スクリプトを実行する
Sec-13.C言語の中で lua 環境にデータを与えたり、lua 環境からデータを持ってきたりする
Sec-14.Lua スクリプトから Cの関数を呼ぶ

Sec-31.組み込み等やるときの注意点、特に guile




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つが良かった。と言うか 2019-09 年時点では他に適当なサイトが見つかりませんでした。
なお、現時点 (2024-05) では上記2つは閉鎖されています。良い解説だったので残念です。

参考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 系仕様になおしてコンパイル・実行してみました。

***パターン1***
まず 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/ ってディレクトリの中にパスを書いたファイルをいれるか、そのどっちかだと思います。
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 ライブラリも自動的にインストールされますから、パスを通してキャッシュを更新するだけです。



***パターン2***
システム標準の開発セット(がある場合はそれ)を使ってみます。
以下のようにします。

# cd /usr/include/
# ln -s guile/3.0/libguile   ./libguile
# ln -s guile/3.0/libguile.h   ./libguile.h

とします。これでほとんどコンパイルオプションなしでコンパイル出来ます。先程の例だと

$ gcc sample.c   -lguile-3.0  または  $ cc sample.c   -lguile-3.0

です。
システム標準のものを使うとコンパイルオプションをほとんどつけなくても作業できるので楽です。ただしディストリによってはポイントリリース番号で1つか2つくらい古い、笑。



***パターン3***
 今度は自分でコンパイルした 3.0.9 を /usr/local/ にインストールします。このパターンではシステム標準の guile-3.0-dev があるとそれとバッティングします。ただしメジャー番号もマイナー番号も一緒でポイントリリース番号が1つか2つ違うだけ、なのでバッティングしても支障ないです。so ファイルのロード (/etc/ld.so.conf) 順は /usr/local/lib をシステム標準より優先とします。
先ほどと同じように

# cd /usr/local/include/
# ln -s guile/3.0/libguile   ./libguile
# ln -s guile/3.0/libguile.h   ./libguile.h

とします。これでほとんどコンパイルオプションなしでコンパイル出来ます。先程の例だとパターン2と同様に

$ gcc sample.c   -lguile-3.0  または  $ cc sample.c   -lguile-3.0

です。/usr/local/ に入れると、いろいろインストールしていくうちにぐちゃぐちゃになりがち。そのうち管理できなくなるかもしれませんが、コンパイル上は楽です。
既存のソフトをいじる場合、もとの Makefile がややこしくて、そこにまたコンパイルオプションをいっぱいつけるのは避けたいです。
そんなときは、パターン2か3が楽だと思います。

適当に、お好きにどうぞ。以下の例はパターン2か3でやる例とします。





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 を以下のようにコンパイルします。

$ gcc exm1.c -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"
(パターン1の場合です)

OPT="-lguile-3.0"
(パターン2または3の場合です)


注: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.10 対応)
最初の関数は "abcde\n" でも出力出来るようになっています。試したのは Slackware-15、Debian-12、Ubuntu-24.04 + 自前で入れた guile-3.0.10 です。
しかし依然として第二の関数と第三の関数はおかしい。
ただし、こんな風にして C の関数を scheme から呼び出す際に「単に出力するだけの関数」なんて使うはずはありません。このため実用上は(多分・・)問題ないと思います。
更に加えていいますと、scheme スクリプトをCプログラム中で実行するのが主な利用形態でしょう。なので、そもそも scheme スクリプト中でCライブラリやシステムコールを呼び出すようなことはほぼ無いだろうと思います。

修正:2024-07
 gcc-11 とそれ以降(gcc-12、gcc-13、gcc-14)ではコンパイルオプションの仕様が変わっています。
gcc-11 だと
$ gcc -lguile-3.0 sample.c
でも
$ gcc sample.c -lguile-3.0
でもコンパイルできました。
それ以降の gcc だと
$ gcc sample.c -lguile-3.0
でないとコンパイル出来ません。
$ gcc -lguile-3.0 sample.c
だとエラーとなります。



いかがでしょうか? 参考AからCまでの主だった例を実行してみました。どれも、何となく役に立ちそうな気がします。

 なお、最後の例を見てCの関数を so 化して使ってみようと考えたら、まず guile のマニュアルをよく見ることをおすすめします。随分とたくさんの関数が予め組み込まれています。自分でラッピングしなくても既にたくさんあります。









ここから下は lua です

Sec-11.Lua のインストール

 lua は自分でコンパイル・インストールしました。2024-10 時点だと 5.4.7 です。
(コンパイルするときに必要な開発ライブラリ等は Chap-7. を参考にしてください)
prefix=/usr/local としました。
もちろん、システム標準パッケージの lua でも同様に出来ると思います。

組み込みのCプログラムをコンパイルするときは、たいてい下記のとおりです。

$ gcc hoge.c -llua -lm

これでいけなければ、-ldl を追加すれば大丈夫でしょう。
ここから先は以上の前提で話を進めます。





Sec-12.C言語の中で lua スクリプトを実行する

 いきなりサンプルを示します。scheme と同じ様な感じ。分かりやすいと思います。

Cプログラム (sample.c)
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
int main(void) {
    lua_State *L = luaL_newstate();  ---  lua 実行環境を Cプログラム中に作る
    luaL_openlibs(L);         ---  lua 実行に必要なライプラリをロードする
    luaL_dofile(L, "sample.lua");   ---  lua スクリプト実行を指示する
    lua_close(L);           ---  lua 実行環境を閉じる
    return 0;
}


Lua スクリプト (sample.lua)
--
--
print "hello lua"
print "this is sample"
--


コンパイル及び実行
$ gcc sample.c -llua -lm
$
$ ./a.out


実行結果
hello lua
this is sample


 後で lua スクリプトの内容を変えても Cプログラムの再コンパイルは不要で、Scheme とこれは同じです。
schame では scheme 空間を作るだけで閉じる操作はやりませんでした。「呼び出し元の関数等が終われば自動的に scheme 空間が消滅するので、あえて閉じなくても良い」って判断だと思います。
一方で lua は丁寧に閉じています。そういう作法を lua 開発者さん(イエルサレムスキー教授)が選択したようです。
どっちかというと全体に少しゆるい気がする lua で、ここの作法は妙に丁寧です、笑。




Sec-13.C言語の中で lua 環境にデータを与えたり、lua 環境からデータを持ってきたりする

 Cから lua 環境にデータを与えるのは、基本的に以下の感じです。

 サンプルとしては以下のような感じです。





Cプログラム (sample2.c)

#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>


int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
    lua_pushstring(L, "aaaaa");      ---スタックに文字列を積む
    lua_pushstring(L, "bbbbb");      ---スタックに文字列を積む
    lua_setglobal(L, "data1");       ---スタックから降ろして lua 内の data1 にあてがう(持ち込む)
    lua_setglobal(L, "data2");       ---スタックから降ろして lua 内の data2 にあてがう(持ち込む)
    luaL_dofile(L, "sample2.lua");     ---スクリプトを実行する
    lua_close(L);
    return 0;
}




Lua スクリプト (sample2.lua)
--
--
print (data1 .. "   " .. data2)
--




コンパイル及び実行
$ gcc sample2.c -llua -lm
$
$ ./a.out


実行結果
bbbbb     aaaaa


基本的にこんな感じです。







 lua 環境からデータをCプログラム側に持ってくるのは、基本的に以下の感じです。


 サンプルとしては以下のような感じです。



Cプログラム (sample3.c)

#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

int main(void) {
    const char * p, * q;
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
    luaL_dofile(L, "sample3.lua");        --- まず lua 内での変数の値を決める
    lua_getglobal(L, "data1");          --- lua 内の変数の値をスタックに積む
    lua_getglobal(L, "data2");          --- 同上
    p = lua_tostring(L, -1);           --- スタックの変数をCプログラム内のポインター p がさすようにする
    q = lua_tostring(L, -2);           --- 同上 (q)
    printf("%s  ****  %s\n", p, q);
    lua_pop(L, 2);                --- スタックを空にする
    lua_close(L);
    return 0;
}



Lua スクリプト (sample3.lua)      --- lua 空間内でデータを定義するだけ
--
data1 = "aaaaa"
data2 = "bbbbb"
--




コンパイル及び実行
$ gcc sample3.c -llua -lm
$
$ ./a.out


実行結果
bbbbb ****  aaaaa


基本的にこんな感じです。

sec-11 から 13 まであれば既存ソフトを改造して組む込みして簡単なスクリプトを既存ソフト実行時に利用するくらいは出来ると思います。






Sec-14.Lua スクリプトから Cの関数を呼ぶ

 基本的なやり方は scheme に似ています。Cの関数をシェアードオブジェクト化し、lua から呼び出せるように登録します。
登録したら、使うときは lua でシェアードオブジェクトをロードして関数をコールするだけです。


#include <time.h>
#include <math.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>


#define NAME  "cfunc_value_time"


int lctime(lua_State * L){
  struct timespec  p;
  double  a;
  clock_gettime(CLOCK_MONOTONIC, &p);
  a = (double)(floor(p.tv_nsec / 1000000)) / 1000.0;
  a = a + (double)p.tv_sec;
  lua_pushnumber(L, a);
  lua_setglobal(L, NAME);
  return 1;
}

int luaopen_lctime(lua_State * L){
  lua_register(L, "lctime", lctime);
  return 1;
}


************** compile ****************
gcc  hoge.c -llua  -shared -o lctime.so -fPIC


**************  使い方  ****************
$ lua
> require "lctime"
./lctime.so
> lctime()
function: 0x564eaed867e0
> cfunc_value_time
665.525
> lctime()
function: 0x564eaed61fe0
> cfunc_value_time
676.102
> lctime()
function: 0x564eaed87ad0
> cfunc_value_time
680.111

こんな感じで、ミリセコンドの時間が出力される。lua で lctime() を呼ぶたびに Cの関数が実行されて、時間を lua 実行空間内のグローバル変数 cfunc_value_time に書き込む。
(大域変数でデータの受け渡しをしているので、ちょっと無骨なやり方)




以下は、上記のを利用した stp-watch.lua のソース

--
require ("lctime")
--
function stop_watch()
    local t = 0
    local obj = {}
    local function set()
        lctime()
        t = cfunc_value_time
        return true
    end
    local function get()
        lctime()
        return cfunc_value_time - t
    end
    obj.set = set; obj.get = get;
    return obj
end





以下は、ストップウォッチの実行結果。set() で時間を0にする。get() でそれからの経過時間を得る。

> dofile "stp-watch.lua"
> p = stop_watch()
> p.set()
true
> p.get()
2.409
> p.get()
8.828
> p.get()
10.057
> p.get()
10.988
> p.set()
true
> p.get()
1.489
>
> q = stop_watch()
> q.set()
true
> p.get()
18.381
> q.get()
9.779
>

こんな具合に、同時に p、q 独立でストップウォッチとして使える。いくつでも可。単位はmm-sec
Lua プログラム実行中に時間で制御するときに、この関数が必要になったりします。
scheme では scheme からCの関数を呼ぶ機能は全く使いませんでした、すくなくともこれまで。
しかし lua のこの機能はストップウォッチで実際に使いました。使ったのはそれだけですけど・・・、笑。





Sec-31.組み込み等やるときの注意点、特に guile

 各種解説などによると lua も guile も実行時にはバイトコンパイルして高速化を図っているらしい。lua は逐次やっているもようですが、guile は script.scm をバイトコンパイルしてらそれ(script.go)を一旦保存します。次回実行するときはそれを再利用して、バイトコンパイルの時間短縮を図っているようです。
保存する場所は実行ユーザのホームディレクトリ内。$HOME/.cache/hoge/hogehoge/script.go みたいな感じです。
例えばサーバが root で実行されていて、その中で guile スクリプトを走らせるなら、/root/.cache/hoge/hogehoge/script.go となります。それは特に何も問題ないです。
問題になるのは親プロセス(root)が子プロセスを作り、それにサーバ業務を任せている形。その時、子プロセスで guile を実行しているときに時として問題が発生します、笑。
というのは、セキュリティ上の理由から大抵の場合に子プロセスは root 以外のユーザを割り当てているからです。例えば daemon のアカウントで子プロセスを実行しその中で guile スクリプトが走るなら、 daemon のホームディレクトリ内に go ファイルを保存しようと試みます。
ところが、daemon のホームは /bin、/sbin、/usr/sbin、/usr/bin とかです。で、その所有者は root:root でディレクトリの mode は 755 とか 555 (RedHat クローン)とかです。なので、ここには daemon は書き込みができず、バイトコンパイルに失敗したというエラーメッセージが出てきます、汗。
この場合の対処法は、daemon に普通のホームディレクトリを与えて go ファイルが書き込み出来るようにすることです。
例えば /usr/local/home/daemon/ とか /home/daemon/ とか。
そこは所有者が daemon:daemon で mode が 700 とか 750 とか 770 とかにしておくと良いと思います。
それで guile がエラーなく実行されるようになります。

なお、lua ではそのような特段の工夫は必要無いです。




Sec-31. なんて離れた番号を選びましたが、特に意味はありません、笑。





  







一つ前の版  https://www.quinos.net/topicf/topicf.01.html