Unsafe と連携する

たいていの場合、アンセーフな Rust を扱うツールは、限定された状況やバイナリでしか使えないようになっています。 残念なことに、現実はそれよりも極めて複雑です。例えば、以下の簡単な関数を見てみましょう。


#![allow(unused)]
fn main() {
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
    if idx < arr.len() {
        unsafe {
            Some(*arr.get_unchecked(idx))
        }
    } else {
        None
    }
}
}

この関数は明らかに安全です。インデックスが範囲内である事をチェックし、 範囲内であれば未チェックで配列をインデックス参照します。 しかしこのような自明な関数でさえも、unsafe ブロックのスコープには疑問が残ります。 <<= に変えてみましょう。


#![allow(unused)]
fn main() {
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
    if idx <= arr.len() {
        unsafe {
            Some(*arr.get_unchecked(idx))
        }
    } else {
        None
    }
}
}

安全なコードを変更しただけなのに、今やこのプログラムは安全ではなくなりました。 これが安全性の本質的な問題です。局所的ではないのです。 アンセーフな操作の健全性は、通常 "安全" な操作によって構築された状態に依存しているのです。

安全性は、アンセーフな操作をしたからといってあらゆる他の悪い事を考慮する必要はない、という意味ではモジュール化されています。 例えば、スライスに対して未チェックのインデックスアクセスをしても、スライスがヌルだったらどうしようとか、 スライスが未初期化のメモリを含んでいるかもといった心配をする必要はありません。基本的には何も変わりません。 しかし、プログラムは本質的にステートフルであり、アンセーフな操作はその他の任意の状態に依存しているかもしれない、 という意味で、安全性はモジュール化されてはいないのです。

実際にステートフルな状況を考えると、事態はもっと厄介になります。 Vec の簡単な実装を見てみましょう。

use std::ptr;

// この定義は不完全であることに注意してください。Vec の実装に関するセクションをみてください。
pub struct Vec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
}

// この実装ではサイズが 0 の型を正しく扱えないことに注意してください。
// ここでは、すべてが 0 以上の固定サイズの型しか存在しない素敵な仮想的な世界を仮定します。
impl<T> Vec<T> {
    pub fn push(&mut self, elem: T) {
        if self.len == self.cap {
            // この例では重要ではありません。
            self.reallocate();
        }
        unsafe {
            ptr::write(self.ptr.offset(self.len as isize), elem);
            self.len += 1;
        }
    }

    fn reallocate(&mut self) { }
}

fn main() {}

このコードはとてもシンプルなので、それなりに監査して検証できるでしょう。 それでは次のメソッドを追加してみましょう。

fn make_room(&mut self) {
    // キャパシティを大きくする
    self.cap += 1;
}

このコードは 100% 安全な Rust ですが、同時に完全に不健全です。 キャパシティの変更は、Vec の普遍条件(cap は Vec にアロケートされたスペースを表している)を破ることになります。 Vec の他のコードはこれを防げません。 Vec は cap フィールドを検証できないので、信頼しなくてはならない のです。

unsafe は関数そのものを汚染するだけでなく、モジュール 全体を汚染します。 一般的に、アンセーフなコードのスコープを制限する唯一で完全無欠の方法は、モジュール境界での非公開性を利用することです。

しかしこれは 完璧な やり方です。 make_room は、public メソッドではないので、Vec の健全性の問題にはなりません。 この関数を定義しているモジュールだけがこの関数を呼べるのです。 また、make_room は Vec の private フィールドを直接アクセスしているので、 Vec と同じモジュールでのみ定義できます。

このように、複雑な普遍条件に依存した安全な抽象化を提供することは可能なのです。 これは安全な Rust とアンセーフな Rust の関係において決定的に重要です。 すでに見たように、アンセーフなコードは 特定 の安全なコードを信頼しなくてはなりませんが、 安全なコード 一般 を信頼することはできません。 安全なコードを書くときには気にする必要はないのですが、アンセーフなコードでは、 トレイトの任意の実装や渡された任意の関数が行儀よく振る舞うことを期待することはできないのです。

しかし、安全なコードが状態をあらゆる方法でぐちゃぐちゃにすることを、アンセーフなコードが防げないのだとしたら、 安全性とは絵に描いた餅かもしれません。 ありがたいことに、非公開性を利用することで、 任意のコードが重要な状態をめちゃくちゃにしないよう防ぐことができるのです。

安全性は無事です!

関連キーワード:  コード, Vec, セーフ, self, Unsafe, 連携, 関数, arr, idx, 実装