みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

RustとPythonの連携

PythonからRustを呼び出すために,PyO3を使用します.

環境

  • 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

RustでPython用のClassを作成

Cargo.toml

PyO3の説明の通りに作成します.

[package]
name = "pyt"
version = "0.1.0"
authors = ["atuhiro-me.net"]
edition = "2018"

[lib]
name = "pyt"
crate-type = ["cdylib"]

[dependencies.pyo3]
version = "0.8.5"
features = ["extension-module"]

pytというモジュールに,PyTというクラスを作成します.外部から呼び出せるようにします.PyTクラスのコンストラクタを作成するときは,#[new]とし,new(&PyRawObject)を定義します.&PyRawObject のinit()で,必要な構造体のメモリを確保します.戻り値は不要です.

PyTにnumというメンバを作成し,get()で数値を取得できるようにします.

lib.rs

use pyo3::prelude::*;

#[pyclass(module = "pyt")]
struct PyT {
    num: i32,
}

#[pymethods]
impl PyT {
    #[new]
    fn new(obj: &PyRawObject, num: i32) {
        obj.init(PyT { num: num })
    }

    fn get(&self) -> PyResult<i32> {
        Ok(self.num)
    }
}

#[pymodule]
fn pyt(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_class::<PyT>()?;
    Ok(())
}

これらをビルドすると,pyt.dll が作成されます. pyt.pyd に名前を変更すれば,Pythonからimport文で呼び出せるようになります.

import pyt

r = pyt.PyT(12)
print(r.get())

これを実行すると,

12

となります.PythonでRustで作ったクラスを作成し,メンバを呼び出せることが分かりました.

Pythonのlistを扱う

一般的に,Python→Rustは,FromPyObjectのextract()を使います.Genericな関数なので,ほとんどの型に対応しています.

pyo3::FromPyObject - Rust

Rust→Pythonは,RustでPython用の変数を作成します.Pythonの代表的な型が作成できます.

pyo3/src/types at master · PyO3/pyo3 · GitHub

具体的に,Python List から Rust Vecに変換するには,v.extract::<Vec>().unwrap(); を使い,Rust Vec から Python List に変換するには,PyList::new(py, &v); を使います. Rustの戻り値は,Pythonと同じ lifetime である必要があるので,fn get<'p>(&self, py: Python<'p>) -> &'p PyList; などと lifetime を明示する必要があります.*1

lib.rs

use pyo3::prelude::*;
use pyo3::types::PyList;

#[pyclass(module = "pyt")]
struct PyT {
    data: Vec<i32>,
}

#[pymethods]
impl PyT {
    #[new]
    fn new(obj: &PyRawObject, d: &PyList) {
        let v = d.extract::<Vec<i32>>().unwrap();
        obj.init(PyT { data: v })
    }

    fn get<'p>(&self, py: Python<'p>) -> &'p PyList {
        let plist = PyList::new(py, &self.data);
        &plist
    }
}

#[pymodule]
fn pyt(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_class::<PyT>()?;
    Ok(())
}

これをPythonから呼び出します.

import pyt

r = pyt.PyT([1, 2, 3, 4, 5, 6, 7])
print(r.get())
[1, 2, 3, 4, 5, 6, 7]

となります.なお,int でない要素を含む list を使うと,Rustでエラーとなります.エラーをPythonに伝えるには後述します.

import pyt

r = pyt.PyT([1, 2, 3, None, 5, 6, 7])
print(r.get())
thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: PyErr { type: Py(0x7ffb6dfec670, PhantomData) }', src\lib.rs:13:17

tuple を含む list を扱う

型をVec<(String, i32)>のように変更すれば問題なく動作します.RustのGenerics凄い.

lib.rs

use pyo3::prelude::*;
use pyo3::types::PyList;

#[pyclass(module = "pyt")]
struct PyT {
    data: Vec<(String, i32)>,
}

#[pymethods]
impl PyT {
    #[new]
    fn new(obj: &PyRawObject, d: &PyList) {
        let v = d.extract::<Vec<(String, i32)>>().unwrap();
        obj.init(PyT { data: v })
    }

    fn get<'p>(&self, py: Python<'p>) -> &'p PyList {
        let plist = PyList::new(py, &self.data);
        &plist
    }
}

#[pymodule]
fn pyt(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_class::<PyT>()?;
    Ok(())
}

Pythonから呼び出してみます.

r = pyt.PyT([
    ("apple", 1),
    ("banana", 2),
    ("orange", 3)
])
print(r.get())
[("apple", 1), ("banana", 2), ("orange", 3)]

いろいろなデータを渡せます.

exceptionを使う

Rustでエラーが発生したときに,Pythonにその結果を伝える必要があります.

Pythonの標準的なexceptionは,pyo3::exceptions::ValueError::py_err("parse error.")などを使います*2.これをPyResultを通じて戻り値とします.

また,新たにset()を作りました.メンバ変数を変える関数なので,&mut self で書き換え可能にします.

lib.rs

use pyo3::exceptions;
use pyo3::prelude::*;
use pyo3::types::PyList;

#[pyclass(module = "pyt")]
struct PyT {
    data: Vec<(String, i32)>,
}

#[pymethods]
impl PyT {
    #[new]
    fn new(obj: &PyRawObject) {
        obj.init(PyT {
            data: Vec::<(String, i32)>::new(),
        })
    }
    fn get<'p>(&self, py: Python<'p>) -> &'p PyList {
        let plist = PyList::new(py, &self.data);
        &plist
    }
    fn set(&mut self, d: &PyList) -> PyResult<()> {
        match d.extract::<Vec<(String, i32)>>() {
            Ok(u) => {
                self.data = u;
                Ok(())
            }
            Err(_) => Err(exceptions::ValueError::py_err("parse error.")),
        }
    }
}

#[pymodule]
fn pyt(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_class::<PyT>()?;
    Ok(())
}

これを,Pythonからいろいろなデータを設定したり取得したりしてみます.

import pyt

r = pyt.PyT()
print(r.get())
r.set([
    ("apple", 1),
    ("banana", 2),
    ("orange", 3)
])
print(r.get())
try:
    r.set([
        (None, 1), # invalid data.
    ])
    print(r.get())
except ValueError as e:
    print(e)
[]
[('apple', 1), ('banana', 2), ('orange', 3)]
parse error.

エラーが処理できることが分かりました.