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

2019-09 初稿(guile-2.2.6)、何度か補足して 2025-03 版

このチャプターでは一環して「Cプログラムの中にインタープリター系言語を埋め込む」という説明です。
埋め込むための最低限度の記述だけです。エラー処理等は一切書きません(説明が冗長になってポイントがずれるから)
実用で使う際にエラー処理が必要なら、各自でやってください。



 ひと桁番は Scheme(guile)、10番台は lua、20番台は Scheme(chicken)、30番台は Scheme(chibi-scheme) です。

Sec-1.FFI とは
Sec-2.guile 編 -- guile のインストール
Sec-3.C言語の中で guile スクリプトを実行する
Sec-4・C言語の中で guile 環境にデータを与えたり、guile 環境からデータを持ってきたりする
Sec-5・組み込みをするときの注意点

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

Sec-21.chicken 編、 chicken のインストール
Sec-22.C言語の中で chicken スクリプトを実行する
Sec-23.C言語の中で chicken 環境にデータを与えたり、chicken 環境からデータを持ってきたりする

Sec-31.chibi-scheme 編、 chibi-scheme のインストール
Sec-32.C言語の中で chibi-scheme スクリプトを実行する
Sec-33.C言語の中で chibi-scheme 環境にデータを与えたり、chibi-scheme 環境からデータを持ってきたりする





Sec-1.FFI とは

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

パターン1)この場合、欲しい機能として以下のイメージを考えます。左から右にプログラムが進む感じです。

embedding1

これだけの機能を備えて組み込みが出来るのが普通だと思います。





パターン2)
 一度ディスクにデータを書き込んで受け渡しをやるイメージ(下図)です。
このイメージなら組み込みでなく system("hogehoge.scm") のごとく外部プログラムを走らせればすみます。(組み込みをやる必要が無い)

embedding2



パターン3)
 なお、C言語とインタープリター言語間でデータやりとりの必要が無い場合も当然あります。その場合は組み込みを使っても良いし system("hogehoge.scm") と単に外部的にインタープリターのプログラムを走らせるだけでも良いです。実用ではこのパターンが割合とあると思います。








Sec-2.guile 編 -- guile のインストール 

 Chap-7. にコンパイルのことを書いています。
prefix は /usr/local にしておくのが良さそうです。インタープリターとして使うだけならどこにインストールしても困らないです。が、組み込みで使うならコンパイルオプションが少なく出来る方が良いです。
インストール後は

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

とします。それで組み込み系をコンパイルするときは

$ gcc hoge.c -lguile-3.0

だけですむようになります。

以下の関数の説明は guile のオンラインマニュアルで確認してください。




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

まず scheme プログラムを実行するだけ

--- test01.c ------
#include <stdio.h>
#include <libguile.h>
 
int main(void ){
  scm_init_guile();
  scm_c_primitive_load( "test01.scm" );
  return 0;
}



--- test01.scm ---
;;
(display "aaaaaaaaaaa")
(newline)
;;

これで実行結果は

aaaaaaaaaaaa

このようになります。




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

 

これはC言語から scheme の中にデータを持ち込む(定義する)

---  test02.c ---
#include <stdio.h>
#include <libguile.h>

int main(void){
  scm_init_guile();

  scm_c_eval_string("(define name \"ossan\")");
  scm_c_define("age", scm_from_int(20));
  scm_c_define("weight", scm_from_double(80.08));
  scm_c_primitive_load("test02.scm");
  return 0;
}



--- test02.scm ---
;;
(display (string-append "name = " name "\n"))
;;
(display "age = ")
(display age)
(newline)
;;
(display "weight = ")
(display weight)
(newline)
;;

Cプログラムは以下のようにしてもいけます。こっちの方が分かりやすいかもしれません。

--- test02d.c ---
#include <stdio.h>
#include <libguile.h>

int main(void){
  scm_init_guile();

  scm_c_eval_string("(define name \"ossan\")");
  scm_c_eval_string("(define age 20)");
  scm_c_eval_string("(define weight 80.08)");
  scm_c_primitive_load("test02.scm");
  return 0;
}



どっちにせよ、以下のような実行結果になります。

name = ossan
age = 20
weight = 80.08







次に、scheme の結果を受けて値をC言語側に持ってくる

---  test04.c ---
#include <stdio.h>
#include <libguile.h>
 
