Skip to content

Ruby_extLib 3

Ko-ichiro Sugiyama edited this page Jun 14, 2023 · 24 revisions

ポーティング & C 言語で mruby/c 拡張ライブラリの作成

mruby/c は mruby, Ruby と同様に, ソースコードをバイトコード (中間コード) にコンパイルし, それを仮想マシン (Virtual Machine; VM) で実行するような処理の流れになっている. コンパイルまでの複雑な処理と仮想マシンの実行環境を 別々にすることができるため,mruby/c では以下のように一連の処理をパソコンとマイコンに分けている.

  • Rubyプログラムをバイトコードにコンパイル →パソコンで行う
  • VM (Virtual machine) を使って実行 →マイコンで行う mrubyc-compile

mruby/c では Parser と Code Generator の部分 (mrbc コマンド) は mruby の実装をそのまま使用し, VM だけ独自のコンパクトな実装を用いている. mruby/c でクラスを新たに定義する場合は,[準備] C 言語で Ruby 拡張ライブラリの作成 の場合と異なり,マイコンの公式開発環境 (C 言語) に Ruby を組み込む形となる. すなわち,メーカ等が用意した C 言語開発環境と VM のソースコードを使って, 既存の C 言語のプログラムに mruby バイトコードを結合する.

mrubyc-run2

mruby/c の ESP32 マイコン (ESP-IDF 環境) へのポーティング

ESP-IDF のサンプルをコピーして,それを作業用プロジェクトとする.

$ cp -r ~/esp/esp-idf/examples/get-started/hello_world ./mrubyc-hello_world
$ cd mrubyc-hello_world/

ESP-IDF では,components ディレクトリ以下に読み込むソースを置き, そのソースディレクトリ内にコンパイル方法を指示する component.mk ファイルを置くのが流儀である. そのため,components ディレクトリを作成し,そこに mrubyc のソースを git clone する. なお,mruby-3.1.0 を使うので,mrubyc のブランチは release3.1 とすること.

$ mkdir components
$ cd components
$ git clone -b release3.1 https://github.com/mrubyc/mrubyc.git
$ cd ../

make のルールを記述する.components/mrubyc/component.mk の中身は以下のようにする.

CFLAGS = -mlongcalls -DMRBC_USE_HAL_ESP32
COMPONENT_ADD_INCLUDEDIRS := src
COMPONENT_SRCDIRS := src

main/component.mk の中身は以下のようにする.

CFLAGS = -mlongcalls -DMRBC_USE_HAL_ESP32

hal (Hardware Abstraction Layer) はマイコンの種類に依存するので, src 以下の hal_esp32 (ESP32 用) を hal へリネームする.

$ cd main/
$ ln -s ../components/mrubyc/src/hal_esp32/hal.* .
$ cd ../

メインプログラム (main/hello_world_main.c) を編集して簡素化しておく.

#include <stdio.h>
void app_main(void)
{
  int i;
  for (i = 0; i < 100; i++) {
    printf("%02d, Hello world!\n", i);
  }
}

mrubyc ディレクトリ内のソースのコンパイル & 実行

$ make
$ make flash 
$ make monitor
    ...(略)...
  0, Hello world!
  1, Hello world!
  2, Hello world!
  3, Hello world!
  4, Hello world!
    ...(中略)...
  98, Hello world!
  99, Hello world!

mruby/c 化 (C 言語に mruby/c コードを組み込む)

mruby/c で hello world を書く.mruby/c の文法は Ruby のサブセットなので, hello world 程度のプログラムは mruby/c と Ruby で全く同じとなる. まずは Ruby のメインプログラムを置くディレクトリ (src) を作成する.

$ mkdir src

カレントディレクトリに .ruby-version を作成する.

mruby-3.1.0

作成したディレクトリ内にメインプログラムを作成する.src/master.rb を以下のように書く.

100.times do |i|
  puts "#{sprintf('%02d',i)} Hello World! from ESP32 by mruby/c"
  sleep 1
end

