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
を仮定しないことを明示した場合。- 組み込みのポインタ型と参照型。
また、 enum
や struct
でユーザーが定義した型は、全ての要素が 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
文字列スライス。Read
やFn(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::new
は T: 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]
と見なせるのも同じ仕組みだと思われる。
内部の仕組み
Sized
は core::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_typeck::check
Sizedを制約に加える (パターンマッチの変数束縛、代入の左辺、関数の引数、関数の戻り値、構造体リテラルのメンバ)rustc::ty::wf
Sizedを制約に加える (配列とスライスの要素)rustc_typeck::check::wfcheck
Sizedを制約に加える(structの最後以外の全ての要素と、enumの全ての要素)rustc_typeeck::collect
?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への変換は不可能。)