初めてのmaturin

:::note info
この記事は、2024/8/7に書かれた記事を2024/9/15に加筆改訂したものです。
元の記事は編集履歴からご覧ください。
:::

RustをPythonから呼び出せるライブラリ、maturinの基本的な使用方法をまとめます。

## 事前準備
### 1. 必要なもののインストール
Python, Rust, maturinが必要です。
Pythonは公式ページからインストールしておいてください。
Rustとmaturinは以下のコマンドをそれぞれ入力すればインストールできます。
```terminal
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
```terminal
pip3 install maturin
```
### 2. プロジェクト作成
maturinプロジェクトを新たに作成します。
```terminal
maturin new -b pyo3 ./my_project
```

:::note info
-bはmaturinが内部的に使うシステムを指定します。今回は最も人気なpyo3を使います。
:::


これで準備完了です。
ディレクトリはこんな形になっているはずです。
```
my_module
├─ Cargo.toml
├─ pyproject.toml
└─ src
   └─ lib.rs
```

## Rustコード
基本的な形は
```rust: sample.rs
#[pyなんとか]
(Rustコード)
```
です。
また、モジュールに含めるためには専用の[関数定義](#モジュールにまとめる)を行う必要があります。

### 関数
Pythonから呼び出せる関数を定義します。
```rust: my_project/src/lib.rs
use pyo3::prelude::*; //忘れずに

#[pyfunction] //重要！
fn add_two(n: i64) -> i64 {
    n + 2
}
```
`#[pyfunction]`は、Pythonに関数を公開するために必要です。これがないとPython側から関数を参照できません。
また、Python側でエラーを起こしたい場合は、返り値を`PyResult<()>`と注釈した上で[`PyErr`](https://docs.rs/pyo3/latest/pyo3/prelude/struct.PyErr.html)を返すこともできます。

### クラス
Pythonのクラスを表現できます。
```rust: my_project/src/lib.rs
#[pyclass] // 重要！
struct MyClass {
    a: i64, 
    b: i64,
}

#[pymethods] // クラスのメソッドを定義
impl MyClass {
    // 普通のメソッド
    fn sum(&self) -> i64 {
        self.a + self.b 
    }

    // クラスメソッド
    #[classsmethod]
    fn class_method(cls: &Bound<'_, PyType>, a: i64, b: i64) -> i64 {
        // 第１引数を忘れるとエラー
        // クラスが要らない場合は#[staticmethod]が使える
        a + b
    }

    // 特殊メソッドも定義できる
    fn __str__(&self) -> String {
        format!("<MyClass with {} and {}", self.a, self.b)
    }
}
```
これらの他にも、[コンストラクタなどを指定することができます](https://pyo3.rs/v0.22.2/class)。

### モジュール
```rust: my_project/src/lib.rs
#[pymodule] //重要！
fn my_module(m: &Bound<PyModule>) -> PyResult<()> {
    // 関数
    m.add_function(wrap_pyfunction!(add_two, m)?)?;
    // クラス
    m.add_class::<MyClass>()?; // やや変則的なので注意
    Ok(())
}
```
`#[pymodule]`は、RustクレートをPythonのモジュールとして公開するために必要です。これがないとImportErrorになります。

:::note info
**モジュールの作り方**
maturinのガイドによると、
```rust
#[pymodule]
mod my_module {
    //関数
}
```
とすることでもPythonモジュールを作成できるようです。
:::
:::note warn
**`#[pymodule]`** 関数
`#[pymodule]`が付いた関数は、**そのプロジェクトの名前**（Cargo.toml参照）と同じ名前でなければなりません。
:::
:::note warn
**my_moduleの引数**
引数を`&PyModule`とするとエラーになります。
@Yosh31207 様によると、これはPyO3（内部実装）の仕様変更によるもので、従来のAPIが変更されたことに起因するようです。
:::

### サブモジュール
サブモジュールは、専用の関数を使って作成すると親モジュールに登録できます。

```rust: rust: my_project/src/lib.rs
// 引数は前項の方法で受け取ったモジュールか、そのサブモジュールであるべきです
fn child_module(parent: &Bound<'_, PyModule>) -> PyResult<()> {
    // サブモジュールの作成
    let child_module = PyModule::new_bound(parent.py(), "child_module")?;

    // ここで親モジュールと同じように関数やクラスを登録できる
    
    parent.add_submodule(&child_module)
}
```

## モジュールのビルド
コマンドラインで実際にモジュールを作成します。
### `maturin build`を使用する場合
以下のコマンドを実行すると**ビルド**できます。その際、Cargo.tomlがあるディレクトリに移動することを忘れないでください
```terminal
cd ./my_module
```
```terminal
maturin build
```
```terminal
cd ..
```
ただし、この状態ではPythonからのインポートはできません。別途pip installが必要です。
```terminal
pip install ./target/wheels/*
```
:::note info
```terminal
maturin build --release
```
とすると最適化してくれます。
:::

### `maturin develop`を使用する場合
**仮想環境の構築**が必要ですが、pip installを省けます。
```terminal
cd ./my_module
```
```terminal
maturin develop
```
```terminal
cd ..
```

これでモジュールがインストールできたはずです。

## 実際に使う
**ディレクトリ構成**
```
.
├─main.py
└─my_module
```

:::note warn
**pyファイルの位置**
my_moduleディレクトリ内に.pyファイルを含めないでください。AttributeErrorが送出されます。[^1]
:::

[^1]: この時、maturinはmy_moduleをpure-Rustではなく、**PythonとRustの混合プロジェクト**と解釈します。今回のディレクトリ構成は混合プロジェクトには適していないので、モジュールは何も含まない空のモジュールになってしまいます。正しいディレクトリ構成は**その他**章を参照してください。

main.pyの例：
```python:main.py
import my_module # pip installしたモジュールをインポートするときと同じ

assert my_module.add_two(6) == 8
```

## その他
- 実はPythonプログラムとRustプログラムを同じディレクトリに入れることも可能です。その場合は[こんな](https://www.maturin.rs/project_layout#mixed-rustpython-project)ディレクトリ構成になります

- `#[pyo3(name = "..")]`でモジュールの公開名を変えられます
- `#[args()]`で引数のデフォルト値を指定できます
- `maturin develop`コマンドを使うと、変更をリアルタイムで反映できます

## 参考
- [PythonとRustの融合⁠⁠：PyO3/maturinを使ったPythonバインディングの作成入門](https://gihyo.jp/article/2023/07/monthly-python-2307)
- [Maturin User Guide](https://www.maturin.rs)

## 実験した環境
Github Actions（コマンドラインがわり）
- ubunstu-latest(ubuntsu 22.0.4)
- CPython 3.12.4 (Python 3.12)
- rustc 1.80.0
- maturin 1.7.0