mruby/c のクラス作成チュートリアル (ESP32 マイコン版)

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

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

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

準備

Cコンパイラ

mruby のコンパイルに用いたものと同じものが必要となる. UNIX 系で gcc パッケージをインストールしておけば良い.

mruby

mruby/c では,コードのコンパイルに mrbc (mruby の提供するコンパイラ) を使っている. そのため,予め mruby をインストールしておく必要がある. 以下では mruby-2.1.0 の利用を前提としている.

ESP32 マイコンを使う場合

ESP32 マイコンの公式開発環境である ESP-IDF のサンプルを利用する. まずは hello world のサンプルを mrubyc-hello_world にコピーする.

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

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

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

mrubyc を make するためのルールを追加する. mrubyc/src および mrubyc/hal をコンパイルするように指示する.

$ vi mrubyc/component.mk

  CFLAGS = -mlongcalls 
  COMPONENT_ADD_INCLUDEDIRS := src
  COMPONENT_SRCDIRS := src src/hal

hal (Hardware Abstraction Layer) はマイコンの種類に依存するので, src 以下の hal_esp32 (ESP32 用) を hal へリネームする. mruby/c のソース内で #include "hal/hal.h" がなされているためである.

$ mv mrubyc/src/hal_esp32 mrubyc/src/hal

mrubyc ディレクトリ内のソースがコンパイルされるか確かめる (この段階ではメインプログラムは編集していない).

$ cd ..     (このコマンドで ~/esp/mrubyc-hello_world に移動するはず)
$ make
$ make flash monitor

マウスで make monitor のログをスクロールして, "CC build/mrubyc/src/mrblib.o" のような出力があるか確認する. さらに,build/mrubyc/src/ 以下にオブジェクトファイルが作成されていることを確認する.

最後に,mrubyc-hello_world 以下にコンパイルをする際に使う Ruby のバージョンを書き入れておく.そうしておくと,Ruby の バージョンの切替えの手間が無くなる.

$ vi .ruby-version

  mruby-2.1.0

具体例 (1) : "Hello World" (クラス定義無し)

以下では,ESP32 マイコンの開発環境 ESP-IDF の提供している "hello world" のサンプルプログラムを元にする.

準備:サンプルプログラムの修正と実行 (この段階では C 言語のみ)

デフォルトの hello_world_main.c は ESP32 チップの情報などを出しているので, include するヘッダファイルの数が多く, プログラムの行数も多い. 単に "hello world" を表示するだけであれば, 以下のようなよくある単純な hello world プログラムで十分である. 但し, メイン関数名は main でなく app_main であることに注意されたい. また試してみると分かるが, app_main の型は void でないといけない (int 型ではダメ).

$ cp main/hello_world_main.c main/hello_world_main.c.bk

$ vi main/hello_world_main.c  

エディタで以下のようなプログラムを書く.

1 #include <stdio.h>
2 
3 void app_main(void)
4 {
5   int i;
6   for (i = 0; i < 100; i++) {
7     printf("%02d, Hello world!\n", i);
8   }
9 }
  • 6-8 行目
    • for 文を使って "Hello World!" を 100 回表示.

書き終わったら, make して実行する.

$ make

$ make flash monitor

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

このプログラムは 100 回表示させたら "Hello World!" の表示が止まる. 教育ボードのリスタートスイッチ (緑色のボタン) を押すと, 再びプログラムが実行されることがわかるだろう.

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

mruby/c で hello world を書く.mruby/c の文法は Ruby のサブセットなので, hello world 程度のプログラムは mruby/c と Ruby で全く同じとなる.

$ vi main/master.rb

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

以下の図にあるように,mruby/c のコードを中間コードにコンパイルする 必要がある.

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

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

$ ls main/master.*

  main/master.h  main/master.rb

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

