Ruby
mruby
Piece

P/ECEでmrubyを動かす

前文

mruby(軽量Ruby)というものを知って、組み込み環境で使えるぐらい消費メモリが削減されているそうなので、P/ECEでも動かせるんじゃね? という実験をしてみたら、コンパイラの古さに泣きそうになった話。

mrubyリポジトリ

P/ECE向けにクロスコンパイルするための設定を入れたfork
https://github.com/zurachu/mruby/tree/cross_build_for_piece

P/ECE仕様(今回キモになるところだけ抜粋)

CPU EPSON S1C33209 24MHz(32ビットRISC)
SRAM 256KB
アプリケーション開発ツール(C言語)付属

アプリケーション開発ツールはEPSONのS1C33向けGCCがベースになっていて、なんとGCCのバージョンは2.7.2(うーんこの)
mrubyのコンパイルにはC99が要求されるそうなので、コンパイルエラーが出た箇所をなんとかやっつけていきます。

ビルド

これのやり方は色々な方が書かれているので、ここでは手短に。

必要環境(括弧内は私が使用したバージョン)

手順

「すべてのプログラム」→「Visual Studio 2017」→「Developer Command Prompt for VS 2017」を開き、以下を実行すると、Windows用の各種実行ファイル&ライブラリ、P/ECE用のライブラリが出力されます。

> cd [リポジトリをチェックアウトしたパス]
> make

サンプルプログラム

examples/targets/PIECE に簡単なサンプルプログラムを用意しています。
ライブラリビルド後に、以下コマンドで実行できます。

> cd [リポジトリをチェックアウトしたパス]\examples\targets\PIECE
> make
> run

mrubyスクリプト

mrb_hello.rb
def prime?(n)
  (2...n).each do |i|
    return false if n % i == 0
  end
  true
end

def puts(x)
  Pce::Font::put_str "#{x}\n"
end

a = (2..100).select {|n| prime?(n) }
puts a

実行結果

実行結果

画面表示されるまで、だいぶ時間かかります。
また、関数名の後ろに書いてるのが実行後のヒープ残量なのですが、VMの初期化(mrb_open_allocf())が終わった時点で4KBしかないというギリギリさw
本当に動かせただけというレベル。

P/ECE用に対応したところ

ビルド環境編

pcc33がサブディレクトリ以下のファイルのコンパイルに失敗する

コンパイラドライバーであるpcc33がカレントディレクトリでの実行しか想定されておらず、また出力先を指定する-oオプションが無いので、そのままではビルドに使えない。
これを実現するためのラッパーをRubyで書いて、それをconf.cc.commandに与える。

ライブラリ編

stdint.h, inttypes.hが無い

C99じゃないので。
こちらのファイルを改変して使用。
https://github.com/chemeris/msinttypes

構造体メンバのサイズ不明配列がincomplete typeになる

src/pool.c
struct mrb_pool_page {
  struct mrb_pool_page *next;
  size_t offset;
  size_t len;
  void *last;
  char page[];
};

これもC99?
使われ方を見ていると、メンバ(上記page)自体のサイズはゼロにしつつ、構造体が格納されている次のメモリ領域を指すポインタとして扱っているもよう(こういうことできるの知らなかった)
当該メンバを削除してメンバ参照箇所は構造体のポインタをchar*にキャスト後sizeof(struct mrb_pool_page)を加算するコードに書き換え。

stddef.hとstdlib.hでwchar_tのtypedefが競合

P/ECE開発環境のヘッダファイルで定義済みを表すマクロ文字列が違っているため。コメントの感じから、stddef.hがEPSONのS1C33コンパイラセットに含まれるものではないっぽくて、それを作った人がミスってる気がする。

stddef.h
#ifndef _WCHART
#define _WCHART
typedef unsigned short  _Wchart;
typedef _Wchart wchar_t;
#endif
stdlib.h
#ifndef _WCHAR_T
#define _WCHAR_T
typedef unsigned short  wchar_t;    /* wide character type */
#endif

開発環境に手をつけて良いなら、stddef.h側のマクロを書き換えるのが安いけど、あまりシステムインクルードパスの中身を書き換えたくないので、マクロの定義状態を見て、うまいこと細工する。

include/mruby.h
#ifdef PIECE
  #if defined(_WCHAR_T) && !defined(_WCHART)
    #define _WCHART
  #endif
#endif

#include <stdint.h>
#include <stddef.h>

無名共用体が使えない

include/mruby.h
#ifdef MRB_METHOD_TABLE_INLINE
typedef uintptr_t mrb_method_t;
#else
typedef struct {
  mrb_bool func_p;
  union {
    struct RProc *proc;
    mrb_func_t func;
  };
} mrb_method_t;
#endif

こちらの共用体へのアクセスがエラーになる。
union { ... } u;と名前を付けて、呼び出し箇所(proc.hのマクロ)にも名前を付けるでも良いが、MRB_METHOD_TABLE_INLINEマクロを定義しても回避できるし、そちらの方がメモリ消費量が少なく済んだので、後者を採用。

浮動小数点演算をカット

NAN, INFINITY, isinf(), isfinite() など、C99 math.h で実装されているものが未定義で、src/numeric.cのコンパイルが失敗するため。
頑張って実装したところでメモリが足りなさそうなので、MRB_WITHOUT_FLOATマクロを定義して、あきらめる。

標準入出力をカット

stdin/out などがないため、MRB_DISABLE_STDIOマクロを定義。(ライブラリ作成時点では警告止まりで、リンク時にエラー)

ターゲットプログラム編(リンカエラーへの対処など)

64ビット整数演算の実装が無い

Warning: Unresolved external symbol '__muldi3'.
Warning: Unresolved external symbol '__adddi3'.

こちらのファイルをリンクする。
http://www.piece-me.org/piece-lab/piece-lab-2002.txt

  • Sun Oct 13 10:03:00 JST 2002 Naoyuki Sawa
  • 64ビット整数演算ライブラリ

exit(),_exit()が無い

Warning: Unresolved external symbol '_exit'.
Warning: Unresolved external symbol '_exit'.

exit()だけじゃなくて_exit()も要ることが分かりにくいエラーメッセージ…。
while(1);で適当に実装する。
ただ、P/ECEのアプリの終了はpceAppReqExit()を呼んでpceAppExit()が呼び出されるという流れをふまないといけないので、そもそもこいつらが呼ばれるような事態があっては困る。
それでいうと、真面目に液晶にエラー出力するようなコードを書いた方が良いかも。

errnoが無い

Warning: Unresolved external symbol 'errno'.

定義したら良い。

独自のアロケータを渡す

mrb_open()ではなくmrb_open_allocf()を使い、独自のアロケータを渡す。

hello.c
static void* allocf(mrb_state* mrb, void* p, size_t size, void* ud)
{
  void* ret;
  if(size == 0)
  {
    if(p)
    {
      pceHeapFree(p);
    }
    return NULL;
  }
  ret = pceHeapAlloc(size);
  if(p)
  {
    memcpy(ret, p, size);
    pceHeapFree(p);     
  }
  return ret;
}

pceHeapRealloc()の仕様がrealloc()と若干異なる(しかもバグあり)ので、pceHeapAlloc()/pceHeapFree()のみで実装する。

今後の課題

  • mrubyから呼び出せるP/ECE開発環境のAPIを増やす(メモリ残量が‥)
  • gem使ってみる(同上)
  • なんとかしてメモリを浮かせる。ただ、マクロでの設定は相当切り詰めているので、他の要素を探さないと。

おまけ

ビルド環境編で詰まっていた時にMatzさんからリプライが飛んできてビビったという話。

参考文献