しまねソフト研究開発センター(略称 ITOC)にいます、東です。
mruby/c 向けのC言語を使った開発資料の充実のために、数回にわたり解説記事を書いています。今回は、C言語を使ってキーワード引数をもつ関数(メソッド)を記述する方法を説明します。
Abstract
mruby/c でキーワード引数を持つ関数(メソッド)を、C言語で記述するには、
① 関数本体を以下のフォーマットで記述する
static void c_kwfunc(mrbc_vm *vm, mrbc_value v[], int argc)
{
// キーワード引数 k1, k2 を使うことを宣言
MRBC_KW_ARG(k1, k2);
// 引数エラー(例外発生)の処理
// k1, k2 とも必須引数であることを宣言
if( !MRBC_KW_MANDATORY(k1, k2) ) goto out;
// k1, k2 以外のキーワード引数が指定されていたらエラーとする
if( !MRBC_KW_END() ) goto out;
// 確認のための表示
mrbc_print("k1="); mrbc_p(&k1);
mrbc_print("k2="); mrbc_p(&k2);
out:
// 宣言した変数 k1, k2 を解放
MRBC_KW_DELETE(k1, k2);
}
② この関数をRubyから呼ぶことができるように登録する
mrbc_define_method( 0, 0, "kwfunc", c_kwfunc );
目標・方針
キーワード引数のさまざまなパターン(必須引数、デフォルト値、一括取得など)について、具体的なコードを示して解説します。
事前準備・環境
先日公開した記事、mruby/c をPCで動かす の環境での動作を想定しています。今回も、sample_c/sample_concurrent.c に追記する形で説明します。
基本的な記述方法
まず、最も基本的なパターンについて説明します。
仕様
kwfunc( k1:, k2: )
- キーワード引数 k1, k2 を持つ
- k1, k2 引数が指定されていないとエラー
- k1, k2 以外が指定されているとエラー
static void c_kwfunc(mrbc_vm *vm, mrbc_value v[], int argc)
{
// キーワード引数 k1, k2 を使うことを宣言
MRBC_KW_ARG(k1, k2);
// 引数エラー(例外発生)の処理
// k1, k2 とも必須引数であることを宣言
if( !MRBC_KW_MANDATORY(k1, k2) ) goto out;
// k1, k2 以外のキーワード引数が指定されていたらエラーとする
if( !MRBC_KW_END() ) goto out;
// 確認のための表示
mrbc_print("k1="); mrbc_p(&k1);
mrbc_print("k2="); mrbc_p(&k2);
out:
// 宣言した変数 k1, k2 を解放
MRBC_KW_DELETE(k1, k2);
}
mrubyも、mruby/c も、内部的にはキーワード引数は Hash として表現されることは同じです。そのため Hash から直接値を取り出すことも可能ですが、コードが非常に煩雑になります。特に mruby/c ではリファレンスカウント方式の GC を採用しているため、メモリ管理(参照カウントの増減)に細心の注意を払う必要があります。
そこで、 mruby/c ではこれらの複雑な処理を隠蔽し、安全に扱うためのマクロ (MRBC_KW_*) を用意しました。
実行例
テスト1
kwfunc( k1:11, k2:"ABC" )
実行結果
k1=11
k2="ABC"
テスト2
kwfunc( k1:11 )
実行結果
Exception(vm_id=1): in `kwfunc': missing keyword: k2 (ArgumentError)
テスト1では正しくキーワード引数が取得できており、テスト2ではマクロによって自動的にエラーハンドリングが行われていることがわかります。
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_kwfunc(mrbc_vm *vm, mrbc_value v[], int argc)
{
// キーワード引数 k1, k2 を使うことを宣言
MRBC_KW_ARG(k1, k2);
// 引数エラー(例外発生)の処理
// k1, k2 とも必須引数であることを宣言
if( !MRBC_KW_MANDATORY(k1, k2) ) goto out;
// k1, k2 以外のキーワード引数が指定されていたらエラーとする
if( !MRBC_KW_END() ) goto out;
// 確認のための表示
mrbc_print("k1="); mrbc_p(&k1);
mrbc_print("k2="); mrbc_p(&k2);
out:
// 宣言した変数 k1, k2 を解放
MRBC_KW_DELETE(k1, k2);
}
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, "kwfunc", c_kwfunc );
// 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 ) ) {
free(mrbbuf);
return 1;
}
}
// and execute all.
int ret = mrbc_run();
return ret == 1 ? 0 : ret;
}
取得した引数をC言語の型に変換する
C言語では、mrbc_value型 からC言語のプリミティブ型(intやchar*など)への変換が必要です。先ほどの例に変換処理を加えると以下のようになります。
static void c_kwfunc(mrbc_vm *vm, mrbc_value v[], int argc)
{
// キーワード引数 k1, k2 を使うことを宣言、変数 k1, k2 も宣言される
MRBC_KW_ARG(k1, k2);
// 引数エラー(例外発生)の処理
// k1, k2 とも必須引数であることを宣言
if( !MRBC_KW_MANDATORY(k1, k2) ) goto out;
// k1, k2 以外のキーワード引数が指定されていたらエラーとする
if( !MRBC_KW_END() ) goto out;
// Cプリミティブタイプへの変換。変換ができなければ例外が発生する
int k1i = MRBC_VAL_I(&k1);
const char *k2s = MRBC_VAL_S(&k2);
if( mrbc_israised(vm) ) goto out;
// 確認のための表示
mrbc_printf("k1=%d\nk2=%s\n", k1i, k2s );
out:
// 宣言した変数 k1, k2 を解放
MRBC_KW_DELETE(k1, k2);
MRBC_VAL_I() および MRBC_VAL_S() マクロを使いました。
このマクロは、以前の記事 mruby/c 3.4 に導入した引数取得マクロの解説 で、紹介済みです。
デフォルト値がある(必須ではない)キーワード引数
先の例では、k1, k2 とも必須の引数でした。
次は、省略可能な引数、言い換えるとデフォルト値がある例です。
仕様
kwfunc( k1:, k2:"ABC" )
- キーワード引数 k1, k2 を持つ
- k1 が指定されていないとエラー
- k2 が指定されていない場合は、デフォルト値 "ABC" を使う
- k1, k2 以外が指定されているとエラー
static void c_kwfunc(mrbc_vm *vm, mrbc_value v[], int argc)
{
// キーワード引数 k1, k2 を使うことを宣言、変数 k1, k2 も宣言される
MRBC_KW_ARG(k1, k2);
// 引数エラー(例外発生)の処理
// k1 のみ必須引数であることを宣言
if( !MRBC_KW_MANDATORY(k1) ) goto out;
// k2 が指定されていなければ、デフォルト値を使う
if( !MRBC_KW_ISVALID(k2) ) k2 = mrbc_string_new_cstr(vm, "ABC");
// k1, k2 以外のキーワード引数が指定されていたらエラーとする
if( !MRBC_KW_END() ) goto out;
// Cプリミティブタイプへの変換。変換ができなければ例外が発生する
int k1i = MRBC_VAL_I(&k1);
const char *k2s = MRBC_VAL_S(&k2);
if( mrbc_israised(vm) ) goto out;
// 確認のための表示
mrbc_printf("k1=%d\nk2=%s\n", k1i, k2s );
out:
// 宣言した変数 k1, k2 を解放
MRBC_KW_DELETE(k1, k2);
}
MRBC_KW_MANDATORY() のリストから k2 を外し、MRBC_KW_ISVALID() を使って値が渡されたかどうかを判定しています。
残りのキーワード引数を Hash で一括取得する
特定のキーワード以外をすべて Hash として受け取る方法です。
仕様
kwfunc( k1:, k2:, **dict )
- キーワード引数 k1, k2 を持つ
- k1, k2 が指定されていないとエラー
- それ以外の未知のキーワード引数はすべて dict(Hash)に格納する
static void c_kwfunc(mrbc_vm *vm, mrbc_value v[], int argc)
{
// キーワード引数 k1, k2 を使うことを宣言、変数 k1, k2 も宣言される
MRBC_KW_ARG(k1, k2);
// 残りのキーワード引数を Hashで受け取る、変数 dict も宣言される
MRBC_KW_DICT(dict);
// 引数エラー(例外発生)の処理
// k1, k2 とも必須引数であることを宣言
if( !MRBC_KW_MANDATORY(k1, k2) ) goto out;
// Cプリミティブタイプへの変換。変換ができなければ例外が発生する
int k1i = MRBC_VAL_I(&k1);
const char *k2s = MRBC_VAL_S(&k2);
if( mrbc_israised(vm) ) goto out;
// 確認のための表示
mrbc_printf("k1=%d\nk2=%s\n", k1i, k2s );
mrbc_printf("dict="); mrbc_p(&dict);
out:
// 宣言した変数 k1, k2, dict を解放
MRBC_KW_DELETE(k1, k2, dict);
}
ポイントは、未知の引数をエラーにする MRBC_KW_END() を削除し、代わりに MRBC_KW_DICT(dict) を追加することです。dict もマクロ内で変数宣言されるため、MRBC_KW_DELETE() での解放が必要です。
マクロ リファレンス
MRBC_KW_ARG(引数リスト)
- キーワード引数の使用を宣言します
- 引数リストに指定した名前の mrbc_value 型の変数がローカル変数として宣言されます
MRBC_KW_ARG(k1, k2); // 内部的に mrbc_value k1, k2 と変数宣言がされる
MRBC_KW_DELETE(引数リスト)
- キーワード引数用に確保された変数を解放します
MRBC_KW_ARG および MRBC_KW_DICT で宣言したすべての変数をここに含める必要があります。これは mruby/c のメモリ管理上、プログラマが明記しなければならないルールです。
MRBC_KW_DELETE(k1, k2, dict); // 宣言した変数 k1, k2, dict を解放
MRBC_KW_MANDATORY(引数リスト)
- 必須のキーワード引数を指定します
- 指定された引数がすべて存在すれば true を返します
- 不足があれば false を返して ArgumentError 例外を発生させます
// k1,k2 のどちらかが指定されていなければ、エラー(ArgumentError)として終了
if( !MRBC_KW_MANDATORY(k1, k2) ) goto out;
MRBC_KW_END()
- 宣言されていないキーワード引数が渡されていないかチェックします
- 正しく確認ができれば true を返します
- 未知の引数があれば false を返し、ArgumentError を発生させます
if( !MRBC_KW_END() ) goto out;
MRBC_KW_DICT(変数名)
- 規定以外のキーワード引数を Hash として一括取得します
- 指定した
変数名で mrbc_value 型の変数が宣言されます
// 残りのキーワード引数を Hashで受け取る、変数 dict も宣言される
MRBC_KW_DICT(dict);
MRBC_KW_ISVALID(変数名)
- その引数に有効な値が渡されたかどうかを判定します(デフォルト値の設定に使用します)
// k2 が指定されていなければ、デフォルト値を使う
if( !MRBC_KW_ISVALID(k2) ) k2 = mrbc_string_new_cstr(vm, "ABC");
おわりに
今回は、C言語を使ってキーワード引数をもつ関数(メソッド)を記述する方法を説明しました。
記事 「mruby/c の拡張関数を、C言語で記述する」 や、「mruby/c 3.4 に導入した引数取得マクロの解説」 と併せて読んでいただくことで、Ruby の柔軟な引数指定をC言語側で効率的かつ安全に実装する方法がより深く理解できるはずです。