rustで動的にバッファを確保する方法

  1. Boxを使う
  2. Vecを使う
  3. std::heap::Allocを使う
  4. placement-in を使う
  5. mallocを使う

1. Boxを使う

rustで動的にメモリを確保する方法といってまず思いつくのはBoxを使う方法だと思います.例えば,以下のようにすれば長さ1000のu8のバッファを確保できます.

let buffer : Box<[u8]> = Box::new([0;1000]);

ただし,この方法は以下のような特徴があります.

  1. 確保した領域は必ず初期化する必要がある.
  2. 一旦スタック上にデータを確保したあとに,ヒープにそのデータをコピーする

1.に関しては,これは変数は初期化しないと利用できないというrustの原則に則ったものですが,場合によってはこの初期化コストが大きい場合があります.また,2. の方が問題で,あまりにも大きい領域を確保しようとするとスタックオーバフローが発生する可能性があります.コピー分のオーバヘッドも生じます.

2. Vecを使う

Vecを使う場合はVec::new()で可変長配列を作成した後Vec::pushでデータを追加していくのが最も基本的な方法ですが,当然効率はあまり良くありません.

Vec::with_capacityを使うと,指定したサイズ分だけのメモリ領域を確保してくれます.さらに,Vec::set_lenを使うことで,初期化をスキップして確保した領域にアクセスすることができます.ただし,set_lenはunsafeです.

unsafe{
  let len = 1000;
  let mut vec = Vec::<u8>::with_capacity(len);
  vec.set_len(len);
}

また,確保した要素にアクセスする際rustはデフォルトで境界のチェックをしますが, そのオーバヘッドを減らしたい場合はget_uncheckedあるいはget_unchecked_mutが使えます((get_uncheckedget_uchecked_mutはsliceの関数で,boxやvecからderef coercionを通じて呼び出せます)).

ffiで利用するなどでraw pointerが欲しい場合は以下のような手法が使えます.

unsafe fn alloc(len: usize) -> *mut u8 {
    let mut vec = Vec::<u8>::with_capacity(len);
    vec.set_len(len);
    Box::into_raw(vec.into_boxed_slice()) as *mut u8
}

unsafe fn free(raw: *mut u8, len : usize) {
    let s = std::slice::from_raw_parts_mut(raw, len);
    let _ = Box::from_raw(s);
}

Vec::with_capacityで指定した長さ分の領域を確保したあと,それをvec::into_boxed_sliceでBoxに変換し,さらにそれをBox::into_rawすることで実際のポインタを得ます.

確保した領域を解放する場合はポインタを再びslice::from_raw_parts_mutでスライスに直し,それをさらにBox::from_rawでBoxに直してBoxのDropが呼ばれるようにします.

上記のコードはservoの一部のコードを参考にしたものです. 実際のservoのコードではアラインメントをワード境界に揃えるため,以下のようなことをしています (簡単化のため実際のコードとちょっと異なります.ここでは実際に確保したバイト数も戻り値として返しています).

unsafe fn alloc_buffer(len : usize) -> (*mut u8, usize){
    let word_size = std::mem::size_of::<usize>();
    let num = (len + word_size-1) / word_size;
    let mut vec = Vec::<usize>::with_capacity(num);
    vec.set_len(num);
    (Box::into_raw(vec.into_boxed_slice()) as *mut u8, num*word_size)
}

3. std::heap::Allocを使う

rustのメモリアロケータはAllocator traitを実装することになっており,またstd::heap::Heapでデフォルトのアロケータにアクセスできます.コードはliballocにあります. これを利用すると,以下のようにメモリを確保することができます.

#![feature(allocator_api)]
use std::heap::{Alloc, Heap, Layout};

// 第一引数: サイズ, 第二引数: アラインメントサイズ
let layout = Layout::from_size_align(10, std::mem::align_of::<u32>()).unwrap();
unsafe {
    let p = Heap.alloc(layout.clone()).unwrap() as *mut u8;
    // ...
    Heap.dealloc(p, layout);
}

この関数を利用すれば,アラインメントも指定可能です.raw pionterが欲しい場合,この手法が一番良さそうに見えますが,残念ながらこの機能はunstableなため,nightlyな環境でしかまだ使えません.また,今後インタフェースが変わる可能性も十分あると思います.(詳細: https://github.com/rust-lang/rust/issues/32838

4. placement-in を使う

こちらも現時点ではunstableな機能ですが,確保したメモリ上に直接データを配置するplacement-in構文があります(詳細: https://github.com/rust-lang/rust/issues/27779). これを利用すると,boxでも大きなメモリ領域を確保することが可能です.

#![feature(placement_in_syntax, box_heap)]


// OK
let x: Box<[u8]> = std::boxed::HEAP <- [0;10000000];

// これは自分の環境ではoverflow
// let b: Box<[u8]> = Box::new([0; 10000000]);

5. mallocを使う

場合によってはlibc crateを使ってmalloc & freeを直接呼んでしまうのも手かもしれません.こちらも当然unsafeです.

// https://github.com/rust-lang/libc
unsafe {
    let p : *mut c_void = libc::malloc(10);
    // ...
    libc::free(p)
}