Zenn
🤮

Rustが嫌いです。

2025/04/08に公開
20
33

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を使わざるを得ない皮肉。これが「直感的」で「安全」なコードの書き方なのか?

初心者だった私は、なぜか各フィールドを&'static str型で定義するという罠にはまったが、実は単にString型を使えば解決する問題だったのだ

// 正しい実装はこんなに簡単
pub struct BuildInfo {
    version: String,
    build_date: String,
    commit_hash: Option<String>,
    rust_version: String,
}

pub fn build_info() -> BuildInfo {
    BuildInfo {
        version: VERSION.to_string(),
        build_date: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
        commit_hash: option_env!("GIT_HASH").map(|s| s.to_string()),
        rust_version: rustc_version_runtime::version().to_string(),
    }
}

同じコードを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は素晴らしい言語だ...もしあなたが苦痛を楽しめるマゾヒストであるなら。
私がそうでないことを知ったのは、このプロジェクトの途中だった・・・。

33

Discussion

todeskingtodesking

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

BuildInfoのメンバの型を &'static strではなくStringにすればリークさせる必要は一切ないと思うのですが、あえて &'static str を使わなければいけない事情があったのでしょうか……?

あと記事の趣旨とはそれますが、このBuildInfoという構造体はなんの用途で使っているのでしょうか?
ビルド時間としてプログラム実行時の現在時刻が指定されているのはおかしい気がする。

パラッコリーパラッコリー

BuildInfoのメンバの型を &'static strではなくStringにすればリークさせる必要は一切ないと思うのですが、あえて &'static str を使わなければいけない事情があったのでしょうか……?

おっしゃる通りです!これは完全に私の設計ミスでした。「静的文字列参照を返す関数」という固定観念にとらわれていました。ご指摘いただき、ありがとうございます。

あと記事の趣旨とはそれますが、このBuildInfoという構造体はなんの用途で使っているのでしょうか?
ビルド時間としてプログラム実行時の現在時刻が指定されているのはおかしい気がする。

こちらも本来ならbuild_dateはビルド時の時刻を示すべきで、実行時の時刻ではおかしいですね。正しい実装であれば、ビルド時の情報を埋め込むマクロを使うべきでした。

pub fn build_info() -> BuildInfo {
    BuildInfo {
        version: VERSION.to_string(),
        // コンパイル時の時刻を埋め込む
        build_date: env!("BUILD_DATE").to_string(),
        commit_hash: option_env!("GIT_HASH").map(|s| s.to_string()),
        rust_version: rustc_version_runtime::version().to_string(),
    }
}

この構造体は主にログ出力アプリのAbout画面デバッグ情報表示に使っていました。実際にはビルドスクリプトでコンパイル時の環境変数を設定し、それを埋め込むのが正しいアプローチですね。私の設計と理解の問題だったと認めざるを得ません。

morohamoroha

Rustを趣味のプログラムで使っているRust信者です。
ビルド時間の部分は特にすごくわかりますね。本当に遅いし、依存関係周りで問題が起こると本当に分かりづらい。本当に...。ただ、Rustは実行時の速度とビルドの速度を天秤にかけて実行時の速度に重きをおいているという点は補足しておきたいです。プログラマーにではなく、ユーザーに優しい言語です。

あと、デバッグが大変なのもわかります。wasmはまだ触ってないのでprintデバックすら厳しいのは知らなかったですが、スレッド内のパニックとかも要因がなかなか掴めなくてしんどいです。ここはネットで検索する限りRustの弱点として言われてることが(観測範囲では割と)多く、解決策らしい解決策も少なくともWindows環境では知らないです。Linux、macだと割とあるらしいですが。

所有権やライフタイムも初心者が苦しむところですね。というか自分も'a'bとかを指定するときはまだ苦しんでいます。ただ、基本的にBox::leakを使わないといけない時点で設計のほうが間違えてるのでは?と自分なら考えますね。オブジェクト指向プログラミング並に結構プログラムの考え方自体を変えないといけない部分がある言語なので。このあたりが学習難易度を上げている要因だと思います。Rustには思想がくっついています。なので、基本的に「嫌なニオイ」がしたら、自分が間違えていて思想に則った方法があるはずだという考えで進めると割とRust君は怒ってきません。優しい子になってくれます。

