きっかけ
SVGファイルをparseして中のxmlやCSSから色情報などを取り出す、これのローカル版を作りたいということでやりたいことは
- SVGをパースしてdom上の特定の要素を抽出
- dom上だけではなくinlineのCSSや内のCSSから特定の要素を抜いてくる
- 汎用性を考えてjsonで結果を表示できるようにする
たったこれだけ。
なのでxmlパーサー(XPathなら嬉しい)と正規表現は必須、cssパーサーは元々の状態で使ってなかったのであれば良しとする(jsonはいかようにでもなる)。
因みにチャレンジしたフレームワークや言語は
で、途中で意地になってmrubyでこねくり回し泥沼にハマったが、最終的になんとかなったという話です。
(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があったのでサクと30分ぐらいでCUI版のコードができ、とりあえずbuildして色々な環境(Intel Mac/M1 Mac/Ubuntuの複数Version/Raspberry piなど)で動作確認してる時に思いました。1
『LLVMをインストールが超絶に面倒だと…(何回同じコマンドを叩いてるんだろうと)』
いや、自分の開発用の環境なら別にいいんですけど、『他人に配る時わざわざLLVMのインストールさせるのってスーパーいけてないな』と。
Macでさえ、下手するとBrewをインストールして、そこからコマンド叩くわけで…
なので、人に渡すならシングルバイナリ = Static Link(静的リンク)が生成できないとダメだよなぁ(最悪windowsはdll同封)と思ってCrystalの公式を見ると「--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のバイナリ生成できるんだっけ?』と公式を調べ直したところ、あやしい匂いがプンプンです。
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」のすべて 総集編】の影響もありmrubyを使った時期がありました。
その時はmruby-cliの存在などを知らなくて、mrbcでコンパイルしてCのヘッダーを書くという本末転倒なことをやってたんですけどね。(Rake使いましょうね。)
昨年の春mrubyは3.0として生まれ変わったらしいのは耳にしていたので、mruby-cliを久々に覗いて見たら1系のままでした…
どうするか悩んだところmruby-cliを使いたいがために1系でやることにしました。
ところが、ESP32でやってたIoT系の処理とは違って、今回必要なのはテキスト処理系の正規表現とXML parserです。
で、正規表現はmruby-regexp-pcreがあったのでいいんですが、xmlパーサーが中々見つかりません。
『うーん、Ruby(cRuby)って標準で正規表現使えてrexmlも装備されているのがいいところなのに…nokogiriほどとは言わんが、remxlみたいなのないかなぁ?』とmgem(mruby版のgem)眺めていたらxml生成用ののmruby-tinyxml2というmgemはあったもの見つけらませんでした。(後に見つかります。2)
で、『どうするかなぁ』と思ったのですが、一度決めた道なので一旦正規表現で頑張ってxmlを力技でParseして頑張ってみたところ
くっそ、遅いんです。(あくまでCrytal版は瞬間返ってくるのに体感0.5ぐらい待つ感じなので遅い)
(↑2020/02/20にベンチマークをとった結果を下に記載しています。)
ただ、ここでちょっと好奇心が生まれます。
これって純正LLのRuby(cRuby)で書いたのとどっちが早いんだろうと…
結局Ruby(cRuby)でも書いちゃう…
そこからNokogiriを使ったものを書いて見たところ、cRuby版が圧勝でした。(体感Crystalと変わらないくらい早い)
まぁ、『そりゃぁ、メモリ使用量も違うわけだし、Nokogiri使って正規表現最小限にしたらそうなるわな』と思いながら、mrubyと同じxmlのパース部分も正規表現でやってみたところ
やっぱり、cRubyの方が圧倒的に早い…
いくらmrubyが非力なマシンで使えるようにというコンセプトなのは分かるんですが、これは実用的には問題があるぞと。
1ファイルならまだしも複数ファイルをLinterとして使用する時に、スクリプトより遅いというのはちょっと違うなぁと。
で、この辺でかなり飽きてきてるんですね、もう何個言語書いて(いや似てるけど)何個のフレームワーク使ってるんだと。
そもそも『CUI版なんて使える人なんてbrew使える人じゃねーか?』とか思ったんですよね。
そこで思い出したのがexerbというWindowsでRubyのexe化する古の技。3
その時代(Ruby1.8.7)から約9年以上経ってるわけで『流石に違うのがあるだろう(Shoes3とかじゃなく)』と思って辿り着いたのがruby-packerでした。
が、自分のMacではコンパイルできませんでした。(自分のローカルのRubyが2.7だったからかな?)
『うーん、こうなったらRustかなぁ、でもなんかもうLLVMはCrystalで満足したし面白みがないからなぁ』と思ったところ思い出したのがDenoです。
Denoをやってみよう
とりあえずDenoにxml parserがあるか調べたらxmlとドンズバなものがありました。
でも、申し訳ないんですが私TSを書くのが苦手なのです。
何がアレかって最終的にJSになるくせに、JSで一番使われるであろう、webでとってきたjsonをパースするのに一々interfaceかtypeを書くのが納得いかないんですよ。(そこがいいところなのは分かってるんですが、すいません。)
で、案の定xmlをパースするとinterfaceを書かなきゃいけないと。
この時点でもうやる気がかなりなくなってきています。何よりRubyより明らかにタイピング量が増えているんですよ。(promiseベースになったと言えど)
文句を言いながら途中まで実装し、息抜きにmruby3.0の情報を拾っていたところ、なんとmruby-expatを見つました。4
ただいまmruby
もう何回浮気したんだ?って言うぐらい遠回りしてますが、その前に気になったことがありました。
『最近のcRubyの正規表現エンジンってって鬼雲使ってるんじゃなかったっけ?』と
で、wikiにあるように見事鬼雲だったので、『mrubyに鬼雲か鬼車の正規表現のないかなぁ?』と思った所mruby-onig-regexpを見つけてmgemだけ変えてbuildした所、そこそこの速度が出るようになりました。(mruby-regexp-pcreの公式ページにも書いてあったんですけど、公式見てなかったorz)
鬼雲+expatで実装した所、cRuby版と変わらない(というかコマンドを叩いた瞬間に出てくる)ようになったので、やっとクロスコンパイルのお時間です。
(ここまではMacでRakeコマンドでbuildしてdebugしてました)
やっぱり鬼門のWindows
まぁ、『簡単には行かないだろう』と思ってましたが、クッソ時間がかかりました。
『RubyとWindowsって相性悪い』ってすんごい前から言われてますし、ホントWindows系の記事少ない…5
まず、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
で、あれこれするうちにやっとWindows用のバイナリを作る版になって気づきます。
Windowsのbuildに鬼雲が対応してないことに!!!!!(参考:mrbgems 評価リスト)
因みに↑の表にexpatが載ってないので、expat自体もwindows対応できるかは神のぞみそ知ることになります。
正規表現のmgemを変えるとDir.globが死ぬ
はい、上記のサイトを参考にして鬼雲がダメということなので7、別の正規表現のエンジンが対応しているか確認しましょう
name | mac OS | Ubuntu Linux | Visial C++ | MinGW | Cygwin |
---|---|---|---|---|---|
mruby-onig-regexp | ○ | ○ | - | - | - |
mruby-regexp-pcre | ○ | ○ | ○ | ○ | - |
で、ですね。実は前述したexpatが載ってないことでもお分かりだと思いますが、実はこの2つだけじゃないのですよ。
私が探したところ
はい、これで前述の2個と合わせて6個ですね。(こちらが参考になったのですがここは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
ぶっちゃけ、このために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に対応する記事を見ていたので!
sys/wait.hに限らずsys系がwindowsにないのは知っていたので、ガリガリ消します。(結局Cのファイルいじってる)
ちょっと納得いかないのはmgemで「mruby-process」を設定していないのにmruby-processでwindows buildできないってことは『殆どのプロジェクトってwindows buildできないんじゃね?』っていうところです。
(本題とは外れますがWasmの影響でRustはすんごい流行しつつありますけど、近い将来GoやRustだけじゃなくCrystal含めLLVM系の言語もemccがあればWasm対応は好きな言語で書けるようになりそうな気はするんですけどねぇ。)
Windowsで確認する
無事にexeがコンパイルできたところで、VM上のWindowsでexeを叩いたところ。
- libxpat-1.dll が見つかりません
- libpcre-1.dll が見つかりません
と出たので、該当のdllを探してきて再度exeある所に置いたところ、ちゃんと実行されることが確認できました!!!!
GUIはどうするか…
もうここまでくる間に、色々悩んだのですが『Electronでこのバイナリのフロントエンド作ってしまおうか?』と思っています。
昔はfltk3の話もあったのですが、そもそもfltk3ってpendになってしまい、無難なところで行くとlibui系のものなんでしょうけど、これも複数ありそうで…
というか、MVCじゃないけど『View部分を分離したいよなぁ…』と、そうなるとやっぱりElectronの思想は素晴らしいと思うのです。
結論&感想
肝心のバイナリはGUIをつけてから公開にしますが、やっぱりRustでやればよかった…
うん、RubyでWindowsはやっぱり鬼門な気がしなくもないが、かと言ってWSL上にやらせるのは違うと思う。
なんだかんだ、**一般の人に使ってもらうのには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を始める皆さまへ
- mrubyとCRubyの非互換とその対応方法
- Rubyにもポータビリティを! シングルバイナリを作る3つの方法
- ruby-packerでRubyコードをシングルバイナリにコンパイルしてみた
- Rubyスクリプトをcross-platformなstandaloneバイナリにしようとして諦めた
-
ちょうどラズパイの64bitが出た時期だし環境用意するの面倒でした ↩
-
後に見つかりますが、この辺りはcRubyのgemと比較すると色々情報を整理した方がよいなと思います。 ↩
-
私はVisualuRuby+Vbic+exerbの組み合わせで作ったツールがPCJapanという雑誌に載ったことがあるぐらい使い&作ってました。 ↩
-
Qiitaの自作mrbgemsのCIが数週間前に壊れていたので直した話からです。)
はい、Deno君との時間は終わりだ! ↩ -
まぁ個人的にそこまで相性悪いとは思わないし、Rubyと言いながらRailsの記事が多いのも残念なことですけど。 ↩
-
ただこの辺りの作業って複数OSでC/C++でのmakeとかやったことある人じゃないと難しいんじゃないかなぁと思ったり… ↩
-
この記事を書いた後、作者のmattnさんからtwitterにてWindows対応しているという話でbuildしてみましたが、libtoolのところでこけたのでそのままにしています。 ↩
-
もちろん、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できないですよー ↩