uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Rustを30分で紹介する(訳)

以下は30 minute introduction to Rustという文書(現在ではオフィシャルドキュメントの一部)の翻訳メモです。C/C++の代替に(将来的に?)なり得る言語として興味を持っています。Heartbleed問題について、ソースが汚いとか、レビュー体制がどうのとか、そもそもSSLの仕様的にどうなの、なんていう議論もあるんでしょうが、人間が間違いを犯す可能性があるということを前提とするなら、問うべき根源的な問題の一つとして、memcpyの使用ミスがあったときに、パスワードのような重要情報を含み得るメモリ領域内全内容を原理的には読まれ得る、っていう根本アーキテクチャを許しておくこと、すなわち、原理主義的にはC言語のごときをセキュアな用途に採用するのが悪い、もしくは採用せざるを得ない状況になっているのが悪いというのが一つとしてあるんじゃねーの、とは思います(誰もが思うでしょうが)。なので、Heartbleedが社会問題化するに伴ない、今後余波で流行る・流行る圧力が増すんではないでしょうか。goやSafeD、などと比べてどんなもんでしょう。



STEVE KLABNIK

最近私は、Rustの包括的なドキュメント化を提案しました。特に、短かくてシンプルな「Rustについて聞いたことがあるけれど、自分にとって適切かどうかを知りたい人向けに紹介する文章」が重要だと思っています。こちらのプレゼンテーションを見て、そのような紹介の基礎となると思いました。この文書を、RFCとしてみてください。コメントはrust-devメーリングリストTwitterにてお願いします。

Rustは、コンパイル時に正しさを保証することに強く主眼を置いた、システムプログラミング言語です。C++やD、Cycloneといった他のシステム言語のアイデアを、強い保証と記憶領域ライフサイクルの明示的制御で改良したものです。Rustでは、強力な記憶領域の保証によって、他の言語よりも容易に正確な並行処理コードを書くことができます。

ここでは、Rustにおける最も重要な概念「オーナーシップ」と、それがプログラマにとっての高難易度タスク「並行処理」の記述にどう影響を及ぼすか、について説明してみます。

オーナーシップ

「オーナーシップ」は、Rustにおいて、主要な、興味深い、もっともユニークな機能の一つです。オーナーシップは、記憶領域の各部分が、プログラムのどの部分によって変更されても良いかを指定します。まずはとあるC++コードを見てみましょう。

int *dangling(void)
{
    int i = 1234;
    return &i;
}

int add_one(void)
{
    int *num = dangling();
    return *num + 1;
}

この関数danglingは整数をスタック上にアロケートして、変数iに格納し、iへの参照を返しますが、一つだけ問題があります。スタック上の記憶領域は関数からリターンすると無効になってしまうということです。これが意味するのは、add_oneの二行目では、numはゴミの値を指しており、望む結果は得られないということです。小さな例ですが、これはしばしば実際にC++のコードで起こります。同様な問題が、malloc(もしくはnew)で割り当てられて解放された後のメモリ領域へのポインタに対して何か処理しようとしたときにも生じます。現代的なC++は、RAIIをコンストラクタ・デストラクタで使用しますが、それでも結局は同じです。この問題は「ダングリングポインタ」と呼ばれるのですが、この問題を持ったRustコードを書くことはできません。やってみましょう。

fn dangling() -> &int {
    let i = 1234;
    return &i;
}

fn add_one() -> int {
    let num = dangling();
    return *num + 1;
}

このプログラムをコンパイルしようとすると、以下のような興味深い(そして長い)エラーメッセージが表示されます。

temp.rs:3:11: 3:13 error: borrowed value does not live long enough
temp.rs:3     return &i;

temp.rs:1:22: 4:1 note: borrowed pointer must be valid for the anonymous lifetime #1 defined on the block at 1:22...
temp.rs:1 fn dangling() -> &int {
temp.rs:2     let i = 1234;
temp.rs:3     return &i;
temp.rs:4 }

temp.rs:1:22: 4:1 note: ...but borrowed value is only valid for the block at 1:22
temp.rs:1 fn dangling() -> &int {      
temp.rs:2     let i = 1234;            
temp.rs:3     return &i;               
temp.rs:4  }                            
error: aborting due to previous error

このエラーメッセージを完全に理解するためには、何かを「所有する(own)」という概念について説明する必要があります。とりあえずここでは、Rustがダングリングポインタのコードを書くことを許さない、ということを知って、それから「オーナーシップ(ownership,所有権)」について理解したあとでこのコードについて再度戻って来て考えることにしましょう。

プログラミングについてはちょっとだけ忘れて、「本」について考えてみましょう。私は物理的な本を読むことが好きですが、時々本当に気に入った本は、友達に「この本は読むべきだ!!」と言うことがあります。私が私の本を読んでいる間、私がこの本を「所有」しています。この本を誰かにしばらく貸したとき、彼は私から本を「借り」ます。もしあなたが本を借りたならば、その本はある期間だけあなたの物になります。そして私に本を返したら、わたしは本を再び「所有」することになります。いいですよね。

