mruby/cでStringを無効化


mruby/cをコンパイルしたときのサイズを小さくするには、いくつかの方法がある。 その中で、mruby/cの設計段階から想定されている方法が、FloatまたはStringを無効にする方法である。 FloatやStringを無効にするオプションの指定は、vm_config.hにで定義されているだが、3.4.1や4.0.0などのリリースでは、そのままでは動かない。 Floatについては自分でファイルを変更して対応していたが、Stringについてはどこをいじるべきか分らずに、放置していた。 しかし、github上ではつい最近これらの問題に対する対策がされたようである。 そこで、それを参考にしてmruby/cをStringを削除して使ってみた。

検証は、最近いじっているch32fun上のCH32X035用の環境で行った。 Taskは消しているので、mruby/cのそれ以外の部分を、Stringを無効にしても大丈夫なように修正するシェルスクリプトは以下のようになった。

awk '/MRBC_CLASS\(String/{$0="#if MRBC_USE_STRING\n"$0"\n#else\n  0,\n#endif"}1' class.c >temp.tmp && mv temp.tmp class.c
cat c_object.c |awk '/void mrbc_object_inspect/{$0="#if MRBC_USE_STRING\n"$0;a=1}a&&/^}/{$0=$0"\n#endif";a=0}1' |awk '/static int set_sym_name_by_id/{$0="#if MRBC_USE_STRING\n"$0;a=1}a&&/^}/{$0=$0"\n#endif";a=0}1' >temp.tmp && mv temp.tmp c_object.c
cat c_object.h |awk '/void mrbc_object_inspect/{$0="#if MRBC_USE_STRING\n"$0"\n#endif"}1' >temp.tmp && mv temp.tmp c_object.h
cat error.c |awk '/static void c_exception_message/{$0="#if MRBC_USE_STRING\n"$0;a=1}a&&/^}/{$0=$0"\n#endif";a=0}1' |awk '/TT_STRING/{$0="#if MRBC_USE_STRING\n"$0;a=1}a&&/}/{$0=$0"\n#endif";a=0;b=1}b&&/}$/{$0="#if MRBC_USE_STRING\n"$0"\n#endif";b=0}1' |awk '/c_exception_message /{$0="#if MRBC_USE_STRING\n"$0"\n#endif"}1' >temp.tmp && mv temp.tmp error.c
cat _autogen_class_exception.h |awk '/c_exception_message/{$0="#if MRBC_USE_STRING\n"$0"\n#endif"}1' >temp.tmp && mv temp.tmp _autogen_class_exception.h

私はmruby/cにCH32X035用のGPIO,ADC,PWM,I2C,SPI,UARTと標準入力のAPIを追加して使っている。 Stringを無効化した場合でも、GPIO,ADC,PWMは、Stringとは無関係なので、そのまま動く。 しかし、それ以外のAPIはStringを使用しているので、変更が必要になる。 まず、対策が簡単なのが標準入力であるが、 getcやgetsは文字列を返すmethodなので、これらは無効にしなければならず、標準入力としてはgetbyteだけが使えることになる。

I2CやSPIについては、出力と入力のそれぞれで、Stringを使わないようにしなければならない。 出力については、make_output_bufferの中で、引数がStringだった場合の処理をスキップするようにすれば良く、整数かその配列を使って指定することになる。 入力については、これまではStringを作って、そのバッファにC言語からアクセスして、読み込んだデータを書き込んで、そのStringを返していた。 まず、これをStringを使わない場合に対応できるように、 C言語でバッファを作ってから、そこにデータを読み込み、mrbc_string_new_allocを使って、そのバッファを取り込んでStringを作って返すように変更した。 Stringを使わない場合には、バッファをStringの代わりにArrayに変換して返せば良いのである。 そのような関数は無かったので、以下のように作ってみた。

mrbc_value mrbc_array_new_alloc(mrbc_vm *vm, const uint8_t *c_array, int size) {
  mrbc_value ruby_array = mrbc_array_new(vm, size);
  for (int i = 0; i < size; i++) {
    mrbc_value val = mrbc_integer_value(c_array[i]);
    mrbc_array_set(&ruby_array, i, &val);
  }
  return ruby_array;
}

Stringに変換した場合には、そのバッファはそのまま使われており、そのStringが不要になったときには、おそらく自動で開放されるのだろう。 しかし、Arrayに変換した場合には、バッファの内容を整数に変換して、Arrayにしており、そのバッファは直接使われているわけでな無いので、mrbc_freeで開放する必要がある。 その処理を関数に組み込んでも良いかも知れないが、今回はこの関数の外でメモリを開放する仕様とした。 例えば、I2Cの場合には、以下のような感じである。

    buf = mrbc_alloc(vm, size);
    i2c_start(adrs,1);
    i2c_read(buf,size);
#if MRBC_USE_STRING
    ret = mrbc_string_new_alloc(vm, buf, size);
#else
    ret = mrbc_array_new_alloc( vm, buf, size);
    mrbc_free( vm, buf );
#endif

SPIも同様に変更でき、これでI2CとSPIも使えるようになる。 ただしこれまでは、例えば次の一行目のように実行していたものを、二行目のように実行することになる。

d=i2c.read("\x38",2).bytes
d=i2c.read(0x38,2)

Arrayにも自分をそのまま返すbytesを以下のように定義したら、互換性が上がるかなと思ってやってみたら、80byteも増加してしまった。

class Array
 def bytes
  self
 end
end

mruby/cのArrayに自分自身を返すmethodがあれば、aliasを使えるのだが、to_aは無くて、dupは無駄な処理をするので、上記のように書かざるを得ない。

一番問題なのは、UARTであり、これはStringと強く結び付き過ぎている。 I2CやSPIのwriteは文字列以外の引数も取れたが、UARTのwriteの引数はStringである。 readの返す値もStringである。 今回はUARTは対応させないことにした。

問題のコンパイルサイズは、Stringを消すとmruby/c本体は約10k減った。 flashにかなり余裕が出来たので、string.rb以外のmrblibを組み込んでも大丈夫である。 しかし、Stringが使えないことで、標準入出力などの使い勝手が非常に悪くなる。 SPIなどのコマンドは、すべて数値や数値の配列で指定しなければならないのは当然として、得られた数値などは、print,puts,pなどで表示するしか無い。 つい文字列を使うと、以下のようなエラーが出る。

Exception(vm_id=0): Not support such type (IREP_TT=0) (Exception)

しかし、表示に関してはSymbolを駆使することによって、文字列なども表示できることに気が付いた。 例えばこんな感じである。

puts :reading
print 3,:".",1415
puts

数字だけしか表示できないとかなり辛いが、この手法を使うと、Stringが無くてもなんとか使えるものになりそうである。