int main(void){
  SCM s_cymb, s_value;
  int  n = 0;
  double  a = 0.0;
  char * p = "x";

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

  s_cymb = scm_c_lookup("data1");
  s_value = scm_variable_ref(s_cymb);
  if(scm_is_string(s_value))
    p = scm_to_locale_string(s_value);

  s_cymb = scm_c_lookup("data2");
  s_value = scm_variable_ref(s_cymb);
  if(scm_is_integer(s_value))
    n = scm_to_int(s_value);
  
  s_cymb = scm_c_lookup("data3");
  s_value = scm_variable_ref(s_cymb);
  if(scm_is_real(s_value))
    a = scm_to_double(s_value);

  printf("p = %s\n", p);
  printf("n = %d\n", n);
  printf("a = %g\n", a);

  return 0;
}


--- test04.scm ---
;;
(define data1 "aaaaa")
(define data2  100)
(define data3   13.33)
;;


実行結果は以下のようになります。

p = aaaaa
n = 100
a = 13.33


以上で、組み込みの標準的な処理である、上記の (a) (b) (c) が出来ました。






Sec-5.組み込みをするときの注意点

 guile は実行時にはバイトコンパイルして高速化を図っています。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 がエラーなく実行されるようになります。









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-21.chicken 編、 chicken のインストール

 コンパイルは簡単です。Debian-12、slackware-15 で最新版の chicken が一発で出来ました。説明省略です。

また、組み込みのコンパイルをするときは、$ gcc hoge.c -lchicken とするだけです。




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

いきなりやり方から示します。割合とシンプル・簡単です。
関数の説明は chicken のオンラインマニュアルで確認してください。しっかり書いてあります。



まずCプログラム
#include <chicken/chicken.h>

int main (){
  CHICKEN_run(CHICKEN_default_toplevel);
  CHICKEN_load("test01.scm");
  return 0;
}


次いで scheme  (test01.scm)
;;
;;
(display "aaaaaaaa")
(newline)
(display "bbbbbbbb")
(newline)
;;

結果は

aaaaaaaa
bbbbbbbb

のようになります。
guile 同様にシンプルで「マニュアルを見ながらやっていたらなんとなく出来た」って感じです。




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

まずC言語から scheme 空間にデーターを持っていく(決める)

まずCプログラム
#include <chicken/chicken.h>
#include <stdio.h>

int main (){
  C_word * result;
  CHICKEN_run(CHICKEN_default_toplevel);
  CHICKEN_eval_string("(define exp 'aaaaaaaaaaaaaa')", result);
  CHICKEN_load("test03.scm");
  return 0;
}

次いで scheme  (test03.scm)
;;
(display exp)
;;

結果は
aaaaaaaaaaaaaa

のようになり、C言語からあらかじめ scheme 空間にデーターを持っていき、その後で scheme プログラムが実行出来ます。同じやり方で整数も実数も scheme 空間に持っていく事が出来ます。



次に scheme 空間のデータをC言語空間に持ってくる。最初は文字列。

まずCプログラム
#include <chicken/chicken.h>
#include <stdio.h>

int main (){
  char result[1024];
  char * p;
  p = "exp";

  CHICKEN_run(CHICKEN_default_toplevel);
  CHICKEN_load("test02.scm");
  CHICKEN_eval_string_to_string(p, result, 1024);
  printf("result = %s\n", result);

  return 0;
}



次いで scheme  (test02.scm)
;;
(define exp "abcde")
;;



結果は以下のようになります。本来なら abcde と表示されるべきところで、前と最後に " が付加されていて、これは余分ですが・・・


"abcde"

次は数字です。数字は数のままでは持って来られないので、一旦文字列にして持ってきます

--- 最初はCプログラム ---
#include <stdio.h>
#include <string.h>
#include <chicken/chicken.h>

int main (){
  char result[1024];
  char * p = "exp";
  CHICKEN_run(CHICKEN_default_toplevel);
  CHICKEN_load("test04.scm");
  CHICKEN_eval_string_to_string(p, result, 1024);
  printf("result = %s\n", result);
  result[0] = ' ';
  result[strlen(result) - 1] = ' ';
  printf("atoi(result) = %d\n", atoi(result));

  return 0;
}


---  次に scheme ---
;;
(define exp "100")
;;


Cプログラム中で、まず文字列の exp を表示しています。文字列にしておけばC言語側に持ってくることが出来ます。
次に atoi で数字化した exp を表示しています。
scheme からCに文字列を持ってくると、最初と最後に " が付加されているのでここをスペースに変更してから atoi しています。" を取っておかないと atoi がうまく動きません。

結果は以下のとおりです。


result = "100"
atoi(result) = 100


以上、Chicken でも最初に書いた(a) (b) (c) が全て順調に出来ました。数字を直接 scheme からC空間に持ってこられませんが、文字列化して持って来てから atoi atof すれば実質的には困りません。

(注:chicken で数字を scheme からC言語に持ってこられないというのは、私がやった限りではうまく出来なかったという意味です)