$ less main/master.h

  /* dumped in big endian order.
     use `mrbc -e` option for better performance on little endian CPU. */
  #include <stdint.h>
  #ifdef __cplusplus
  extern const uint8_t master[];
  #endif
  const uint8_t
  #if defined __GNUC__
  __attribute__((aligned(4)))
  #elif defined _MSC_VER
  __declspec(align(4))
  #endif
  master[] = {
  0x52,0x49,0x54,0x45,0x30,0x30,0x30,0x36,0xee,0x20,0x00,0x00,0x00,0xf9,0x4d,0x41,
  0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0xc1,0x30,0x30,
  0x30,0x32,0x00,0x00,0x00,0x56,0x00,0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x0d,
  0x03,0x01,0x64,0x55,0x02,0x00,0x2f,0x01,0x00,0x00,0x37,0x01,0x67,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x01,0x00,0x05,0x74,0x69,0x6d,0x65,0x73,0x00,0x00,0x00,0x01,
  0x0b,0x00,0x03,0x00,0x09,0x00,0x00,0x00,0x00,0x00,0x2a,0x00,0x33,0x04,0x00,0x00,
  0x10,0x03,0x4f,0x04,0x00,0x10,0x05,0x4f,0x06,0x01,0x01,0x07,0x01,0x2e,0x05,0x00,
  0x02,0x50,0x04,0x4f,0x05,0x02,0x50,0x04,0x2e,0x03,0x01,0x01,0x10,0x03,0x07,0x04,
  0x2e,0x03,0x02,0x01,0x37,0x03,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x04,
  0x25,0x30,0x32,0x64,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,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,0x1a,0x00,
  0x00,0x00,0x02,0x00,0x01,0x69,0x00,0x01,0x26,0x00,0x00,0x00,0x01,0x00,0x01,0x00,
  0x02,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
  };

mruby/c のコードを組み込む先のメインプログラムを作成する. これは C 言語で書く.

$ vi  main/hello_world_main.c

   1 #include "mrubyc.h"
   2 #include "master.h"
   3
   4 #define MEMORY_SIZE (1024*40)
   5
   6 static uint8_t memory_pool[MEMORY_SIZE];
   7
   8 void app_main(void) {
   9   mrbc_init(memory_pool, MEMORY_SIZE);
  10
  11   mrbc_create_task( master, 0 );
  12   mrbc_run();
  13 }

上記プログラムは以下のようになっている.

  • 1 行目:mruby/c のヘッダファイルの読み込み
  • 2 行目:mruby/c 側のメインプログラム (master.rb) のバイトコードの読み込み
    • バイトコードをヘッダファイルとして読み込む
  • 4 行目:使用するメモリサイズの指定.現在は 40 k バイト.
  • 6 行目:4 行目で指定したメモリ量を用いてメモリ用の変数を定義.
  • 8-13 行目:メイン関数
    • 型は void
    • 9 行目:メモリサイズを指定して初期化
    • 11 行目:指定したバイトコード "master" を実行することを宣言
      • 第 1 引数は master.h でバイトコードを格納する配列の名前に合わせる
      • 第 2 引数は,初期実行状態を指示するための拡張用.普通に実行する場合はゼロで良い.
    • 12 行目:mruby/c プログラムの実行開始

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

$ make flash monitor

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

具体例 (2) : "Hello World" (クラス定義有り.)

Ruby 側のメインプログラムでは,Greeter クラスの hello メソッドを呼ぶようにしておく

$ vi main/master.rb

   1 greeter = Greeter.new
   2
   3 100.times do |i|
   4   greeter.hello
   5   sleep 1
   6 end

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

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

mruby/c のクラスを自作する場合は, Ruby のクラス作成チュートリアル で 説明したのと同様に, それを C 言語側で定義するか, mruby/c 側で定義するかの 2 通りの選択肢がある. 以下では,それぞれについて例示する.

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

C 側のメインプログラムを以下のように修正する. Ruby のクラス作成チュートリアル では クラス定義に rb_define_class,メソッド定義に rb_define_method 関数を用いたが, mruby/c では mrbc_define_class と mrbc_define_method 関数を用いる.

