Rustで書いた小さなツール（空間ID生成）をPyO3を利用してPythonから呼び出してみる！

つい先日、こんな記事を書きました！

https://qiita.com/nokonoko_1203/items/c548f24d2e779118ac7c

これは「**空間ID**」と呼ばれる位置情報を特定するためのキーを生成するRustのユーティリティを紹介したものなんですが、こういった全然外部ライブラリに依存してないとても小さなRustのプログラムであれば、他プログラム言語のラッパーを作成するのはすごい簡単です。

GIS（地理空間情報）の領域ではPythonのライブラリがとても充実しているため、こういったツールはPythonから利用したくなるケースが多いです。
なのでこの記事では**RustのライブラリをPythonから利用できるようにする**手法をサクッと書いてみます。

## まずはプログラムの準備

基本的には上記記事のこの辺りを見てRustでプログラムを構築してみてください。
[経緯度・標高・ズームレベルから空間IDを算出する](https://qiita.com/nokonoko_1203/items/c548f24d2e779118ac7c#%E7%B5%8C%E7%B7%AF%E5%BA%A6%E6%A8%99%E9%AB%98%E3%82%BA%E3%83%BC%E3%83%A0%E3%83%AC%E3%83%99%E3%83%AB%E3%81%8B%E3%82%89%E7%A9%BA%E9%96%93id%E3%82%92%E7%AE%97%E5%87%BA%E3%81%99%E3%82%8B)

一応、リポジトリも用意しているのでそちらを利用しても良いです。

https://github.com/nokonoko1203/spatial-id-rs

Pythonで利用するためには、Rustで書いたプログラムのうち、どの関数（など）をPythonで利用するのか決めてあげる必要があります。

今回はシンプルに「**経緯度・標高・ズームレベル**」を渡すと、空間IDが返ってくるというシンプルな実装を作ってみます。

spatial-id-pyという名称でPythonラッパーの実装を保存しているので、細かいことはこちらをみてください。
https://github.com/nokonoko1203/spatial-id-rs/tree/main/spatial-id-py

## Pythonのラッパープロジェクトを作成

まずはプロジェクトを作成します。

```bash
uv init --build-backend maturin spatial-id-py
```

そうするとRustプロジェクトのようなものが出来上がりますが、pyproject.tomlのようにPythonプロジェクトで利用されるファイルも格納されているのがわかるので、後で説明します。

まずはCargo.tomlを以下のように書きかえてください。
今回は`spatial-id-py`という名称でインポートできるように名称などを設定します。
`spatial-id-core`という名称でRustのバックエンドを書いているので、それも依存関係として指定しておきます。

```toml
[package]
name = "spatial-id-py"
version = "0.1.0"
edition = "2021"
description = "Python bindings for spatial-id-rs using PyO3"
license = "MIT"

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

[dependencies]
pyo3 = { version = "0.21", features = ["extension-module"] }
spatial-id-core = { path = "../spatial-id-core" }

[package.metadata.maturin]
name = "spatial_id_py"
```

ここで重要なのは、crate-typeをcdylibとして指定し、PyO3とRustのコアロジックを依存関係に追加することです。

## RustコードをPython用にラップ

`spatial-id-py/src/lib.rs`に以下のように書き込みます。
たったこれだけでPythonからRustのロジックを利用することができます。

```rust
use pyo3::prelude::*;
use spatial_id_core::cell::zfxy::ZFXY;

#[pyfunction]
#[pyo3(text_signature = "(lat, lon, alt, zoom)")]
fn generate_spatial_id(lat: f64, lon: f64, alt: f64, zoom: u8) -> PyResult<String> {
    let cell = ZFXY::from_lat_lon_alt(lat, lon, alt, zoom);
    Ok(cell.to_spatial_id_str())
}

#[pymodule]
fn spatial_id_py(m: &Bound<PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(generate_spatial_id, m)?)?;
    Ok(())
}
```

Rustのコアロジックでは`ZFXY`という空間IDを生成するための構造体が作成されています。
それを利用して、空間ID生成を行う関数を書いています。

pyo3を利用することで、Rustの関数を比較的簡単にPythonから利用することができます。

## Pythonパッケージのビルドと利用

まずはmaturinでPythonパッケージにビルドします。

```bash
uv run maturin develop
```

Python側で利用する際には、プロジェクトのpyproject.tomlに次のように設定します。

 ```toml
 [project]
name = "spatial-id-py-example"
version = "0.1.0"
description = "Example for using spatial-id-py as an external package."
requires-python = ">=3.8"
dependencies = [
    "spatial-id-py",
]

[tool.maturin]

[tool.uv.sources]
spatial-id-py = { path = "../../spatial-id-py" }
```

パッケージは公開されていないので、パスを指定してspatial-id-pyを読み込んであげます。
パスを指定することで、ローカルのRustパッケージを簡単にPythonプロジェクトに取り込めます。

パッケージの内容が更新された後は、`uv sync`コマンドなどでインストールします。
その後、任意の.pyファイルで以下のようなプログラムを書き、実行できます。

```py
from spatial_id_py import generate_spatial_id

if __name__ == "__main__":
    lat, lon, alt, zoom = 0.0, 0.0, 10.0, 25
    spatial_id = generate_spatial_id(lat, lon, alt, zoom)
    print(f"Spatial ID: {spatial_id}")
```

実行結果は次のようになります。

```bash
$ uv run python example_generate_id.p$ y                
      Built spatial-id-py @ file:///Users/...
Uninstalled 1 package in 0.86ms
Installed 1 package in 1ms
Spatial ID: /25/10/16777216/16777216
```

このような形で簡単に空間IDを生成することができました！

## おわりに

現実的には、もっと複雑なライブラリを取り扱うことになると思うので、依存関係の整理がかなり大変だともいますが、既存のRustのプログラムにちょっとした設定を加えてあげるだけでRustの高速な空間ID生成ロジックを簡単にPythonから利用することができます！

今回書いたコードは全てここに保存してあるので、参考にしてみてください！

https://github.com/nokonoko1203/spatial-id-rs/tree/main/spatial-id-py

皆さんもぜひ活用してみてください！