RustやElectron、denoに浮気しつつmrubyを使ってMacでクロスコンパイルしwindows用のexeを作るのに苦労した話

## きっかけ
SVGファイルをparseして中のxmlやCSSから色情報などを取り出す、[これ](https://qiita.com/kurokky/items/ca7598ca4a742bcbcdea)のローカル版を作りたいということでやりたいことは

- SVGをパースしてdom上の特定の要素を抽出
- dom上だけではなくinlineのCSSや<style></style>内のCSSから特定の要素を抜いてくる
- 汎用性を考えてjsonで結果を表示できるようにする

たったこれだけ。
なので**xmlパーサー(XPathなら嬉しい)と正規表現は必須**、cssパーサーは元々の状態で使ってなかったのであれば良しとする（jsonはいかようにでもなる）。

因みにチャレンジしたフレームワークや言語は

- [Rust](https://www.rust-lang.org/ja)
- [Crystal Language](https://ja.crystal-lang.org/)
- [Electron](https://www.electronjs.org/)
- [deno](https://deno.land/)
- [ruby-packer](https://github.com/pmq20/ruby-packer)
- [mruby](https://mruby.org/)

で、途中で**意地になってmrubyでこねくり回し泥沼にハマったが、最終的になんとかなった**という話です。

![スクリーンショット 2022-02-19 3.41.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1043424/164d249f-8b4c-de08-4d9f-0050b2380148.png)
(goやphpのディレクトリがあるのは前回のサーバー版を書く時の名残りだったりします。!)
※尚、文章中に遅いという表現がありますが、正確にベンチマークをとったわけではないのでご注意ください。


## 事の始まり

最初はRustで書いていたんですが、ふと『あー、GUI書くとなると面倒だなぁと』思って『せっかくJS版があるのでElectronでやるか…』となり書き直していた時に、ふと思いました。
『あ、CIとかでチェックツールとして使うならLint用としてCUI版もあった方がいいな』と。

で、世の中はやれノーコードだのAIがコードを生成だの言っているので、実験も兼ねて以下のような感じで行く事にしました。

- クロスコンパイルが(mac or Linuxでwindowsのexeを生成できる)あること 
- C/C++やJAVAじゃなくLLに近い言語で書きたい(少なくとも高級言語は書かない)
- CUI/GUIもいける
- あまり人がやってなさそうだがやれば意外と簡単にできそうなもの

結果としては、表題の通り自分が想定が甘かったのですが、ここから地獄の始まりでした。

そもそも**ElectronってChromium+nodeの技術なのでCUIアプリを作るのにオモクソ向いてない**んですよね。
むしろ**見た目の部分がcss+htmlでできるから利点**なのであって、**CUIになったらそChromiumって重いだけ**でnodeだけでいいわけです。

なので、また『Rustで書くか…』とElectronで書いていたHTML/CSSのGUIをeguiに移植しようとした時に『あれ？LLVMならCrystal langでもいいじゃん』って悪魔が囁いたわけです。

とりあえず、[naqvis/crystal-html5](https://github.com/naqvis/crystal-html5)があったのでサクと30分ぐらいでCUI版のコードができ、とりあえずbuildして色々な環境(Intel Mac/M1 Mac/Ubuntuの複数Version/Raspberry piなど)で動作確認してる時に思いました。[^1]

[^1]: ちょうどラズパイの64bitが出た時期だし環境用意するの面倒でした

『LLVMをインストールが超絶に面倒だと…(何回同じコマンドを叩いてるんだろうと)』

いや、自分の開発用の環境なら別にいいんですけど、『**他人に配る時わざわざLLVMのインストールさせるのってスーパーいけてない**な』と。
Macでさえ、下手するとBrewをインストールして、そこからコマンド叩くわけで…

なので、人に渡すならシングルバイナリ = Static Link(静的リンク)が生成できないとダメだよなぁ(最悪windowsはdll同封)と思ってCrystalの[公式](https://crystal-lang.org/reference/1.3/guides/static_linking.html)を見ると「--static」付けるといいよってあったので読み進めたところ

## CrystalのStatic LinkってMacに非対応？

一番下に

> macOS doesn't officially support fully static linking because the required system libraries are not available as static libraries.

『OMG！え？そうなの？』
つまり『MacユーザーやCIの管理者権限がない人にCrystalのバイナリ渡すときは漏れなく「**apt(Brew) install LLVM** も叩いてねぇ」ってReadmeに書くってこと…』と思ったのと同時に『あれ、CrystalってWindowsのバイナリ生成できるんだっけ？』と[公式](https://crystal-lang.org/reference/1.3/syntax_and_semantics/cross-compilation.html)を調べ直したところ、あやしい匂いがプンプンです。

LLVMなんで原理的にはいけそうだけど、経験上こういうページに**『MacでLinuxのができるよー』しか書いてないのって99.9%以上Windowsのバイナリをまともに作れない(hello worldぐらいはできます)のが多い**のです。
（そして、私の今手元で作業しているPCはIntel MacじゃなくM1 Macなのです。）

で、そこで思い出したのがmrubyだったのです。

## そうだ！mrubyで書こう
もう4、5年前の話ですが、会社のメンバーにArduinoやESP-32でIoTのプロトタイプを作るお題を出していたとき、当の本人はその1年以上前からESP8266などで色々やっていたので既にC++を書くのに飽きていたりして(というかC系の言語なんてObj-Cも含めて脳細胞からなくなってしまった)amazonのオススメに出てきた【[まつもとゆきひろ直伝 組込Ruby「mruby」のすべて 総集編](https://www.amazon.co.jp/dp/B00HZGBN58)】の影響もありmrubyを使った時期がありました。  


その時は[mruby-cli](https://github.com/hone/mruby-cli)の存在などを知らなくて、mrbcでコンパイルしてCのヘッダーを書くという本末転倒なことをやってたんですけどね。(Rake使いましょうね。)

昨年の春mrubyは[3.0として生まれ変わった](https://www.youtube.com/watch?v=OboepniCrv0&ab_channel=%E7%A6%8F%E5%B2%A1%E7%9C%8CRuby%E3%83%BB%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%83%93%E3%82%B8%E3%83%8D%E3%82%B9%E6%8C%AF%E8%88%88%E4%BC%9A%E8%AD%B0)らしいのは耳にしていたので、mruby-cliを久々に覗いて見たら1系のままでした…

どうするか悩んだところmruby-cliを使いたいがために1系でやることにしました。
ところが、ESP32でやってたIoT系の処理とは違って、今回必要なのはテキスト処理系の**正規表現**と**XML parser**です。
で、正規表現は[mruby-regexp-pcre](https://github.com/iij/mruby-regexp-pcre)があったのでいいんですが、xmlパーサーが中々見つかりません。

『うーん、Ruby(cRuby)って**標準で正規表現使えてrexmlも装備されているのがいいところ**なのに…[nokogiri](https://nokogiri.org/)ほどとは言わんが、remxlみたいなのないかなぁ？』と[mgem](https://github.com/mruby/mgem-list)(mruby版のgem)眺めていたらxml生成用のの[mruby-tinyxml2](https://github.com/scalone/mruby-tinyxml2)というmgemはあったもの見つけらませんでした。(後に見つかります。[^2])

[^2]: 後に見つかりますが、この辺りはcRubyのgemと比較すると色々情報を整理した方がよいなと思います。

で、『どうするかなぁ』と思ったのですが、一度決めた道なので一旦正規表現で頑張ってxmlを力技でParseして頑張ってみたところ

**くっそ、遅い**んです。（あくまでCrytal版は瞬間返ってくるのに体感0.5ぐらい待つ感じなので遅い）
(↑2020/02/20に[ベンチマーク](#ベンチマーク(2022/02/21追記))をとった結果を下に記載しています。)
ただ、ここでちょっと好奇心が生まれます。

これって**純正LLのRuby(cRuby)で書いたのとどっちが早いんだろう**と…

## 結局Ruby(cRuby)でも書いちゃう…

そこからNokogiriを使ったものを書いて見たところ、cRuby版が圧勝でした。(体感Crystalと変わらないくらい早い)
まぁ、『そりゃぁ、メモリ使用量も違うわけだし、Nokogiri使って正規表現最小限にしたらそうなるわな』と思いながら、mrubyと同じxmlのパース部分も正規表現でやってみたところ

**やっぱり、cRubyの方が圧倒的に早い**…

いくらmrubyが非力なマシンで使えるようにというコンセプトなのは分かるんですが、これは**実用的には問題がある**ぞと。

1ファイルならまだしも複数ファイルをLinterとして使用する時に、**スクリプトより遅いというのはちょっと違う**なぁと。

で、この辺で**かなり飽きてきてる**んですね、もう何個言語書いて（いや似てるけど）何個のフレームワーク使ってるんだと。
そもそも『**CUI版なんて使える人なんてbrew使える人じゃねーか？**』とか思ったんですよね。

そこで思い出したのが[exerb](https://exerb.osdn.jp/man/README.ja.html)というWindowsでRubyのexe化する古の技。[^3]

[^3]: 私は[VisualuRuby](http://www.osk.3web.ne.jp/~nyasu/software/vrproject.html)+Vbic+exerbの組み合わせで作ったツールが[PCJapan](https://ja.wikipedia.org/wiki/PC_Japan)という雑誌に載ったことがあるぐらい使い&作ってました。

その時代(Ruby1.8.7)から約9年以上経ってるわけで『流石に違うのがあるだろう([Shoes3](https://github.com/shoes/shoes3)とかじゃなく)』と思って辿り着いたのが[ruby-packer](https://github.com/pmq20/ruby-packer)でした。

が、**自分のMacではコンパイルできませんでした**。(自分のローカルのRubyが2.7だったからかな？)

『うーん、こうなったらRustかなぁ、でもなんかもうLLVMはCrystalで満足したし面白みがないからなぁ』と思ったところ思い出したのが[Deno](https://deno.land/)です。

## Denoをやってみよう

とりあえずDenoにxml parserがあるか調べたら[xml](https://deno.land/x/xml@2.0.4/mod.ts)とドンズバなものがありました。  
でも、申し訳ないんですが**私TSを書くのが苦手**なのです。

何がアレかって**最終的にJSになるくせに、JSで一番使われるであろう、webでとってきたjsonをパースするのに一々interfaceかtypeを書くのが納得いかない**んですよ。(そこがいいところなのは分かってるんですが、すいません。)

で、案の定xmlをパースするとinterfaceを書かなきゃいけないと。

この時点でもうやる気がかなりなくなってきています。何よりRubyより明らかにタイピング量が増えているんですよ。(promiseベースになったと言えど)

文句を言いながら途中まで実装し、息抜きにmruby3.0の情報を拾っていたところ、なんと[mruby-expat](https://github.com/bamchoh/mruby-expat)を見つました。[^4]

[^4]: Qiitaの[自作mrbgemsのCIが数週間前に壊れていたので直した話](https://qiita.com/bamchoh/items/eabebbbb330cb0e0470a)からです。）
はい、**Deno君との時間は終わりだ！**

## ただいまmruby

もう何回浮気したんだ？って言うぐらい遠回りしてますが、その前に気になったことがありました。

『最近のcRubyの正規表現エンジンってって[鬼雲](https://ja.wikipedia.org/wiki/%E9%AC%BC%E8%BB%8A_(%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA))使ってるんじゃなかったっけ？』と

で、wikiにあるように見事鬼雲だったので、『mrubyに鬼雲か鬼車の正規表現のないかなぁ？』と思った所[mruby-onig-regexp](https://github.com/mattn/mruby-onig-regexp)を見つけてmgemだけ変えてbuildした所、そこそこの速度が出るようになりました。(mruby-regexp-pcreの公式ページにも書いてあったんですけど、公式見てなかったorz)


鬼雲+expatで実装した所、cRuby版と変わらない(というかコマンドを叩いた瞬間に出てくる)ようになったので、やっとクロスコンパイルのお時間です。
(ここまではMacでRakeコマンドでbuildしてdebugしてました)

## やっぱり鬼門のWindows
まぁ、『**簡単には行かないだろう**』と思ってましたが、クッソ時間がかかりました。
**『RubyとWindowsって相性悪い』ってすんごい前から言われてます**し、ホントWindows系の記事少ない…[^5]

[^5]: まぁ個人的にそこまで相性悪いとは思わないし、Rubyと言いながらRailsの記事が多いのも残念なことですけど。

まず、ruby-cliの**dockerのyamlが古い…**まぁ、この辺は書き直せばいいので問題ないんですけど。

そんなことより、マジで**全然mgemのbuildが通らない**。Windowsだけじゃなく**64bit系のMac/Linux用のもbuildできない**のです。(BSD系とLinuxでbuildできないってどういうことよ？)
しかも**dockerでbuild失敗するところがMacだとbuildできたり逆もあったり**します…

expat含め**soファイルがない、headerファイルが見つからない**と言われるので、その度にmakeしたりcpして対処します。
（色々書き換えるとRake cleanすると作業が無駄になるので）[^6]

[^6]: ただこの辺りの作業って複数OSでC/C++でのmakeとかやったことある人じゃないと難しいんじゃないかなぁと思ったり…

で、あれこれするうちにやっとWindows用のバイナリを作る版になって気づきます。

**Windowsのbuildに鬼雲が対応してない**ことに！！！！！(参考:[mrbgems 評価リスト](http://forum.mruby.org/download/index30.html))
因みに↑の表にexpatが載ってないので、expat自体もwindows対応できるかは神のぞみそ知ることになります。

## 正規表現のmgemを変えるとDir.globが死ぬ
はい、上記のサイトを参考にして鬼雲がダメということなので[^7]、別の正規表現のエンジンが対応しているか確認しましょう

[^7]: この記事を書いた後、作者のmattnさんからtwitterにてWindows対応しているという話でbuildしてみましたが、libtoolのところでこけたのでそのままにしています。

|name|mac OS|Ubuntu Linux|Visial C++|MinGW|Cygwin|
|-|-|-|-|-|-|
|mruby-onig-regexp|○|○|-|-|-|
|mruby-regexp-pcre|○|○|○|○|-|

で、ですね。実は前述したexpatが載ってないことでもお分かりだと思いますが、実はこの2つだけじゃないのですよ。
私が探したところ

- [mruby-pure-regexp](https://github.com/WindProphet/mruby-pure-regexp)
- [mruby-hs-regexp](https://github.com/masamitsu-murase/mruby-hs-regexp)
- [mruby-regexp-posix](https://github.com/iij/mruby-regexp-posix)
- [mruby-pcre-regexp](https://github.com/mattn/mruby-pcre-regexp)


はい、これで前述の2個と合わせて6個ですね。([こちら](https://mattn.kaoriya.net/software/lang/ruby/20130221172829.htm)が参考になったのですがここは3つです)

で、一番無難そうなmruby-pure-regexpを試したところ、エラーも吐かずに固まります。
適当にprint(実際はp)を入れてdebugしたところ

```
Dir.glob("**/*.svg")
```

が問題とのこと。

マジですか…
『**Windows対応どころかMacでも動かないんじゃぁ意味ない**なぁ』と思い、mruby-regexp-pcreを試してみます。
**はい、MinGWでbuildできません。**(少なくとも自分のMac上では無理でした)

『mruby-regexp-posixは違うし、mruby-hs-regexpはそもそもif文に「=~」が使えるかも怪しいし…』と思って色々試した所、**mruby-pcre-regexpでなんとかなりました**。[^8]

[^8]: もちろん、mingw-w64-x86_64-pcreとmingw-w64-x86_64-expatのそれぞれのtarをDLしてlibファイルを/usr/local/Cellar/mingw-w64/9.0.0_3/toolchain-x86_64/lib/当たりに入れ込んだりmruby_pcre_regexp.cのディレクトリにpcre.hを追加するなどをしないとbuildできないですよー

ぶっちゃけ、このために**Mac Mini(こっちはInterl製)のVMにWindows入れて、Cygwin環境作ったりして半日以上潰した**りしてました。

## 結局Cのファイルをいじってなんとかbuildに成功

で、この辺りになると段々エラーが少なくなってきていたんですが、最後のトラップは

```
mruby-process/src/process.c:14:10: fatal error: sys/wait.h: No such file or directory
```

このprocess.cのsys/waitのところでした。

ただ、このエラーを見た時は『来たな！』って感じでした。なぜなら[mrubyでWebAssemblyに対応する記事](https://eng-blog.iij.ad.jp/archives/10875)を見ていたので！
sys/wait.hに限らず**sys系がwindowsにない**のは知っていたので、**ガリガリ消します**。(結局Cのファイルいじってる)

ちょっと納得いかないのは**mgemで「mruby-process」を設定していない**のにmruby-processでwindows buildできないってことは『殆どのプロジェクトってwindows buildできないんじゃね？』っていうところです。

(本題とは外れますがWasmの影響でRustはすんごい流行しつつありますけど、近い将来GoやRustだけじゃなくCrystal含めLLVM系の言語も[emcc](https://emscripten.org/docs/introducing_emscripten/about_emscripten.html)があればWasm対応は好きな言語で書けるようになりそうな気はするんですけどねぇ。)

## Windowsで確認する

無事にexeがコンパイルできたところで、VM上のWindowsでexeを叩いたところ。

- libxpat-1.dll が見つかりません
- libpcre-1.dll が見つかりません

と出たので、該当のdllを探してきて再度exeある所に置いたところ、**ちゃんと実行されることが確認できました!!!!**


## GUIはどうするか…

もうここまでくる間に、色々悩んだのですが『Electronで**このバイナリのフロントエンド作ってしまおうか？**』と思っています。
昔は[fltk3](https://mattn.kaoriya.net/software/lang/ruby/20130116150605.htm)の話もあったのですが、そもそもfltk3ってpendになってしまい、無難なところで行くとlibui系のものなんでしょうけど、これも複数ありそうで…

- [mruby-ui](https://github.com/jbreeden/mruby-ui)
- [LibUI](https://github.com/jbreeden/mruby-ui)
- [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui)

というか、MVCじゃないけど『View部分を分離したいよなぁ…』と、そうなるとやっぱりElectronの思想は素晴らしいと思うのです。


## 結論&感想
肝心のバイナリはGUIをつけてから公開にしますが、やっぱりRustでやればよかった…

うん、RubyでWindowsはやっぱり鬼門な気がしなくもないが、かと言って[WSL](https://docs.microsoft.com/ja-jp/windows/wsl/install)上にやらせるのは違うと思う。
なんだかんだ、**一般の人に使ってもらうのにはGUIにしてインストールを手軽にするか？**が鍵だと思うし…

色々考えると個人的には書きやすさ・学習コスト・安定性・知見などの中間地点をとってGo最強になるんじゃないかなぁ…
RustってCの知識必要になることが多々あるし。

**クロスコンパイルは幻想ではないが、かなり棘の道**な気がする。

## ベンチマーク(2022/02/21追記)
リクエストがあったので残ってるソースを元に軽くベンチマークをとりました(各10回テスト)
cRubyは2.7、mrubyは1.2.0を使用しています。

### svgファイル17kb(バイナリ埋め込みなし)
|Language|total|real
|-|-|-|
|Crystal(naqvis/crystal-html5)|0.010000|0.012198|
|cRuby(+nokogiri)|0.090000|0.098570|
|mruby(mruby-regexp-pcre) |0.020000|0.017061|
|mruby(onigmo)            |0.030000|0.028933|
|mruby(expat+onigmo)      |0.010000|0.014273|

### ディレクトリ指定からのsvgファイル(バイナリ埋め込みなし)12ファイル
|Language|total|real
|-|-|-|
|Crystal(naqvis/crystal-html5) |0.080000|0.073611|
|cRuby(+nokogiri)              |0.180000|0.181775|
|mruby(mruby-regexp-pcre)      |0.290000|0.292853|
|mruby(onigmo)                 |0.400000|0.396008|
|mruby(expat+onigmo)           |0.280000|0.287776|


### svgファイル(バイナリ埋め込みあり)18.7MB

|Language|total|real
|-|-|-|
|Crystal(naqvis/crystal-html5)|0.320000|0.297812|
|cRuby(+nokogiri)             |1.310000|1.322973|
|mruby(mruby-regexp-pcre)     |2.590000|2.598856|
|mruby(onigmo)                |0.240000|0.237174|
|mruby(expat+onigmo)          |0.240000|0.240899|

### ディレクトリ指定からのsvgファイル100個(一部バイナリ埋め込み含む91MB)

|Language|total|real
|-|-|-|
|Crystal(naqvis/crystal-html5)| 2.000000| 1.744164|
|cRuby(+nokogiri)             | 4.970000| 4.990069|
|mruby(mruby-regexp-pcre)     |12.820000|12.910552|
|mruby(onigmo)                | 4.410000| 4.426831|
|mruby(expat+onigmo)          | 2.880000| 2.890795|

テスト時には10ファイルぐらいしかやっていなかったのでそんなに差を感じていませんでしたが、totalで見るとバイナリフォーマットがあると若干パフォーマンスが落ちますがCrystalがパフォーマンスが一番いい感じがします。
ただ、mrubyが1系なので3系にするとパフォーマンスが上がるかもしれません。

-----

文章中に記載しきれなかった参考URL
- [2021年にmrubyを始める皆さまへ](https://udzura.hatenablog.jp/entry/2021/03/11/232423)
- [mrubyとCRubyの非互換とその対応方法](https://qiita.com/takahashim/items/d283a093f8d95df59703)
- [Rubyにもポータビリティを! シングルバイナリを作る3つの方法](https://zenn.dev/koduki/articles/f2e37169564c12)
- [ruby-packerでRubyコードをシングルバイナリにコンパイルしてみた](https://techracho.bpsinc.jp/hachi8833/2020_06_04/92808)
- [Rubyスクリプトをcross-platformなstandaloneバイナリにしようとして諦めた](https://aligach.net/diary/2019/1003/)