$ vi  main/hello_world_main.c

   1 #include "mrubyc.h"
   2 #include "master.h"
   3
   4 #define MEMORY_SIZE (1024*40)
   5
   6 static uint8_t memory_pool[MEMORY_SIZE];
   7
   8 static struct RClass* mrbc_class_greeter;        
   9
  10 static void
  11 mrbc_hello(mrb_vm* vm, mrb_value* v, int argc){
  12   printf("Hello world! by C\n");
  13 }
  14
  15 void mrbc_greeter_init(struct VM* vm){
  16   mrbc_class_greeter = mrbc_define_class(vm, "Greeter", mrbc_class_object);
  17   mrbc_define_method(vm, mrbc_class_greeter, "hello",   mrbc_hello);
  18 }
  19
  20 void app_main(void) {
  21   mrbc_init(memory_pool, MEMORY_SIZE);
  22
  23   mrbc_greeter_init(0);
  24
  25   mrbc_create_task( master, 0 );
  26   mrbc_run();
  27 }

上記プログラムであるが,前述の「具体例 (1)」と異なる部分は以下の通りである.

  • 8 行目:C 側でクラス定義するために必要な構造体を定義
  • 10-13 行目:メソッドとして使う関数を定義.
    • 型は常に void. static を付ける.
    • 引数は常に (mrb_vm* vm, mrb_value* v, int argc) としておく.変更の必要はない.
  • 15-18 行目:C 側のクラスの初期化関数
    • mrbc_define_class でクラス名を定義.
      • 第 1 引数:常に vm
      • 第 2 引数:クラス名
      • 第 3 引数:上位クラス (常に mrbc_class_object で良い)
    • mrbc_define_method でメソッドを定義.
      • 第 1 引数:常に vm
      • 第 2 引数:クラス用の構造体の名前
      • 第 3 引数:メソッド名
      • 第 4 引数:メソッドに対応する C の関数名
  • 20-27 行目:メイン関数
    • 23 行目で C 側で定義したクラス (15-18行目) を有効化
      • 引数:ゼロ (NULL ポインタ)
      • C 言語でクラスや関数を定義する場合には,初期化処理においてまだタスクの宣言を行っていない状態, すなわち mrbc_create_task() を呼んでいないために VM 構造体が確保されていない状態,で行いたいことが多い.この場合もそうである. そういった場合にはゼロ(=NULLポインタ)でも許すような言語デザインになっている.

それ以外の部分は,前述の「具体例 (1)」と同じである.

  • 1 行目:mruby/c のヘッダファイルの読み込み
  • 2 行目:mruby/c 側のメインプログラム (master.rb) のバイトコードの読み込み
    • バイトコードをヘッダファイルとして読み込む
  • 4 行目:使用するメモリサイズの指定.現在は 40 k バイト.
  • 6 行目:4 行目で指定したメモリ量を用いてメモリ用の変数を定義.
  • 20-26 行目:メイン関数
    • 型は void
    • 21行目:メモリサイズを指定して初期化
    • 25 行目:指定したバイトコード "master" を実行することを宣言
      • 第 1 引数は master.h でバイトコードを格納する配列の名前に合わせる
      • 第 2 引数は,初期実行状態を指示するための拡張用.普通に実行する場合はゼロで良い.
    • 26 行目:mruby/c プログラムの実行開始

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

$ make flash monitor

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

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

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

$ mkdir -p mrblib/class

クラス定義のファイルを作成する. Ruby のクラス作成チュートリアル で 作成した greeter.rb と内容は同じである.

$ vi mrblib/class/greeter.rb

   1 class Greeter
   2   def hello
   3     puts "Hello World! by mruby/c"
   4   end
   5 end

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

上記の mruby/c コードを mrbc でコンパイルして中間コード (バイトコード) を生成する. 以下の例では,今後にクラス定義のファイルが複数作られるようになることを想定して, まず最初に cat コマンドでクラス定義用のファイルを 1 つのファイル (mrblib/mrblib.rb) にまとめ,その後に mrbc でコンパイルして中間コード (バイトコード) を生成している. なお,C 側のメインプログラムでの読み込み方が異なるので, コンパイル結果の拡張子は .c (main/mrblib.c) としておく.