mrubyc-compile 上記の mruby/c コードを mrbc でコンパイルして中間コード (バイトコード) を生成する. mruby/c 側のメインプログラムはヘッダファイルとして C 言語側のメインプログラム (main/hello_world_main.c) に読み込むので,出力ファイル名は main/master.h としている.

$ mrbc -B master -o main/master.h  src/master.rb
$ ls main/master.*
   main/master.h  main/master.rb         (<-- .h ファイルが出来ている)

コンパイルした結果得られたファイル (main/master.h) 内を見てみると, 上述の mrbc コマンドの -B の引数で渡した名前 (master) の 配列が定義されており,そこの中にバイトコードが埋め込まれていることが分かる.

$ less main/master.h

  #include <stdint.h>
  #ifdef __cplusplus
  extern
  #endif
  const uint8_t master[] = {
  0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x00,0xeb,0x4d,0x41,0x54,0x5a,
  0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0xb9,0x30,0x33,0x30,0x30,
  0x00,0x00,0x00,0x29,0x00,0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x0d,
  0x03,0x01,0x64,0x57,0x02,0x00,0x30,0x01,0x00,0x00,0x38,0x01,0x69,0x00,0x00,0x00,
  0x01,0x00,0x05,0x74,0x69,0x6d,0x65,0x73,0x00,0x00,0x00,0x00,0x84,0x00,0x03,0x00,
  0x09,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x24,0x34,0x04,0x00,0x00,0x51,0x04,0x00,
  0x51,0x06,0x01,0x01,0x07,0x01,0x2d,0x05,0x00,0x02,0x52,0x04,0x51,0x05,0x02,0x52,
  0x04,0x2d,0x03,0x01,0x01,0x07,0x04,0x2d,0x03,0x02,0x01,0x38,0x03,0x00,0x03,0x00,
  0x00,0x00,0x00,0x00,0x00,0x04,0x25,0x30,0x32,0x64,0x00,0x00,0x00,0x23,0x20,0x48,
  0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0x20,0x66,0x72,0x6f,0x6d,
  0x20,0x45,0x53,0x50,0x33,0x32,0x20,0x62,0x79,0x20,0x6d,0x72,0x75,0x62,0x79,0x2f,
  0x63,0x00,0x00,0x03,0x00,0x07,0x73,0x70,0x72,0x69,0x6e,0x74,0x66,0x00,0x00,0x04,
  0x70,0x75,0x74,0x73,0x00,0x00,0x05,0x73,0x6c,0x65,0x65,0x70,0x00,0x4c,0x56,0x41,
  0x52,0x00,0x00,0x00,0x16,0x00,0x00,0x00,0x02,0x00,0x01,0x69,0x00,0x01,0x26,0x00,
  0x00,0x00,0x01,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
  };

mruby/c のコードを組み込む先のメインプログラム (main/hello_world_main.c) を作成する. これは C 言語で書く. main/hello_world_main.c を以下のように修正する.

#include "mrubyc.h"  //mruby/c のヘッダファイルの読み込み
#include "master.h"  //mruby/c 側のメインプログラム (master.rb) のバイトコードの読み込み (バイトコードをヘッダファイルとして読み込む)

#define MEMORY_SIZE (1024*40)   //使用するメモリサイズの指定.現在は 40 k バイト.

static uint8_t memory_pool[MEMORY_SIZE];  //指定したメモリ量を用いてメモリ用の変数を定義.

//メイン関数
void app_main(void) {
  mrbc_init(memory_pool, MEMORY_SIZE);   //メモリサイズを指定して初期化

  //指定したバイトコード "master" を実行することを宣言
  // 第 1 引数は master.h でバイトコードを格納する配列の名前に合わせる
  // 第 2 引数は,初期実行状態を指示するための拡張用.普通に実行する場合はゼロで良い.
  mrbc_create_task( master, 0 );

  //mruby/c プログラムの実行開始
  mrbc_run();
}

メインプログラムのコンパイルと実行.ESP-IDF の流儀に従って, make すれば良い.

$ make
$ make flash monitor

  ....(略)....
  98 Hello World! from ESP32 by mruby/c
  99 Hello World! from ESP32 by mruby/c

mruby/c 化 (クラス定義あり)

