Zenn
🤮

Rustが嫌いです。

2025/04/08に公開

0. 前書き - リモートデスクトッププロジェクトとの悲しき邂逅

私がremote-desktop-rsというクロスプラットフォームのリモートデスクトッププロジェクトを始めたとき、Rustの評判を信じていた。「メモリ安全性とパフォーマンスの素晴らしい組み合わせ」「優しいコンパイラエラー」「素晴らしいエコシステム」——本当にそうだったのか?

https://github.com/paraccoli/remote-desktop-rs

1. 学習曲線は「少し急」ではなく「エベレスト級」

「所有権の概念を理解すれば、あとは簡単です」と言われ続けた。嘘だ。絶対に嘘だ。

1.1 所有権地獄

私はcommon/src/lib.rsで単純な「ビルド情報」構造体を作成しようとした:

// 素晴らしい所有権システムと戦った結果の姿
pub fn build_info() -> BuildInfo {
    let build_date = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
    
    // この黒魔術がなければコンパイルすらできない
    BuildInfo {
        version: VERSION,
        build_date: &*Box::leak(build_date.into_boxed_str()), // メモリリークを起こさないと静的文字列が作れない皮肉
        commit_hash: option_env!("GIT_HASH"),
        rust_version: &*Box::leak(format!("{}", rustc_version_runtime::version()).into_boxed_str()),
    }
}

「メモリ安全な言語」なのに、静的文字列を生成するために意図的にメモリリークを起こすBox::leakを使わざるを得ない皮肉。これが「直感的」で「安全」なコードの書き方なのか?

同じコードをGoで書くなら、こんなにシンプル:

func BuildInfo() BuildInfo {
    return BuildInfo{
        Version:     VERSION,
        BuildDate:   time.Now().Format("2006-01-02 15:04:05"),
        CommitHash:  os.Getenv("GIT_HASH"),
        GoVersion:   runtime.Version(),
    }
}

Pythonならさらに簡単:

def build_info():
    return {
        "version": VERSION,
        "build_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "commit_hash": os.environ.get("GIT_HASH"),
        "python_version": platform.python_version()
    }

1.2 ライフタイム地獄

一度ライフタイムの概念を理解したと思っても、次のようなエラーに頭を抱える:

error[E0597]: `data` does not live long enough
  --> src/protocol.rs:248:13
   |
248 |             &data[..]
   |             ^^^^^^^ borrowed value does not live long enough
249 |         };
   |         - `data` dropped here while still borrowed

あなたはこのエラーを見て、すぐに解決策がわかりますか?私には分からなかった。3時間後(誇張)、スタックオーバーフローの助けを借りて、やっと解決策を見つけた:データをクローンするか、所有権を明示的に移動させる必要があった。

// 解決策1: クローンする(パフォーマンスに影響)
let data_clone = data.clone();
let slice = &data_clone[..];

// 解決策2: 所有権を移動し、新しい参照を作成
let owned_data = data;
let slice = &owned_data[..];

他の言語ではこんな問題は存在しない。例えばJavaScriptでは:

const data = getDataFromSomewhere();
const slice = data.slice(); // 何も考えずに脳死で使える

2. マルチプラットフォーム対応 = イベント駆動型悪夢

2.1 #[cfg]の迷宮

server/src/input/system.rsは、条件付きコンパイルの迷宮と化した:

/// システム入力処理
pub struct SystemInput {
    // プラットフォーム固有の実装
    #[cfg(target_os = "windows")]
    impl_: WindowsSystemInput,
    
    #[cfg(target_os = "linux")]
    impl_: LinuxSystemInput,
    
    #[cfg(target_os = "macos")]
    impl_: MacOsSystemInput,
    
    #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
    impl_: DummySystemInput,
}

impl SystemInput {
    pub fn new() -> Result<Self, SystemError> {
        Ok(Self {
            #[cfg(target_os = "windows")]
            impl_: WindowsSystemInput::new()?,
            
            #[cfg(target_os = "linux")]
            impl_: LinuxSystemInput::new()?,
            
            #[cfg(target_os = "macos")]
            impl_: MacOsSystemInput::new()?,
            
            #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
            impl_: DummySystemInput::new()?,
        })
    }
}

このコードを見て、美しいと感じるだろうか?私にはコードの20%が実際の機能実装で、残りの80%は条件付きコンパイルの呪文にしか見えない。

対照的に、TypeScriptでは優雅なファクトリーパターンで実装できる:

// システム入力ファクトリー
class SystemInputFactory {
  static create(): SystemInput {
    const platform = process.platform;
    
    if (platform === 'win32') return new WindowsSystemInput();
    if (platform === 'linux') return new LinuxSystemInput();
    if (platform === 'darwin') return new MacOsSystemInput();
    
    return new DummySystemInput();
  }
}

2.2 Feature Flag地獄

server/Cargo.tomlは、依存関係の条件付き宣言でごった煮状態になった:

[features]
default = ["system-tray", "clipboard", "screen-capture", "system-info", "file-transfer"]
full = [
    "system-tray", 
    "clipboard", 
    "screen-capture", 
    "system-info", 
    "file-transfer", 
    "webrtc-support", 
    "webp-support", 
    "async-support", 
    "windows-capture",
    "x11-support",
    "macos-support"
]
windows-capture = ["dep:windows-capture", "dep:windows", "dep:winapi"]
x11-support = ["x11rb", "xcb"]
macos-support = ["core-foundation", "objc", "cocoa"]

これを書いた私は、エディターで開いたCargo.tomlの数が多すぎて、どれが正しいバージョンなのかわからなくなり、.backup.temp.standaloneというファイルを作り始めた。web-client/Cargo.toml.backupはその悲痛な証拠だ。

