Skip to content

Instantly share code, notes, and snippets.

@t-nissie
Last active December 19, 2024 11:18
Show Gist options
  • Save t-nissie/8b0ebccab3fe41ada28ea0e7fb0b6c23 to your computer and use it in GitHub Desktop.
Save t-nissie/8b0ebccab3fe41ada28ea0e7fb0b6c23 to your computer and use it in GitHub Desktop.
mrubyをC言語のシミュレーションのプログラムに組み込んでDSLとして利用する

mrubyをC言語のシミュレーションのプログラムに組み込んでDSLとして利用する

シミュレーション、CAD、画像処理、エディタなどのアプリで Tcl, Scheme, Lua, Python, LISPなどが domain-specific language (DSL) として採用されていて、 パラメーターサーチ、 繰り返し処理、 機能拡張、 カスタマイズ、 自動化など 複雑な処理をしたい時に便利なことがあります。 このgistではC言語でmrubyを組み込んだ 超簡単な等速直線運動をするだけのシミュレーションのプログラムを例示してみます。 マイコンに「組み込む」のではなく、パソコンのプログラムに「組み込む」かんじです。

シミュレーターの入力ファイルはこんなかんじです:

v10 = Constantv.new(10.0, 25.0)
v10.print()
(0..3).each{|i|
  v10.evolve(time_step: 2.0*0.5**i).print()
}
v10.evolve(  time_step: 0.125, temperature: 30.0).print()

このgist文書の著者は週末に Fortranで強誘電体のシミュレーターを書いたりしています。 ユーザさんはわがままで外部電場や外部応力や温度を時間の関数として自由に設定したがります。 そんな時にRubyでコントロールできたら便利だと思っています。

mrubyを利用するのは初めてなのでアドバイスなどをいただけないかと思い mrubyファミリ (組み込み向け軽量Ruby) Advent Calendar 2024 に挑戦してみます。

要件

シミュレーターにDSLを導入するにあたって次のような要件があると思います。

  • 独自の文法でなく、よく知られたスクリプト言語にして説明を大幅に削減する。
  • スクリプト言語のバージョンアップなどで混乱を招かないために、スクリプト言語はプログラムに組み込まれていること。
  • キーワード引数を使ってパラメーターを簡単に与えられる。
  • キーワード引数の綴り間違いを含め、文法エラーを適切に指摘してくれる。

これらの要件を満たすにはmrubyがうってつけだと思います。

読むべきモノ

たぶん以下を読んで見よう見まねでプログラミングすればmrubyをCのプログラムに組み込むことができます。

  • まつもとゆきひろ直伝 組込Ruby「mruby」のすべて 総集編 Kindle版
  • mrubyのドキュメント、特に mruby/doc/capi/html/mruby_8h.html 。mruby.org にあるものは古いことがあるので、手元でrake docしたものを参照すること。
  • mruby/mrbgems/mruby-bin-mruby/tools/mruby/mruby.c
  • mruby/examples/mrbgems/cdata_extension_example/src/example.c

mrubyのコンパイル

LinuxでもWindowsのCygwinでもmacOSでも

  • gcc(Cコンパイラ)
  • rake(RubyといっしょにインストールされるRuby版make)
  • doxygen(ソースコードのコメントからドキュメントを生成するツール)
  • graphviz(グラフ構造の画像を描いてくれるツール)

があれば

git clone https://github.com/mruby/mruby.git
cd mruby
CC=gcc rake
rake doc
build/host/bin/mruby --version   #動作確認
build/host/bin/mruby-config --cc              #後にMakefileに書き込むCコンパイラ
build/host/bin/mruby-config --cflags          #後にMakefileに書き込むFLAGSを確認
build/host/bin/mruby-config --libmruby-path   #後にMakefileでリンクするアーカイブファイルを確認

で簡単にコンパイルできます。

mrubyを組み込んだ超簡単なシミュレーションのプログラム

