しまねソフト研究開発センター(略称 ITOC)にいます、東です。
mruby/c 向けのC言語を使った開発資料の充実のために、数回にわたり解説記事を書こうと計画しています。
今回は、C言語を使って Rubyから呼び出すことができる関数(メソッド)を記述する方法を説明します。
目標・方針
概要をざっと説明する事で、関数(メソッド)の作り方の大枠を理解してもらう。
事前準備・環境
先日公開した記事、mruby/c をPCで動かす の動作環境での動作テストを想定しています。
マイコンの開発環境を使っても良いですが、どうしても開発のサイクルが遅くなるので、今回のような例は PC上で試す方が良いと思います。
先の記事では、sample_c
ディレクトリの sample_concurrent
を実行する前提で説明しました。今回もそれに則り、sample_c/sample_concurrent.c
に追記する形で説明します。
なお、当記事の実行例は、すべてMacOS上で実行した結果です。
関数を定義(記述)する
まず、Rubyでコールすることができる関数、名前を func1
として作ることにします。
以下のRubyコードと同等の関数を、Cで記述します。
def func1
printf "func1 called\n"
end
関数 func1 を定義
C言語で関数を書く場合、動作を実行するための関数本体の用意と、それを Rubyの関数(メソッド)として登録するという2つの作業が必要です。
関数の本体は、以下のように C言語の関数として定義します。
// 関数の定義
static void c_func1(mrbc_vm *vm, mrbc_value v[], int argc)
{
mrbc_printf("func1 called.\n");
}
引数の名前、vm, v, argc は、この後使うマクロの都合により、この名前にしておいてください。
mruby/c のHALを経由してコンソール表示する関数 mrbc_printf
を使っています。C言語標準関数の printf
を使ってもほとんどの場合OKですが、たまにバッファリングの関係で表示順番が乱れますので注意が必要です。
この関数を Rubyの関数として登録するには、以下のようにします。
// 関数の登録
mrbc_define_method( 0, 0, "func1", c_func1 );
これを、mruby/c VM の初期化後(mrbc_init関数の後)に記述します。
第1、第2引数のゼロは、とりあえず無視してください。
第3引数にRubyの関数名(メソッド名)を、第4引数に先ほど定義した関数名を記述します。
sample_concurrent.c 全体
/*
* This sample program executes multiple mruby/c programs concurrently.
*/
#include <stdio.h>
#include <stdlib.h>
#include "mrubyc.h"
#if !defined(MRBC_MEMORY_SIZE)
#define MRBC_MEMORY_SIZE (1024*60)
#endif
static uint8_t memory_pool[MRBC_MEMORY_SIZE];
uint8_t * load_mrb_file(const char *filename)
{
FILE *fp = fopen(filename, "rb");
if( fp == NULL ) {
fprintf(stderr, "File not found (%s)\n", filename);
return NULL;
}
// get filesize
fseek(fp, 0, SEEK_END);
size_t size = ftell(fp);
fseek(fp, 0, SEEK_SET);
// allocate memory
uint8_t *p = malloc(size);
if( p != NULL ) {
fread(p, sizeof(uint8_t), size, fp);
} else {
fprintf(stderr, "Memory allocate error.\n");
}
fclose(fp);
return p;
}
// 関数の定義
static void c_func1(mrbc_vm *vm, mrbc_value v[], int argc)
{
mrbc_printf("func1 called.\n");
}
int main(int argc, char *argv[])
{
int vm_cnt = argc-1;
if( vm_cnt < 1 || vm_cnt > MAX_VM_COUNT ) {
printf("Usage: %s <xxxx.mrb> <xxxx.mrb> ... \n", argv[0]);
printf(" Maximum number of mrb file: %d\n", MAX_VM_COUNT );
return 1;
}
/*
start mruby/c with rrt0 scheduler.
*/
mrbc_init(memory_pool, MRBC_MEMORY_SIZE);
// 関数の登録
mrbc_define_method( 0, 0, "func1", c_func1 );
// create each task.
for( int i = 0; i < vm_cnt; i++ ) {
fprintf( stderr, "Loading: '%s'\n", argv[i+1] );
uint8_t *mrbbuf = load_mrb_file( argv[i+1] );
if( mrbbuf == 0 ) return 1;
if( !mrbc_create_task( mrbbuf, NULL ) ) return 1;
}
// and execute all.
int ret = mrbc_run();
return ret == 1 ? 0 : ret;
}
ファイルが用意できたら、ビルドして実行してみます。
[~/work/mrubyc%] make
cd mrblib ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
make[1]: Nothing to be done for `all'.
cd src ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
make[1]: Nothing to be done for `all'.
cd sample_c ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
cc -I../hal/posix -I../src -Wall -g -o sample_concurrent sample_concurrent.c ../build/libmrubyc.a
実行テスト用の Ruby ファイルを用意します。
func1()
では、準備ができたので実行してみます。
[~/work/mrubyc%] mrubyc tst.rb
Loading: './tst.mrb'
func1 called.
意図通り c_func1 が呼ばれて、func1 called.
と表示されました。
引数の取得
mruby/c では、全ての値は mrbc_value 型の構造体で表されます。引数も例外ではなく、Rubyプログラムで渡された引数は mrbc_value の配列 v[] で渡され、その数は argc個です。
実用的なコードを示す前に、小さな実験をしてみましょう。先ほどの c_func1 関数を、以下の通り書き換えます。
// 関数の定義
static void c_func1(mrbc_vm *vm, mrbc_value v[], int argc)
{
// 関数に渡された引数を一覧表示
mrbc_printf("argument list.\n");
for( int i = 0; i <= argc; i++ ) {
mrbc_printf(" v[%d]=", i);
mrbc_p( &v[i] );
}
}
Rubyプログラムでも、引数を渡すように追記します。
func1(123, "ABC")
ビルドして実行してみます。
[~/work/mrubyc%] make
cd mrblib ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
make[1]: Nothing to be done for `all'.
cd src ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
make[1]: Nothing to be done for `all'.
cd sample_c ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
cc -I../hal/posix -I../src -Wall -g -o sample_concurrent sample_concurrent.c ../build/libmrubyc.a
[~/work/mrubyc%] mrubyc tst.rb
Loading: './tst.mrb'
argument list.
v[0]=#<Object:0c6010ec>
v[1]=123
v[2]="ABC"
このように、
- v[0]: self
- v[1]: 1つ目の引数
123
- v[2]: 2つ目の引数
"ABC"
と、渡されていることがわかります。
mruby/c 3.3 までは、この mrbc_value 配列を直接参照して引数を得ていました。
これでは記述量が多く、同じようなコードが多くなるとの指摘により、mruby/c 3.4 からは、記事「mruby/c 3.4 に導入した引数取得マクロの解説」 で書いたように、引数の型と位置を指定して C言語プリミティブな型で参照できるコンビニエンスマクロを用意しました。
このマクロを使って引数を取得するコードを以下に示します。
// 関数の定義
static void c_func1(mrbc_vm *vm, mrbc_value v[], int argc)
{
int arg1 = MRBC_ARG_I(1); // 1つ目の引数を、int型で取得する。
const char *arg2 = MRBC_ARG_S(2); // 2つ目の引数を、char *型で取得(参照)する。
// 引数が足りない等のエラーの場合、例外が発生するので return する。
if( mrbc_israised(vm) ) return;
// 確認
mrbc_printf("arg1=%d\n", arg1 );
mrbc_printf("arg2=%s\n", arg2 );
}
ビルドして実行してみます。
[~/work/mrubyc%] make
cd mrblib ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
make[1]: Nothing to be done for `all'.
cd src ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
make[1]: Nothing to be done for `all'.
cd sample_c ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
cc -I../hal/posix -I../src -Wall -g -o sample_concurrent sample_concurrent.c ../build/libmrubyc.a
[~/work/mrubyc%] mrubyc tst.rb
Loading: './tst.mrb'
arg1=123
arg2=ABC
意図通り動作しています。
引数取得用マクロの一覧を示します。具体的な動作と使い方は、該当記事 をご覧ください。
// 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 * )
戻り値
戻り値の指定について説明します。
func1を、2つの整数を引数とし、その和を返す関数に改造してみます。
// 関数の定義
static void c_func1(mrbc_vm *vm, mrbc_value v[], int argc)
{
int arg1 = MRBC_ARG_I(1); // 1つ目の引数を、int型で取得する。
int arg2 = MRBC_ARG_I(2); // 2つ目の引数を、int型で取得する。
// 引数が足りない等のエラーの場合、例外が発生するので return する。
if( mrbc_israised(vm) ) return;
// Integer型の値を戻り値に指定する
SET_INT_RETURN( arg1 + arg2 );
}
Rubyプログラムも、以下のように書き換えます。
puts func1(123, 456)
ビルドして実行してみます。
[~/work/mrubyc%] make
cd mrblib ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
make[1]: Nothing to be done for `all'.
cd src ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
make[1]: Nothing to be done for `all'.
cd sample_c ; /Applications/Xcode.app/Contents/Developer/usr/bin/make all
cc -I../hal/posix -I../src -Wall -g -o sample_concurrent sample_concurrent.c ../build/libmrubyc.a
[~/work/mrubyc%] mrubyc tst.rb
Loading: './tst.mrb'
579
SET_*_RETURN マクロには、以下の種類があります。
SET_RETURN(n)
set a return value when writing a method by C.
SET_NIL_RETURN()
set a return value to nil when writing a method by C.
SET_FALSE_RETURN()
set a return value to false when writing a method by C.
SET_TRUE_RETURN()
set a return value to true when writing a method by C.
SET_BOOL_RETURN(n)
set a return value to true or false when writing a method by C.
SET_INT_RETURN(n)
set an integer return value when writing a method by C.
SET_FLOAT_RETURN(n)
set a float return value when writing a method by C.
注意が必要なのは、このマクロをコールしても、そこで関数からリターンするわけではないということです。関数の途中で引数を伴ってリターンする場合は、このマクロをコールした後に return 文が必要です。
実はこれらは、最初期に適当に名前をつけたマクロがまだそのまま使われており、MRBCプレフィクスで始まっていないなど、本来あるべき姿に改善が必要な箇所でもありますので、今後変更するかもしれません。
まとめ
mruby/c の拡張関数を、C言語で記述するには、
① 関数本体を以下のフォーマットで記述する
// 関数の定義
static void c_func1(mrbc_vm *vm, mrbc_value v[], int argc)
{
int arg1 = MRBC_ARG_I(1); // 1つ目の引数を、int型で取得する。
const char *arg2 = MRBC_ARG_S(2); // 2つ目の引数を、char *型で取得(参照)する。
// 引数が足りない等のエラーの場合、例外が発生するので return する。
if( mrbc_israised(vm) ) return;
// 確認
mrbc_printf("arg1=%d\n", arg1 );
mrbc_printf("arg2=%s\n", arg2 );
// Integer型の値を戻り値に指定する
SET_INT_RETURN( arg1 );
}
② この関数をRubyから呼ぶことができるように登録する
mrbc_define_method( 0, 0, "func1", c_func1 );
おわりに
C言語を使って Rubyから呼び出すことができる関数(メソッド)を記述する方法を説明しました。
今回は、簡単に概要的な部分を説明する事が目的なので細かい部分には触れませんでしたが、今後これらを補完する形で記事を書いていきます。