Rustコトハジメ

プログラミング言語Rustと競プロに関する情報をお届けします。

Readトレイトの実装からファットポインタを学ぶ

Readトレイトというのは、何かからデータを読み込むことを抽象化したトレイトです。もっとも基本的な実装はreadです。

pub trait Read {
    #[stable(feature = "rust1", since = "1.0.0")]
    fn read(&mut self, buf: &mut [u8]) -> Result<usize>;

このうち、&[u8]型への実装から学びがあります。

impl<'a> Read for &'a [u8] {
    #[inline]
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let amt = cmp::min(buf.len(), self.len());
        let (a, b) = self.split_at(amt);

        // First check if the amount of bytes we want to read is small:
        // `copy_from_slice` will generally expand to a call to `memcpy`, and
        // for a single byte the overhead is significant.
        if amt == 1 {
            buf[0] = a[0];
        } else {
            buf[..amt].copy_from_slice(a);
        }

        *self = b;
        Ok(amt)
    }

selfが&[u8]なので、readのシグニチャread(&mut &[u8], &mut [u8])となります。第一引数が読み取るバッファで、第二引数が書き込むバッファです。面白いのは、1つ目には&がついていて、2つ目にはついてないことです。この違いは一体何なのでしょうか。

Vec<u8>から&[u8]のスライスをとった場合を例にして図示します。コードとしては

fn main() {
    let mut v: Vec<u8> = vec![0,1,2,3,4];
    let mut b = [0;2];
    let mut vv: &[u8] = &v[..];
    vv.read(&mut b).unwrap();
}

のようなことをする前提です。

f:id:akiradeveloper529:20181221173342j:plain)

Vec<u8>のデータはヒープ領域にあり、その所有権をもったポインタ(ポインタ、キャパシティ、長さがスタックにあります。スライスは、データへのファットポインタ(ポインタ、長さ)です。第二引数のバッファはスタックにあります。

したがって、それぞれの引数の意味は、

  • 第一引数&mut &[u8]: ファットポインタ自体をmutate出来る
  • 第二引数&mut [u8]: スタック上のデータ列自体をmutate出来る

となります。

&[u8]のread実装に戻ると、まず、コピーする長さamtを決めて、split_atによって、「消費する分」「残る分」に分離しています。split_atはslice型[T]に定義されており、

pub fn split_at(&self, mid: usize) -> (&[T], &[T])

です。Tがu8ならば、&[u8]を2つの&[u8]に分離するという意味となります。

それから、バッファに対してコピーを行い、

*self = b

しています。これがまさに、ファットポインタの上書きです。第一引数のファットポインタ自体に&mutが必要な理由はこの上書きをしたかったからだとわかります。

実行後は以下のようなメモリ状態になるのだと思います。

f:id:akiradeveloper529:20181221173623j:plain