62
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rustオブジェクトの削除タイミングを手動で制御する

Last updated at Posted at 2016-05-11

Rust はガベージコレクタを持たない言語だ。コンパイル時に、構造体などのオブジェクトの所有権と生存状態(ライフタイム)を分析することで、オブジェクトを適切なタイミングでメモリから削除する仕組みになっている。この仕組みはとても良くできているが、時々、削除のタイミングを開発者が自分で制御したいこともある。

特にそれが必要になるのが、FFI(他言語関数インターフェイス)経由で、Rust のオブジェクトを他の言語に渡したい時だろう。この場合、他言語にはオブジェクトへのポインタを渡すことになるが、ライフタイムの分析をコンパイラに任せると、Rust の関数を抜けた時にオブジェクトの寿命が尽きたと判断されて、削除されてしまうことがある。これが起こると、他言語に渡したポインタは、参照先のオブジェクトが存在しない状態、いわゆる「ダングリングポインタ」になってしまう。

この記事では簡単な例を使って、ダングリングポインタが作られてしまう悪いやり方と、それが作られない、正しいやり方を紹介する。

なお、この記事で紹介するプログラムは、Arch Linux 上の Rust 1.8.0 安定版で動作を確認してある。Rust 1.4.0 か、それ以降なら、変更なしで動くはずだ。

% rustc --version --verbose
rustc 1.8.0 (db2939409 2016-04-11)
binary: rustc
commit-hash: db2939409db26ab4904372c82492cd3488e4c44e
commit-date: 2016-04-11
host: x86_64-unknown-linux-gnu
release: 1.8.0

% uname -a
Linux mini-arch 4.5.3-1-ARCH #1 SMP PREEMPT Sat May 7 20:43:57 CEST 2016 x86_64 GNU/Linux

FFI の例

簡単な例で実験してみよう。以下の Rust の構造体を他言語に渡してみる。

main.rs
#[derive(Debug)]
pub struct MyStruct {
    string: String,
    vec: Vec<i32>,
}

impl MyStruct {
    fn new() -> Self {
        MyStruct {
            string: "Hello".to_owned(),
            vec: vec![1, 2, 3],
        }
    }
}

以下が使う側(他言語側)のコードだ。って、どう見ても Rust のコードなのだが。ここでは Rust だけで実験できるように、あえて Rust で書いている。これが本当は他の言語、例えば C、Python、Ruby、JavaScript(Node.js)とかで書かれているのだと、自由に想像してもらいたい。(Python から呼び出す方法も、最後に追記しました)

main.rs
fn main() {
    // MyStruct を作成して、それを指す生ポインタ *mut MyStruct を得る
    let p = make_mystruct();

    // MyStruct の内容を表示する
    print_mystruct(p);

    // MyStruct の内容を更新する
    update_mystruct(p);

    // MyStruct の内容を表示する
    print_mystruct(p);

    // MyStruct を削除する
    destroy_mystruct(p);
}

呼び出されている側の関数は、Rust で実装したものになる。make_mystruct()MyStruct 構造体を作り、生ポインタ *mut MyStruct を返す。最後の destroy_mystruct() は構造体を削除する。

その間にある print_mystruct()update_mystruct() は、構造体の内容を表示したり、更新したりする関数だ。この2つの関数は、以下のように実装した。

main.rs
#[no_mangle]
pub extern "C" fn print_mystruct(p: *const MyStruct) {
    println!("{:?}", unsafe { &*p });
}

#[no_mangle]
pub extern "C" fn update_mystruct(p: *mut MyStruct) {
    unsafe {
        (*p).string.push_str(", world!");
        (*p).vec.push(4);
    }
}

このように、生ポインタ(*const*mut)の参照外しは、unsafe ブロックで囲む必要がある。これは、生ポインタについては、コンパイラによる所有権やライフタイムの追跡は行われず、また、null 値も許されているため、コンパイラが参照外しの安全性を保証できないためだ。もし生ポインタが、ダングリングポインタや、null ポインタだったりしたら、参照外しの際に実行時エラーとなる。

FFI における構造体の間違った渡しかた

本題の関数を定義しよう。まずは、問題の起こるやり方で実装する。make_mystruct() では、MyStruct へのミュータブルな参照 &mut をとり、それをミュータブルな生ポインタ *mut MyStruct へキャストして返してみよう。

main.rs
#[no_mangle]
pub extern "C" fn make_mystruct() -> *mut MyStruct {
    let mut s = MyStruct::new();
    &mut s as *mut MyStruct
}

destroy_mystruct() は、何をしたらいいのかわからないので、なにも書いていない。

main.rs
#[no_mangle]
pub extern "C" fn destroy_mystruct(_p: *mut MyStruct) {
    // ???
}

このプログラムは明らかに実行時エラーになるのだが、コンパイルはできてしまう。なぜなら、コンパイラが、生ポインタに関連するライフタイム分析をしないからだ。

実行してみよう。