$ cat mrblib/class/*rb > mrblib/mrblib.rb
$ mrbc -E -B myclass --remove-lv -o main/mrblib.c mrblib/mrblib.rb

コンパイルした結果作られた main/mrblib.c の中身の確認する. myclass の配列の中身として,中間バイトコードが埋め込まれていることがわかる.

$ less main/mrblib.c

  /* dumped in big endian order.
     use `mrbc -e` option for better performance on little endian CPU. */
  #include <stdint.h>
  #ifdef __cplusplus
  extern const uint8_t myclass[];
  #endif
  const uint8_t
  #if defined __GNUC__
  __attribute__((aligned(4)))
  #elif defined _MSC_VER
  __declspec(align(4))
  #endif
  myclass[] = {
  0x52,0x49,0x54,0x45,0x30,0x30,0x30,0x36,0x6d,0x27,0x00,0x00,0x00,0xcc,0x4d,0x41,
  0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0xae,0x30,0x30,
  0x30,0x32,0x00,0x00,0x00,0x58,0x00,0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x0d,
  0x0f,0x01,0x0f,0x02,0x5a,0x01,0x00,0x5c,0x01,0x00,0x37,0x01,0x67,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x01,0x00,0x07,0x47,0x72,0x65,0x65,0x74,0x65,0x72,0x00,0x00,
  0x00,0x00,0x56,0x00,0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x0d,0x00,0x00,0x00,
  0x61,0x01,0x56,0x02,0x00,0x5d,0x01,0x00,0x0e,0x01,0x00,0x37,0x01,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x01,0x00,0x05,0x68,0x65,0x6c,0x6c,0x6f,0x00,0x00,0x00,0x00,
  0x77,0x00,0x02,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x0f,0x00,0x33,0x00,0x00,0x00,
  0x10,0x02,0x4f,0x03,0x00,0x2e,0x02,0x00,0x01,0x37,0x02,0x00,0x00,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,0x00,0x01,0x00,0x04,0x70,
  0x75,0x74,0x73,0x00,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
  };

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

$ vi main/hello_world_main.c

   1 #include "mrubyc.h"
   2 #include "master.h"
   3
   4 #define MEMORY_SIZE (1024*40)
   5
   6 static uint8_t memory_pool[MEMORY_SIZE];
   7
   8 void app_main(void) {
   9   mrbc_init(memory_pool, MEMORY_SIZE);
  10
  11   extern const uint8_t myclass[];
  12   mrbc_run_mrblib(myclass);
  13
  14   mrbc_create_task( master, 0 );
  15   mrbc_run();
  16 }

上記プログラムであるが,前述の「具体例 (1)」と異なる部分は以下の通りである.

  • 8-16 行目:メイン関数
    • 11 行目:クラス用の mruby/c コードをコンパイルして作られた C のファイルの読み込み
      • バイトコードは myclass 配列に格納されている
    • 12 行目:mruby/c 側のバイトコードを有効化

それ以外の部分は,前述の「具体例 (1)」と同じである.

  • 1 行目:mruby/c のヘッダファイルの読み込み
  • 2 行目:mruby/c 側のメインプログラム (master.rb) のバイトコードの読み込み
    • バイトコードをヘッダファイルとして読み込む
  • 4 行目:使用するメモリサイズの指定.現在は 40 k バイト.
  • 6 行目:4 行目で指定したメモリ量を用いてメモリ用の変数を定義.
  • 8-16 行目:メイン関数
    • 型は void
    • 9 行目:メモリサイズを指定して初期化
    • 14 行目:指定したバイトコード "master" を実行することを宣言
      • 第 1 引数は master.h でバイトコードを格納する配列の名前に合わせる
      • 第 2 引数は,初期実行状態を指示するための拡張用.普通に実行する場合はゼロで良い.
    • 15 行目:mruby/c プログラムの実行開始