Ruby 側のメインプログラムでは,Greeter クラスの hello メソッドを呼ぶようにしておく. src/master.rb を以下のように修正する.

greeter = Greeter.new

100.times do |i|
  greeter.hello
  sleep 1
end

Ruby コードを mrbc でコンパイルする.

$ mrbc  -B master -o main/master.h src/master.rb

mruby/c のクラスを自作する場合は, [準備] C 言語で Ruby 拡張ライブラリの作成 で 説明したのと同様に, それを C 言語側で定義するか, mruby/c 側で定義するかの 2 通りの選択肢がある. 以下では,それぞれについて例示する.

パターン (1) : C 言語側で Greeter クラスの定義

C 側のメインプログラム (main/hello_world_main.c) を以下のように修正する.[準備] C 言語で Ruby 拡張ライブラリの作成 では クラス定義に rb_define_class,メソッド定義に rb_define_method 関数を用いたが, mruby/c では mrbc_define_class と mrbc_define_method 関数を用いる.

#include "mrubyc.h"  //mruby/c のヘッダファイルの読み込み
#include "master.h"  //mruby/c 側のメインプログラム (master.rb) のバイトコードの読み込み (バイトコードをヘッダファイルとして読み込む)

#define MEMORY_SIZE (1024*40)   //使用するメモリサイズの指定.現在は 40 k バイト.

static uint8_t memory_pool[MEMORY_SIZE];  //指定したメモリ量を用いてメモリ用の変数を定義.

static struct RClass* mrbc_class_greeter; //C 側でクラス定義するために必要な構造体を定義

//メソッドとして使う関数を定義.
// 型は常に void. static を付ける.
// 引数は常に (mrb_vm* vm, mrb_value* v, int argc) としておく.変更の必要はない.
static void
mrbc_hello(mrb_vm* vm, mrb_value* v, int argc){
  printf("Hello world! by C\n");
}

//C 側のクラスの初期化関数
void mrbc_greeter_init(struct VM* vm){

  //mrbc_define_class でクラス名を定義
  //第 1 引数:常に vm, 第 2 引数:クラス名, 第 3 引数:上位クラス (常に mrbc_class_object で良い)
  mrbc_class_greeter = mrbc_define_class(vm, "Greeter", mrbc_class_object);

  //mrbc_define_method でメソッドを定義
  //第 1 引数:常に vm, 第 2 引数:クラス用の構造体の名前, 第 3 引数:メソッド名, 第 4 引数:メソッドに対応する C の関数名
  mrbc_define_method(vm, mrbc_class_greeter, "hello",   mrbc_hello);
}

//メイン関数
void app_main(void) {
  mrbc_init(memory_pool, MEMORY_SIZE);   //メモリサイズを指定して初期化

  //C 側で定義したクラスを有効化
  //引数:ゼロ (NULL ポインタ)
  //C 言語でクラスや関数を定義する場合には,初期化処理においてまだタスクの宣言を行っていない状態,
  //すなわち mrbc_create_task() を呼んでいないために VM 構造体が確保されていない状態,で行いたいことが多い.
  //この場合もそうである. そういった場合にはゼロ(=NULLポインタ)でも許すような言語デザインになっている.
  mrbc_greeter_init(0);

  //指定したバイトコード "master" を実行することを宣言
  // 第 1 引数は master.h でバイトコードを格納する配列の名前に合わせる
  // 第 2 引数は,初期実行状態を指示するための拡張用.普通に実行する場合はゼロで良い.
  mrbc_create_task( master, 0 );

  //mruby/c プログラムの実行開始
  mrbc_run();
}

メインプログラムのコンパイルと実行.ESP-IDF の流儀で行えば良い.

$ make
$ make flash monitor

  ....(略)....
  Hello world! by C
  Hello world! by C

パターン (2) : mruby/c 側で Greeter クラスの定義

ファイルを置くためのディレクトリを作成しておく.

$ mkdir -p mrblib

mrblib/greeter.rb を以下のように作成する.

class Greeter
  def hello
    puts "Hello World! by mruby/c"
  end
end