Nodeプロジェクトならpackage.json一つで済む:

{
  "name": "remote-desktop-web",
  "scripts": {
    "build:windows": "cross-env PLATFORM=windows webpack",
    "build:linux": "cross-env PLATFORM=linux webpack",
    "build:macos": "cross-env PLATFORM=macos webpack"
  },
  "optionalDependencies": {
    "windows-capture": "^1.0.0",
    "x11rb": "^1.0.0",
    "cocoa": "^1.0.0"
  }
}

3. エコシステムの断片化 - "Cargo cult" programming

3.1 依存関係地獄

PS C:\Users\e2258\python\remote-desktop\remote-desktop-rs\web-client> npm run start
# ... 省略 ...
Error: failed to start `cargo metadata`: program not found
Caused by: failed to start `cargo metadata`: program not found
Caused by: program not found

どこかで見たことのあるエラーメッセージではないだろうか?Rustの「素晴らしいエコシステム」の現実は、依存関係がバージョン競合を起こし、wasm-packのような準必須ツールがインストールできないことが日常茶飯事だということだ。

解決策を探すと、単に「wasm-packをインストールしてください」では終わらない:

# 最初は単純に思えた
cargo install wasm-pack

# しかし実際は...
rustup target add wasm32-unknown-unknown
cargo install -f wasm-pack
rustup override set nightly
npm install --global wasm-pack

コントラストとして、Node.jsのエコシステムでは:

# 通常はこれだけでOK
npm install
npm start

3.2 ドキュメント地獄

READMEには簡単に見える手順が書かれている:

cd web-client
npm install
npm run build

しかし現実は:

  1. wasm-packをインストールしてください」
  2. 「いや、別のバージョンです」
  3. 「もしかしてWindowsですか?ならこの特殊な手順を...」
  4. 「あ、その依存関係はnightly版のRustでしか動きません」
  5. 「rustup default nightly」を実行
  6. 一晩寝て起きる(誇張)
  7. 「そのconfigは古いです」

4. ビルド時間 = 賢者タイム

4.1 ビルドプロファイル地獄

.cargo/config.tomlには、開発環境でも動くことを祈って書いた呪文がある:

[profile.release]
lto = true
codegen-units = 1
opt-level = 3
panic = "abort"
strip = true

Pythonならpython setup.py build、GoはシンプルにRustと比較して約3倍の速さでビルドが完了する:

# Rust: 30秒以上かかる小さなプロジェクト
cargo build --release

# Go: 同等の機能なら10秒以内
go build -ldflags="-s -w" .

4.2 素晴らしいビルド体験

> cargo build --release
    Updating crates.io index
    Updating git repository `https://github.com/some-abandoned-crate`
# ... 30分後 ...
error: failed to compile `remote-desktop-rs v0.1.0`, 99 warnings, 1 error

エラーの原因は、鬼のように深い依存関係ツリーのどこかにある、使われていないマクロだった。解決策:

# 依存関係の特定のバージョンに固定
cargo update -p problematic-crate --precise 0.1.2

# さらに問題が続く場合、依存関係を手動で解決
cargo tree -i problematic-crate
cargo build --verbose  # 詳細なエラーを確認

Node.jsならnpm audit fixで多くの問題が自動修正される。

5. エラーメッセージは「親切」ではなく「冗長」

5.1 エラーメッセージ地獄

error[E0382]: borrow of moved value: `client_hello`
   --> src/encryption.rs:128:20
    |
123 |     let client_hello = ClientHello { random, cipher_suites };
    |         ------------ move occurs because `client_hello` has type `ClientHello`, which does not implement the `Copy` trait
...
128 |     let message = serialize_message(&client_hello)?;
    |                    ^^^^^^^^^^^^^^^ value borrowed here after move

「素晴らしいエラーメッセージ」の現実は、問題解決のために関連性のない情報が山ほど表示され、初心者は途方に暮れる。

修正は実は簡単:#[derive(Clone, Copy)]を追加するか、明示的にクローンする:

let message = serialize_message(&client_hello.clone())?;

JavaScriptやPythonなら、そもそもこのようなエラーは発生しない。

6. デバッグ = 苦行

6.1 デバッグ地獄

Rustのデバッグ体験は、1990年代のC++よりも悪い場合がある。特にWebAssemblyでは、単なるprintln!デバッグすら至難の業となる。

// デバッグするためにコンソールAPIを呼び出す黒魔術
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

macro_rules! console_log {
    ($($t:tt)*) => (log(&format!($($t)*)))
}

一方、Pythonでは:

print(f"デバッグ情報: {variable}")

JavaScriptでも:

console.log(`デバッグ情報: ${variable}`);

7. あとがき - 絶望の先にあるもの

remote-desktop-rsプロジェクトは、技術的には素晴らしいものになる可能性があった。しかし、開発の苦痛と引き換えに得られるものが本当に価値あるのか疑問に思うようになった。

同じ機能をTypeScriptやPythonで実装していたら、おそらく半分の時間で2倍の機能を実装できていただろう。確かにメモリ安全性は低下するかもしれないが、開発者のメンタルヘルスは保たれていただろう。

もしくはGoを選ぶべきだったかもしれない。Goはシンプルな構文、高速なコンパイル、実用的な並行処理モデル、そして何より「合理的に動く」という強みがある。Rustのような完璧なメモリ安全性はないが、ガベージコレクションの予測可能性と良好なパフォーマンスが得られる。

Rustは素晴らしい言語だ...もしあなたが苦痛を楽しめるマゾヒストであるなら。
私がそうでないことを知ったのは、このプロジェクトの途中だった・・・。

※Rustガチ勢にはすこし(かなり)不快だったかもしれないです。ごめんなさい。

Discussion

ログインするとコメントできます