等速直線運動をするだけのCのプログラムとMakefileとシミュレーターの入力スクリプトが本gistに置いてあります。 クラスConstantvのインスタンスを生成する時に定速度と初期温度を与えて、 evolveメソッドで時間発展させます。 タイムステップと温度をそのキーワード引数で任意に変更できます。 キーワード引数temperature:を与えなければ前回のイテレーションから温度を変更しません。 ただし、本シミュレーションでは温度は飾りです。 温度は等速直線運動になんら影響を与えません。

  • !mruby.md このgist文章
  • Makefile
  • constantv.c メインプログラム
  • constantvclass.c クラスの定義
  • constantvclass.h そのヘッダファイル
  • v10.constantv シミュレーターのインプットファイル

mruby内の例を組み合わせたコードなので、コードのライセンスはmrubyと同じMIT licenseです。

コマンドライン引数の有無やfoepn()の成否などはチェックしていません。

コンパイルと実行の様子

LinuxでもWindowsのCygwinでもmacOSでも以下のようにコンパイルと実行ができます。 Gnuplotで結果をプロットもしています。 入力のv10.constantvに故意に文法エラーやキーワード引数のスペルミスを入れると エラーメッセージとともに止まることも試してみてください。

$ git clone https://gist.github.com/8b0ebccab3fe41ada28ea0e7fb0b6c23.git constantv
$ cd constantv
### 自分の環境に合わせてMakefileの中のCC, CFLAGS, CPPFLAGS, LDFLAGSを編集 ###
$ make clean
$ make
$ ./constantv v10.constantv | tee v10.data
#BEGIN v10.constantv
#i     t         x        T
 0     0.000     0.00    25.00
 1     2.000    20.00    25.00
 2     3.000    30.00    25.00
 3     3.500    35.00    25.00
 4     3.750    37.50    25.00
 5     3.875    38.75    30.00
#END   v10.constantv
$ gnuplot
gnuplot> plot 'v10.data' using 2:3 with lp, '' u 2:4 w lp
gnuplot> quit
$

C++にはmrubybindか

C++のクラスやメソッドを半自動的にmrubyに変換(bind)してくれるmrubybindが便利そうなので 別のgist文章で調査しました。

まとめ

C言語でmrubyを組み込んだ 超簡単な等速直線運動をするだけのシミュレーションのプログラムを書いてみました。 mrubyを利用するのは初めてなのでアドバイスをいただけたら幸いです。 Rubyで制御できるシミュレーション、CAD、画像処理、エディタなどさまざまなアプリができてくるとよいですね。

