以前gitのGUI作りたいと思っていろんな試行錯誤して落ち着いたのはClojureScript(re-frame使ったReact)とElectronで作ったUIにRustのgit2-rsで生のgitとのやりとりする方式。まだちょっとややこしいところも残っているが、ほとんどはそれなりにスムーズな開発フローに乗れた。

最終的に実行環境はElectron、つまりNode.jsになるので全体的な共通点はそのJavaScript。ClojureScriptは元からJSをターゲットにするものだから当たり前のようにそれができるが、Rustはそうでもない。RustからJSを目指すなら選択肢は2つ:WebAssemblyにコンパイルするか、Node.jsのnative moduleへコンパイルするか。ただ今回はCのlibgitの上に成り立つgit2-rsが使いたいものだったのでWebAssemblyで実験するよりもネイティブモジュールを使った方が安心。

ClojureScriptはshadow-cljsという伝統的なセットアップで行くことにした。主な理由としてはそれ以外のところでも十分実験的な組み合わせになるから、せめてCLJSのところで問題なく動くでしょうと思えるものを使いたかった。今回は土台にしたのは苔が生えたテンプレートだったが、中身をちょっと整理・更新して問題なく動いた。JS側ではCLJSを担当するshadow-cljs以外でelectron系を動かすelectron、electron-builderと後で触れるelectron-build-envが必要。

neon build --release

RustはNeonというツールでNode.jsのネイティブモジュールが生成できる。今回の構造だとRustで作ったものを個別にnpmに乗せて提供するつもりはまずないから、プロジェクトの中の一つのフォルダーでまたプロジェクトとして作っている:別のpackage.jsonがある。ただそれを直接呼び出すことはほとんどない。

Neon使った子プロジェクトをfile:の依存関係として親プロジェクトに追加すると、他の依存をnpmなどから引っ張ってくる時に勝手にビルドしてくれる。その関係上、Rustのコードを触る度にElectronを再起動する必要があるが、開発中にRust単体でも実行できるようにコードを構築すると、そう頻繁にビルドやる必要はない。

shadow-cljs watch main renderer

ElectronのUI側で実行されるCLJSはほぼリアルタイムで変更が反映されるが(ブラウザでのshadow-cljsと同じ振る舞い)”裏側”に関しては起動時にしか反映されないのでElectronの再起動が必要になる。とはいえ一回Electronのプロセス間のコミュニケーション(イベント伝達)の仕組みを整えたら、後はほとんど触ることがないのであまり気にならない。

Electronの”裏側”(”main”)でやるのはウィンドウ自体の描画(メニューバーや初期の大きさの設定など)と(例えばファイル選ぶための)ダイアログ開いたりOSへの呼び出しの処理なので、UIを変える度に必要はない。できるだけ再利用できるインターフェースを作ってその”main”を変える頻度を抑える。

型の心配

Rustは型にうるさい言語だが、JSとClojureScriptはそうでもない。そのために一番型変換で汗かくのはNeonの形式に沿ってJS側から情報を受け取るのと、返り値をJSに返す時。とはいえその大半も機械的に作れる”boilerplate”コードで悩むことはほとんどない。ただいちいち型の指定やキャストが必要になるのでJSとCLJSみたいに柔軟に束縛することなどはまぁ簡単にはできない。

現状のワークフローで痛いところで言うと、すでに手元にあるデータとre-frameでは収まらないgitの呼び出しを新規追加したりする時の型変換のコピペのだるさ。関数の入出力で期待してる形(型)を事前に定義しておいてそこから自動で生成できるだろうとは思っているが、やはりまだその仕組みを作るほど痒くない。

動かす時

  1. yarn
    おなじみのyarn はJSからの依存関係をやってくれる。
  2. electron-build-env neon build git --release
    これでfile: 依存として”git”の名称で定義したNeonモジュールがビルドされnode_modules配下にバイナリ含めコピーされる。
  3. shadow-cljs watch main renderer
    shadow-cljsはCLJS側の変更をhot reloadしてくれる。”main”はElectronの”main”に当り :target :node-script としてファイルに出力され、それが起動時にElectronに使われるので、変更加えた場合はElectronの再起動が必要。一方”renderer”は普通のブラウザ版CLJSと同様に即時反映され、shadow-cljsの開発ツールも使える。

上記をまとめて一つのyarnエリアスとして定義するとプロジェクト初期化の時に便利。開発中は2番めの electron-build-env と3番目のshadow-cljsの呼び出しを主に使う。

UIをいじるだけの開発はshadow-cljsの恩恵受けてとても生産性が高い。RustもRust側のツール(IDEなど)を活かすとかなり楽になる。ただRustのコードをNode.jsのモジュールとして出力する時はJS側の依存関係もまとめてビルドし直す必要があって速いとは言い難い。その代わりに実行時は爆速。

このセットアップ使っているプロジェクトはGitHub上で公に開発しているのでこれで興味持ってくれた人はぜひ見てみてください〜