Python
PR

Pythonから自作Rustプログラムを実行する方法

えりる
記事内に商品プロモーションを含む場合があります

この記事ではRustで書いた高速なロジックをPythonから呼び出す方法と、
「混合ライブラリ」としてのベストプラクティスな構成を解説します。

最終的なディレクトリ構成

まず最初に、ゴールとなるディレクトリ構成を書いちゃいましょう。
プロジェクトの全体像は以下のようになります。
例として、振り子の物理シミュレーションをするときにフォルダの名前にしています。

rust-python-tutorial/
├── Cargo.toml                # Rustのビルド設定(lib名に _ を付ける)
├── pyproject.toml            # Pythonのビルド設定(maturinを使用)
├── src/
│   └── lib.rs                # Rustソースコード(#[pymodule]名に _ を付ける)
├── python/
│   ├── pendulum_lib/         # Pythonパッケージフォルダ
│   │   └── __init__.py       # Rustモジュールを読み込んで公開する
│   └── run_example.py        # 実行サンプル
└── venv/                     # 仮想環境

Step 1: プロジェクトの準備

Cargo.toml

まず、以下を実行してRust側の初期化を行います。

cargo init --lib

その後、Python連携用に必要な設定をCargo.toml に追加します。
サンプルは以下の通りです。

[package]
name = "pendulum_lib"
version = "0.1.0"
edition = "2024"

[lib]
name = "_pendulum_lib" # アンダースコアを付けるのがポイント
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.23", features = ["extension-module"] }
  • [lib] name をアンダースコア付きにしておく。(後述)
  • crate-type = ["cdylib"]:
    通常、Rustのライブラリ(rlib)はRust同士で使うための形式。しかし、PythonはC言語と同じ形式(共有ライブラリ)のファイルを読み込む仕組みになっているため、cdylib(C-Dynamic Library)を指定して、Pythonが理解できる形式で出力させる必要がある。
  • features = ["extension-module"]:
    PyO3を使用する際に必須のフラグ。これを指定することで、PyO3はPythonの拡張機能として動作するための特別なリンク設定を行う。これを付け忘れると、コンパイル時や実行時にエラーが発生する原因になる。

pyproject.toml

pyproject.toml は、Python プロジェクトのビルド方法や依存関係を定義する「設計図」です。
module-name にパッケージ構造を反映させます。

