uvとPyO3でPythonからRustを呼ぶ③（ビルドとクロスコンパイル）

# はじめに

備忘用。
この記事では、uvとPyO3を使ってPythonからRustを呼び出すプロジェクトにおける、ビルド方法とWindows⇔WSL間のクロスコンパイルの方法について述べます。

一応、以下の記事からの続きとなります。

+ [uvとPyO3でPythonからRustを呼ぶ①（プロジェクト作成・Hello World実行まで）](https://qiita.com/nukipei/items/f096a1df6c8074b16150)
+ [uvとPyO3でPythonからRustを呼ぶ②（基本的なデータ型の値をやり取りする）](https://qiita.com/nukipei/items/1081b1de90df3332bc13)

# 対象のプロジェクト

ビルド対象のプロジェクトは以下の`example-ext`プロジェクトです。

https://github.com/nukipei/example-ext

前々回（①）、前回（②）の記事で扱ったプロジェクトと同様です。

この記事の後半で、Windows⇔WSL間のクロスコンパイルを行いますが、ビルドがうまくいったかのテストとして、`example_ext.main()`が実行できるかを確認します。

```
>>> import example_ext
>>> example_ext.main()
3
3
3.0
Hello, World!
Hello, Bob!
[2, 4, 6]
[2, '2', 6, [1, 2], ('1', 2), {'key': 'value'}]
{'text1': 3, 'text3': 4, 'text2': 2}
2
None
```

# 実行環境

## Windows

Windows 11
uv 0.6.3
python 3.12.9
rustup 1.27.1
rustc 1.85.0
cargo 1.85.0
pyo3 0.22.4

※前の記事と比べて微妙に更新してます。

## WSL

Ubuntu 22.04
※それ以外のツールのバージョンはWindowsと同じ



# Python&Rustプロジェクトのビルド

ビルド方法の話に入る前に、`example-ext`プロジェクトをローカルにクローンして、ディレクトリを移動しておきます。

```
> git clone https://github.com/nukipei/example-ext.git
> cd example-ext
```

クローン＆ディレクトリ移動ができたら、早速ビルドを行います。uvでは`uv build`[^1]を実行することでビルドを行います。


```
> uv build
```

ログを見ると、ビルドシステムに指定したmaturinのビルドコマンドが走っていることや、Rustのコンパイルが行われていることがわかると思います。

<details><summary>uv buildのログ</summary>

```
> uv build
Building source distribution...
Running `maturin pep517 write-sdist --sdist-directory C:\path\example-ext\dist`
🍹 Building a mixed python/rust project
🔗 Found pyo3 bindings with abi3 support for Python ≥ 3.9
From `cargo package --list --allow-dirty --manifest-path C:\path\example-ext\Cargo.toml`:
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.  
📦 Built source distribution to C:\path\example-ext\dist\example_ext-0.1.0.tar.gz
example_ext-0.1.0.tar.gz
Building wheel from source distribution...
Running `maturin pep517 build-wheel -i C:\Users\path\AppData\Local\uv\cache\builds-v0\.tmpJ7aONa\Scripts\python.exe --compatibility off`
🍹 Building a mixed python/rust project
🔗 Found pyo3 bindings with abi3 support for Python ≥ 3.9
   Compiling target-lexicon v0.12.16
   Compiling once_cell v1.20.2
   Compiling proc-macro2 v1.0.93
   Compiling unicode-ident v1.0.15
   Compiling libc v0.2.169
   Compiling autocfg v1.4.0
   Compiling heck v0.5.0
   Compiling cfg-if v1.0.0
   Compiling unindent v0.2.3
   Compiling indoc v2.0.5
   Compiling memoffset v0.9.1
   Compiling quote v1.0.38
   Compiling pyo3-build-config v0.22.6
   Compiling syn v2.0.96
   Compiling pyo3-ffi v0.22.6
   Compiling pyo3-macros-backend v0.22.6
   Compiling pyo3 v0.22.6
   Compiling pyo3-macros v0.22.6
   Compiling example_ext v0.1.0 (C:\Users\path\AppData\Local\uv\cache\sdists-v8\.tmpX2fVS2\example_ext-0.1.0)
    Finished `release` profile [optimized] target(s) in 17.25s
📦 Built wheel for abi3 Python ≥ 3.9 to C:\Users\path\AppData\Local\uv\cache\sdists-v8\.tmpX2fVS2\example_ext-0.1.0\target\wheels\example_ext-0.1.0-cp39-abi3-win_amd64.whl
C:\Users\path\AppData\Local\uv\cache\sdists-v8\.tmpX2fVS2\example_ext-0.1.0\target\wheels\example_ext-0.1.0-cp39-abi3-win_amd64.whl
Successfully built dist\example_ext-0.1.0.tar.gz
Successfully built dist\example_ext-0.1.0-cp39-abi3-win_amd64.whl
```

</details>

ビルドされたモジュールは、`dist`ディレクトリ配下に出力されます。なお、出力先のディレクトリを指定したい場合は、`uv build -o <path>`で指定できます。

```
>tree /f dist
    .gitignore
    example_ext-0.1.0-cp39-abi3-win_amd64.whl
    example_ext-0.1.0.tar.gz
```

デフォルトでは、`.whl`と`.tar.gz`が生成されますが、片方の形式のみの生成も可能であり、`uv build --wheel`で`.whl`のみを、`uv build --sdist`で`.tar.gz`のみを生成できます。（`.tar.gz`にはRustのバイナリが含まれないことに注意してください。）

`example_ext-0.1.0-cp39-abi3-win_amd64.whl`を7zipなどで解凍すると、以下のようなディレクトリ構成となっているはずで、example_extプロジェクトのPythonファイルに加え、RustのWindows用のバイナリである`_core.pyd`[^2]が含まれていることが確認できます。

```
├─example_ext
│      _core.pyd
│      _core.pyi
│      __init__.py
│
└─example_ext-0.1.0.dist-info
        entry_points.txt
        METADATA
        RECORD
        WHEEL
```

# Windows⇔WSL間のクロスコンパイル

## Windowsでビルド⇒WSLで実行

### Linux向けのコンパイル＆ビルド

uvを使ってはいるものの、ビルドの際は、maturinのビルドコマンドが走っています。そこで、maturinのドキュメントの内、クロスコンパイルについて述べた箇所を[^3]参照してみると、クロスコンパイルの方法として、dockerを利用する方法とZigを利用する方法が述べられていました。


この記事では、Zigを使ってみようかと思います。（恥ずかしながら、Zigを触ったことがなかったこともあり。。）
まず、Zigをインストールします。インストール方法はいろいろありそうですが、ここでは公式[^4]からバイナリを含むzipを直接ダウンロードして、ローカルで解凍しました（パスも設定済）。バージョンは以下の通りです。
```
> zig version
0.14.0-dev.3348+8683f25d2
```

次に、RustのLinux向けのコンパイルに必要なライブラリやコンポーネントをダウンロードします。
この記事では、ターゲット（対象の環境）として`x86_64-unknown-linux-gnu`を指定します。
`x86_64-unknown-linux-gnu`向けの諸々がダウンロードされているかは、`rustup target list`を実行することで確認できます。（`x86_64-unknown-linux-gnu`の行に`installed`が記載されていれば、ダウンロード済）
```
> rustup target list 
（省略）
x86_64-unknown-linux-gnu (installed)
x86_64-unknown-linux-gnux32
（省略）
```
もし、ダウンロードされていなければ、`rustup target add`を実行し、ダウンロードします。
```
> rustup target add x86_64-unknown-linux-gnu
```

これで、必要な設定が完了しましたので、`x86_64-unknown-linux-gnu`向けのビルドを行います。これは、以下のコマンドを実行します。

```
> uv build -C build-args="--target x86_64-unknown-linux-gnu --zig"
```

`-C build-args=hogehoge`によって、maturinにビルドオプションを渡すことができます。ここでは、ビルドオプションを2つ指定しており、`--target`によってターゲットを指定し、`--zig`によってZigを利用してコンパイルするよう設定しています。

以下ログを見れば、maturinによるビルド時に、指定したビルドオプション（`--target`と`--zig`）が追加されていることがわかると思います。

<details><summary>uv buildのログ</summary>

```
> uv build -C build-args="--target x86_64-unknown-linux-gnu --zig"
Building source distribution...
Running `maturin pep517 write-sdist --sdist-directory C:\path\example-ext\dist`
🍹 Building a mixed python/rust project
🔗 Found pyo3 bindings with abi3 support for Python ≥ 3.9
From `cargo package --list --allow-dirty --manifest-path C:\path\example-ext\Cargo.toml`:
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.  
📦 Built source distribution to C:\path\example-ext\dist\example_ext-0.1.0.tar.gz
example_ext-0.1.0.tar.gz
Building wheel from source distribution...
Running `maturin pep517 build-wheel -i C:\Users\path\AppData\Local\uv\cache\builds-v0\.tmppJ156N\Scripts\python.exe --compatibility off --target x86_64-unknown-linux-gnu --zig`
🍹 Building a mixed python/rust project
🔗 Found pyo3 bindings with abi3 support for Python ≥ 3.9
🐍 Not using a specific python interpreter
   Compiling target-lexicon v0.12.16
   Compiling once_cell v1.20.2
   Compiling proc-macro2 v1.0.93
   Compiling unicode-ident v1.0.15
   Compiling autocfg v1.4.0
   Compiling libc v0.2.169
   Compiling heck v0.5.0
   Compiling indoc v2.0.5
   Compiling unindent v0.2.3
   Compiling cfg-if v1.0.0
   Compiling memoffset v0.9.1
   Compiling quote v1.0.38
   Compiling pyo3-build-config v0.22.6
   Compiling syn v2.0.96
   Compiling pyo3-macros-backend v0.22.6
   Compiling pyo3-ffi v0.22.6
   Compiling pyo3 v0.22.6
   Compiling pyo3-macros v0.22.6
   Compiling example_ext v0.1.0 (C:\Users\path\AppData\Local\uv\cache\sdists-v8\.tmpcgxtaN\example_ext-0.1.0)
    Finished `release` profile [optimized] target(s) in 19.67s
📦 Built wheel for abi3 Python ≥ 3.9 to C:\Users\path\AppData\Local\uv\cache\sdists-v8\.tmpcgxtaN\example_ext-0.1.0\target\wheels\example_ext-0.1.0-cp39-abi3-linux_x86_64.whl
🛠️ Using zig for cross-compiling to x86_64-unknown-linux-gnu
C:\Users\path\AppData\Local\uv\cache\sdists-v8\.tmpcgxtaN\example_ext-0.1.0\target\wheels\example_ext-0.1.0-cp39-abi3-linux_x86_64.whl
Successfully built dist\example_ext-0.1.0.tar.gz
Successfully built dist\example_ext-0.1.0-cp39-abi3-linux_x86_64.whl
```

</details>

先ほどと同様に、`dist`ディレクトリにビルドされたモジュールが出力されますが、`.whl`の名前が`example_ext-0.1.0-cp39-abi3-linux_x86_64.whl`と先ほどと異なっています。

```
> tree /f dist       
    .gitignore
    example_ext-0.1.0-cp39-abi3-linux_x86_64.whl
    example_ext-0.1.0.tar.gz
```

また、`example_ext-0.1.0-cp39-abi3-linux_x86_64.whl`を解凍して中身を確認すると、バイナリの拡張子が`.pyd`から`.so`に変化しており、Linux向けのバイナリが生成できていることが確認できます。

```
├─example_ext
│      _core.so
│      _core.pyi
│      __init__.py
│
└─example_ext-0.1.0.dist-info
        entry_points.txt
        METADATA
        RECORD
        WHEEL
```

### WSLで実行

Windowsでビルドした`example_ext-0.1.0-cp39-abi3-linux_x86_64.whl`を、WSLで呼び出してみます。

まず、uvプロジェクトを作成します。プロジェクト名を`test_whl`としています。
```
> uv init test_whl
```

PythonのバージョンがWindowsと異なれば、事前に揃えておきます。
```
> cd test_whl
> uv pin 3.12
```

次に、生成された`pyproject.toml`と同じディレクトリに`example_ext-0.1.0-cp39-abi3-linux_x86_64.whl`を配置し、`pyproject.toml`を以下のように修正します。

```toml: pyproject.toml
[project]
name = "test-whl"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["example_ext"]

[tool.uv.sources]
example_ext = { path = "example_ext-0.1.0-cp39-abi3-linux_x86_64.whl" }
```

そして、`uv sync`によって仮想環境を作成し、`example_ext`モジュールをインストールします。
```
uv sync
```

インストールが成功していれば、以下のように`example_ext`モジュールを呼び出せるようになっているはずです。
```
> uv run python
Python 3.12.9 (main, Feb 12 2025, 14:50:50) [Clang 19.1.6 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import example_ext
>>> example_ext.main()
3
3
3.0
Hello, World!
Hello, Bob!
[2, 4, 6]
[2, '2', 6, [1, 2], ('1', 2), {'key': 'value'}]
{'text1': 3, 'text3': 4, 'text2': 2}
2
None
```


## WSLでビルド⇒Windowsで実行

次に、WSLでビルドしたモジュールをWindowsで呼びだしてみます。（Windows→WSLと流れはほとんど変わりませんので、端折りながら説明します。）

まず、rustupやuvなどWindowsでも利用したツールに加え、Windowsのバイナリ生成に必要な`mingw-w64`[^5]をインストールします。
```
sudo apt install g++-mingw-w64-x86-64
```

インストールができたら、WSLに`example_ext`プロジェクトをクローンし、ディレクトリを移動しておきます。
```
git clone https://github.com/nukipei/example-ext
cd example-ext
```

次に、`rustup target list`でWindows向けのターゲットがあるかを確認し、無ければ`rustup target add`でダウンロードしておきます。（今回のターゲットは`x86_64-pc-windows-gnu`としています。）
```
rustup target add x86_64-pc-windows-gnu
```

ここで、先ほどと異なる点として、pyproject.tomlの`pyo3`の`features`として、`generate-import-lib`[^6]を追加します。これにより、コンパイルに必要なWindowsのPythonライブラリファイル（python3.dllやpython3x.dll）を自動で生成してくれるとのことです。

```
[dependencies]
# "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so)
# "abi3-py39" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.9
pyo3 = { version = "0.22.4", features = ["extension-module", "abi3-py39", "generate-import-lib"] }
```

:::note
Pythonライブラリファイルの生成の際、中身では`python3_dll_a`[^7]クレートを使っているとのことです。
:::

:::note warn
maturinのでドキュメント[^6]によると、`generate-import-lib`は`experimental feature`とのことなので、実際の案件で利用の際は注意してください。
:::

最後に`uv build`を実行します。オプションとして、`-C build-args="--target=x86_64-pc-windows-gnu"`を設定しています。
```
uv build -C build-args="--target=x86_64-pc-windows-gnu"
```

これにより、`dist`ディレクトリ配下に`example_ext-0.1.0-cp39-abi3-win_amd64.whl`が生成されます。

あとは、`example_ext-0.1.0-cp39-abi3-win_amd64.whl`をWindowsにインストールすれば、`example_ext.main()`を呼び出せるはずです。


:::note warn
WSLにZigをインストール[^8]した上で、Windowsでビルドしたときと同様に、オプションに`--zig`を指定して`uv build`を実行したところ、以下のような警告が発生し、正常なモジュールを生成することができませんでした。
```
⚠️  Warning: Couldn't find the symbol `PyInit__core` in the native library. Python will fail to import this module. If you're using pyo3, check that `#[pymodule]` uses `_core` as module name
```
類似の事象を報告した、maturinのissue[^9]ではzigのバグとのコメントもあり、WSLに関してはZigを使ったビルドを断念しました。
:::

# 最後に

この記事では、ythonからRustを呼び出すプロジェクトにおける、ビルドの方法とWindows⇔WSL間のクロスコンパイルの方法について述べました。

勉強したてなので、もし、「この方法のほうが良いよ」などありましたら、コメントよろしくお願いします。


[^1]: https://docs.astral.sh/uv/concepts/projects/build/
[^2]: pydはdllと同じようなものだが、少し異なるとのこと。詳しくは、https://docs.python.org/ja/3.12/faq/windows.html#is-a-pyd-file-the-same-as-a-dll 参照。
[^3]: https://www.maturin.rs/distribution#cross-compiling
[^4]: https://ziglang.org/download/
[^5]: インストール方法はhttps://qiita.com/kerupani129/items/5d68a80f402cbc6a623f を参考にさせていただきました。
[^6]: https://www.maturin.rs/distribution#cross-compile-to-windows
[^7]: https://docs.rs/python3-dll-a/latest/python3_dll_a/
[^8]: インストール方法はhttps://qiita.com/bam_b0o_/items/27770ffcf0551ce9e813 を参考にさせていただきました。
[^9]: https://github.com/PyO3/maturin/issues/922