クラス定義ファイルであっても, 以下の図にあるように,mruby/c のコードを中間コードにコンパイルする必要がある.

mrubyc-compile

上記の mruby/c コードを mrbc でコンパイルして中間コード (バイトコード) を生成する. なお以下の例では,C 側のメインプログラムでの読み込み方が異なるので, コンパイル結果の拡張子は .c (main/mrblib.c) としておく.

$ mrbc -B myclass_bytecode --remove-lv -o main/mrblib.c  mrblib/greeter.rb
$ less main/mrblib.c

  #include <stdint.h>
  #ifdef __cplusplus
  extern
  #endif
  const uint8_t myclass_bytecode[] = {
  0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x00,0xbc,0x4d,0x41,0x54,0x5a,
  0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0xa0,0x30,0x33,0x30,0x30,
  0x00,0x00,0x00,0x2b,0x00,0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x0d,
  0x11,0x01,0x11,0x02,0x5c,0x01,0x00,0x5e,0x01,0x00,0x38,0x01,0x69,0x00,0x00,0x00,
  0x01,0x00,0x07,0x47,0x72,0x65,0x65,0x74,0x65,0x72,0x00,0x00,0x00,0x00,0x26,0x00,
  0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x0a,0x63,0x01,0x58,0x02,0x00,
  0x5f,0x01,0x00,0x38,0x01,0x00,0x00,0x00,0x01,0x00,0x05,0x68,0x65,0x6c,0x6c,0x6f,
  0x00,0x00,0x00,0x00,0x43,0x00,0x02,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x0d,0x34,0x00,0x00,0x00,0x51,0x03,0x00,0x2d,0x02,0x00,0x01,0x38,0x02,0x00,0x01,
  0x00,0x00,0x17,0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0x20,
  0x62,0x79,0x20,0x6d,0x72,0x75,0x62,0x79,0x2f,0x63,0x00,0x00,0x01,0x00,0x04,0x70,
  0x75,0x74,0x73,0x00,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
  };

C 言語側のメインプログラム (main/hello_world_main.c) の修正し,mruby/c 側で作成したクラスを読み込むようにする.

#include "mrubyc.h"  //mruby/c のヘッダファイルの読み込み
#include "master.h"  //mruby/c 側のメインプログラム (master.rb) のバイトコードの読み込み (バイトコードをヘッダファイルとして読み込む)

#define MEMORY_SIZE (1024*40)   //使用するメモリサイズの指定.現在は 40 k バイト.

static uint8_t memory_pool[MEMORY_SIZE];  //指定したメモリ量を用いてメモリ用の変数を定義.

//メイン関数
void app_main(void) {
  mrbc_init(memory_pool, MEMORY_SIZE);   //メモリサイズを指定して初期化

  //mrblib.c ではバイトコードは myclass_bytecode 配列に格納されている
  extern const uint8_t myclass_bytecode[];

  //クラス用の mruby/c コードをコンパイルして作られた C のファイルの読み込み
  mrbc_run_mrblib(myclass_bytecode);

  //指定したバイトコード "master" を実行することを宣言
  // 第 1 引数は master.h でバイトコードを格納する配列の名前に合わせる
  // 第 2 引数は,初期実行状態を指示するための拡張用.普通に実行する場合はゼロで良い.
  mrbc_create_task( master, 0 );

  //mruby/c プログラムの実行開始
  mrbc_run();
}

メインプログラムのコンパイルと実行.ESP-IDF の流儀で行えば良い.

$ make
$ make flash monitor

  ....(略)....
  Hello World! by mruby/c
  Hello World! by mruby/c

C で定義した関数を mruby/c で利用する

以下では,上記の「パターン (2) mruby/c 側で Greeter クラスの定義」 で作成したプログラムを拡張し,C 側で定義された足し算用関数 c_add を mruby/c 側のクラス Calc から利用する例を作成する.

mruby 側のメインプログラム (src/master.rb) を修正して, 以下のように Greeter クラスの hello メソッドと, Calc クラスの add メソッドを呼ぶようにしておく.

greeter = Greeter.new
greeter.hello
sleep 1

