Zenn
🐙

C/C++ プログラムに Scheme によるスクリプト拡張を組み込む

2025/03/06に公開

C/C++ プログラムにちょっとしたスクリプト拡張を組み込みたい場合、Scheme は有用な選択肢の一つです。
本記事では Scheme インタプリタ s7 を C/C++ プログラムに組み込む方法について、小さな例を用いて説明します。

例:2項演算を拡張可能にするCプログラム

ここでは2つの数を入力とし、1つの数を出力するコマンド bop を作るとします。ただし、その計算内容は次のように lambda 式で定義可能にします。

./bop 68 12 "(lambda (x y) (/ (* x y) 1000))"

準備

最新の s7.tar.gz を取得し展開します。その中から s7.cs7.h をご自身のプログラムにコピーして使用します。
https://ccrma.stanford.edu/software/s7/

プログラムの作成

適当なディレクトリに s7.cs7.h をコピー、bop.cMakefile を次のような内容で作成し、make を実行すると bop がビルドされます。

bop.c
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "s7.h"

void usage() {
  fprintf(stderr, "Usage: bop x y lambda\n");
}

int main(int argc, char **argv) {
  if (argc != 4) {
    usage();
    return EXIT_SUCCESS;
  }

  char* end;
  long int x = strtol(argv[1], &end, 10);
  if (errno == EINVAL || errno == ERANGE) {
    perror("strtol");
    fprintf(stderr, "failed to parse x as a number: %s\n", argv[1]);
    return EXIT_FAILURE;
  }

  long int y = strtol(argv[2], &end, 10);
  if (errno == EINVAL || errno == ERANGE) {
    perror("strtol");
    fprintf(stderr, "failed to parse y as a number: %s\n", argv[2]);
    return EXIT_FAILURE;
  }

  s7_scheme *sc = s7_init();

  // 手続きとして評価されたかどうかを確認
  s7_pointer fn = s7_eval_c_string(sc, argv[3]);
  if (!s7_is_procedure(fn)) {
    fprintf(stderr, "failed to parse lambda as a procedure: %s\n", argv[3]);
    return EXIT_FAILURE;
  }

  // 与えられた関数の引数の数を確認
#if 1
  s7_pointer r_arity = s7_arity(sc, fn);
#else // is equivalent to below:
  s7_pointer arity = s7_name_to_value(sc, "arity");
  s7_pointer r_arity = s7_call(sc, arity, s7_cons(sc, fn, s7_nil(sc)));
#endif
  s7_int nargs_min = s7_integer(s7_car(r_arity));
  if (nargs_min != 2) {
    fprintf(stderr, "lambda has wrong number of argumnets (%ld)\n", nargs_min);
    return EXIT_FAILURE;
  }

  fprintf(stderr, "  x: %ld\n", x);
  fprintf(stderr, "  y: %ld\n", y);
  fprintf(stderr, "  lambda: %s\n", argv[3]);

  // bind fn to "bop-fn" in s7 environment
#if 1
  s7_pointer v_bop_fn = s7_define_variable(sc, "bop-fn", fn);
#else // is equivalent to below:
  s7_pointer s_bop_fn = s7_make_symbol(sc, "bop-fn");
  s7_pointer v_bop_fn = s7_define(sc, s7_nil(sc), s_bop_fn, fn);
#endif

  // fn を直接使うこともできるが以下の例ではあえて "bop-fn" として呼び出す
  s7_pointer bop_fn = s7_name_to_value(sc, "bop-fn");

#if 0
  // 見比べてみよう
  fprintf(stdout, "fn is a procedure? => %d\n", s7_is_procedure(fn));
  fprintf(stdout, "v_bop_fn is a procedure? => %d\n", s7_is_procedure(v_bop_fn));
  fprintf(stdout, "bop_fn is a procedure? => %d\n", s7_is_procedure(bop_fn));
#endif

  // 引数リストの作成
  s7_pointer args = s7_list(sc, 2, s7_make_integer(sc, x), s7_make_integer(sc, y));

  // bop-fn の実行
  s7_pointer r = s7_call(sc, bop_fn, args);
  // s7_pointer r = s7_call(sc, fn, args); // もちろんこのように呼び出してもよい

  if (s7_is_number(r)) {
    fprintf(stdout, "bop-fn (%ld, %ld) => %f\n", x, y, s7_real(r));
  } else {
    fprintf(stdout, "bop-fn (%ld, %ld) => failed\n", x, y);
  }

  // s7 の終了と開放
  s7_quit(sc);
  s7_free(sc);
  return EXIT_SUCCESS;
}
Makefile
SRCS    = bop.c
OBJS    = $(SRCS:.c=.o) # without s7.o
CFLAGS  = -Wall -I.
LDFLAGS = -Wl,-export-dynamic
LIBS    = -lm -ldl

all: bop

.c.o:
        $(CC) $(CFLAGS) -c $< -o $@

clean:
        $(RM) $(OBJS) bop

bop: bop.o s7.o
        $(CC) bop.o s7.o $(LDFLAGS) $(LIBS) -o bop

プログラムの実行

第一引数、第二引数に整数値、第三引数に計算内容を表す式を与えて bop を実行すると、コマンドライン引数の評価結果と計算結果が出力されます。

$ ./bop 325 12 "(lambda (x y) (/ (* x y) 1000))"
  x: 325
  y: 12
  lambda: (lambda (x y) (/ (* x y) 1000))
