mruby/cでstdin


様々なマイコンで、簡単に mruby/c を使えるように出来るのではないかと思って、 mrubyc_arduino を拡張して、 これまでに、GPIO,ADC,PWM,I2C,SPIは動くようになった。 また、必要なflashの容量を節約するために、FLOATを使わなくする機能のバグを修正したり、マルチタスクを無効にすることが出来るようになった。

次はUARTを組み込もうかなとも思ったが、関連する機能を別の方法で実現してみた。 mruby/cでは、puts,print,printfなどのstdoutに出力するコマンドを、hal_writeを介して使うことができる。 一方、gets,getcなどのstdinから入力するコマンドは、 mrubyにはあるようだが、mruby/cには無い。 mrubyc_arduinoでは、hal_writeはSerialから出力するように指定してあり、Serialからの入力もできるようになれば、UARTを組み込まなくてもSerialを使えるようになることになる。 そこで、stdinからの入力コマンドをどのようにすれば実現できるかを試してみた。

まずやったのが、putsなどと同様にしてgetbyteなどを定義できるのでは無いかと予想して、 c_object.cや_autogen_class_object.hやhal.hに必要だと思われる修正をしたが、メソッドが無いとエラーが出てしまう。 autogen_class*.hで定義された定数をどこかでmethodとして定義できるようにしているのだと思っていたが、kernelのコマンドとして動かす場合は、別のやり方が必要なようだ。 どうなっているのだろう。

仕方が無いので、sleepなどを定義しているのと同じ方法で、mrbc_define_methodを使ってやってみたら、動作させることに成功した。 Arduinoで一文字読み出すようにhal_readを定義する。

unsigned char hal_read(int fd) {
  while (Serial.available() == 0);
  return Serial.read();
}

標準入力を組み込むためのヘッダファイルmrbc_get.hを作る。

#ifndef _MRBC_GET_H
#define _MRBC_GET_H

#include <Arduino.h>

#ifdef __cplusplus
extern "C" {
#endif

#include "mrubyc.h"

void mrbc_init_class_get(void);

#ifdef __cplusplus
}
#endif

#endif //extern

getbyteはhal_readの値をそのまま返すように書けば良いが、getcやgetsはC言語でどう書くのか考えていたが、getbyteを使ってrubyで書いて、それをmrbcのコードとして組み込んじゃえば良いということになった。 そのためのrubyのスクリプトget.rbがこれである。

def getc
  getbyte.chr
end
def gets()
  s=""
  s<<c=getc until c=="\n"
  s
end

面倒だったので、getsは引数を取れるようにはしていない。 これをmrbcで処理してbyteコードを取り出す。

mrbc -B bytecode_get -o get.h get.rb

このコードを組み込んで、mrbc_get.cを作る。

#include "mrbc_get.h"

static void c_object_getbyte(struct VM *vm, mrbc_value v[], int argc)
{
  uint8_t ret=hal_read(0);
  SET_INT_RETURN( ret );
}

const uint8_t bytecode_get[] = {
0x52,0x49,0x54,0x45,0x30,0x33,0x30,0x30,0x00,0x00,0x00,0xf8,0x4d,0x41,0x54,0x5a,
0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0xc2,0x30,0x33,0x30,0x30,
0x00,0x00,0x00,0x35,0x00,0x01,0x00,0x03,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x13,
0x63,0x01,0x58,0x02,0x00,0x5f,0x01,0x00,0x63,0x01,0x58,0x02,0x01,0x5f,0x01,0x01,
0x38,0x01,0x69,0x00,0x00,0x00,0x02,0x00,0x04,0x67,0x65,0x74,0x63,0x00,0x00,0x04,
0x67,0x65,0x74,0x73,0x00,0x00,0x00,0x00,0x32,0x00,0x02,0x00,0x04,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x0e,0x34,0x00,0x00,0x00,0x2d,0x02,0x00,0x00,0x2f,0x02,0x01,
0x00,0x38,0x02,0x00,0x00,0x00,0x02,0x00,0x07,0x67,0x65,0x74,0x62,0x79,0x74,0x65,
0x00,0x00,0x03,0x63,0x68,0x72,0x00,0x00,0x00,0x00,0x4f,0x00,0x04,0x00,0x07,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x26,0x34,0x00,0x00,0x00,0x51,0x02,0x00,0x01,0x04,
0x03,0x51,0x05,0x01,0x42,0x04,0x26,0x04,0x00,0x11,0x01,0x04,0x02,0x2d,0x05,0x00,
0x00,0x01,0x03,0x05,0x2f,0x04,0x01,0x01,0x25,0xff,0xe3,0x38,0x02,0x00,0x02,0x00,
0x00,0x00,0x00,0x00,0x00,0x01,0x0a,0x00,0x00,0x02,0x00,0x04,0x67,0x65,0x74,0x63,
0x00,0x00,0x02,0x3c,0x3c,0x00,0x4c,0x56,0x41,0x52,0x00,0x00,0x00,0x1a,0x00,0x00,
0x00,0x02,0x00,0x01,0x73,0x00,0x01,0x63,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x01,
0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08,
};

void mrbc_init_class_get(void){
  mrbc_define_method(0, 0, "getbyte", c_object_getbyte);
  mrbc_run_mrblib(bytecode_get);
}

あとは、mrubyc.hでヘッダファイルを取り込むようにして、 Arduinoでmrbc_init_class_getを実行すれば、stdinが使えるようになる。

79バイトのrubyが、248バイトのmrbになって、思ったよりも大きくなった。 おそらく、c言語で書いた方がflashの節約になるだろうが、今回は楽にできる方法を取ってしまった。 実際、GPIOのmethodの一部を、同様の方法でバイトコードにして、コンパイルしてみたら、300バイトほどサイズが大きくなった。

UARTの替わりにstdinを使うようにすれば、小型のワンチップマイコンに必要な機能が多くが使えるようになった。 getcやgetsを実装しても、それほどflashやメモリは使わないと思うので、本家のmruby/cにも組み込んでくれても良い気がする。 GPIO,ADC,PWM,I2C,SPIとstdinを組み込んで、FLOATとマルチタスクをoffにしてArduinoでコンパイルすると、 マイコンやコンパイラの種類によるだろうが、64kバイト程になった。 64kのflashのマイコンで使うには、機能をさらに制限するなどの必要がありそうだ。

2026/5/12追記 hal.hに以下の定義を加えないといけないのを書くのを忘れてた。

unsigned char hal_read(int fd);