[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"

[project]
name = "pendulum_lib"
version = "0.1.0"
dependencies = [
    "numpy",
    "matplotlib"
    # 他のライブラリはここ
]

[tool.maturin]
python-source = "python" # Pythonのフォルダを指定
module-name = "pendulum_lib._pendulum_lib" # 正しい配置先を指定
  • [build-system]:
    「このプロジェクトをインストールするときは maturin を使ってコンパイルしてね」という道具の指定。これがあるおかげで、pip install 時に自動的に Rust のコンパイルが走る。
  • [project] dependencies:
    他の Python ライブラリ(numpy や matplotlib など)を使いたい場合はここにリスト形式で書きます。ここに書かれたライブラリは、自身のプロジェクトをインストールする際に自動的に一緒にインストールされます。
  • [tool.maturin]:
    Rust と Python の橋渡し設定です。
    • python-source: Python のソースコードがどのフォルダにあるかを指定します。
    • module-name: コンパイルされた Rust バイナリを Python パッケージ内のどこに置くかを指定します。混合ライブラリ構成では パッケージ名._バイナリ名 とするのがベストプラクティスです。

Step 2: Rustコードを書く (src/lib.rs)

src/lib.rs にRustのコードを書きます。以下にサンプルを示します。
Pythonに公開したい要素に `#[pyo3]` 系の属性を付けておきましょう。

えりる
えりる

余計なコメント多くてごめん!

use pyo3::prelude::*;

// 1. 構造体をPythonクラスとして公開する
#[pyclass]
pub struct Params {
    #[pyo3(get, set)] // Python側から obj.m1 のように読み書き可能にする
    pub m1: f64,
    #[pyo3(get, set)]
    pub l1: f64,
    #[pyo3(get, set)]
    pub m2: f64,
    #[pyo3(get, set)]
    pub l2: f64,
    #[pyo3(get, set)]
    pub g: f64,
}

#[pymethods]
impl Params {
    // Python側で Params(1.0, 1.0, ...) と呼べるようにするコンストラクタ
    // Pythonの __init__ に相当します
    #[new]
    fn new(m1: f64, l1: f64, m2: f64, l2: f64, g: f64) -> Self {
        Params { m1, l1, m2, l2, g }
    }
}

// 2. Pythonから呼び出すメイン関数
// 引数を &Params (参照) にすることで、データのコピーを避け、
// 大規模なシミュレーションでもメモリ効率よく高速に動作します
#[pyfunction]
fn run_simulation(p: &Params, theta1: f64, theta2: f64, dt: f64, steps: usize) -> Vec<(f64, f64)> {
    let mut history = Vec::with_capacity(steps);
    
    // ... ここで物理計算を実行 ...
    
    // Rustの Vec<(f64, f64)> は、Python側では
    // 自動的にタプルのリスト [(f1, f2), ...] に変換されます
    history
}

// 3. モジュールの登録
// アンダースコア付きの「隠しモジュール」として登録
#[pymodule]
fn _pendulum_lib(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<Params>()?; // クラスを登録
    m.add_function(wrap_pyfunction!(run_simulation, m)?)?; // 関数を登録
    Ok(())
}

PyO3の主要なマクロ(アトリビュート)の解説

RustのコードをPythonへ繋ぐために使用する、接着剤のようなマクロの役割を解説します。

  • #[pyclass]:
    • 役割: Rustの構造体を、Pythonの「クラス」として定義します。
    • ポイント: これを付けることで、Python側でインスタンス化が可能になります。構造体のフィールドに #[pyo3(get, set)] を付けると、Python側からその値を直接読み書きできるようになります。
  • #[pymethods]:
    • 役割#[pyclass] で定義した構造体に対して、Python側から呼べるメソッドを定義します。
    • ポイントfn new(...) に #[new] を付けると、Pythonのコンストラクタ(__init__)になります。その他の関数は、Pythonのインスタンスメソッドとして公開されます。
  • #[pyfunction]:
    • 役割: Rustの関数を、Pythonの「関数」としてそのまま公開します。
    • ポイント: 引数や戻り値の型(f64StringVectuple など)は、PyO3が自動的にPythonの対応する型へ変換してくれます。

モジュール登録コードの詳細解説

上記のモジュール登録部分には、PyO3の重要なエッセンスが詰まっています。

  • #[pymodule]:
    この関数がPythonモジュールの「入り口」であることを宣言するマクロです。関数名はビルドされるバイナリ名(_pendulum_lib)と一致させる必要があります。
  • &Bound<'_, PyModule>:
    PyO3 0.21から導入された新しい標準API(Bound API)です。
    • Bound: そのオブジェクトが「Pythonのメモリ管理(GIL)の保護下にあること」を型システムで保証します。これにより、メモリ安全性が格段に向上しました。
    • PyModule: Pythonのモジュールオブジェクトそのものを指します。
  • m.add_class::<Params>()?
    #[pyclass] を付けた構造体を、Python側のクラスとして登録します。これにより Python側で pendulum_lib.Params(...) と呼べるようになります。
  • wrap_pyfunction!(run_simulation, m)?:
    Rustの関数をPythonが理解できる形式にラップ(変換)して登録します。
  • ? 演算子:
    もし登録に失敗した場合、即座にエラーを返します。これはPython側では適切な例外(Exception)として処理されます。

Step 3: Pythonパッケージの作成

__init__.py を作成 してRustモジュールをインポート

Rustの機能を外部へ公開するために、__init__.py を作成します。

python/
└── pendulum_lib/
    └── __init__.py

python/pendulum_lib/__init__.py の内容は以下の通りです。

# 裏方のRustモジュールからすべてを読み込んで公開する
from ._pendulum_lib import *

from ._pendulum_lib import * の詳細解説

ここでの「すべて(*)」とは、Rust側の #[pymodule] ブロック内で登録したすべてのクラスと関数を指します。
今回の例では、以下の要素がパッケージの直下に公開されます。

  • m.add_class::<Params>()? で登録した Params クラス
  • m.add_function(wrap_pyfunction!(run_simulation, m)?)? で登録した run_simulation 関数

なぜこの一行が必要なのでしょう。
この一行がない場合、ユーザーは from pendulum_lib._pendulum_lib import Params のように、アンダースコア付きの内部モジュールを意識して深く指定しなければなりません。
__init__.py で中身を表面に引き出すことで、ユーザーは from pendulum_lib import Params という自然で使いやすい形式でインポートできるようになります。
このように「中身の実体(Rustバイナリ)」を隠しつつ、「使いやすい窓口(Pythonパッケージ)」を提供するのが使いやすくておすすめです。

Step 4: ビルドと実行

ここまでできたらあとばRustコードをビルドするだけです。
まずは仮想環境を有効化しておきましょう。
そして、maturin という「ビルドツール(RustとPythonを繋ぐ橋を作る道具)」をインストールします。pip でインストールすることができます。

pip install maturin 

maturin がやってくれることは以下の通りです。

  1. Rustを特殊な設定でコンパイルする(共有ライブラリ化)。
  2. OS(Windows/Mac/Linux)に合わせた適切な拡張子(.pyd.so)にする。
  3. Pythonの検索パスが通っている場所にファイルを置く。
  4. 配布する場合は、pip install できる形式(Wheel)にパッケージングする。

インストールができたら、maturin でビルドするとPython側にパッケージとしてインストールすることができます。

maturin develop

あとはPython側で、

import pendulum_lib

とすれば使用することができます。

開発のポイント

1. なぜアンダースコアを付けるのか?

Pythonパッケージ名(フォルダ名)とRustモジュール名が全く同じだと、Pythonがどちらを読み込めばいいか混乱してしまいます。
Rust側を _ 付きの「裏方」にすることで、この衝突を防ぎ、Python側で __init__.py 使った柔軟な設計が可能になります。

2. なぜ「別名」ではなく「アンダースコア付き」なのか?

pendulum_engine のような全く別の名前にせず、あえて _ を付けるのには、Pythonの設計思想に基づいた理由があります。

Python標準ライブラリの慣習:

Pythonの標準ライブラリ(ssl や socket など)も、
高速なC言語の実装を _ssl や _socket という名前で内部に隠し、ユーザーには _ なしの使いやすいAPIを提供しています。
この慣習に従うことで、他のエンジニアが見た時に「これは内部実装用の高速化パーツだな」と直感的に理解できます。

補完を汚さない

IDEのオートコンプリートにおいて、_ で始まるモジュールは優先順位が低く設定されます。
これにより、ユーザーが import した際に「表の顔」であるパッケージだけが候補に上がり、迷わせない設計になります。

親子関係の明示 

_pendulum_lib とすることで、「これは pendulum_lib パッケージ専用のコアエンジンである」という親子関係が名前だけで明確になります。

3. 混合ライブラリの拡張性

この構成なら、python/pendulum_lib/utils.py のようなファイルを追加して、Rustの高速計算とPythonの便利なライブラリを組み合わせた「最強のツールセット」を簡単に作ることができます。

まとめ

この記事ではRustで書いた高速なロジックをPythonから呼び出す方法と、
「混合ライブラリ」としてのベストプラクティスな構成を解説しました。

Pythonでは遅い処理をRustに任せると、劇的に高速化できるのでぜひトライしてみてください!

えりるさんが気になっている商品紹介コーナー

Minisforum のミニPC UN150P です。CPUは Intel N150 というもので基本的な消費電力は6Wくらいとかなり低消費電力でありながら、ネットサーフィンやYouTubeの動画視聴くらいなら困らないくらいの性能があります。Raspberry Pi 5 よりも性能が良いそうです。
消費電力が少ないので、Ubuntu等を入れてサーバー的に使うこともできますね。同じような用途だとRaspberry Piでもできますが、ケース等を買っていると結局同じくらいの値段になるので、それだったらミニPCにした方がいいかなと思います。
えりるさんはサーバー運用のお勉強に買ってみようかなあと計画中です。(Raspberry Pi 4 1GBだとちょっとスペックが足りなかった…)

えりるについて
えりる
えりる
日本のどこかに生息する平成生まれの研究者。とっても理論家と思いきや気分屋さんでもある。基本的にめんどくさがり。修士(工学)を持っている。 Windows, Mac, Linuxの三刀流。
記事URLをコピーしました