しまねソフト研究開発センター(略称 ITOC)にいます、東です。
私は、mruby/c の開発をしており、このたび mruby/c 3.4 を、2025/6/30 にリリースしました。今回のリリースには、C言語でメソッドを書く時に、呼び出し側(Ruby側)で指定した引数をC言語で取り扱うのに便利な関数とそれをラップするマクロを、試験的に導入しています。当記事ではこの機能の設計時に検討した事と、最終的に策定したAPIの使い方などについて書いてみようと思います。
Abstract
この機能を使うと、
こう書いていたのが ⇒ | こうなる |
---|---|
![]() |
![]() |
開発の経緯
それはひとつの Issue から始まった
3月12日、ある Issue が登録されました。
issues #230
https://github.com/mrubyc/mrubyc/issues/230
nyasu3W さんが登録してくださったもので、一言で言うと、「C言語を使ったメソッド実装時、引数取得で共通の処理が多いので、安全かつ簡潔に型チェックと取得を行うための仕組みが欲しい」との提案です。
mruby/c では、「実行時のメモリ(RAM)使用量を十分小さく保つ」という設計思想により、わりと実装内部が露出する傾向にあります。一般には、実装内部を利用者に意識させることは、良いことではないとされます。しかしその反面 mruby/cの目標でもある、実行時リソース使用量を低く保てるとか、実装を簡潔にできる(可能性がある)などのメリットもあります。
とはいえ、mruby/cで C言語を使って Rubyメソッドを記述する場合において、エラー処理などをすべて実装者が記述しなければならず、面倒であったのも事実です。
C言語でメソッドを記述する例 (Geminiによる自動生成を少し修正)
/*
(ruby)
def method1( arg_int, arg_str )
*/
static void c_my_method1(mrbc_vm *vm, mrbc_value v[], int argc)
{
// 引数の数をチェック
// レシーバv[0]を除く、必要な引数は2つなので、2つ以外ならエラー
if (argc != 2) {
mrbc_raise(vm, MRBC_CLASS(ArgumentError), "wrong number of arguments (expected 2)");
return;
}
// 1番目の引数(Ruby側では第1引数)をint型として取得
// v[1] がRubyの第一引数に対応します
if (v[1].tt != MRBC_TT_FIXNUM) {
mrbc_raise(vm, MRBC_CLASS(TypeError), "argument 1 must be an Integer");
return;
}
int arg_int = v[1].i; // v[1]の値を取得
// 2番目の引数(Ruby側では第2引数)をchar*型(文字列)として取得
// v[2] がRubyの第二引数に対応します
if (v[2].tt != MRBC_TT_STRING) {
mrbc_raise(vm, MRBC_CLASS(TypeError), "argument 2 must be a String");
return;
}
char *arg_str = (char *)(v[2].string->data); // v[2]の値を取得
// 取得した引数をmrbc_printfで出力
mrbc_printf("Received int argument: %d\n", arg_int);
mrbc_printf("Received string argument: %s\n", arg_str);
}
私などは、面倒な言語である C言語に慣れすぎているらしく、この「面倒さ」が「あたりまえ」で、それどころか「面倒さを華麗ににクリアするパズルゲーム」であるかのように楽しんでいるきらいがあるようです。「C言語では面倒になりがちな部分は、高級言語のRubyで楽をしようよ」とキャンペーンしている身としては反省すべき等と、この Issue を見て考えていました。
Issue での提案
Issue では例として、引数として指定された型にかかわらずC言語の文字列 (char *) として値を取得する便利関数が提案されていました。たしかに便利そうです。しかしどんな型が指定されていても文字列に変換して取得したいケースと、文字列以外が指定されたらエラーにしたいケースは分けて考えるべきだし、要求はもうすこし検討する余地があると考えました。
mrubyではどうなのか
本家 mruby では、mrb_get_args(mrb, format, ... )
関数が用意されており、言わばインタプリタ的に引数をCレイヤーに持ってくる戦略が採用されています。mruby/c でもこれを真似ることは可能でしたし今後つくる可能性もあります。
今回は次の項で示すユースケースをすべて網羅し、エラー処理もデフォルト値のサポートもまかせる事ができる別案を思いついたので、それを実装しました。
ユースケースの導出
設計にあたり、まずはどのような要求のパターンがあるか、考えてみました。
最終的には、「C言語プリミティブなタイプでメソッド引数を取得したい」という要求なので、C言語のタイプとしては以下の3種類あれば、だいたいの要求は通るだろうと考えました。
- int
- double
- char *
以下の検討では、代表して int で値を得たい場合を考えます。double でも char * でも、ユースケースのパターンは同じです。
思いついたパターンを、系統図的に書いてみます。
(要求) (実際に与えられた引数) (結果)
int値取得 ------ Integer ----------------> OK (1)
|
+-- Float ------------------> 変換してOK (4)
|
+-- それ以外 ----------------> Integer以外なのでエラー (2)
| |
| +-- to_i -----> 変換OK (5)
| |
| +--> 変換エラー (5)
|
+-- なし --------------------> 引数が足りなくてエラー (2)
|
+-------------> デフォルト引数を使いOK (3)
- カッコ内数字は、ユースケースとして良くあるだろう順を考えてみたもの
- to_i で変換までして欲しいケースは、割合としては少ないように思われる
- 一律に to_i してしまうと、上から3番目の「Integer以外が指定されたのでエラー」にしたいケースを満たせない
以上のことから、引数の取得機能と、to_i 等による変換機能は分離した方がニーズを網羅できそうだということがわかりました。
試験実装
まず実装したのは、MRBC_ARG_I
マクロと、マクロの先で実際に仕事をする関数です。マクロにした理由は、後述します。
MRBC_ARG_I(n) マクロ
仕様
- n番目の引数を、C言語のint型で参照する。
- 引数が IntegerかFloat以外なら、例外(TypeError)を発生させる。
- n番目の引数が与えられていない場合は、例外(ArgumentError)を発生させる。
- デフォルト値の指定もできる
想定している使い方は、こうです。
static void class_display_color_value( mrbc_vm *vm, mrbc_value *v, int argc )
{
// 関数引数を標準的な名前 vm,v,argc としておくと、MRBC_ARG_I() マクロを使ってn番目の引数を取得できる。
uint8_t r = MRBC_ARG_I(1);
uint8_t g = MRBC_ARG_I(2);
uint8_t b = MRBC_ARG_I(3);
// 引数が足りない等のエラーの場合、例外が発生するので return する。
if( mrbc_israised(vm) ) return;
...
またデフォルト値の指定もできるよう設計しています。
uint8_t r = MRBC_ARG_I(1, 0xff); // 1番目の引数を取得する。引数が指定されていなければ、デフォルト値 0xff を採用する。
このマクロ(及び裏で動く関数)を使うと、先ほどのユースケースパターン系統図にあるうちの5つはクリアできます。
(要求) (実際に与えられた引数) (結果)
int値取得 ------ Integer ----------------> OK (1) MRBC_ARG_I(n)
|
+-- Float ------------------> 変換してOK (4) MRBC_ARG_I(n)
|
+-- それ以外 ----------------> Integer以外なのでエラー (2) MRBC_ARG_I(n)
| |
| +-- to_i -----> 変換OK (5)
| |
| +--> 変換エラー (5)
|
+-- なし --------------------> 引数が足りなくてエラー (2) MRBC_ARG_I(n)
|
+-------------> デフォルト引数を使いOK (3) MRBC_ARG_I(n, default_value)
- 「引数 Integer を想定していて、Float が与えられた場合にエラーとしたい」という要求がもしあるならば、これだけでは対応できないが、あまり問題にならないと思う
- 一方、「引数 Float を想定していて、Integer が与えられた場合にエラーとしたい」という要求は無いはず。言い換えればFloatを想定したメソッドにIntegerを与えられたら暗黙に変換してほしいと思う
残りのケース
残っていたユースケース「実際に与えられた引数が Integer 以外なので、to_i で変換するケース」について、当初思いついた「引数の取得機能と、to_i 等による変換機能は分離した方がニーズを網羅できそう」というアイデアを、そのまま実装しました。
MRBC_ARG(n) 引数の取得マクロ
- n番目の引数へのポインタ (mrbc_value *) を返す
- 引数が与えられていない場合は、例外を発生させる
MRBC_TO_I( mrbc_value *val ) 値の変換マクロ
- val が Integer型の場合、値を int型で返す
- val が Integer 型ではない場合、to_i で変換してから、値を int型で返す
- 変換できなければ、例外を発生させる
想定している使い方
単独で使う場合
mrbc_value *arg1 = MRBC_ARG(1);
if( mrbc_israised(vm) ) return;
組み合わせて使う場合
int arg1 = MRBC_TO_I( MRBC_ARG(1) );
int arg2 = MRBC_TO_I( MRBC_ARG(2) );
if( mrbc_israised(vm) ) return;
これらをユースケースパターン系統図に追記すると、以下の通りひととおり網羅できました。
(要求) (実際に与えられた引数) (結果)
int値取得 ------ Integer ----------------> OK (1) MRBC_ARG_I(n)
|
+-- Float ------------------> 変換してOK (4) MRBC_ARG_I(n)
|
+-- それ以外 ----------------> Integer以外なのでエラー (2) MRBC_ARG_I(n)
| |
| +-- to_i -----> 変換OK (5) MRBC_TO_I( MRBC_ARG(n) )
| |
| +--> 変換エラー (5) MRBC_TO_I( MRBC_ARG(n) )
|
+-- なし --------------------> 引数が足りなくてエラー (2) MRBC_ARG_I(n)
|
+-------------> デフォルト引数を使いOK (3) MRBC_ARG_I(n, default_value)
マクロの使い方
前項を踏まえ、開発したマクロの使い方をまとめます。
前提条件
前提として、メソッドを実装するC関数の引数名が、標準的な名前になっている必要があります。
以下の vm
, v
, argc
のことです。
static void c_my_method1(mrbc_vm *vm, mrbc_value v[], int argc)
メソッド引数に関するもの
int MRBC_ARG_I( int n )
- n番目の引数を、C言語のint型で参照する
- 引数が IntegerかFloat以外なら、例外(TypeError)を発生させる
- n番目の引数が与えられていない場合は、例外(ArgumentError)を発生させる
int MRBC_ARG_I( int n, int default_value )
- n番目の引数が与えられていない場合は、デフォルト値が返る
- その他の仕様は同じ
同様の機能で、型別に以下を定義しています。
double MRBC_ARG_F(n) // Float or Integer(Ruby) to double(C)
double MRBC_ARG_F(n, default_value )
const char * MRBC_ARG_S(n) // String(Ruby) to const char * (C)
const char * MRBC_ARG_S(n, "default_value" )
int MRBC_ARG_B(n) // True or False(Ruby) to integer(C)
int MRBC_ARG_B(n, default_value )
mrbc_value * MRBC_ARG( int n )
- n番目の引数を、mrbc_value(へのポインタ)型で参照する
- 引数の指定が無い場合は、例外(ArgumentError)を発生させる
使用例
static void c_my_method1(mrbc_vm *vm, mrbc_value v[], int argc)
{
int arg_int = MRBC_ARG_I(1); // 1つ目の引数を得る
const char *arg_str = MRBC_ARG_S(2); // 2つ目の引数を得る
if( mrbc_israised(vm) ) return; // 例外が発生していたら戻る
// 取得した引数をmrbc_printfで出力
mrbc_printf("Received int argument: %d\n", arg_int);
mrbc_printf("Received string argument: %s\n", arg_str);
}
mrbc_value からの値の取り出しに関するもの
int MRBC_VAL_I( mrbc_value * )
- mrbc_value から、C言語のint型で値を返す
- 引数が IntegerかFloat以外なら、TypeError(型変換 to_i は行わない)
同様の機能で、型別に以下を定義しています。
double MRBC_VAL_F( mrbc_value * )
const char * MRBC_VAL_S( mrbc_value * )
mrbc_value の型を変換するもの
int MRBC_TO_I( mrbc_value * )
- mrbc_valueの型を Integerに変換(to_i)する
- 変換できなければ、例外(NoMethodError)を発生させる
- C言語のint型で値を返す
同様の機能で、型別に以下を定義しています。
double MRBC_TO_F( mrbc_value * )
const char * MRBC_TO_S( mrbc_value * )
使用例
static void c_my_method1(mrbc_vm *vm, mrbc_value v[], int argc)
{
// 1つ目, 2つ目の引数を得る。
// 型が違う引数が与えられた場合、to_i, to_s で変換しようとする。
int arg_int = MRBC_TO_I( MRBC_ARG(1));
const char *arg_str = MRBC_TO_S( MRBC_ARG(2));
if( mrbc_israised(vm) ) return; // 例外が発生していたら戻る
// 取得した引数をmrbc_printfで出力
mrbc_printf("Received int argument: %d\n", arg_int);
mrbc_printf("Received string argument: %s\n", arg_str);
}
一覧
定義したマクロの一覧です。
// Get int value from argument.
int MRBC_ARG_I( n )
int MRBC_ARG_I( n, default_value )
// Get double value from argument.
double MRBC_ARG_F(n)
double MRBC_ARG_F(n, default_value )
// Get char * from argument.
const char * MRBC_ARG_S(n)
const char * MRBC_ARG_S(n, "default_value" )
// Get boolean as int from argument.
int MRBC_ARG_B(n)
int MRBC_ARG_B(n, default_value )
// Get mrbc_value from argument.
mrbc_value * MRBC_ARG(n)
// Get value from mrbc_value without type convert.
int MRBC_VAL_I( mrbc_value * )
double MRBC_VAL_F( mrbc_value * )
const char * MRBC_VAL_S( mrbc_value * )
// Convert mrbc_value and return a value of primitive C type.
int MRBC_TO_I( mrbc_value * )
double MRBC_TO_F( mrbc_value * )
const char * MRBC_TO_S( mrbc_value * )
実装について
関数とマクロ
mruby/c でメソッド関数に渡る引数は以下の3つがあります。
- mrbc_vm *vm
- mrbc_value v[]
- int argc
これの意味するところは、呼び出し側(Ruby)で argc個の引数が与えられており、v[] 配列に引数が入っているという構造です。引数を取得するには、引数の数や引数の型を精査し必要であればエラー(例外)を出す必要があります。
これを実行しているのは実際には関数です。この関数には、これら3つの引数を全て与えた上で、さらに要求にそった引数を加える必要があり、引数が多くなりすぎて見た目が悪くタイプ量も多くなってしまいます。そのため、引数名が標準的な、vm
, v
, argc
にそろっている事を前提にして関数をラップするマクロを定義することにしました。
たとえば、MRBC_ARG_I(n)
マクロですと以下のように定義します。
#define MRBC_ARG_I(n) mrbc_arg_i(vm,v,argc,(n))
後述しますが、デフォルト値をサポートするために、実際にはもう少し複雑な定義になっています。
実際の関数
実際に仕様を実現している関数は、int値取得に関しては、以下の2つです。
// n番目の引数を、C言語のint型で参照する関数。引数が与えられていない場合、例外を発生させる。
mrbc_int_t mrbc_arg_i(struct VM *vm, mrbc_value v[], int argc, int n);
// n番目の引数を、C言語のint型で参照する関数。引数が与えられていない場合、デフォルト値を返す。
mrbc_int_t mrbc_arg_i2(struct VM *vm, mrbc_value v[], int argc, int n, mrbc_int_t default_value);
可変長引数マクロ
C99から、可変長引数マクロがサポートされました。今回はこの機能を使い、オプションでデフォルト値の指定ができるように設計しています。
uint8_t r = MRBC_ARG_I(1, 0xff); // 1番目の引数を取得する。引数が指定されていなければ、デフォルト値 0xff を採用する。
ただしこの可変長引数マクロ機能、おそらくですが策定者は単純に printf 等の可変長引数関数を wrap する事しか想定していなかったのではないかと思います。
#define MY_PRINTF(...) printf(__VA_ARGS__)
ですので、今回の要求のように、
- 2つめの引数はオプションで、指定しても指定しなくても良い
といった要求を実現するのには、全く向いていない仕様です。
ところが、世の中には壁があると乗り越える人がでてくるもので、この要求は以下のようにネストしたマクロにすると実現できることがわかっています。
#define MRBC_ARG_I(...) MRBC_arg_choice(__VA_ARGS__, mrbc_arg_i2, mrbc_arg_i) (vm,v,argc,__VA_ARGS__)
#define MRBC_arg_choice(a1,a2,a3,...) a3
引数が1つだけ与えられた場合
詳しく見てみましょう。まず、引数が1つだけ与えられた場合です。
MRBC_ARG_I(1);
これは、プリプロセッサによって以下のように展開されます。
MRBC_arg_choice(1, mrbc_arg_i2, mrbc_arg_i) (vm,v,argc,1)
MRBC_arg_choice
も define されているため、再度展開されます。この場合は a3、つまり3つめの引数のみが残るように定義されているため、3つ目の mrbc_arg_i
が残り、
mrbc_arg_i (vm,v,argc,1)
このように見事に目的の関数が選ばれ、関数呼び出しの形になります。
引数が2つ与えられた場合
次に引数が2つ与えられた場合です。
MRBC_ARG_I(1, 0xff);
これは、プリプロセッサによって以下のように展開されます。
MRBC_arg_choice(1, 0xff, mrbc_arg_i2, mrbc_arg_i) (vm,v,argc,1, 0xff)
MRBC_arg_choice
が再度展開されます。この場合の a3は、mrbc_arg_i2
であるため、
mrbc_arg_i2 (vm,v,argc,1, 0xff)
こうなり、目的の関数が呼び出されることがわかります。
ほらね!! ここでもパズルを楽しんでますよね。
おわりに
今回は、mruby/c 3.4 に新たに加えた引数取得マクロについて、策定に至った経緯から仕様の決定および実装を交えて説明しました。これでより便利に使えるようになったと言ってもらうことを願って、記事を終わりにしたいと思います。