Rustのエコシステムは本来、Rust内で閉じてさえいれば問題が起こったことがないので、「Rustで完結していれば」本当に優秀なエコシステムだと思っています。ただ、C/C++だったり、wasmだったり外部の連携をしようとすると、しんどいことはあります。ただこれはある程度仕方ない部分が大きい気がしています。外部の側が悪いこともありますし、そもそも全く別の言語同士をつなげること自体に少し無理がある気がするからです。

マルチプラットフォーム対応はしたことがないのでわかりませんが、Rustライブラリのexampleを見たときに#[cfg]の嵐で面食らったことはあります。ただ、トレイトかEnumを使って抽象化すればある程度#[cfg]の嵐は防げるのかな...?ここは本当に詳しくないのでなんとも言えませんが。

エラーメッセージは好み分かれますね。たまに長すぎて読む気の起きないやつはあります。特にトレイト系は。ただ、基本は必要十分だと思います。VSCode使ってると、ざっくりとしたエラーだけも確認できるので、必要だったら見るという感覚でした。

それぞれの章に言いたいことがあったから全部書いてたらめっちゃ長くなっちゃいましたが、言いたいことはこんな感じです!改めてRustについて考えさせてくれる良い記事をありがとうございます!やっぱり、批判なしに良い言語コミュニティは作れないので。

パラッコリーパラッコリー

コメントありがとうございます。

Rustを趣味のプログラムで使っているRust信者です。
ビルド時間の部分は特にすごくわかりますね。本当に遅いし、依存関係周りで問題が起こると本当に分かりづらい。本当に...。ただ、Rustは実行時の速度とビルドの速度を天秤にかけて実行時の速度に重きをおいているという点は補足しておきたいです。プログラマーにではなく、ユーザーに優しい言語です。

確かにおっしゃる通りで、Rustの設計哲学はプログラマーの快適さよりも、エンドユーザーの体験を優先しています。これは一理あります。パフォーマンスが重要なアプリケーションでは、ユーザー体験が最優先であるべきだと私も思います。

ただ、基本的にBox::leakを使わないといけない時点で設計のほうが間違えてるのでは?と自分なら考えますね。

ご指摘ありがとうございます。実は記事を書いた後、多くの方から同様のコメントをいただきました。おっしゃる通り、私のコード例は明らかに設計ミスです。BuildInfoの各フィールドを単にString型にすれば、Box::leakのような黒魔術は不要でした。
これは私がRustの思想を十分理解していなかったことを示しています。「何か難しいことをしていたら、それは間違った設計かもしれない」というのは良い指針ですね。

Rustには思想がくっついています。なので、基本的に「嫌なニオイ」がしたら、自分が間違えていて思想に則った方法があるはずだという考えで進めると割とRust君は怒ってきません。優しい子になってくれます。

私は「Rustの型システムと戦う」というアプローチでコードを書いていましたが、むしろ「Rustの思想に従う」という姿勢の方が生産的だったかもしれません。言語の哲学に逆らうのではなく、それに沿って考えることが重要だということですかね。

Rustのエコシステムは本来、Rust内で閉じてさえいれば問題が起こったことがないので、「Rustで完結していれば」本当に優秀なエコシステムだと思っています。

確かに私が苦労したのはRustとWebAssemblyの連携部分でした。純粋なRustのみのプロジェクトでは、もっと良い体験だったかもしれません。初心者が出しゃばりすぎただけですね。

マルチプラットフォーム対応はしたことがないのでわかりませんが、Rustライブラリのexampleを見たときに#[cfg]の嵐で面食らったことはあります。ただ、トレイトかEnumを使って抽象化すればある程度#[cfg]の嵐は防げるのかな...?

これは良い案だと思います。トレイトを使った抽象化で、プラットフォーム固有のコードをより整理できたかもしれません。私の実装が最適でなかった可能性は高いです。

エラーメッセージは好み分かれますね。たまに長すぎて読む気の起きないやつはあります。特にトレイト系は。ただ、基本は必要十分だと思います。

確かに、慣れてくるとエラーメッセージから必要な情報だけを素早く抽出できるようになります。VSCodeの統合も役立つのは確かです。

私の経験の多くは「Rustの問題」というより「Rustの学習曲線と私のアプローチの不一致」だったのかもしれないと考えさせられました。
言語そのものを批判するのではなく、その言語がどのような問題に適しているか、どのような思想で設計されているかを理解することが重要だと気付きました。

建設的なフィードバックをくださり、ありがとうございます。

prbtprbt

ぱっと見クローンで躓いているようなので所有権がまだ分かっていないのかなと思いました。
メモリ安全性等のメリットはあくまでC++比較ということでその他のGCを使っている言語からすれば複雑なことをひたすらやっているように感じます。仕方のないことです。
正直個人で使うメリットは薄いです。大規模開発で保守性が活きてくるようなイメージです

パラッコリーパラッコリー

コメントありがとうございます。
確かに私は所有権の概念は理解しているつもりでしたが、実際のコードでスムーズに適用するレベルには達していなかったと思います。クローンの使い方で躓いている点から、それが透けて見えるのでしょう。

おっしゃる通りRustのメリットはC++との比較で際立つもので、GoやPython等のGC言語からすると「わざわざこんな複雑なことをする必要があるのか」と感じる部分が多いです。私は普段GC言語を使っていたので、そのギャップが特に大きく感じられました。

個人プロジェクトでのメリットが薄いという点も同意です。私のは個人で開発するには複雑すぎるプロジェクトだったかもしれません。Rustの強みは大規模開発での型安全性や保守性にあり、小規模プロジェクトでは学習コストの方が大きく感じられます。

もし最初からこの視点を持っていれば、アプローチも変わっていたでしょうね。貴重なご意見ありがとうございました!

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

この設定ですが意味を理解して書かれてます?
LTOはリンク時最適化なので有効にすればビルドは遅くなります。
codegen-units は最大何並列でコード生成するかなので1にすれば当然ビルドは遅くなります。

パラッコリーパラッコリー

この設定ですが意味を理解して書かれてます?
LTOはリンク時最適化なので有効にすればビルドは遅くなります。
codegen-units は最大何並列でコード生成するかなので1にすれば当然ビルドは遅くなります。

ご指摘ありがとうございます。恥ずかしながら、これらの設定の意味を完全に理解せずに書いていました。皮肉なことに、私はビルド時間の遅さを批判しながら、自分でそれを悪化させる設定を追加していたようです。

lto = truecodegen-units = 1はおっしゃる通りビルド時間を大幅に増加させる設定で、実行時のパフォーマンスと実行ファイルサイズを最適化するためのものですね。開発中にこの設定を使うべきではなかったことは明らかです。

貴重なご指摘をいただき、ありがとうございました。これを機に、使用している設定の意味をしっかり調べることの重要性を再認識しました。

すずねーうすずねーう

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

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

過去に node-gyp のエラーで2日無駄にしたので、Rust エコシステムと npm だけで解決してる方が良くないすか?と思ってしまう

パラッコリーパラッコリー

過去に node-gyp のエラーで2日無駄にしたので、Rust エコシステムと npm だけで解決してる方が良くないすか?と思ってしまう

ご指摘ありがとうございます。
確かに、Node.jsエコシステムを「シンプルで問題がない」と描写したのは、かなり理想化しすぎていますね。特にnode-gypを含むネイティブアドオンやバイナリモジュールの依存関係は、まさに悪夢になることがあります。

どの言語・エコシステムも一長一短あり、Rustだけを不当に批判するのは公平ではなかったと思います。特にネイティブコードとの連携が必要な場合、どのエコシステムでも複雑な問題は発生します。

バランスの取れた比較ではなかった点、反省しています。ご指摘に感謝します。

タコタコ

過激な言葉で批判している割に、所有権やライフタイムを全く理解しておらず、出鱈目で無知を晒しているだけの面白みに欠ける記事だと思いました。

同じ機能をTypeScriptやPythonで実装していたら、おそらく半分の時間で2倍の機能を実装できていただろう。

TypeScriptやPythonで済むのであれば、Rustを使う必要はないでしょうね。でも、TypeScriptやPythonしか書けない人は、それらのインタプリタを修正することすらできないでしょうね。
CPythonのコードを読んだことありますか?自分でメモリの確保と解放をしなければならず、少し注意が散漫になったら不正なコードを書いてしまいそうになります。Rustの所有権システムは、CやC++で人間が注意しなければならなかったことをコンパイラーが代替するものです。

パラッコリーパラッコリー

コメントありがとうございます。
記事の冒頭で「これはあくまで私の完全なる主観であり、独断である。あくまでネタとして見てくれるとありがたい」と明記していたのですが、それが伝わっていなかったようで残念です。

私はRustの技術的価値や所有権システムの革新性を否定しているわけではありません。記事は「Rustを学び始めた開発者の挫折と苦悩」という極めて個人的な体験をユーモラスに誇張したものです。

確かにCPythonやV8のようなインタプリタ実装レベルの話になれば、Rustの価値は明らかです。メモリ安全性を型システムで保証するというRustの革新性は素晴らしいものです。ただ、すべてのプロジェクトにRustのような厳格な言語が最適とは限りません。

あなたの指摘する通り、TypeScriptやPythonで十分なプロジェクトには、Rustは必要ないでしょう。それぞれの言語には適材適所があります。私の記事はそのバランスを誇張して描いたものであり、「Rustは常に悪い選択肢」と言いたかったわけではありません。

技術的な議論を深める機会をいただきありがとうございます。Rustの学習を諦めたわけではないので、これからも精進していきたいと思います。

makoto-developermakoto-developer

慣れるとRustのエラーメッセージは親切にしかみえなくなる。
たしかに学習曲線は中級者まで角度が高めかもですね。私も最初は本を読みながらやってたけどバージョンを同じにしてもビルドで動かず結構詰みました。手前味噌ですがRustをやる前にC++を学んだほうが良いかもしれないです。私は遠回りしてC++から入ってRustを学びました。なぜRustが誕生しなければならなかったのかを体験することでありがたみを理解できました。ここはやはりメモリ管理の地獄を渡り歩く経験をしておくとRustの所有権がなんて素晴らしい仕組みなんだとありがたみを知ることで恩恵を受けられます。何のアドバイスにもならないですがRustはただ速いとか安全とかだけで踏み込むとそこはジャングルですぐ帰りたくなるかもしれないそんな気持ちは確かに理解できます。
なんかおすすめの本とかないかな。結局私は公式のチュートリアルとRustのソースコードを読んで乗り切ったのでなんともですが良いのを見つけたらここに書きますね。

パラッコリーパラッコリー

とても共感できるコメントをありがとうございます!
特に「Rustをやる前にC++を学んだほうが良い」という視点は非常に興味深いです。

確かに私はC/C++のメモリ管理の苦労をあまり経験せずにRustに飛び込んだので、所有権システムが「解決策」ではなく「面倒な制約」に感じられてしまったのかもしれません。C++でセグフォルトやメモリリークと格闘した経験があれば、Rustの厳格さをより前向きに受け止められたでしょうね。

「なぜRustが誕生しなければならなかったのか」という背景を理解することの重要性、本当に同感です。言語設計の哲学と歴史的文脈を知れば、その制約の意味も自ずと見えてくるものですね。

Rustの学習リソースについては、私も常に探しています。もし良い本や教材を見つけたらぜひ教えてください!

あなたの「ジャングルですぐ帰りたくなる」という表現に思わず笑ってしまいました。まさにそんな気持ちだったのですが、もう一度Rustの森に足を踏み入れる勇気が湧いてきました。ありがとうございます!

