Rustのsize_of_valはどこで実装されているか

概要: std::mem::size_of_val はRustでもCでも実装できない。どこで実装されているかを調べた。

size_of_val が特殊な理由

std::mem::size_of_valstd::mem::size_of の親戚である。 size_of はCのsizeofと似たようなものだと言ってよい。 RustはCとは異なり、値によってバイト数が異なるような型 ([T], str, trait objects, dynamically sized structures) があり、これらの型に対して一般化されたsizeofが size_of_val である。

Rustの標準ライブラリの関数の多くは、通常のRustのコードとして実装されている。そうでなくても、ほとんどはunsafe Rustや、 extern "C" を用いた外部関数として与えられている。

size_of_val はRustのプリミティブであるからRust/unsafe Rustでは書けないし、多相だからCの外部関数としては書けない。

size_of_val のライブラリ定義

std::memcore::mem の別名である。 (std 375行目)

#[stable(feature = "rust1", since = "1.0.0")]
pub use core::mem;

core::mem::size_of_valcore::intrinsics::size_of_val をsafeに言い換える薄いラッパーである。 (core::mem 217行目)

#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn size_of_val<T: ?Sized>(val: &T) -> usize {
    unsafe { intrinsics::size_of_val(val) }
}

そして、 core:intrinsics::size_of_valextern "rust-intrinsic" で定義されている。 (core::intrinsics 622行目)

extern "rust-intrinsic" {
    ...
    pub fn size_of_val<T: ?Sized>(_: &T) -> usize;
    ...
}

extern "rust-intrinsic" はどこで処理されているのか

"rust-intrinsic" という文字列は syntax::abi 90行目 で解釈され、 syntax::abi::Abi::RustIntrinsic になる。

#[allow(non_upper_case_globals)]
const AbiDatas: &'static [AbiData] = &[
    ...
    AbiData {abi: Abi::RustIntrinsic, name: "rust-intrinsic", generic: true },
    ...
];

HIRからMIRへの変換で関数呼び出し構文を処理する際に、 rustc_trans::callee::CalleeData::Intrinsic というマークがつけられる。 rustc_trans::callee 102行目

    pub fn def<'a>(ccx: &CrateContext<'a, 'tcx>,
                   def_id: DefId,
                   substs: &'tcx Substs<'tcx>)
                   -> Callee<'tcx> {
        ...
        if let ty::TyFnDef(.., f) = fn_ty.sty {
            if f.abi == Abi::RustIntrinsic || f.abi == Abi::PlatformIntrinsic {
                return Callee {
                    data: Intrinsic,
                    ty: fn_ty
                };
            }
        }
        ...
    }

Intrinsic のついた関数呼び出しは rustc_trans::mir::blockMirContext::trans_block という関数内で2つに分けて処理されている。

move_val_inittransmuterustc_trans::mir::block 432行目 で処理されている。

残りのintrinsicsは rustc_trans::mir::block 528行目 で処理され、コード生成は rustc_trans::intrinsic::trans_intrinsic_call に移譲されている。

ここに様々なintrinsic関数のコード生成処理が直書きされているが、今回の目当ての size_of_valrustc_trans::intrinsic 161行目 にある。

        (_, "size_of_val") => {
            let tp_ty = substs.type_at(0);
            if !type_is_sized(tcx, tp_ty) {
                let (llsize, _) =
                    glue::size_and_align_of_dst(&bcx.build(), tp_ty, llargs[1]);
                llsize
            } else {
                let lltp_ty = type_of::type_of(ccx, tp_ty);
                C_uint(ccx, machine::llsize_of_alloc(ccx, lltp_ty))
            }
        }

型に Sized がついている場合は size_of と同じ処理をしている。つまり、sizeofをコンパイル時に計算し、定数を入れるだけのLLVMコードを生成する。

Sized でない場合は、 rustc_trans::glue::size_and_align_of_dst を呼ぶ。この関数はなぜかDrop glue関連コードと一緒になっている。 (Drop glueからも利用されるからだと思われる)

pub fn size_and_align_of_dst<'blk, 'tcx>(bcx: &BlockAndBuilder<'blk, 'tcx>,
                                         t: Ty<'tcx>, info: ValueRef)
                                         -> (ValueRef, ValueRef) {
    ...
    match t.sty {
        ty::TyAdt(def, substs) => {
            ...
            let (unsized_size, unsized_align) = size_and_align_of_dst(bcx, field_ty, info);
            ...

            (size, align)
        }
        ty::TyDynamic(..) => {
            ...
            (bcx.load(size_ptr), bcx.load(align_ptr))
        }
        ty::TySlice(_) | ty::TyStr => {
            ...
            (bcx.mul(info, C_uint(bcx.ccx(), unit_size)),
             C_uint(bcx.ccx(), unit_align))
        }
        _ => bug!("Unexpected unsized type, found {}", t)
    }
}

確かに、Sizedでない型の成り立ちに応じて場合分けをしていることがわかる。

まとめ

size_of_val など、Rustの型システムに依存する一部のプリミティブはcompiler intrinsicsとして扱われ、その実装はコンパイラ自身が動的に生成している。