RustのSizedとfatポインタ

概要: RustにはSizedというトレイトがあり、一部の例外を除いて暗黙のうちに実装されている。Sizedが実装されていない型はDynamically Sized Typeと呼ばれ、これらのデータはfatポインタを経由してアクセスする。この仕組みを説明する。

Sizedの使い方はAPIリファレンスThe Bookの該当部分その日本語訳Rustonomiconの該当部分をまず読むとよい。

この記事では、コンパイラがSizedをどう実装しているかという観点からまとめ直してみた。

Sizedとは何か

Sizedは標準ライブラリで定義されているトレイトである。

pub trait Sized {}

Sizedトレイトは次の2つの意味をもつようだ。

  • Sizedを実装する型は、全て同じバイト数である。C言語のsizeofに相当するstd::mem::size_of が使える。(Sizedでない場合は値によって異なるため、std::mem::size_of_valを使う)
  • Sizedを実装する型は、データ本体以外の余計な情報を持たない。(逆に、Sizedでない場合は、余計な情報が必要になる。それらの余計な情報を持つためにfat pointerが採用される)

Sizedは実装を持たない「マーカートレイト」の一種であり、言語処理系によって特別扱いされている。

特別扱いその1: 暗黙の制約と暗黙の実装

プログラマが明示しなくても、型変数は Sized を実装していることが仮定される。例えば、

pub struct Foo<X>(PhantomData(X));

と書いた場合、 X:Sized が仮定される。例外は以下の3つである。

  • トレイト宣言におけるトレイト自身は、 Sized を仮定しない。 (トレイトの型引数や関連型はこの限りではない)
  • X: ?Sized という専用構文を使って、 Sized を仮定しないことを明示した場合。
  • 組み込みのポインタ型と参照型。

また、 enumstruct でユーザーが定義した型は、全ての要素が Sized ならば、自動的に Sized の実装が与えられる。逆に、ユーザーが手動で Sized の実装を追加したり削除したりすることはできない。

特別扱いその2: プログラム中での要請

以下の状況で Sized が要請される。

  • let やパターンマッチで束縛できる変数の型は Sized でなければならない。
  • 関数の引数と戻り値の型は Sized でなければならない。
  • structの最後を除く全ての要素と、enumの全ての要素は、 Sized でなければならない。

例えば、

fn f(a: [u8]);

という関数は書けない ([u8] はSizedではないため) が、

fn f(a: &[u8]);

という関数は書ける (&[u8] はSizedであるため) 。

特別扱いその3: ポインタの大きさ

Sized を実装しない型へのポインタは、通常fat pointerで実装される。

例えば、x86-64環境なら、 *const T, *mut T, &'a T, &mut 'a T, Box<T> などのポインタ自身の大きさは通常8byteだが、TがSizedでないときは16byteになる。

fat pointerは何を保持しているのか

Sizedでない型は限られる。具体的には:

  • [i64] などのスライス。
  • str 文字列スライス。
  • ReadFn(i32) -> i32 などのトレイトオブジェクト。
  • Sizedでない要素を末尾にもつstruct。

これらの値へのポインタは16byteになる。最初の8byteにはデータの先頭番地が入っている。続く8byteには、以下の内容が入っている。

  • &[i64] の場合: スライスの要素数
  • &str の場合: バイト数。
  • &Read の場合: vtableへのポインタ。
  • これらを末尾要素にもつstruct: 末尾要素のfat pointerの内容を継承する。

このことは以下のような実験で確かめられる。

use std::ops::Deref;

// xの中身をバイト列として見るための関数
fn as_raw_bytes<'a, T:?Sized>(x: &'a T) -> &'a [u8] {
    unsafe {
        std::slice::from_raw_parts(
            x as *const T as *const u8,
            std::mem::size_of_val(x))
    }
}

pub struct S<T:?Sized> {
    pub x: u8,
    pub y: T,
}