Sec-31.chibi-scheme 編、 chibi-scheme のインストール

 chibi-scheme もコンパイルは簡単です。(Debian-12、slackware-15)
git でソースを持ってきて

  $ git clone https://github.com/ashinn/chibi-scheme
  そのディレクトリの中で
  $ ./configure
  $ make -j N (Nは適当)
  # make install

とするだけです。デフォルトで prefix = /usr/local になっています。

組み込みのコンパイルをするときは

$ gcc hoge.c -lchibi-scheme

でいけます。




Sec-32.C言語の中で chibi-scheme スクリプトを実行する

例によって、関数の説明は chibi のオンラインマニュアルで確認してください。
chibi は全体としてタイピング量の多くなる処理系です。が、きっちり動くし問題は無いと思います。単にタイピング量が多いだけの話です。


--- まずCプログラム ---
#include <chibi/eval.h>

int main(int argc, char** argv) {
  sexp ctx;
  sexp_scheme_init();               /** scheme を使えるようにする **/
  ctx = sexp_make_eval_context(NULL, NULL, NULL, 0, 0);  /** 最低限のキーワードのある環境を作る  **/
  sexp_load_standard_env(ctx, NULL, SEXP_SEVEN);     /** 一般的 scheme 関数が使える環境にするが、ポートはまだ   **/
  sexp_load_standard_ports(ctx, NULL, stdin, stdout, stderr, 1);   /** ポートを作る **/

  sexp_gc_var1(obj1);           /** obj1 のためのガベージを作る **/
  obj1 = sexp_c_string(ctx, "test01.scm", -1);   /** obj1 を登録する **/
  sexp_load(ctx, obj1, NULL);       /** obj1 を実行する **/
  sexp_gc_release1(ctx);         /** obj1 のガベージを開放する **/

  sexp_destroy_context(ctx);      /** すべて終わりにする **/
}


--- 次いで scheme プログラム(chibi) ---
;;
(display "aaaaaaaa")
(newline)
;;

結果は以下のようになります。

aaaaaaaa





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

まずC言語から scheme 環境にデータを持ち込む

--- Cプログラム ----
#include <chibi/eval.h>

int main(int argc, char** argv) {
  sexp ctx;
  sexp_scheme_init();
  ctx = sexp_make_eval_context(NULL, NULL, NULL, 0, 0);
  sexp_load_standard_env(ctx, NULL, SEXP_SEVEN);
  sexp_load_standard_ports(ctx, NULL, stdin, stdout, stderr, 1);
  sexp_eval_string(ctx, "(define data 9999)", -1, NULL);
  sexp_eval_string(ctx, "(define data2 \"dddd\")", -1, NULL);

  sexp_gc_var1(obj1);
  obj1 = sexp_c_string(ctx, "test03.scm", -1);
  sexp_load(ctx, obj1, NULL);
  sexp_gc_release1(ctx);

  sexp_destroy_context(ctx);
}



--- 次いで scheme (chibi) ---
;;
(display data)
(newline)
(display data2)
(newline)
;;



結果は以下のようになります。

  9999
  dddd





次に scheme 環境のデーターを scheme 実行後にC環境に持ってくる

--- まずCプログラム --- 
#include <stdio.h>
#include <chibi/eval.h>

int main(int argc, char** argv) {
  long int   n;
  float      a;
  char *     p;
  sexp     ctx;

  sexp_scheme_init();
  ctx = sexp_make_eval_context(NULL, NULL, NULL, 0, 0);
  sexp_load_standard_env(ctx, NULL, SEXP_SEVEN);
  sexp_load_standard_ports(ctx, NULL, stdin, stdout, stderr, 1);
  sexp_gc_var1(obj1);
  obj1 = sexp_c_string(ctx, "test05.scm", -1);
  sexp_load(ctx, obj1, NULL);

  sexp d1 = sexp_eval_string(ctx, "data1", -1, NULL);
  n = sexp_unbox_fixnum(d1);

  sexp d2 = sexp_eval_string(ctx, "data2", -1, NULL);
  a = sexp_flonum_value(d2);
      
  sexp d3 = sexp_eval_string(ctx, "data3", -1, NULL);
  p = sexp_string_data(d3);

  printf("*** data1 = %d\n",  n);
  printf("*** date2 = %f\n",  a);
  printf("*** date3 = %s\n",  p);
  
  sexp_gc_release1(ctx);
  sexp_destroy_context(ctx);
}




--- 次いで scheme (chibi) です ---
;;
(define data1  33)
(define data2  14.24)
(define data3  "abcde")
;;


結果は次のとおりです。

*** data1 = 33
*** date2 = 14.240000
*** date3 = abcde



こんな具合で、最初に書いた組み込みで必要な(a)(b)(c) が全部出来ました。













  







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