Dockerを使ってクロスコンパイルしてmrubyのワンバイナリを作成してみた

# 背景

普段はOSXやLinuxを好んで使っています。しかし、2,3万円台で買えるWindowsノートPCも魅力的で、実際、
~~奥さんに内緒で~~買って使ってます。ちょっと前に次男にコーンポタージュをこぼされて壊れてしまい、
今のは2台目になります。。


そんな環境なので自作のプログラムがOSX,Linux,Windowsで動いてくれると、いろいろ便利だと思っています。

ただ、特に2,3万のPCでは非力なので、出来ればどこか別のPCでビルドを行い、
微調整だけ非力なPCで行えれば、激安ノートPCやWindowsタブレットにVSやMingwを入れずに済みます。
([Cygwinはh2oを動かすために外せませんw](http://abrakatabura.hatenablog.com/entry/2015/10/14/072432))

それにできれば、開発環境を入れるPCは限定して、そこで一括してビル出来るような環境が嬉しいです。

## Go言語ではクロスコンパイルしてワンバイナリが出来る

以下の記事が最近の事情を反映しており参考なる。自分が試したのは1.4以前であったが、[gox](https://github.com/mitchellh/gox)使ってOSXでWindowsやLinux向けのバイナリが作れるのに感動した。

- [Go1.5はクロスコンパイルがより簡単 | SOTA](http://deeeet.com/writing/2015/07/22/go1_5-cross-compile/)

## mrubyでもmruby-cli使えばクロスコンパイルしてワンバイナリが出来る

- gccはLinuxやOSX、Windows、その他多くのプラットフォームで動く
- 各OS上で自分のプラットフォーム向け以外のコードも比較的容易に吐けるのかも
- Linux上で、WindowsやOSX向けのバイナリを吐けるクロスコンパイル可能なgccの処理系が既に存在している

ということで、DockerでLinux上のLinux以外のコードを吐けるクロスコンパイル可能なgccの処理系を詰め込むことで
これを実現している便利ツールがmruby-cliということになりそうです。

mruby-cli自体のビルドもdocker-composeを使って可能で、以下の手順で、
mruby/buildディレクトリ配下に、Linux,Windows,OSXの各種バイナリが出来ます。

```
git clone https://github.com/hone/mruby-cli
cd mruby-cli
docker-compose run compile
```
### mruby-cliで参考になりそうな日本語記事

- [mruby-cliがアツいですね - Kentaro Kuribayashi's blog](http://blog.kentarok.org/entry/2015/10/27/000522)
- [mruby-cliを知る - k0u5uk3's blog](http://blog.k0u5uk3.net/entry/2015/10/26/170820) 
 
# 問題

## 外部のC/C++のライブラリを使うようなケースへの対応

以下の記事を読む限り、Goの場合、cgoを使う必要があり、クロスコンパイルするには、ターゲット向けのバイナリを吐けるgccを自前で用意するっぽい。

- [Go 1.5: Cross compilation — Medium](https://medium.com/@rakyll/go-1-5-cross-compilation-488092ba44ec#.bbpx0tqb2)

mrubyの場合も、mruby-cliを素の状態では難しいが、goに比べて、すでにLinux,Windows,OSXの3環境向けの
gccが入っているので、手間いらず。

mrubyに組み込みたいCライブラリとそのライブラリに依存するライブラリ全てを
Linux以外のWindows,OSXの2環境分頑張ってクロスコンパイル可能にして、lib*.aの形式でライブラリを
揃えれば出来るはず！

# 本題

そんな訳で、Raspberry Pi2ではなく、Raspberry Piを持っているのにクロスコンパイルしたことのない自分が
無謀にもC++ライブラリを使っているmrbgemをDockerを使って、Windows向けのmrubyに組み込んだワンバイナリを
作成することにした。

## mruby-mrmagickをWindowsでうごかす

いきなり、mruby-cliに組み込むのは、mruby-cliの構造の理解も必要で大変そうだったので、
とりあえず、Dockerのコンテナ上でWindowsのバイナリを吐けるgccの処理系を動かして、自分の作っている
ImageMgaickライブラリに依存した[mruby-mrmagick](https://github.com/kjunichi/mruby-mrmagick)を[Docker](https://www.docker.com/)でクロスコンパイルして[Windows](http://www.microsoft.com/ja-jp/windows)向けの
バイナリを生成して、これを動かそうと試みた。

また、対象も前述の激安のWindowsが32ビットのため、32ビットのWinodwos向けに絞ってやってみることにした。

### 下準備

まさにこの記事を書いている時に気が付きましたが、mruby-cliで使用しているDockerイメージを使えば良かったのですが

- https://github.com/kjunichi/docker-mingw-w64-32

こんなの作って、このコンテナ内で試行錯誤してました。

### クロスコンパイルの基礎

./configureなコードは簡単にクロスコンパイル出来ることが多い模様。
--hostオプションでビルド対象のプラットフォームを指定すれば、よきに計らってくれることが多い感じです。
実際、ImageMagickで依存するjpegやpngのライブラリもこの指定プラスアルファ程度でビルド出来ました。

- [Debian GNU/LinuxでWindows用バイナリをビルドする方法 - ククログ(2011-10-13)](http://www.clear-code.com/blog/2011/10/13.html)
- [configureのbuild、host、targetの違い - maminusのメモか何か](http://d.hatena.ne.jp/maminus/20100129/1264781242)

### mrubyをDockerのLinux上でクロスコンパイルしてWindows向けのバイナリを作る

通常のbuild_config.rbの末尾に以下のような記述をすることで、出来ました。
この例ではWindowsの32ビット向けの設定になります。

```rb
MRuby::CrossBuild.new('i686-w64-migw') do |conf|
  toolchain :gcc  
  conf.cc do |cc|
    cc.command = "/usr/bin/i686-w64-mingw32-gcc"
  end
  conf.linker do |linker|
    linker.command = "/usr/bin/i686-w64-mingw32-gcc"
  end 
  
  conf.archiver do |archiver|
    archiver.command = "/usr/bin/i686-w64-mingw32-ar"
  end
  conf.gem :github => 'kjunichi/mruby-mrmagick'
  conf.gembox 'default'  
end
```

## 結果

ImageMagick本体をクロスコンパイルしてビルドする際に、いくつかコンパイルエラーが発生し、これらに対応して、
無事lib*.a形式でライブラリが作成され、convert.exe等も出来ました。

しかし、どうも自分が[Windows向けのクロスコンパイルを通すために当てたパッチ](https://gist.github.com/kjunichi/5f9fdaf74bd42f1cf689)が原因の可能性が高そうではあるのですが。画像ファイルを読み込もうとすると落ちます。。

こんな感じで画像ファイルを開いた瞬間に落ちる。。


convert.exeなども、--help等は動くが、画像を開くと即死という状態。


## 失敗したが作業を通じて学べたこと

### クロスコンパイルする場合にもホスト向けのmrubyが必要

mrbcで.rbファイルをコンパイルしてバイトコードを生成して、
これをターゲット先のmrubyに組み込む為、ホスト上で動くmrubyが必要な
ようです。

ただし、当然、ホストで動かすmrubyの構成は最小構成で良いので、
クロスコンパイル先で使いたいmrbgemsの指定はクロスコンパイルのセクション内に
書くだけで良いようです。

### gcc(mingw32-w64)で明示的に静的リンクを指定するには

まぁ、クロスコンパイルやろうとする方なら何を今更といった感もありますが、
前述の通りのヘタレな自分は今回の作業で初めて知りました。

```
-Wl,-dn,-lhoge
```

これで、Windowsマシンで作成されたバイナリをもっていって
hode-N.dllが見つかりませんいった自体を避けられました。

## nmコマンドもクロスコンパイル用のものが用意されていた

特こ今回の様に静的リンクを行うと、エラーが出やすい気がする、シンボル参照エラーの
調査にクロスコンパイル用のi686-w64-mingw32-nmコマンドもあり、助けられている。

## lddコマンドはobjdump -p

lddコマンドは検索するとクロスコンパイル環境でも環境によって存在しているようだが、
今回のmingw32-w64環境下では無かった。代わりに、i686-w64-mingw32-objdumpコマンドを-pオプション付で
実行することで、同様の情報が得られた。

### OpenMPを使った場合、静的リンクはできなくなりつつある模様

詳しくは以下

- [gccでopenmpでstaticリンク：fartrip's diary：So-netブログ](http://fartrip.blog.so-net.ne.jp/2014-01-11)
- [OpenMP* ライブラリーの使用](https://www.xlsoft.com/jp/products/intel/compilers/ccw/12/ug/optaps/common/optaps_par_compat_libs_using.htm)


# ImageMagickがダメならOpenCVは？

[OpenCV](http://opencv.org/)つかってmrubyでwebcam動かせるモジュール作成して、
これを試そう！

## OpenCVは実績あり

実は、OpenCVは以下の記事にあるようにクロスコンパイルしてWindowsで動いた実績があるのです。
この実績に気を良くしてしまい、今回のアドカレでmruby-mrmagcik取り上げたのでした。

- [クロスコンパイルしてwindows用のOpenCVライブラリを作る - Qiita](http://qiita.com/kjunichi/items/60a14d73fab2b41cc6bc)

## mruby-webcamをDockerをつかってWindowsで動くワンバイナリにする

### まずは通常のmrbgemをつくった

- https://github.com/kjunichi/mruby-webcam

### Dockerでmruby-webcamを組み込んだWindows向けのmruby.exeを作る

- https://github.com/kjunichi/mruby-webcam-docker

## 動かす

```
docker exec container_id cat /root/src/mruby/build/i686-mingw32-w64/bin/mruby.exe > mruby.exe
```

として、dockerのホスト上のファイルを落として、あとは何らかの方法でWindows機に転送して
動かします。


```rb
cam = Webcam.new

# スペースキーを押すとここで登録したブロックが呼ばれます。
cam.capture {|img|
puts img.length # <- imgにJPEG形式でバイナリデータが入ってきます。
}

# Webcamの画像を表示する
cam.start

# 表示されたウィンドウでESCを押すと終了
```

```
mruby hello_webcam.rb
```

[![](https://j.gifs.com/wEne27.gif)](https://www.youtube.com/watch?v=_YFswQSockw)



## 積み残し(ややステマw)

現状では、バイナリのWindows機への転送を

```bash
docker exec container_id cat /root/a.exe > ~/Dropbox/work/a.exe
```

などという、スマートとは程遠い運用をしてDropbox経由で激安Windows10 2-WayタブレットPC(<a rel="nofollow" href="http://www.amazon.co.jp/gp/product/B00YG5FB22/ref=as_li_ss_tl?ie=UTF8&camp=247&creative=7399&creativeASIN=B00YG5FB22&linkCode=as2&tag=misc-22">Acer 2in1 タブレット ノートパソコン Aspire Switch 10E SW3-013-N12P/W /10.1インチ</a><img src="http://ir-jp.amazon-adsystem.com/e/ir?t=misc-22&l=as2&o=9&a=B00YG5FB22" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />)にバイナリを
持ち込んでいるので、作成されたバイナリを[Let's Encrypt](https://letsencrypt.org/)で証明書の問題もクリア出来つつある現状踏まえると、 mrubyのhttp2サーバーで配布するところあたりまで、
記事にしたかったです。

ちなみにmrubyで動くHttp2サーバーは以下があります。

- https://github.com/matsumoto-r/trusterd

# まとめ

- mruby自体が組み込み用途向けに作られていることもあり、本体のクロスコンパイルはスムーズにおこなえる
- 既存のC/C++のライブラリのクロスコンパイルは割と手軽に出来ることもある
 - SVGライブラリのlibrsvgをクロスコンパイル試みたが、依存ライブラリ大量だし、それぞれ癖があり、断念した
- すべてのライブラリを静的リンクするにはいろいろ壁があるのかも
- いろんなライブラリをクロスコンパイルしたDockerイメージを作るともっとmrubyが面白くなるかも
 - 現状まだ、前述のオレオレコンテナの/usr/i686-w64-mingw32/lib配下しか出来てないが。。
- Dockerのコンテナ内なので、クロスコンパイル向けのヘッダ等を誤って/usr配下のホストが参照するような場所にばら撒いても、コンテナが駄目になるだけで被害を最小限にとどめられる。
- 実は最近mruby-cliがビルド出来ない状態だったのを[Issue挙げて](https://github.com/hone/mruby-cli/issues/31)微力ながら問題解決に協力で来た

一見、CRubyに比べて、動的にgemを組み込めないmrubyが不便と考える向きもありますが、
Docker上のコンテナで全部入りのバイナリを作って配布することで、依存ライブラリが更新されてある日動かなくなる
といったこともなく便利に使えるかもしれません。

これは、子供たちにプログラム教える際にも有効かもしれません、皆がそれぞれ持参したPCにmrubyやmruby.exeを入れて、あとは
rubyスクリプトを書くだけで同じ環境でプログラムを動かせ、環境構築に時間がかかり、本来の学習に時間を取られることも
避けられると思います。

となんとなく、最後に[Twitterのプロフ](https://twitter.com/kjunichi)に書いていることにこじつけ、今回の記事を終えます。