メインプログラムのコンパイルと実行.

$ make flash monitor

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

具体例 (3) : C で定義した関数を mruby/c で利用する

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

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

$ vi main/master.rb

   1 greeter = Greeter.new
   2 greeter.hello
   3 sleep 1
   4
   5 obj = Test.new
   6 sum = obj.add(1,2)
   7 puts "1 + 2 = #{sum}"

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

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

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

$ vi mrblib/class/test.rb

   1 class Test
   2   def add(a, b)
   3     c_add(a, b)  #Cの関数を呼ぶ
   4   end
   5 end

クラス定義のファイルを追加したので,改めてコンパイルを行う. 以下のように cat コマンドでクラス定義ファイル (今の場合は mrblib/class/greeter.rb と mrblib/class/test.rb の 2 つ) を 1 つのファイル mrblib/mrblib.rb にまとめて,それを mrbc でバイトコードに変換している.

$ cat mrblib/class/*rb > mrblib/mrblib.rb
$ mrbc -E -B myclass --remove-lv -o main/mrblib.c mrblib/mrblib.rb

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

$ vi main/hello_world_main.c

   1 #include "mrubyc.h"
   2 #include "master.h"
   3
   4 #define MEMORY_SIZE (1024*40)
   5
   6 static uint8_t memory_pool[MEMORY_SIZE];
   7
   8 static void c_add(mrb_vm *vm, mrb_value *v, int argc) {
   9   int a = GET_INT_ARG(1);
  10   int b = GET_INT_ARG(2);
  11   int sum = a + b;
  12   SET_INT_RETURN(sum);
  13 }
  14
  15 void app_main(void) {
  16   mrbc_init(memory_pool, MEMORY_SIZE);
  17
  18   extern const uint8_t myclass[];
  19   mrbc_run_mrblib(myclass);
  20
  21   mrbc_define_method(0, mrbc_class_object, "c_add", c_add);
  22
  23   mrbc_create_task( master, 0 );
  24   mrbc_run();
  25 }

上記プログラムであるが,前述の「具体例 (2)」の「パターン (2) : mruby/c 側で Greeter クラスの定義」で挙げた例と異なる部分は以下の通りである.

  • 8-13 行目:mruby/c から呼ぶ関数の実体
    • 型は常に void. static を付ける.
    • 引数は常に (mrb_vm* vm, mrb_value* v, int argc) としておく.変更の必要はない.
    • 9, 10 行目:引数の変換
      • GET_INT_ARG : int 型へ変換
      • GET_STRING_ARG : char 型へ変換
    • 12 行目:戻り値
      • SET_INT_RETURN : int 型を戻す
      • SET_FLOAT_RETURN : float 型を戻す
      • SET_NIL_RETURN : nil を戻す
      • SET_TRUE_RETURN : true を戻す
      • SET_FALSE_RETURN : false を戻す
      • SET_BOOL_RETURN : bool 型を戻す
  • 15-25 行目:メイン関数
    • 21 行目: mrbc_define_method で C の関数を mruby/c 側から呼べるようにする.
      • C 側でクラス定義しない場合は,mrbc_define_method の第 1, 2 引数の与え方が前述の「具体例 (1)」の「パターン (2) : C 側で Greeter クラスの定義」で挙げた例と異なる
        • 第 1 引数:vm でなくゼロ (NULL ポインタ)
          • C 言語でクラスや関数を定義する場合には,初期化処理においてまだタスクの宣言を行っていない状態, すなわち mrbc_create_task() を呼んでいないために VM 構造体が確保されていない状態,で行いたいことが多い.この場合もそうである. そういった場合にはゼロ(=NULLポインタ)でも許すような言語デザインになっている.
        • 第 2 引数:対象のクラス (ここでは上位クラスである mrbc_class_object を指定)
        • 第 3 引数:メソッド名
        • 第 4 引数:メソッドに対応する C の関数名

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

$ make flash monitor

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

参考

課題

課題 (1)

LED の点灯・消灯を行うためのクラスを mruby/c で定義せよ. クラス名は Pin とせよ.メソッド名は on と off とし,それぞれの役目は LED の点灯と消灯とせよ.Pinクラスを用いて「2 つ以上の LED」の ON/OFF を繰り返し実行すること.

  • プログラムの作り方:
    • ~/esp/esp-idf/examples/get-started/blink をベースにし,上記と同様に mruby/c と C を連携させる.
    • 上述の「具体例 (3)」を参考にプログラムを作成する.
      • ~/esp/esp-idf/examples/get-started/blink/main/blink.c に元々含まれる gpio_pad_select_gpio, gpio_set_direction, gpio_set_level といった関数を mruby/c 側から呼べるようにラップする.
      • mruby/c 側で Pin クラスを定義する (ファイル名は mrblib/class/pin.rb とする).
      • mruby/c 側のメインプログラムで無限ループを回し,1 秒毎に LED を ON, OFF させる (ファイル名は main/master.rb とする).
    • 点灯させる LED の GPIO を変更できるようにすること.

      pin.rb の例
      
        class Pin
      
           def initialize(num)   # 初期化するとき (.new するとき) に実行されるメソッドは常に initialize. 
              @pin = num         # クラス変数 @pin の初期化.GPIO 番号を保存.
              ..........         # クラス変数 @pin を使って C 側の関数を呼び出す
           end
      
           def on
              ..............     # クラス変数 @pin を使って C 側の関数を呼び出す
           end
      
           def off
              ..............     # クラス変数 @pin を使って C 側の関数を呼び出す
           end
        end

課題 (2)

make で mruby/c のコードも自動コンパイルできるようにせよ.

  • ヒント
    • ESP-IDF では,components ディレクトリと main ディレクトリ内の .c ファイルは自動的にコンパイルされる.
    • 追加でコンパイルしたいものがある場合には,component.mk ファイルにコンパイル方法を記述する.
      • mruby/c のクラス定義用ファイルや mruby/c のメインプログラムをコンパイルする方法は,main/component.mk に記述すれば良い.

解答例 (main/component.mk)

以下は,<URL:https://github.com/hasumikin/mrubyc-template-esp32/blob/master/main/component.mk> を参考にした. また,以下を前提としていることに注意されたい.

  • C 側のメインプログラムは blink.c である.
  • Ruby 側のメインプログラムは mrblib/master.rb としている.
  • Ruby 側のクラス定義ファイルは mrblib/class/*.rb としている.
  • 予め main/mrblib.c ファイルを作っておく必要がある ($touch main/component.mk しておくと良い).これを回避するための component.mk の書き方は把握できていない....

    main/component.mk
    
      MRBC     = mrbc
      SRCDIR   = $(PROJECT_PATH)/mrblib
      SRCFILES = $(wildcard $(SRCDIR)/*.rb)
      OBJS     = $(patsubst %.rb,%.h,$(SRCFILES))
    
      CLASSDIR   = $(SRCDIR)/class
      CLASSFILES = $(wildcard $(CLASSDIR)/*.rb)
    
      blink.o: $(OBJS)
    
      $(SRCDIR)/%.h: $(SRCDIR)/%.rb
              @echo $(MRBC) -E -B $(basename $(notdir $@)) -o $(subst $(SRCDIR),$(COMPONENT_BUILD_DIR),$@) $^
              $(MRBC) -E -B $(basename $(notdir $@)) -o $(subst $(SRCDIR),$(COMPONENT_BUILD_DIR),$@) $^
              @echo $(MRBC) -E -B myclass --remove-lv -o $(PROJECT_PATH)/main/mrblib.c  $(CLASSFILES)
              $(MRBC) -E -B myclass --remove-lv -o $(PROJECT_PATH)/main/mrblib.c  $(CLASSFILES)

課題 (3)

Pin クラスに LED だけでなくスイッチ用のメソッドも実装し,スイッチと LED の両方を使用可能なプログラムを作成せよ.