このような考え方が、Rustコードではまさに適用されているのです。あるコードはメモリへの特定のポインタを「所有」します。そのコードは、ポインタの唯一の所有者です。メモリをその他のコードにしばらく貸すこともできます。コードは「ライフタイム」と呼ぶある期間だけ、そのメモリを「借りる」のです。

これだけです。難しくないですよね。さて先のエラーメッセージに戻ってみましょう。エラーメッセージはこう言っています「借りた値は十分長くは生存しない(borrowed value does not live long enough)」。ここではRustの借用ポインタ(Borrowed Pointer)「&」を使ってiという変数を貸しだそうとしています。しかしRustは変数は関数からリターンすると無効になることを知っているので、こう表示します。「借用ポインタは無名ライフタイム#1の期間有効でなければならないが、借用値はこのブロックでのみ有効である」。すばらしい!

これはスタックメモリの良い例ですが、ヒープについてはどうでしょう。Rustは二番目の種類のポインタ「ユニーク」ポインタを持っています。(訳注: Rustの仕様がかわり、~は最新のRustではシンタックスとしては削除された。その代わりに、型としてはBox<T>トレイト、Boxを作成するにはBox::new()を使用する*1。この機能はOwned BoxもしくはOwnning Pointer、もしくは単にBoxなどと呼ぶことが多いようだ。)

fn dangling() -> ~int {
    let i = ~1234;
    return i;
}

fn add_one() -> int {
    let num = dangling();
    return *num + 1;
}

このコードは成功裏にコンパイルできます。スタックに割り当てられる1234の代りに、その値への所有ポインタを代りに使っていることに注意してください。大ざっぱには以下のように対応します。

// rust
let i = ~1234;
// C++
int *i = new int;
*i = 1234;

Rustはそのデータ型の格納のために必要な正しい量のメモリを割り当て、指定した値が設定されます。これが意味するのは、初期化されていないメモリが割り当てられることはないということです。Rustはnullの概念を持っていません。やったー!!! RustとC++では一つ違いがあります。Rustコンパイラはiのライフタイムを知っているので、それが無効になったときに対応するfree呼び出しをC++のデストラクタのように挿入してくれるのです。保守を自分でやらなくても、手動でヒープメモリを割り当てる利点を全て得ることができるのです。さらに、ランタイムのオーバーヘッドはありません。あなたはC++で正しく書いたのと(基本的には)正確に同じコードを得ることができ、間違ったバージョンを書くことはできません。ありがとうコンパイラ!

所有権とライフタイムというものが、厳しくない言語で書いた場合に通常危険になってしまうコードを避けるために有用である例を1つを見てきました。さて次に、もう1つの話題を示しましょう。並行性です。

並行処理

並行処理は、ソフトウェアの世界では今やびっくりするほど熱い話題です。計算機科学の研究では、従来から常に興味深い領域でしたが、インターネットの爆発的な利用拡大に伴なって、サービスあたりの利用者数をさらに増やすことが求められています。でも並行処理コードにはかなり大きな不利益があります。非決定性です。並行処理コードを書くための異なるアプローチはいくつかあるわけですが、ここでは所有権とライフタイムの概念を使ったRustの記法が、正しい並行処理を書くのをどう助けてくれるかを見てみましょう。

最初に、単純な並行処理を実行するRustのサンプルコードを示します。Rustは「タスク」を起動(spin up)することを許します。タスクは軽量なグリーンスレッドです。これらのタスクは共有メモリをもたず、チャネルを通じて以下のように通信します。

fn main() {
    let numbers = [1,2,3];

    let (port, chan)  = Chan::new();
    chan.send(numbers);

    do spawn {
        let numbers = port.recv();
        println!("{:d}", numbers[0]);
    }
}

この例では、数値のvectorを作成します。われわれはまず、Chanをnewします。ChanというのはRustがチャンネルを実装するパッケージ名です。これは2つの異なるチャンネル端である「channel」と「port」を返します。channel端にデータを送信し、port端からデータを取り出します。spawn関数は新しいタスクを起動(spin up)します。上のコードで判るように、新しいタスクの内側でport.recv()を呼んでおきます(recvは'receive'(受信)の短縮)。新しいタスクの外側からvectorを渡してchan.send()を呼びます。そして内側のタスクでは受け取ったvectorの最初の要素を表示します。

これはRustがvectorをチャンネルを経由して送信する際にコピーするので動作します。このように、もしvectorがミュータブルであったとしても、競合条件にはなりません。しかしながら、大量のタスクを作ったんら、あるいはデータが巨大な場合、タスクごとにコピーしてしまうとメモリを無駄に使ってしまいます。そうすることの利点もありません。