bop-fn (325, 12) => 3.900000

bop はコマンドライン引数で与えられた式を評価しますが、評価に失敗するような不完全な式や、評価はできても入力の数が合わない式を与えるとエラー終了します。

$ ./bop 2 3 "(lambda (x y) (* x y)"

;read-error ("missing close paren: (lambda (x y) (* x y)")
;    (define-macro (multiple-value-bind...
;    , line 0, position: 21
; (define-macro (multiple-value-bind vars e...


;unbound variable read-error
failed to parse lambda as a procedure: (lambda (x y) (* x y)
$ ./bop 2 3 "(lambda (x) (+ x 1))"
lambda has wrong number of argumnets (1)

s7 を組み込む際のポイント

s7 の環境は s7_scheme として、 s7_init() で構築します。
s7 環境内のオブジェクトは s7_pointer でアクセスできます。例えば s7 内の整数のオブジェクトから C の整数を取得したい場合は、 s7_integer() を用います。

s7_scheme *sc = s7_init();
s7_pointer eight = s7_make_integer(sc, 8);
s7_int eight_c = s7_integer(eight); // s7_int は int64_t

オブジェクトの束縛

s7 の環境内でオブジェクトを指定した名前に束縛するには s7_define_variable() を用います。
オブジェクトの値にアクセスするためには s7_name_to_value() を用います。

s7_pointer foo = s7_make_string(sc, "Hello!");
s7_pointer var_foo = s7_define_variable(sc, "foo", foo);
s7_pointer val_foo = s7_name_to_value(sc, "foo");
fprintf(stdout, "foo(%ld) => %s\n", s7_string_length(val_foo), s7_string(val_foo));

関数の評価

文字列で表現された関数を s7 に評価させるには s7_eval_c_string() を用います。
手続き として評価されたかどうかを確認するには s7_is_procedure() を用います。

s7_pointer fn = s7_eval_c_string(sc, str);

評価された手続きの引数の数の確認には、 s7 の arity が使えます。可変長引数も考慮した cons セルが返ってくることがわかります。

(arity cons) ; => (2 . 2)
(arity list) ; => (0 . 536870912)

これを C から用いるには、s7_arity() を用います。

s7_pointer r_arity = s7_arity(sc, fn);
s7_int nargs_min = s7_integer(s7_car(r_arity));

これは次のように行うこともできます。
s7_call() で関数を実行する際に引数を scheme のリストで与えていることに注意してください。

s7_pointer arity = s7_name_to_value(sc, "arity");
s7_pointer r_arity = s7_call(sc, arity, s7_cons(sc, fn, s7_nil(sc)));
s7_int nargs_min = s7_integer(s7_car(r_arity));

関数の実行

上の例にも挙げたように、関数を実行する際には引数のリストを s7 環境内に作成し s7_call() で呼び出します。
上の例では cons セルでリストを作りましたが、以下のように s7_list() を用いることもできます。

s7_pointer args = s7_list(sc, 2, s7_make_integer(sc, x), s7_make_integer(sc, y));
s7_pointer result = s7_call(sc, fn, args);

TIPS: C++ で使用する場合

これは s7 に限った話ではないのですが c++ でスクリプト拡張可能なプログラムを作る際に、型についてなるべく柔軟かつ簡潔に書きたいということがあります。
その場合 std::visitstd::variant などが有用だと思います。

using Variant = std::variant<int16_t, int32_t, int64_t, uint16_t, uint32_t,
                             uint64_t, float_t, double_t>;

class Visitor {
 public:
  Visitor() : _sc(nullptr) {}
  Visitor(s7_scheme *sc) : _sc(sc) {}
  s7_pointer operator()(int16_t v) { return s7_make_integer(_sc, v); }
  s7_pointer operator()(int32_t v) { return s7_make_integer(_sc, v); }
  s7_pointer operator()(int64_t v) { return s7_make_integer(_sc, v); }
  s7_pointer operator()(uint16_t v) { return s7_make_integer(_sc, v); }
  s7_pointer operator()(uint32_t v) { return s7_make_integer(_sc, v); }
  s7_pointer operator()(uint64_t v) { return s7_make_integer(_sc, v); }
  s7_pointer operator()(float_t v) { return s7_make_real(_sc, v); }
  s7_pointer operator()(double_t v) { return s7_make_real(_sc, v); }

 private:
  s7_scheme *_sc;
};

int main(int argc, char* argv[]) {
  // ... 引数を適切に処理
  s7_scheme *_sc = s7_init();
  Visitor _visitor = Visitor(_sc);
  s7_pointer args = s7_list(
    _sc,
    2,
    std::visit(_visitor, x),
    std::visit(_visitor, y)
  );
  s7_pointer r = s7_call(
    _sc,
    s7_name_to_value(_sc, "bop-fn"),
    args
  );
  s7_quit(_sc);
  s7_free(_sc);
  return 0;
}

私の使い方

私は、ROS でセンサー値を処理する C++ ノードに s7 を組み込み、文字列パラメータで値を処理するlambda式を書けるようにしています。これにより信号処理等で似たような処理を行うノードをいくつも作る必要がなくなりました。

参考文献

https://ccrma.stanford.edu/software/s7/s7.html

Discussion

ログインするとコメントできます