fn main() {
    let arr : [u16; 3] = [1, 2, 3];
    let arrref = &arr;
    let arrptr = &arr as *const [u16; 3];
    let arrslice = &arr[..];
    let arrsliceptr = &arr[..] as *const [u16];
    let strslice = "ほげ";
    let clos = |x, y| x + y;
    let closref : &Fn(u8, u8) -> u8 = &clos;
    let mut a = 0;
    let clos2 = |x| {a += x; a};
    let clos2ref : &FnMut(u8) -> u8 = &clos2;
    let sarr : Box<S<[u16; 3]>> = Box::new(S { x: 3, y: [1, 2, 3] });
    let sarrref = sarr.deref();
    let sslice : Box<S<[u16]>> = Box::new(S { x: 3, y: [1, 2, 3] });
    let ssliceref = sslice.deref();
    println!("arrref = {:?}", as_raw_bytes(arrref));
    println!("&arrref = {:?}", as_raw_bytes(&arrref));
    println!("&arrptr = {:?}", as_raw_bytes(&arrptr));
    println!("arrslice = {:?}", as_raw_bytes(arrslice));
    println!("&arrslice = {:?}", as_raw_bytes(&arrslice));
    println!("&arrsliceptr = {:?}", as_raw_bytes(&arrsliceptr));
    println!("strslice = {:?}", as_raw_bytes(strslice));
    println!("&strslice = {:?}", as_raw_bytes(&strslice));
    println!("closref = {:?}", as_raw_bytes(closref));
    println!("&closref = {:?}", as_raw_bytes(&closref));
    println!("clos2ref = {:?}", as_raw_bytes(clos2ref));
    println!("&clos2ref = {:?}", as_raw_bytes(&clos2ref));
    println!("sarrref = {:?}", as_raw_bytes(sarrref));
    println!("&sarrref = {:?}", as_raw_bytes(&sarrref));
    println!("ssliceref = {:?}", as_raw_bytes(ssliceref));
    println!("&ssliceref = {:?}", as_raw_bytes(&ssliceref));
}
arrref = [1, 0, 2, 0, 3, 0]
&arrref = [82, 202, 147, 108, 255, 127, 0, 0]
&arrptr = [82, 202, 147, 108, 255, 127, 0, 0]
arrslice = [1, 0, 2, 0, 3, 0]
&arrslice = [82, 202, 147, 108, 255, 127, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
&arrsliceptr = [82, 202, 147, 108, 255, 127, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
strslice = [227, 129, 187, 227, 129, 146]
&strslice = [128, 149, 8, 159, 82, 86, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0]
closref = []
&closref = [248, 201, 147, 108, 255, 127, 0, 0, 240, 199, 41, 159, 82, 86, 0, 0]
clos2ref = [231, 201, 147, 108, 255, 127, 0, 0]
&clos2ref = [216, 201, 147, 108, 255, 127, 0, 0, 32, 200, 41, 159, 82, 86, 0, 0]
sarrref = [3, 0, 1, 0, 2, 0, 3, 0]
&sarrref = [8, 0, 98, 112, 68, 127, 0, 0]
ssliceref = [3, 0, 1, 0, 2, 0, 3, 0]
&ssliceref = [16, 0, 98, 112, 68, 127, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]

Sizedでない参照を作る方法

Sizedでない値を直接作ることはまずない。通常は何らかのポインタの形で存在する。これらのポインタは、Sizedな値へのポインタを型強制によって変換することで作ることが多い。例えば、

fn curried_add(x: i32) -> Box<Fn(i32) -> i32> {
    Box::new(|y| x + y)
}

のような関数を書いたとき、この Box::new が返すのは Box<Fn(i32) -> i32> ではない。このBoxが返すのは Box<SomeClosure> のように具体的なクロージャの型が判明した状態のBoxである。(もちろんこのクロージャには実際にはユーザーから見える名前はついていない。) そもそも Box::newT: Sized を仮定しているから、これが Box<Fn(i32) -> i32> を直接返すことはありえない。

つまり Box<SomeClosure> から Box<Fn(i32) -> i32> への型強制が行われていることになる。この部分の仕様はまだ確定はしていないようだが、現状では次のように実装されている。

型強制が可能になる条件はいくつかあるが、ここでは std::ops::CoerceUnsized というトレイトの実装があるかチェックされる。これも言語処理系により特別扱いされているマーカートレイトである。つまりここでは、

impl CoerceUnsized<Box<Fn(i32) -> i32>> for Box<SomeClosure> {}

が定義されているかが調べられる。ここでBoxは

impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<Box<U>> for Box<T> {}

を実装しているため、実際に必要なのは

impl Unsize<Fn(i32) -> i32> for SomeClosure {}

ということになる。これは言語処理系によって自動的に実装される。

CoerceUnsized は他の参照やスマートポインタにも実装されている。例えば &[1; 2; 3]&[i32] と見なせるのも同じ仕組みだと思われる。

内部の仕組み

Sizedcore::marker::Sized で定義されている。

Sizedの定義を見ると、普通のtraitのようにも見えるが、#[lang = "sized"] という記述がある。この lang という属性は、このアイテムが処理系によって特殊な扱いを受けることを表している。langは rustc::middle::lang_item で処理される。 lang="sized"という属性は lang_items::SizedTraitLangItem という名前がつけられ、この属性のついたアイテムを lang_items.sized_trait() で取得できるようになる。

Sized は言語処理系の以下の場所で特殊な扱いを受ける。

  • 条件を満たしたstructやenumに自動的にSizedが付与される。
    • rustc_tyoeck::collect Sizedを付与していいか決めている。この sized_by_default は基本的にYesだが、trait宣言のSelfに対してのみNoとなる
    • rustc_typeck::astconv Sizedを付与している
  • 必要な場面で自動的にSized制約が追加される。
  • rustc::traits::select 必要な場面で自動的にSized実装が追加される。
  • rustc_typeck::coherence::orphan 手動でのSized実装の追加/削除を禁止する。
  • 必要な場面で自動的にSizedが実装されているか検査される。
    • rustc::traits::object_safety トレイトオブジェクト作成時のobject safetyチェック
    • rustc_typeck::check::cast キャストにおけるsizedチェック (unsizedへの変換は不可能。sized ptrからunsized ptrへの変換は不可能。function ptrからunsized ptrへの変換は不可能。unsized ptrからアドレスへの変換は不可能。アドレスからunsized ptrへの変換は不可能。)