Arcを見てみましょう。Arcは「atomically reference counted(自動的な参照カウント)」の頭字語であり、不変データを複数のタスク間で共有する方法です。以下、コード例です。

extern mod extra;
use extra::arc::Arc;

fn main() {
    let numbers = [1,2,3];

    let numbers_arc = Arc::new(numbers);

    for num in range(0, 3) {
        let (port, chan)  = Chan::new();
        chan.send(numbers_arc.clone());

        do spawn {
            let local_arc = port.recv();
            let task_numbers = local_arc.get();
            println!("{:d}", task_numbers[num]);
        }
    }
}

このコードは先ほどのコードととても似ていますが、3回ループしているところが違います。3つのタスクを作成し、それらにArcを送ります。Arc::newは新しいArcを作成し、.clone()は新しいそのArcへのリファレンスを作ります。.get()はArcから値を取り出します。まとめると、それぞれのタスク用に作成した新しいリファレンスをチャンネルに送信し、リファレンスを使って数字を表示します。この場合vectorはコピーされません。

Arcは変更不可データを扱うのには素晴しいですが、変更可能データについてはどうでしょう? 可変状態の共有は、並行処理プログラマにとって悩みのたねです。共有される可変状態を守るのにmutexが使えますが、mutexを取得するのを忘れたら不幸が起き得るでしょう。

Rustは可変状態を共有するためのツールであるRWArcを提供します。こちらは内容変更を許すArcの変種です。以下を確認してみてください。

extern mod extra;
use extra::arc::RWArc;

fn main() {
    let numbers = [1,2,3];

    let numbers_arc = RWArc::new(numbers);

    for num in range(0, 3) {
        let (port, chan)  = Chan::new();
        chan.send(numbers_arc.clone());

        do spawn {
            let local_arc = port.recv();

            local_arc.write(|nums| {
                nums[num] += 1
            });

            local_arc.read(|nums| {
                println!("{:d}", nums[num]);
            })
        }
    }
}

この場合RWArcパッケージで「読み書き可能Arc」を取得します。「読み書き可能Arc」のAPIはArcとは少しだけ異なり、readとwriteがそれぞれデータを読み書きします。これらの関数はクロージャを引数として取ります。Arcに対するwriteはmutexを獲得し、クロージャにデータを渡します。クロージャーの実行後、mutexを解放します。

こうなっているので、mutexを獲得するのを忘れて状態を変更することはできません。可変状態の共有禁止と同じ安全性を保ちながら、可変状態を共有による効率性も得ることができます。

でも、ちょっと待ってくださいよ。そんなことが可能なんでしたっけ? 可変状態を許しつつ禁止することなんかできないはずです。いったいどうなってるんだ?

unsafeについての脚注

Rust言語は共有可変状態を許しませんが、それをするコード例を示しました。どうしてこんなことが可能なんでしょうか? 答えはunsafeです。

Rustコンパイラはとても賢く、失敗しがちな場面で守ってはくれるのですが、人工知能ではありません。私たちはコンパイラより賢いので(時々ですが)、この安全を優先する動作を上書きする必要があるのです。この目的のために、Rustにはunsafeキーワードがあります。unsafeブロック中ではRustは安全性チェックをオフにします。異常があったときは、注意深くコードを監査する必要があるのはunsafeの内側のみであり、プログラム全体ではありません。

もし主要な目的の一つが安全性であるなら、その安全性をオフにできるのはなぜでしょうか? 主には3つの理由があります。(1)例えばFFIを使ったCライブラリなどの外部コードとのインターフェース、(2)パフォーマンスのため(不可避なケースがある)、(3)通常は安全ではない操作の上に安全な抽象を提供するケース、です。Arcsは最後の目的達成のための例になっています。複数の参照をArcで分配することができますが、そのデータを変更不可であると確かに知ってるからで、共有しても安全です。複数のRWArcのリファレンスを分配することもできますが、なぜならそのデータがmutexでラップされることを知っているからであり、共有しても安全です。しかし、Rustコンパイラはこのような選択をしたことを知ることはできません。Arcの内部実装では、(通常は)危険なことをするためにunsafeブロックを使用しています。しかし、我々が公開するのは安全なインターフェースなので、Arcsの使用者は誤まった使用をすることができません。

並列処理プログラミングを困難にするようなミスを発生不可能にし、さらにC++のような言語の効率性も得るために、Rustの型システムがやっているのはこういうことです。

どうもありがとう、みんな

このRustについてのちょっとした情報で「おお、Rustはおれにとって良い言語だわ」と思ってくれたら有り難いです。もしそうなら、Rustの文法と概念についての説明についてのチュートリアルをチェックしてみて下さい。


訳は以上。以下、他の情報

*1:box という記法もあるが現時点ではunstable featureである