やりたいこと
ヒープ上に色々と置きたい!
それもVecやBoxを使わず、しかもStableな方法で!
さっそく結論
すべての答えはこのstd::allocのドキュメントにあった。
// 以下は、上記URLのExampleを引用。(2020/9/6)
use std::alloc::{alloc, dealloc, Layout};
unsafe {
let layout = Layout::new::<u16>();
let ptr = alloc(layout);
*(ptr as *mut u16) = 42;
assert_eq!(*(ptr as *mut u16), 42);
dealloc(ptr, layout);
}
Rustをある程度、使ってきた人なら、もうStableでVecでもBoxでもないメモリーアロケートのやり方がわかったことでしょう。
さて、これで終わりなのですが、これだけだとなんだか寂しいのでこの先ヒマつぶしが続きます。
配列をヒープに設置してみる
Vecでとってもいいのですが、伸長したり、そのためのチェックなんかがいらないということがあるでしょう。
かといって、Boxで取るとなると、オーバーヘッドやスタックオーバーフローが気になる。
そこで、次のようになります。
use std::alloc::{Layout, alloc, dealloc};
struct MyBox<T> {
layout: Layout,
ptr: Option<*mut T>,
}
impl<T> MyBox<T> {
fn new() -> Self {
unsafe {
let layout = Layout::new::<T>();
let ptr = alloc(layout);
MyBox {
layout,
ptr: Some(ptr as *mut T),
}
}
}
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
unsafe {
if let Some(ptr) = self.ptr.take() {
dealloc(ptr as *mut u8, self.layout);
}
}
}
}
fn main() {
let array = MyBox::<[u8;5]>::new();
}
なんと、配列をヒープに置きたかっただけなのに、TがSizedであれば、なんでもヒープに置けちゃうMyBoxが出来上がりました。
機能も少なく、手を入れないといけませんが、逆にMyBoxは応用の利く良い土台になりそうです。
(ニッチ過ぎてほぼほぼなくない? (#○^∀^)=○)゚Д')・:'. )
疑問。情報求む => 情報ありがとう。
疑問としてstd::alloc::allocで確保したら、std::alloc::deallocで解放しないといけないのか?というものが生まれました。
もし、std::deallocでなく、次のようにすることができたならいいのになーと。
struct MyBox<T>{
inner : Box<T>
}
impl<T> MyBox<T>{
fn new() -> Self{
let layout = Layout::new<T>::();
let ptr = alloc::new(layout);
MyBox{
inner : Box::from_raw(ptr as *mut T)
}
}
}
これが可能であれば、Drop時にヒープからTが解放されそうです。
やっていいのでしょうかね?未定義動作になるのでしょうか??
GlobalAlloc::deallocのSafetyを見ると、、、
This function is unsafe because undefined behavior can result if the caller does not ensure all of the following:
・ ptr must denote a block of memory currently allocated via this allocator,
・ layout must be the same layout that was used to allocate that block of memory,
以下、俺的意訳。
この関数はUnsafeです。なぜなら呼び出し元が次のすべてを保証しないと未定義動作になるかもしれないからです。
・ ptr はこのアロケーターによって現在割り当てられているメモリーブロックを示さなければならない。
・ layout はメモリーブロックの割当に使われたものと同じLayoutでなければならない。
なるほど。自身がアロケートしたptrじゃないと困るんですね。そりゃそうか。アロケーターだもの。
さて、ここで重要となるのが、RustがBox等のドロップ時に使うアロケータと、alloc()/dealloc()で使われるアロケーターは同じなのかということです。
残念ながらBoxのDropの実装を見ても、何も書かれていない。コンパイラ側で処理してくれるところなのです。
諦めるしかなさそうだなぁ。。。
まぁ、本当はどうでもいいことなのでかまわないんですけどね。利便性がちょっとした可読性の向上でしかないですから。
(追記)解放をBoxに任せても良い!
詳細はコメント欄を参照してください。Kogia_simaさん、ありがとうございます。
書き直したコードは次のようになります。
use std::alloc::{Layout, alloc, handle_alloc_error};
struct MyBox<T> {
inner: Box<T>,
}
impl<T> MyBox<T> {
fn new() -> Self {
if std::mem::size_of::<T>() == 0 {
panic!("ごめんね。対応してない型です。");
}
let layout = Layout::new::<T>();
let ptr = unsafe { alloc(layout) as *mut T };
if ptr.is_null() {
handle_alloc_error(layout);
}
MyBox {
inner: unsafe { Box::from_raw(ptr) },
}
}
}
変更点は
- std::alloc::allocが失敗したときの挙動を設定した。
- TがZeroSizeのときにpanicするようにした。
- Tの解放処理をBoxのドロップに任せた。
というところです。
box_syntaxへの期待
boxってキーワード、あれなんじゃ??
MyBoxのような構造体を作るにあたってBoxのコードをちょろちょろと読みました。そこにある「box」という気になるキーワードが。。。
さて、これについてはbox_syntaxに書かれています。
直感の通りで、Boxを作成します。ただし、直接ヒープに書き込みます。
つまり
let b1 = Box::new( [0_u128;10000000000000000000] ); // スタックオーバーフローが起きる。
let b2 = box [0_u128;10000000000000000000]; // スタックオーバーフローは起きない。
ということです。
あれ?!これ、さっきのMyBoxいらなくね!?!?
そう、boxがあれば不要です。しかし、これ、Unstableなんですよ。
使えるようになるのが待ち遠しいですね。
(ニッチ過ぎてほぼほぼなくない? (#○^∀^)=○)゚Д')・:'. )