obj = Calc.new
sum = obj.add(1,2)
puts "1 + 2 = #{sum}"

修正したので,mrbc コマンドで main/master.rb をコンパイルし直す.

$ mrbc  -B master -o main/master.h src/master.rb

既に Greeter クラスは作成しているので,ここでは Calc クラスを作成する. mrblib/calc.rb を以下のように作成する.ここで,mruby/c のクラスから C 言語側の関数を呼ぶような形にしている.

class Calc
  def add(a, b)
    c_add(a, b)  #Cの関数を呼ぶ
  end
end

クラス定義のファイルを追加したので,改めてコンパイルを行う. 以下のように mrbc コマンドで引数として与える ruby ファイルを複数与えれば良い.

$ mrbc -B myclass_bytecode --remove-lv -o main/mrblib.c mrblib/greeter.rb mrblib/calc.rb

C 側のプログラム (main/hello_world_main.c) を修正する.今回は mruby/c 側から呼び出す C 側の関数も 1 つのファイルにまとめた.当然,別のファイルに分けることも出来る.

#include "mrubyc.h"  //mruby/c のヘッダファイルの読み込み
#include "master.h"  //mruby/c 側のメインプログラム (master.rb) のバイトコードの読み込み (バイトコードをヘッダファイルとして読み込む)

#define MEMORY_SIZE (1024*40)   //使用するメモリサイズの指定.現在は 40 k バイト.

static uint8_t memory_pool[MEMORY_SIZE];  //指定したメモリ量を用いてメモリ用の変数を定義.

// mruby/c から呼ぶ関数の実体
// 型は常に void. static を付ける.
// 引数は常に (mrb_vm* vm, mrb_value* v, int argc) としておく.変更の必要はない.
//   * GET_INT_ARG : int 型へ変換, GET_STRING_ARG : char 型へ変換
//   * SET_INT_RETURN : int 型を戻す, SET_FLOAT_RETURN : float 型を戻す, SET_NIL_RETURN : nil を戻す,
//     SET_TRUE_RETURN : true を戻す, SET_FALSE_RETURN : false を戻す,   SET_BOOL_RETURN : bool 型を戻す
static void c_add(mrb_vm *vm, mrb_value *v, int argc) {
  int a = GET_INT_ARG(1);
  int b = GET_INT_ARG(2);
  int sum = a + b;
  SET_INT_RETURN(sum);
}


//メイン関数
void app_main(void) {
  mrbc_init(memory_pool, MEMORY_SIZE);   //メモリサイズを指定して初期化

  //mrblib.c ではバイトコードは myclass_bytecode 配列に格納されている
  extern const uint8_t myclass_bytecode[];

  //クラス用の mruby/c コードをコンパイルして作られた C のファイルの読み込み
  mrbc_run_mrblib(myclass_bytecode);

  // mrbc_define_method で C の関数を mruby/c 側から呼べるようにする.
  // * 第 1 引数:vm でなくゼロ (NULL ポインタ)
  // * 第 2 引数:対象のクラス (ここでは上位クラスである mrbc_class_object を指定)
  // * 第 3 引数:メソッド名
  // * 第 4 引数:メソッドに対応する C の関数名
  mrbc_define_method(0, mrbc_class_object, "c_add", c_add);

  //指定したバイトコード "master" を実行することを宣言
  // 第 1 引数は master.h でバイトコードを格納する配列の名前に合わせる
  // 第 2 引数は,初期実行状態を指示するための拡張用.普通に実行する場合はゼロで良い.
  mrbc_create_task( master, 0 );

  //mruby/c プログラムの実行開始
  mrbc_run();
}

メインプログラムのコンパイルと実行.ESP-IDF の流儀に従って, make すれば良い.

$ make
$ make flash monitor

  Hello World! by mruby/c
  1 + 2 = 3

補足:Make のルール作成

上記では mrbc コマンドを手動で実行していたが,Make のルールを作成しておくとコマンド一発で自動実行されるので簡単である. 例えば main/component.mk を 本リポジトリに含まれる main/component.mk のように設定しておけばよい.

Clone this wiki locally