opというツールをGoからRustに書き換えました。とても機能としては単純なツールなのですが、私の中で宅内自作ツール使用率No1かもしれません(笑)
暇すぎたら落として使ってみてください。

ちなみに1PasswordのCLIと名前被るそうですね。。使う方がいましたらエイリアスでもかけといてください

機能はそのままに殆ど主要PCのOSをサポートしたまま、30%程度の容量削減を実現しました。ちなみにRustの知識はそんなにないので、作りながら学んでみました。この時に思ったことを書きます。

Rustの特徴的なところ

私のコードだと以下のところが良く示していると言えます。

let opener = match kind.unwrap() {
    ParamKind::FilePath(f) => Opener::File(File::new(f, plat)),
    ParamKind::Url(u) => Opener::Browser(Browser::new(u, plat)),
};
match opener.open() {
    Ok(_) => return Ok(()),
    Err(e) => return Err(e),
}

Rustの究極的な特徴は、正直「所有権システム」ではなくResultOptionのマッチです。これはどういうものかと言えば、「実際の値がラップされた型」をひたすら返すような構造になっていて、必要なところで解除して判定する、みたいなシステムです。

つまりプログラマの役割としては、「どこでラップを解除する?どういった方法で?」になります。これが返されまくるので、基本的にはAPI知識が豊富で使いこなせてないと、ネストが必然的に発生することになります。方法としては「マッチ式・if解除・トレイト関数」などがあり、綺麗さの為に「意図的なアンラップ」を駆使することになります。この選択肢の多すぎる「うざったさ」が1つRustが好きにならない人の実際の理由になるでしょう。

Rustを触れるとプログラマはインテリセンスで「大量の関数」を目にすることになります。 が、殆どはゴミのようなもので、だけど時々欲しいよね、みたいなものが沢山ある組み込み方が通例です。これにこんがらがることになります。

この正体は、「トレイト」というインターフェース的な機能を備え付けまくるのがアイデアになっているからで、「Display」って何するの…え、これで「to_string」もついてくるの、とか、実際の開発にはそういう知識が恐らく必要と言えます。

ここで出てくるのが「属性」で、Rustは使い方がやや特徴的。例えば「あるトレイトを自動実装する」などの機能が豊富に出てきます。これはパッケージやライブラリで色々異なっていて、ResultErrorも別のライブラリならば別の型、ということは多くなります。例えば「ExitStatus」はstdにあるものなのか、他のパッケージにあるものなのか…そういうことが読めてないとダメよ、という中々大変なシステムです。

トレイトをインターフェースとして見てはいけない、というのもちょっと発想の切り替えが恐らく必要で、例えば上記コードのFileBrowserOpenトレイトを付けてますが、C#的なインターフェースでメタな抽象型に還元して叩く、みたいなことは出来ないです。具体的には「ボクシング化してdynする」という処理が必要なのですが、現実的ではありません。結果的に「Openトレイトを実装した構造体をOpener列挙体に渡し、Opener列挙体はパターンマッチでそれぞれのトレイトを叩く」という設計にしていますが、そういうパターンは結構出てきそうです。

とりあえず出てきた「言語的な難しさ」って、上みたいな部分だと思いました。なので「所有権難しい」「エラーがビルドで沢山出てくる」ということは正直ない方の言語だと思いました。裏を返すと、「中身のルールを沢山しっていた方が有利になりやすい」系の言語で、テキトーにサクサク書けるGoみたいなのとは逆の傾向にあります。「スッゴクマジメな言語」とでも言ったら良いんでしょうか。正直面白くはない言語です(笑)が、「勉強する時間に比例した結果は確実に身につく」言語だと思います。そういう意味で、お仕事プログラマーとかには良い言語かもしれません。

Rustを使う意義

とりあえず分かったことは、やはり「サードパーティの充実性」「静的ツールの成熟ぶり」「バイナリの小ささ」です。これは低レベル向けの言語にしては圧倒的で、やりたいことは多く誰かが作ってる物量が違いすぎます。やはりこれが理由でRust、ということは今後もメインストリームとなるでしょう。

思ったところとして、案外パフォーマンス的な所は余地があります。実は私のツールはパフォーマンス的には、GoからRustにして劣化しています(笑) 通常使用じゃ絶対分からないスピード感ですが。 では何が、ってstd::process::Command叩いただけの差異がすげー出てます。Goってこのあたり、どうやらガチで「数ms」くらいらしく(恐らくOSや環境でも異なるでしょう)、つまり「内部の最適化」が相当優秀ってことです。つまり、とりあえず「Rust使えばパフォーマンスあがるやろ」みたいなのは考えないほうがいいでしょう。アルゴリズム的にそれが解決出来ると確信できるなら、言語を使うのが良いでしょう。

クロスコンパイル

これは思ったよりも大変です。他言語より圧倒的に簡単ですが、GoやZigを知ってしまうと、それよりはハードルがあります。基本的には「ARMマシン、Mac、Alpine」など、特定の要件を満たすマシンごとにビルドするのが基本戦略にはなります。

これを吸収する為にCargo Zigと言うツールがあります。これはzigをCCで使うことでそこそこビルドできちゃうよ、というシロモノで、まさにZig CCの本領発揮ってところです。ただし、全部がビルドできる訳ではありません。

結果的にはビルドはCI依存に任せとく選択をしました。具体的に言うとCrossというツールが便利で、これはコンテナを立ち上げて良い感じにビルドできるツールです。

私の場合、これはGitHub Actionsで次のように組み込んでいます。

- name: Build By Cross
  if: startsWith(matrix.os, 'ubuntu')
  run: |
    targets=(
      "x86_64-unknown-linux-gnu"
      "x86_64-unknown-linux-musl"
      "aarch64-unknown-linux-musl"
      "x86_64-pc-windows-gnu"
    )
    for target in "${targets[@]}"; do
      cross build --release --target $target
    done    

が、Macは絶対に使えないマン(本当にAppleは…)なのでMacのイメージでマトリクスビルドすることで、とりあえず基本必要でありそうなOS・アーキテクチャは対応できた感じです。