% cargo run
   Compiling raw-pointers v0.1.0 (file:///home/tatsuya/misc/qiita/rust/raw-pointers)
     Running `target/debug/raw-pointers`
An unknown error occurred

To learn more, run the command again with --verbose.

不明なエラーとのこと。エラーの詳細を見るには --verbose を付ければいい。

% cargo run --verbose
       Fresh raw-pointers v0.1.0 (file:///home/tatsuya/misc/qiita/rust/raw-pointers)
     Running `target/debug/raw-pointers`
Process didn't exit successfully: `target/debug/raw-pointers` (signal: 11, SIGSEGV: invalid memory reference)

SIGSEGV、つまり、セグメンテーション違反 という、メモリへの不正なアクセスが原因で落ちている。こうなるのは、make_mystruct() を抜ける時に、MyStruct の所有権を持つ変数 s がスコープから抜けるためだ。MyStruct はその時点で削除され、生ポインタが指すメモリ領域は、解放済みとなる。

FFI における構造体の正しい渡しかた

今度は正しい方法で実装する。まず、Rust はオブジェクトを デフォルトでスタックに置く が、そのままだと、関数を抜けたタイミングでスタックが巻き戻され、オブジェクトが削除されてしまう。オブジェクト(ここでは構造体)を Box 化することで、ヒープに置こう。

main.rs
#[no_mangle]
pub extern "C" fn make_mystruct() -> *mut MyStruct {
    let s = Box::new(MyStruct::new());
    // ...
}

この例では sBox<MyStruct> 型になっている。Box の実体はポインタ(と、それと対をなす、ヒープ上に置かれたオブジェクト)なわけだが、生ポインタと違い、コンパイラにより所有権とライフタイムが追跡される。この Box ポインタを、生ポインタ *mut MyStruct 型に変換すれば、コンパイラによる追跡が行われなくなり、従って、構造体の削除を回避できる。試しにキャストしてみよう。

main.rs
#[no_mangle]
pub extern "C" fn make_mystruct() -> *mut MyStruct {
    let s = Box::new(MyStruct::new());
    s as *mut MyStruct
}

残念ながら、これはコンパイルできない。

% cargo build
   Compiling raw-pointers v0.1.0 (file:///usr/home/tatsuya/workhub/master/misc/qiita/rust/raw-pointers)
src/main.rs:45:5: 45:23 error: non-scalar cast: `Box<MyStruct>` as `*mut MyStruct`
src/main.rs:45     s as *mut MyStruct
                   ^~~~~~~~~~~~~~~~~~
error: aborting due to previous error
error: Could not compile `raw-pointers`.

To learn more, run the command again with --verbose.

これを可能にする唯一の方法は、std::mem::transmute() で強制的に型変換することだ。transmute は、変換前後のデータのサイズさえ一致すれば、どんな型にも変換できる unsafe な関数だ。

main.rs
#[no_mangle]
pub extern "C" fn make_mystruct() -> *mut MyStruct {
    let s = Box::new(MyStruct::new());
    unsafe { std::mem::transmute(s) }
}

このように書けば、Rust の型推論によって、s の型である Box<MyStruct> から、関数の戻り値の型である *mut MyStruct へ変換される。

さて、これでコンパイルが可能になるのだが、この変換は FFI で頻出するので、Rust 1.4 からは、Box::into_raw() という便利メソッドが用意された。なので、このように書ける。

main.rs
#[no_mangle]
pub extern "C" fn make_mystruct() -> *mut MyStruct {
    let s = Box::new(MyStruct::new());
    Box::into_raw(s)
}

into_raw() の実装 を見ると、たしかに transmute が使われている。

liballoc/boxed.rs
use core::mem;

impl<T: ?Sized> Box<T> {
    // ...

    #[stable(feature = "box_raw", since = "1.4.0")]
    #[inline]
    pub fn into_raw(b: Box<T>) -> *mut T {
        unsafe { mem::transmute(b) }
    }
}

次は destroy_mystruct() を実装しよう。先ほどの make_mystruct() では、Box から生ポインタへ変換することで構造体の削除を回避したが、今度はそれと逆のことをすればいい。つまり、transmute で Box<MyStruct> へ戻してから関数から抜ければ、構造体が削除できる。

こちらも Rust 1.4 から Box::from_raw() という便利メソッドが用意された。以下のように書ける。

main.rs
#[no_mangle]
pub extern "C" fn destroy_mystruct(p: *mut MyStruct) {
    unsafe { Box::from_raw(p) };
}

from_raw() の実装 は以下の通り。

liballoc/boxed.rs
impl<T: ?Sized> Box<T> {
    // ...

    #[stable(feature = "box_raw", since = "1.4.0")]
    #[inline]
    pub unsafe fn from_raw(raw: *mut T) -> Self {
        mem::transmute(raw)
    }
}

このように from_raw() は Box に戻った構造体を返してくるので、呼び出し側の destroy_mystruct() では、それを消費すればいい。つまり destroy_mystruct() が、MyStruct を戻り値として返さなければ、消費(=削除)できる。

実行してみよう。

% cargo run
   Compiling raw-pointers v0.1.0 (file:///home/tatsuya/misc/qiita/rust/raw-pointers)
     Running `target/debug/raw-pointers`
MyStruct { string: "Hello", vec: [1, 2, 3] }
MyStruct { string: "Hello, world!", vec: [1, 2, 3, 4] }

今度はうまく実行できた。

なお、from_raw() が unsafe になっている理由は、例えば、この関数を、同じ生ポインタに2回適用したりすると、オブジェクトの2重削除が起こる可能性があるからだ。

追記:Python から呼び出してみる

(Rust の main 関数からの呼び出しだけだと説得力に欠けるので、Python からの呼び出し方法も追記することにしました)

実際に他言語から呼び出せるよう、共有ライブラリ化しよう。Cargo.toml に以下の設定を追加する。

Cargo.toml
[lib]
name = "mystruct"
crate-type = ["dylib"]

また、main.rs を、lib.rs にリネームして、main関数を削除する。

cargo build --release を実行すると、以下のように libmystruct.so が作られる。

% tree
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── target
    ├── debug
    │   └── ...
    └── release
        ├── ...
        ├── libmystruct.so
        └── ...

Python スクリプトを作成する。

mystruct.py
from ctypes import cdll, c_void_p

lib = cdll.LoadLibrary("target/release/libmystruct.so")

lib.make_mystruct.restype = c_void_p
lib.print_mystruct.argtypes = [c_void_p]
lib.update_mystruct.argtypes = [c_void_p]
lib.destroy_mystruct.argtypes = [c_void_p]

p = lib.make_mystruct()
lib.print_mystruct(p)
lib.update_mystruct(p)
lib.print_mystruct(p)
lib.destroy_mystruct(p)

実行しよう。

% python mystruct.py 
MyStruct { string: "Hello", vec: [1, 2, 3] }
MyStruct { string: "Hello, world!", vec: [1, 2, 3, 4] }
%

Python 2.7.11 と 3.5.1 で試してみたが、どちらも問題なく動作した。

また、destroy_mystruct() を2重に呼ぶとクラッシュすることから、期待通り、この関数で構造体の削除が行われていることが確認できた。

% python
Python 3.5.1 (default, Mar  3 2016, 09:29:07)
[GCC 5.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from ctypes import cdll, c_void_p
>>> lib = cdll.LoadLibrary("target/release/libmystruct.so")
>>> lib.make_mystruct.restype = c_void_p
>>> lib.print_mystruct.argtypes = [c_void_p]
>>> lib.update_mystruct.argtypes = [c_void_p]
>>> lib.destroy_mystruct.argtypes = [c_void_p]
>>> p = lib.make_mystruct()
>>> lib.print_mystruct(p)
MyStruct { string: "Hello", vec: [1, 2, 3] }
2
>>> lib.update_mystruct(p)
3
>>> lib.print_mystruct(p)
MyStruct { string: "Hello, world!", vec: [1, 2, 3, 4] }
2
>>> lib.destroy_mystruct(p)
0
>>> lib.destroy_mystruct(p)
zsh: segmentation fault (core dumped)  python

まとめ

  • Rust コンパイラは、生ポインタ(*const*mut)について、所有権とライフタイムを追跡しない。transmute で Box ポインタを生ポインタへ変換すると、ヒープに置かれたオブジェクトの削除が行われなくなる。
  • オブジェクトを削除したい時は、transmute で Box ポインタに戻してコンパイラの管理下に置けばいい。ただし、2重削除しないよう注意すること。
  • Rust 1.4 からは、これを行う便利メソッド Box::into_raw()Box::from_raw()標準ライブラリに用意されている

付録:完成したプログラム

Cargo.toml に lib セクションを追加する。

Cargo.toml
[lib]
name = "mystruct"
crate-type = ["dylib"]

ライブラリプログラム

src/lib.rs
#[derive(Debug)]
pub struct MyStruct {
    string: String,
    vec: Vec<i32>,
}

impl MyStruct {
    fn new() -> Self {
        MyStruct {
            string: "Hello".to_owned(),
            vec: vec![1, 2, 3],
        }
    }
}

#[no_mangle]
pub extern "C" fn make_mystruct() -> *mut MyStruct {
    let s = Box::new(MyStruct::new());
    Box::into_raw(s)
}

#[no_mangle]
pub extern "C" fn print_mystruct(p: *const MyStruct) {
    println!("{:?}", unsafe { &*p });
}

#[no_mangle]
pub extern "C" fn update_mystruct(p: *mut MyStruct) {
    unsafe {
        (*p).string.push_str(", world!");
        (*p).vec.push(4);
    }
}

#[no_mangle]
pub extern "C" fn destroy_mystruct(p: *mut MyStruct) {
    unsafe { Box::from_raw(p) };
}

利用者側のプログラム(Python の場合)

mystruct.py
from ctypes import cdll, c_void_p

lib = cdll.LoadLibrary("target/release/libmystruct.so")

lib.make_mystruct.restype = c_void_p
lib.print_mystruct.argtypes = [c_void_p]
lib.update_mystruct.argtypes = [c_void_p]
lib.destroy_mystruct.argtypes = [c_void_p]

p = lib.make_mystruct()
lib.print_mystruct(p)
lib.update_mystruct(p)
lib.print_mystruct(p)
lib.destroy_mystruct(p)
62
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?