Pyo3でPythonとRustを連携させる


普段はPython使いなのですが、可視化部分等はPythonでそれ以外はRustで高速できないかと考え調べてみました.

今回は[Pyo3](https://crates.io/crates/pyo3)を使ってRustとPythonをつないでみました.

Pythonのモジュールとして使われるように,Rustのコードを書いてます

[ドキュメント](https://pyo3.rs/v0.7.0/)はあるものの、情報量もまだまだ足りないので,わからない所含めまとていきます.

ちなみにRustとPyo3の記事だと

- [[Rust] PyO3 で Python パッケージを作成](https://qiita.com/osanshouo/items/671888bdd6afeec1e939)
- [PyO3とrust-numpyを使ってPythonからNumPyをRustに渡して操作する](https://trsasasusu.com/blog/67/)

を参考にしました.



__注意__ rustのバージョン
現在,pyo3を使うには`nighty` 版が必須です.
これは[issue](https://github.com/PyO3/pyo3/issues/5)にも記載がありますが,いくつか必要な機能が`stable`では提供されていないためです.

## Cargo.toml

公式のサンプルを例にPyo3を使うのに必要な設定について説明します.

```
[package]
name = "string-sum"
version = "0.1.0"
edition = "2018"

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

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

使うセクションは

- [package]
- [lib]
- [dependencices]
の三種類です.

`[package]`はcrate用のセクションで,cargoを使ったことがあれば実際にわかると思います.
変数は以下になります.

- name: rustで参照するpackage名
- version: アプリケーションのバージョン

`[lib]`はライブラリの出力のセクションです.

- name:build時に作れるファイルの名前です.
- crate-type: 出力のタイプを指定するものです.

Pythonで使うなら、基本Python用の動的ライブラリになるので、["cdylib"]を指定すればよいです.
詳細は[ここ](https://qiita.com/etoilevi/items/4bd4c5b726e41f5a6689)を御覧ください.

`[dependencies]` は依存ライブラリのついて記載する所です.

- dependencies.pyo3でpyo3に関する条件を記載する箇所になります.
- versionは指定するバージョン
- featuresは条件つきコンパイルを指定する所で今回は外部モジュールとしてコンパイルすることを指定しています.

なので、実際にpyo3を使って開発したい場合は`Cargo.toml`からnameだけ変更すればよいのかと思います.


## 実際のソース
実際にPythonから呼び出す方法を公式のサンプルから説明します.

まずPythonのモジュールとしてみせたいものを
`#[pymodule]` をつけた関数として定義します.
引数の型は{ythonのプロセスを表す`Python`型と, Pythonのモジュールを表すPyModule型になります.

Rustのattributeは基本決まっていますが,
`proc_macro_attribute`を使って新たに作ることができます.(nighty版だけの機能のようですが)

attributeはPythonでいうデコレータになります.おかげで特に何も考えずに使えます.

実際にpythonから呼び出したいオブジェクトを定義してきます.

### 関数を呼び出す場合
Pythonから呼び出す関数の場合は `#[pyfunction]` を設定すればよいです.

公式そのままですが、以下のようにすれば動きます.

```rust
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
/// Formats the sum of two numbers as string
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
    Ok((a + b).to_string())
}

/// This module is a python module implemented in Rust.
#[pymodule]
fn string_sum(py: Python, m: &PyModule) -> PyResult<()> {
    m.add_wrapped(wrap_pyfunction!(sum_as_string))?;

    Ok(())
}
```

注意点ですが, 返り値は`PyResult`にする必要があります.
また,`OK()`は返り値をPyResultにしてくれます.
返り値がない場合は,`Ok(())`とし、型としては`PyResult<()>`とすればよいです.


### 配列の返却方法
さっきの例だとPython側ではStringが返ります.
今度はPython側でlistを返してみます.

```rust
#[pyfunction]
fn make_list<'p>(py: Python<'p>) -> PyResult<&'p PyList> {
    let v1 = vec![vec![vec![99; 3]; 2]; 10];
    let list = PyList::new(py, v1);
    Ok(list)
}
```
でできます.

__注意__

- listの場合は`PyList`型にする必要があります.
- `#[pyfunction]` が着いている場合,引数:py は普通は指定する必要がありません.
  ただlistの場合はnewで引数として渡す必要があるため,指定しています.

- ライフタイム`'p`はpyfunctionとして使用する限りは不要のようです.
  ただ,`#[pymethods]`で実行する場合、ライフタイムを指定しないとエラーになります.
  ソースをおえていないので実態はわからないのですが,pyfunctionはpymoduleにライフタイムが紐づく?ので,ライフタイムを指定せずともエラーにならないのかなと思いました.

## クラスの場合
Rust側ではclassを返却するには二つの処理が必要です.

1. `#[pyclass]`をつけた構造体の定義
2. `m.add_class::<クラス名>` をpymodule内に定義する.

これができれば,動きます.

- newの設定方法

  `#[new]`をつけて以下のように`new`メソッドで実行すると,new相当になります.
  実際に例を書くと以下みたいな形になります.

```rust
#[pyclass]
pub struct Hoge {
    pub x: usize,
}

#[pymethods]
impl Hoge {
    #[new]
    fn new(obj: &PyRawObject, x: usize){
            obj.init({
            Hoge {
                x: x,
            }
        });
    }
```

他にもgetter,setterが定義できたりします.

## Numpy
ndarrayとやり取りできるはずですが、少し頑張ってもエラーになったので、今回はスキップします.
[github](https://github.com/rust-numpy/rust-numpy)はあるものの、そこのexampleも
`#![deny(rust_2018_idioms)]`になっていて,これを外すと動きませんでした.


## テスト
どうやらPyo3がある状況だとMacで`Cargo test`をするとエラーになるようです.
以下のみの実行がエラーになります.(他のPyo3が使わないプロジェクトでエラーにならないことは確認しました)

```rust
#[cfg(test)]
    #[test]
    fn it_works() {
        assert!(false);
    }
```

```
    Finished dev [unoptimized + debuginfo] target(s) in 3.19s
     Running target/debug/deps/python_rust_example-3e2a90b01ebe111d
dyld: Symbol not found: _PyExc_OverflowError
  Referenced from: python_rust_example/target/debug/deps/python_rust_example-3e2a90b01ebe111d
  Expected in: flat namespace
 in python_rust_example/target/debug/deps/python_rust_example-3e2a90b01ebe111d
error: process didn't exit successfully: `python_rust_example/target/debug/deps/python_rust_example-3e2a90b01ebe111d` (signal: 6, SIGABRT: process abort signal)
```

この辺は解決するべきですが,その前に夏休みが終わってしまったので,今回はこの辺でお別れです(涙)

## まとめ
今回は,最低限Pyo3の使い方を解説しました.
Pyo3はまだまだ枯れきっていないのか,調べてみるとかなりトラブルが発生しました.
そうしたトラブル自体が勉強になることもあるので、自分でも使い込みつつどこかでコミットできたらなと思います.