*.o
core*
constantv
*~
*.data
// constantv.c
//
#include <mruby.h>
#include <stdlib.h>
#include <mruby/class.h>
#include <mruby/data.h>
#include <mruby/compile.h>
#include <mruby/dump.h>
#include <mruby/variable.h>
#include <mruby/proc.h>
#include <mruby/error.h>
#include <mruby/presym.h>
#include "constantvclass.h"
int main(int argc, char **argv)
{
mrb_state *mrb = mrb_open();
int ai = mrb_gc_arena_save(mrb);
constantv_class_init(mrb);
mrb_ccontext* cxt = mrb_ccontext_new(mrb); // to detext errors in the script
mrb_ccontext_filename(mrb, cxt, argv[1]); // set the script filename
FILE* file = fopen(argv[1], "r");
mrb_value val = mrb_load_detect_file_cxt(mrb, file, cxt);
fclose(file);
// Check the error
int error_n = 0;
mrb_gc_arena_restore(mrb, ai);
mrb_ccontext_free(mrb, cxt);
if (mrb->exc) {
MRB_EXC_CHECK_EXIT(mrb, mrb->exc);
if (!mrb_undef_p(val)) mrb_print_error(mrb); // report the error
}
mrb_close(mrb);
return error_n;
}
// constavclass.c
//
#include <mruby.h>
#include <stdlib.h>
#include <mruby/class.h>
#include <mruby/data.h>
#include "constantvclass.h"
static void mrb_constantv_free(mrb_state *mrb, void *ptr)
{
/* custom destructor 構造体の中に配列などを動的にmalloc()していたらここでfree()する */
mrb_free(mrb, ptr);
}
struct mrb_data_type mrb_constantv_type = {"Constantv", mrb_constantv_free};
typedef struct Constantv {
mrb_float velocity;
mrb_float t;
mrb_float x;
mrb_float temperature;
mrb_int i_step;
} Constantv;
static mrb_value constantv_initialize(mrb_state *mrb, mrb_value self)
{
Constantv* constv = (Constantv*)mrb_malloc(mrb, sizeof(Constantv));
mrb_float v, temp;
mrb_get_args(mrb, "ff", &v, &temp);
constv->velocity = v;
constv->temperature = temp;
constv->t = 0.0;
constv->x = 0.0;
constv->i_step = 0;
DATA_PTR(self) = constv;
DATA_TYPE(self) = &mrb_constantv_type;
return self;
}
static mrb_value constantv_evolve(mrb_state *mrb, mrb_value self)
{
mrb_int kw_num = 2;
mrb_int kw_required = 0;
mrb_sym kw_names[] = { mrb_intern_lit(mrb, "time_step"), mrb_intern_lit(mrb, "temperature") };
mrb_value kw_values[kw_num];
mrb_kwargs kwargs = { kw_num, kw_required, kw_names, kw_values, NULL };
mrb_get_args(mrb, ":", &kwargs);
if (mrb_undef_p(kw_values[0])) { kw_values[0] = mrb_float_value(mrb, 2.0); }
mrb_float time_step = mrb_float(kw_values[0]);
Constantv* constv = (Constantv*)mrb_data_get_ptr(mrb, self, &mrb_constantv_type);
constv->t += time_step;
constv->x += time_step*constv->velocity;
if (!mrb_undef_p(kw_values[1])) { constv->temperature = mrb_float(kw_values[1]); }
constv->i_step++;
return self;
}
static mrb_value constantv_print(mrb_state *mrb, mrb_value self)
{
Constantv* constv = (Constantv*)mrb_data_get_ptr(mrb, self, &mrb_constantv_type);
printf("%2lli %7.3f %6.2f %6.2f\n", constv->i_step, constv->t, constv->x, constv->temperature);
return self;
}
void constantv_class_init(mrb_state* mrb)
{
struct RClass* class_constantv = mrb_define_class(mrb, "Constantv", mrb->object_class);
MRB_SET_INSTANCE_TT(class_constantv, MRB_TT_CDATA);
mrb_define_method(mrb, class_constantv, "initialize", constantv_initialize, MRB_ARGS_REQ(2));
mrb_define_method(mrb, class_constantv, "evolve", constantv_evolve, MRB_ARGS_ANY() );
mrb_define_method(mrb, class_constantv, "print", constantv_print, MRB_ARGS_NONE());
}
void constantv_class_init(mrb_state* mrb);
#-*-Makefile-*-
# Flags can be found by
# mruby/build/host/bin/mruby-config --cc
# mruby/build/host/bin/mruby-config --flags
# mruby/build/host/bin/mruby-config --libs
##
CC=gcc
CFLAGS = -std=gnu99 -g -O3 -Wall
CPPFLAGS = -DMRB_USE_RATIONAL -DMRB_USE_COMPLEX -DMRB_USE_BIGINT -DMRB_USE_DEBUG_HOOK\
-I/Users/takeshi/mruby/mruby/build/host/include\
-I/Users/takeshi/mruby/mruby/build/host/include/mruby/gems/mruby-time/include\
-I/Users/takeshi/mruby/mruby/build/host/include/mruby/gems/mruby-io/include
LDFLAGS = /Users/takeshi/mruby/mruby/build/host/lib/libmruby.a -lm
constantv: constantv.o constantvclass.o
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
clean:
rm -f *.o constantv core* *.data
#-*-Ruby-*-
##
puts("#BEGIN v10.constantv")
puts("#i t x T")
v10 = Constantv.new(10.0, 25.0)
v10.print()
(0..3).each{|i|
v10.evolve(time_step: 2.0*0.5**i).print()
}
v10.evolve( time_step: 0.125, temperature: 30.0).print()
puts("#END v10.constantv")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment