/dev/null

脳みそのL1キャッシュ

練習のためにRustで簡単なシェルを書いた感想

はじめに

過去に挫折経験があるRustですが、最近また勉強をはじめました。実践Rust入門という本を読みながら勉強していましたが、本を読むだけじゃ身につかない気がしたので、練習でシェルを書いてみました。

成果物は以下にあります。

github.com

機能

fork-exec

今回書いたシェルはfork-execするのような、基本的な機能しかありません。パイプもありませんし、リダイレクトもありません。

fork-execをよく知らない人のために少し説明すると、これはUnix系のOSで新しいプログラムを実行するときの処理パターンです。fork()システムコールで子プロセスを生成し、exec系のシステムコールでプログラムをロードして実行します。

bash上でlsコマンドを実行した時のプロセスの様子を雑に書くと以下のような感じになります。

            |<-------- bash ---------------->|
[process 1] --(fork)-------------------------
[process 2]    \_______(exec)________________
            |<- bash ->|    |<----- ls ----->|
            
===============================================>
時間の流れ

Rustのドキュメントを漁って、システムコールを叩くためのAPIを探したんですけど、どうやらないっぽいので、サードパーティのライブラリであるnixを使いました。

nixを使うと、以下のようにfork-execがC言語のような書き心地で書けます。

...
match fork().expect("fork failed") {
    ForkResult::Parent { child } => {
        let _ = waitpid(child, None);
    }
    ForkResult::Child => execve_wrapper(args),
}
...

ビルトインコマンド

少量ではありますが、以下のビルトインコマンドも実装しました。

Rustを使った感想

今回のシェルを実装するために必要となったRustの知識は、主に以下の通りです。

  1. 所有権システム(所有権、借用、ライフタイム)
  2. パターンマッチ
  3. Result型やOption型
  4. Enum

Rustの全機能を網羅したわけではないので、あくまで一部の機能を触った感想です。

所有権システムがつらい

みんなが言ってることです。Rustでプログラミングをすると必ず所有権システムという壁にぶつかります。

例えば、以下のコードはRustでは許されません。

struct Point {
    x: f32,
    y: f32
}

fn main() {
    let p1 = Point {
        x: 0.0,
        y: 1.0
    };
    let p2 = p1; // [1]
    println!("{} {}", p1.x, p1.y); // [2]
}

[1]の行で、p1の値の所有権がp2に移った後、[2]の行でp1を使用しようとしているため、借用チェッカーに怒られます。これは、あるリソースの所有者は1つだけというRustの思想からくるものです。

同時に複数の変数から同じリソースを参照したい場合、リソースの参照を作り、リソースを借用することができます。明示的に宣言しない限り参照は不変なので、複数の変数からリソースを見てるだけで、変更はしないので安全です。

let v1 = vec![1, 2, 3];
let v2_ref = &v1;

しかし、時には参照先のリソースを変更したいという気持ちにもなってきます。この時&mut vのように可変な参照を作れますが、可変参照を作るとき、(可変参照を含め)参照は1つしか存在できません。ある参照がリソースを書き換え、別の参照が想定外の値を読んでしまうことを防ぐためです。

これ以外にも、参照が有効なスコープを指定するためのライフタイムなる概念もあります。これによって、すでに解放されたリソースを参照することがないよう保証できます。

このようにRustはメモリ安全性のために、数々の厳しい制約を設けています。一つ一つの概念は「まあ、確かに」と納得できるものですが、実際にコードを書くとコンパイラからボコスカ怒られてつらさを感じてしまいます。

所有権システムは、GCを不要としながらも、メモリ管理にまつわるバグを無くしたいという願望から生まれたものだと考えています。なので、GCによる実行時の性能低下を許容できる環境であるなら、無理してRustを使わなくてもいいのかなと思います。

パターンマッチが便利

今回書いたシェルのコードからパターンマッチを使っている部分を抜粋します。

fn execute(cmd: Command) {
    match cmd {
        Command::Exit => builtin::exit(),
        Command::Cd(args) => builtin::cd(&args),
        Command::Pwd => builtin::pwd(),
        Command::External(args) => match fork().expect("fork failed") {
            ForkResult::Parent { child } => {
                let _ = waitpid(child, None);
            }
            ForkResult::Child => execve_wrapper(args),
        },
    }
}

型によって分岐しながら、型をばらしていける。構文解析など、複雑なデータ構造が絡んでくる処理を書くときにとても便利です。他のシステムプログラミング言語でもほしいですのが、今の所OCamlHaskellなどの関数型言語でしかみたことがありません。もっと普及しろ。

null安全でよい

これはもはやRustだけの機能ではないのですが、Rustには値の存在を表現するためにOption型があり、ぬるぽとおさらばできます。いいですね。

おわりに

なんか、途中から脱線してRustを使ってみた的な感想ポエムになっちゃったけど、まあ、いいっか。所有権システムで若干ディスりぎみな文章になっていましたが、僕はRustが好きなほうだと思います。GCなしでここまで安全性を確保できるのすごいし、所有権システム周りがしんどい代わりにそれ以外部分はとても快適です。

今回のシェルに関しては時間があれば、パイプやらリダイレクトやらの機能を加えていきたいと思います。その際には、新しいRustの機能を使ってみて新しい記事も書いてみようかと思います。

参考文献

yuk1tyd.hatenablog.com

www.amazon.co.jp