EdamAmexEdamAmex

Rustで書かれたDenoは実質RustなのでDeno使おう!(?)

パラッコリーパラッコリー

Rustで書かれたDenoは実質RustなのでDeno使おう!(?)

なるほど、DenoがRustで書かれているからDeno使えばRust触ってるも同然という発想ですね😂
その理論だと、Chromeを使ってるだけでC++プログラマーを名乗れるかもしれません...!
Rustの苦しみを味わわずにRustの恩恵を受けられるのはある意味賢い戦略かも。「Rustは書けないけどTypeScriptならOK」な私にとっては魅力的な提案です👍

EdamAmexEdamAmex

なるほど、確かに「Denoを使えばRustを触ってるも同然」という言い方は、一見するとユーモアのある軽いノリに聞こえるかもしれませんね。ただ、もう少し踏み込んで考えてみると、この発想には実は結構意味があるんです。

まず、DenoはRustで書かれていて、その設計思想や機能の多くがRustの特性に根ざしています。たとえば、Denoの高いセキュリティモデル、非同期I/O、リソース管理、FFIまわりの設計など、どれをとってもRustのアプローチが色濃く反映されています。Denoを使うということは、そうしたRustのアーキテクチャの上に乗ってコードを書くことになります。単に「Rustで作られたツールを使ってる」以上の関わり方なんです。

ちなみに、Denoの内部構造について学ぶならこのdocsがおススメです。
https://choubey.gitbook.io/internals-of-deno

もちろん、Chromeの例のように「中身がC++だから自分もC++プログラマー」というのは、ちょっと違和感がありますよね。でも、Denoの場合は開発者が直接Rustの設計に沿ったAPIや制約の中でコードを書くわけで、その関わり方の深さがまったく違います。特に、DenoのFFIでRustのコードを呼び出すようなケースでは、RustとTypeScriptの型の違いや、ライフタイム管理、安全性の考慮など、Rustの考え方を理解しないとちゃんと扱えない場面も出てきます。

それに、「Rustはちょっと敷居が高いけど、TypeScriptなら安心して書ける」という方にとって、Denoはちょうどいい接点だと思います。Rustの恩恵(スピード、安全性、モダンな設計)を享受しつつ、TypeScriptという馴染みある言語で開発できるのは、大きなメリットです。言い換えれば、DenoはRustの強さをTypeScript経由で引き出せる、ちょっとおいしい立ち位置のツールなんですよね。

この「Rustの恩恵を間接的に使う」アプローチは、今後もますます増えていくと思います。たとえば、Bunもそうですし、WASM系の技術でも裏側にRustを使ったものが増えています。そんな中で、Denoのような選択肢があるのは、むしろ技術選択としてすごく合理的で、柔軟なやり方だと思います。

dalancedalance

#[cfg] については記事中にあるように細かい単位で切り替えようと思うと大変なので、ファイル単位くらいにすると楽かもしれません。

// system.rs
// 実装を切り替えつつ、各実装のシンボルをエクスポートする
#[cfg(target_os = "windows")]
mod system_windows;
#[cfg(target_os = "linux")]
mod system_linux;

#[cfg(target_os = "windows")]
pub use system_windows::*;
#[cfg(target_os = "linux")]
pub use system_linux::*;

// system_windows.rs
pub struct System {}

// system_linux.rs
pub struct System {}

// main.rs
// systemからエクスポートされたものを使えば各プラットフォーム毎の実装になっている
let system = system::System {};

WASMは大変ですよね…。Rustのエコシステムの中でも結構ユーザ体験の悪い部類な気はしています。
「CLIやサーバサイド向けに書いたコードがこの程度の手間でブラウザでも動く」という観点では結構ありがたいのですが、
ブラウザをメインターゲットにするならやはりJS/TSが楽だろうと思います。

r-sugir-sugi

色んなコメントがありますが、個人的に面白かったです!
学習コスト高いのに、(Web系の)フリーランス案件数が少ないからRustやることはなさそうという思いました

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