Rustで作成したPython用のクラスがPythonのタイミングで自動的にメモリ開放されるかを確認してみました.
環境
- Windows 10 Pro 64bit
- CPU : Core i3-7100U CPU @ 2.40GHz
- Memory : 12.0 GB
- rustc 1.42.0-nightly (2020-01-17)
- cargo 1.42.0-nightly (2020-01-13)
- Python 3.8.1 for windows 10 64bit
実装
Cargo.toml
[package] name = "pyt" version = "0.1.0" authors = ["atsuhiro-me.net"] edition = "2018" [lib] name = "pyt" crate-type = ["cdylib"] [dependencies.pyo3] version = "0.8.5" features = ["extension-module"]
前回作成したPyTにDrop traitを設定し,メモリ解放されるときに,標準出力にその旨を表示するようにしました.
lib.rs
use pyo3::exceptions; use pyo3::prelude::*; use pyo3::types::PyString; #[pyclass(module = "pyt")] struct PyT { m: String, } impl Drop for PyT { fn drop(&mut self) { println!("[rust] drop: {}", self.m); } } #[pymethods] impl PyT { #[new] fn new(obj: &PyRawObject, d: &PyString) { obj.init(PyT { m: d.extract::<String>().unwrap(), }) } fn get<'p>(&self, py: Python<'p>) -> &'p PyString { &PyString::new(py, &self.m) } fn set(&mut self, d: &PyString) -> PyResult<()> { match d.extract::<String>() { Ok(u) => { self.m = u; Ok(()) } Err(_) => Err(exceptions::ValueError::py_err("invalid argument.")), } } } #[pymodule] fn pyt(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::<PyT>()?; Ok(()) }
これをPythonから呼び出してみます.
import pyt r = pyt.PyT("apple") print(r.get()) r.set("banana") print(r.get()) r = pyt.PyT("orange") s = r print(id(r), id(s)) del r print("r is deleted.") print(s.get())
apple banana [rust] drop: banana 2414632590720 2414632590720 r is deleted. orange [rust] drop: orange
まず,set()メソッドで,PyTの中身をappleからbananaに変更しても,PyT自身のメモリは開放されません.
変数rに別のPyT("orange")を代入すると,もともとのPyT("apple")の参照がなくなるので,Pythonのガーベッジコレクションが実行され,PyT("apple")のメモリが開放されます.
変数sにもPyT("orange")の参照を代入し,PyT("orange")が2箇所で参照される状況を作ります.idを確認するとr, sは同一なので,同じインスタンスを参照していることが確認できます.
rを削除します.このとき,PythonではPyT("orange")の参照が1つ残っているため,まだメモリを開放しません.そして,プログラムが終了するときに,PyT("orange")のメモリが開放されます.
PyO3を利用することで,Pythonのガーベッジコレクションとうまく連帯するRustアプリケーションが簡単に作成できることが分かりました.