Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
PyO3 で既存 Python 評価器を Rust core 化する ー wasm-bindg...
Search
𝕂'
August 21, 2026
Programming
570
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
PyO3 で既存 Python 評価器を Rust core 化する ー wasm-bindgen でブラウザにも配るための設計
PyCon JP 2026.8.21(DAY1) 16:15 – 16:45
https://2026.pycon.jp/ja/talks/FXFNLY
𝕂'
August 21, 2026
More Decks by 𝕂'
See All by 𝕂'
Streamlit は社内ツールだけじゃない!PoC の速さで実現する'商用品質'の分析 SaaS アーキテクチャ
kdash
4
2.8k
Other Decks in Programming
See All in Programming
A2UI for Android: Safely Rendering AI-Generated UI with Jetpack Compose - DroidKaigi 2026
itsmedreamwalker
0
120
iOS開発×AI駆動開発 〜最近使って便利だったスキルの話〜
nogu66
0
150
iOSDC Japan 2026 - Swiftで作って学ぼう!データベース自作入門
kaseken
2
160
Intent as Code
shoppingjaws
6
980
TiDB Cloudのカスタムコントローラーによるオートスケール対応
takaidohigasi
0
110
技術的負債を組織課題として解く-増えすぎたマイクロサービスとの戦い-
reimaru
1
1.3k
ハーネス設計入門 〜プロンプト、コンテキストの次〜
kinopeee
56
38k
DroidKaigi 2026 「個人開発という実験場: Android エンジニアが手にする4つの自由」
slashnephy
0
230
[GoCon2026] When Goroutines Are Not Enough: Runtime Locality in High-Throughput Go
takehaya
6
2.1k
How I Stole PSI from Android Studio - DroidKaigi2026
worker8
0
120
AI に Inclusive UI を書かせよう — Design Rules Skill で Compose UI を作り直す
theoriatec2024
1
490
個人開発基盤をまるごとCloudflareに引っ越して爆速で総合的体験を向上させた話
tinykitten
0
180
Featured
See All Featured
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
The Curious Case for Waylosing
cassininazir
1
500
Marketing to machines
jonoalderson
1
5.8k
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
230
Ten Tips & Tricks for a 🌱 transition
stuffmc
0
230
Test your architecture with Archunit
thirion
2
2.4k
The Organizational Zoo: Understanding Human Behavior Agility Through Metaphoric Constructive Conversations (based on the works of Arthur Shelley, Ph.D)
kimpetersen
PRO
0
450
Building Flexible Design Systems
yeseniaperezcruz
330
41k
New Earth Scene 8
popppiees
3
2.6k
A Tale of Four Properties
chriscoyier
163
24k
How to Talk to Developers About Accessibility
jct
2
550
Building AI with AI
inesmontani
PRO
1
1.2k
Transcript
PyO3 で既存 Python 評価器を Rust core 化する wasm-bindgen でブラウザにも配るための設計 PyCon
JP 2026.8.21(DAY1) Recustomer 株式会社 古川 祐希 16:15 – 16:45
自己紹介 古川 祐希 K-dash 所属: Recustomer 株式会社 Python 歴: 約5年
Rust 歴: 約1年 略歴 2012 - 2018 SIer でインフラエンジニア 2018 - 2025 Webアプリケーション開発 サーバー/ネットワーク構築自動化 2025/04 - Recustomer の Platform Team 所属 2025 年の主な活動 PyCon JP 2025 登壇: Streamlit は社内ツールだけじゃない! Qiita Advent Calendar 2025: Python の型ヒントと共に進化するコード
発表内容のスコープ お話すること ✅ お話しないこと ❌ Python のロジックを Rust に 1
本化して配る話 • • • • Rust 言語そのものの入門・文法解説 • PyO3 の全機能の網羅 • 高速化・ベンチマークの話 なぜ、書き直しではなく Rust core 化なのか 題材: Python 評価器の紹介 Rust core 化から、Python へ配るまでの過程を紹介 → 主目的は速度ではなく実装の一本化 ◦ • 注意ポイント 5 つ どんなときに Rust core 化に踏み込む価値があるか
Python のロジックを別の言語で動かしたい! ...というシチュエーションに遭遇したことはありませんか?
一例をあげると、こんなとき Python 製バックエンドのロジックを JavaScript 製フロントエンドでも動かしたい • Why: ユーザー操作中に処理結果を即座に返したい ▪ UI
の入力中にバリデーション結果を出す ▪ 金額の計算結果を出す ▪ ボタンの有効 / 無効を切り替える …
JavaScript で同じロジックを作ればいいじゃない バックエンド フロントエンド 複雑な判定・計算ロジック 複雑な判定・計算ロジック (移植版) バックエンドの Python ロジックを
JavaScript に移植すればブラウザで動く
ただし、2 つのロジックで答えがズレると困る このシチュエーションにおける前提 • フロントエンドは、あくまでも先読み(UX の向上) • バックエンドが最終判定 → 永続化
◦ 移植元と移植先のロジックは完全に同一の挙動でないといけない
ただし、2 つのロジックで答えがズレると困る このシチュエーションにおける前提 • フロントエンドは、あくまでも先読み(UX の向上) • バックエンドが最終判定 → 永続化
◦ 移植元と移植先のロジックは完全に同一の挙動でないといけない この前提を置くと、 単純に移植する手法にはいくつかの落とし穴があります →
移植の落とし穴①: 同じつもりのコードが違う答えを返す 言語仕様の違い(一例なので、これだけではない) 挙動が違う。答えがひっそりと変わるだけ
移植の落とし穴②: 最初は同じでも別々に育って食い違う 運用のズレ • 修正が片方にだけ入り、もう片方に反映されない • 知らぬ間に、それぞれの言語の暗黙の挙動が実装に入り込む ◦ その結果、2つの仕様は少しずつズレ始める 実装が
2 つに分かれた時点から、同じ答えを返し続ける責任が生まれる
落とし穴の原因の正体 落とし穴①も②も、原因は同じロジックが 2 つあること
落とし穴の原因の正体 落とし穴①も②も、原因は同じロジックが 2 つあること 実装が 1 つならズレる余地がそもそも無い
1つのロジックを正本にして両方に配れたら良さそうでは? 正本ロジック バックエンド フロントエンド
この課題を解決するいい案があります
これです core ロジック 一度だけ書いて 1 本化 PyO3 wasm-bindgen バックエンド フロントエンド
core ロジックを Rust にして両方に配る • Rust には、1 つの実装を他の実行環境へ持ち出す 橋 が揃っている
PyO3 Rust の実装を Python から import できるようにする wasm-bindgen Rust の実装をブラウザ (Wasm) で動かせるようにする 書き直して 2 つ持つのではなく、1 つを両側へ配ることができる
core ロジックを任せる言語としても Rust は堅い 例えば、Rust には暗黙の型変換が無い。1 + "2" を実行すると… null
が無い(Option で明示)。例外も無い(Result で明示) 「知らぬ間に暗黙の挙動が入り込む」が構造的に起きにくい
余談:実はもう、みなさんの環境で Rust が動いています この中で、 中身が Rust のものは?
余談:実はもう、みなさんの環境で Rust が動いています この中で、 中身が Rust のものは? 全部です ※ pydantic
は v2 のコア (pydantic-core) が Rust 製
ここからは、実際にやった話をします まずは前提となる題材の説明
弊社 Recustomer について • EC 事業者向けに、 購入〜購入後の体験(配送追跡・返品・交換・キャンセル)を向上させる プラットフォームを提供 プラットフォームを提供 バックエンド
EC 事業者 ストアで購入 返品・配送追跡などの 画面を提供 買い物客 今日の話は、この Python バックエンドの中で始まります
Recustomer 内にクーポンの発行判定処理がある • 購入後のリピート促進として、条件を満たした注文にクーポンを発行する クーポンの発行の条件は、EC 事業者ごとに設定できる
EC 事業者ごとに違うのは、値ではなく条件の形 EC 事業者A の条件 注文金額が 5,000 円以上 なら発行する 違うのは
5,000 という数値だけ。この場合、事業者ごとの設定値 1 個で足りる
EC 事業者ごとに違うのは、値ではなく条件の形 EC 事業者A の条件 注文金額が 5,000 円以上 なら発行する 違うのは
5,000 という数値だけ。この場合、事業者ごとの設定値 1 個で足りる EC事業者 B の条件 注文金額が 10,000 円以上 かつ 「予約」タグがない なら発行する 条件が 2 つになり、AND で繋がった。タグの名前も EC 事業者が自由に決める 設定値をいくつ足しても、条件の形は持てない
愚直に if 文で書くと EC 事業者の増加に伴い条件も増える EC 事業者ごとの条件を if 文で書いた場合のコード例 def
should_issue_coupon(order, tenant_id: str) -> bool: # tenant = EC 事業者 if tenant_id == "A": return order.total >= 5000 if tenant_id == "B": return (order.total >= 10000 and not any(t == "予約" for t in order.tags)) # EC 事業者が増えるたびに条件分岐が増えていく return False
愚直に if 文で書くと EC 事業者の増加に伴い条件も増える EC 事業者ごとの条件を if 文で書いた場合のコード例 def
should_issue_coupon(order, tenant_id: str) -> bool: # tenant = EC 事業者 if tenant_id == "A": return order.total >= 5000 if tenant_id == "B": return (order.total >= 10000 and not any(t == "予約" for t in order.tags)) # EC 事業者が増えるたびに条件分岐が増えていく return False • 条件が変わるたびに、コードを直してデプロイ ◦ • → EC 事業者は自分で条件を変えられず、こちらのリリース待ち 「予約」という EC 事業者の設定が、コードに埋まる ◦ どの事業者がどんな条件かはコードを読まないと分からない。A を直して B を壊すリスク
条件を expression tree(式木)として持つ 条件を if 文ではなく JSON データで持ち、評価する仕組みを 1 つだけ書く
EC 事業者A 注文金額が 5,000 円以上なら出す EC事業者 B 10,000 円以上 かつ 「予約」タグが無い {"version": 1, "params": ["amount"], "body": {"if": { "cond": {"apply": {"function": "ge", "args": [{"var": {"name": "amount"}}, {"const": {"value": 5000}}]}}, "then": {"const": {"value": true}}, "else": {"const": {"value": false}}}}} {"version": 1, "params": ["amount", "tags"], "body": {"if": { "cond": {"apply": {"function": "and", "args": [ {"apply": {"function": "ge", "args": [{"var": {"name": "amount"}}, {"const": {"value": 10000}}]}}, {"apply": {"function": "not_has_tag", "args": [{"var": {"name": "tags"}}, {"const": {"value": "予約"}}]}}]}}, "then": {"const": {"value": true}}, "else": {"const": {"value": false}}}}} EC 事業者が増えても変わるのは式木データ(JSON)だけ
式木の読み方 {"version": 1, "params": ["amount"], "body": {"if": { "cond": {"apply":
{"function": "ge", "args": [{"var": {"name": "amount"}}, {"const": {"value": 5000}}]}}, "then": {"const": {"value": true}}, "else": {"const": {"value": false}}}}} ※ EC 事業者 B の「かつ (AND)」も、同じ形の入れ子で書ける ノードの組み合わせで、どんな条件の形も表現できる
ある日、この判定を Recustomer の外でも動かしたくなった • 今の判定は Recustomer の Python バックエンドの中で完結している ◦
発行するかどうかを返すだけ(即時性は求められない処理)
ある日、この判定を Recustomer の外でも動かしたくなった • 今の判定は Recustomer の Python バックエンドの中で完結している ◦
• 発行するかどうかを返すだけ(即時性は求められない処理) EC 事業者のサイトは、外部の EC サイトプラットフォームの上で動いている ◦ そのカート画面に「クーポンがもらえるか」をその場で表示したい ◦ カートの中身が変わるたびに再判定 → 買い物客のブラウザの中で動かしたい
つまり、以下のようなことをしたい Recustomer 内 Recustomer の外 EC 事業者ごとの条件(式木) + 注文データ 評価ロジック
Recustomer のバックエンドと 同じ評価ロジックを動かしたい EC 事業者が利用する EC サイトプラットフォーム ブラウザ 評価ロジック
JavaScript で書くとロジックが 2 つになってズレ始める Recustomer 内 Recustomer の外 EC 事業者ごとの条件(式木)
+ 注文データ 評価ロジック Recustomer のバックエンドと 同じ評価ロジックを動かしたい EC 事業者が利用する EC サイトプラットフォーム ブラウザ 評価ロジック 同じ 評価するコードが 2つになる
移植の落とし穴(再掲)
移植の落とし穴(再掲) 導入で紹介した移植の落とし穴①②が、 そのまま自分たちの話になった →だから Rust core 化を決めました
前置きが長くなりましたが、本日のお話 Python の評価ロジックを Rust に1本化して(=Rust core 化して)両側へ配る core ロジック 一度だけ書いて一本化
PyO3 Python サーバ 以降、こちらを 「Rust core」と呼びます wasm-bindgen ブラウザ
最終形がこちら 最終形のディレクトリ構成 evaluator/ ├─ core/ 評価ロジック(純 Rust) ├─ bindings-py/ Python
への橋(PyO3) └─ bindings-wasm/ ブラウザへの橋(wasm-bindgen) 以降、ステップごとに作っていきます
STEP 1 評価ロジックを Rust core として定義する evaluator/ ├─ core/ │
├─ Cargo.toml │ └─ src/lib.rs ├─ bindings-py/ └─ bindings-wasm/ 判定ロジック(純 Rust) ←まずはここを作る
式木のノード 4 種は Rust の enum で 4 つの枝にする Python(既存の式木ノード定義)
from dataclasses import dataclass @dataclass(frozen=True) class If: cond: Expr then: Expr els: Expr @dataclass(frozen=True) class Apply: function: str args: list[Expr] @dataclass(frozen=True) class Var: name: str @dataclass(frozen=True) class Const: value: int | float | str | bool Expr = If | Apply | Var | Const
式木のノード 4 種は Rust の enum で 4 つの枝にする Python(既存の式木ノード定義)
from dataclasses import dataclass @dataclass(frozen=True) class If: cond: Expr then: Expr els: Expr core/src/lib.rs(ノードを enum で定義する) @dataclass(frozen=True) class Apply: function: str args: list[Expr] @dataclass(frozen=True) class Var: name: str @dataclass(frozen=True) class Const: value: int | float | str | bool Expr = If | Apply | Var | Const 書き直す enum Expr { If { cond: Box<Expr>, then: Box<Expr>, els: Box<Expr> }, Apply { function: String, args: Vec<Expr> }, Var { name: String }, Const { value: Value }, }
評価ロジックの本体も定義する これも Python から移植(詳細は割愛) core/src/lib.rs (評価器は以下の再帰関数 1 本) pub fn
evaluate(expr: &Expr, params: &Params) -> Result<Value, EvalError> { match expr { Expr::Const { value } => Ok(value.clone()), } } Expr::Var { name } => params.get(name), // 注文データから値を引く Expr::Apply { .. } => { /* 関数を適用して再帰 */ } Expr::If { .. } => { /* cond を評価して分岐 */ }
評価ロジックの本体も定義する これも Python から移植(詳細は割愛) core/src/lib.rs (評価器は以下の再帰関数 1 本) pub fn
evaluate(expr: &Expr, params: &Params) -> Result<Value, EvalError> { match expr { Expr::Const { value } => Ok(value.clone()), Expr::Var { name } => params.get(name), // 注文データから値を引く Expr::Apply { .. } => { /* 関数を適用して再帰 */ } Expr::If { .. } => { /* cond を評価して分岐 */ } } } これで Rust core が完成 この core 関数が判定を実行する唯一の正本になる →実装が 1 つならズレる余地がそもそも無い
STEP 2 Rust core ロジックを Python から呼ぶ evaluator/ ├─ core/
✅ ├─ bindings-py/ │ ├─ Cargo.toml │ └─ src/lib.rs └─ bindings-wasm/ Python への橋(PyO3) ←次はここを作る
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b }
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x)
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x) ② add(2, 3) を呼ぶ
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x) ② add(2, 3) を呼ぶ ③ add が実行されて 5 が返る
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x) ② add(2, 3) を呼ぶ ③ add が実行されて 5 が返る Python 側から見れば 普通の module と関数
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x) この矢印の正体 = 橋 次はこれを書きます ② add(2, 3) を呼ぶ ③ add が実行されて 5 が返る Python 側から見れば 普通の module と関数
Rust core ロジックを Python から呼べるようにする ⚠ bindings-py/src/lib.rs(このファイルは core ではありません! bindings-py
層に新しく書く関数です) use pyo3::prelude::*; #[pyfunction] #[pyfunction] という属性マクロを付けると Python の関数として利用できる fn evaluate(tree_json: &str, params: Params) -> PyResult<bool> { }
Rust core ロジックを Python から呼べるようにする ⚠ bindings-py/src/lib.rs(このファイルは core ではありません! bindings-py
層に新しく書く関数です) use pyo3::prelude::*; #[pyfunction] #[pyfunction] という属性マクロを付けると Python の関数として利用できる fn evaluate(tree_json: &str, params: Params) -> PyResult<bool> { // ここで、tree_ json をパースし、core の evaluate を呼んで返す(薄いラッパー) }
Rust core ロジックを Python から呼べるようにする ⚠ bindings-py/src/lib.rs(このファイルは core ではありません! bindings-py
層に新しく書く関数です) use pyo3::prelude::*; #[pyfunction] #[pyfunction] という属性マクロを付けると Python の関数として利用できる fn evaluate(tree_json: &str, params: Params) -> PyResult<bool> { // ここで、tree_ json をパースし、core の evaluate を呼んで返す(薄いラッパー) } ただし、これだけではまだ Python から import できない
関数は、モジュールに載せて初めて import できる • • Python の import が探すのは、関数ではなくモジュール #[pyfunction]
を付けただけでは、まだ Python から見えない bindings-py/src/lib.rs(続き) #[pyfunction] Python 側(これで import できる) import evaluator fn evaluate() {...} evaluator.evaluate( #[pymodule] tree_json, fn evaluator(m: &Bound<'_, PyModule>) -> PyResult<()> { {"amount": 7000, "tags": []}, m.add_function(wrap_pyfunction!(evaluate, m)?) } ) # => True
関数は、モジュールに載せて初めて import できる • • Python の import が探すのは、関数ではなくモジュール #[pyfunction]
を付けただけでは、まだ Python から見えない bindings-py/src/lib.rs(続き) Python 側(これで import できる) import evaluator #[pyfunction] fn evaluate() {...} #[pymodule] evaluator.evaluate( モジュールとして公開する属性マクロ fn evaluator(m: &Bound<'_, PyModule>) -> PyResult<()> { tree_json, {"amount": 7000, "tags": []}, 呼べる m.add_function(wrap_pyfunction!(evaluate, m)?) ) # => True } fn の名前が、そのまま import の名前になる(fn evaluator → import evaluator) これで橋は開通
注意ポイント ① Rust core に属性マクロを持ち込まない 「wrapper は用意せず、core の evaluate に直接属性マクロを付ければいいのでは?」
注意ポイント ① Rust core に属性マクロを持ち込まない 「wrapper は用意せず、core の evaluate に直接属性マクロを付ければいいのでは?」
❌ core に付ける ⭕ 橋にだけ付ける // core/src/lib.rs // bindings-py/src/lib.rs #[pyfunction] // ← core に Python の都合が混入 #[pyfunction] // ← OK! fn evaluate() {...} fn evaluate() {...} • core に属性マクロを付けると、core が PyO3 依存になってしまう ◦ • ブラウザへ配るときに全部剥がして回ることになる core に片側の都合を混ぜないのが鉄則
STEP 3 言語の壁を越える「変換層」を用意する evaluator/ ├─ core/ ✅ ├─ bindings-py/ │
├─ Cargo.toml │ └─ src/lib.rs └─ bindings-wasm/ Python への橋(PyO3) ←引き続き、この中の話
引数と返り値は、言語の境界を往復する Rust 側(core) Python 側 import evaluator 引数: Python の値
→ Rust の値 fn evaluate( evaluator.evaluate( expr: &Expr, tree_json, params: &Params, {"amount": 7000, "tags": []}, ) 返り値: Rust の値 → Python の値 # => True この変換は誰がやる? ) -> Result<Value, EvalError>
引数と返り値は、言語の境界を往復する Rust 側(core) Python 側 引数: Python の値 → Rust
の値 import evaluator fn evaluate( evaluator.evaluate( expr: &Expr, tree_json, params: &Params, {"amount": 7000, "tags": []}, ) 返り値: Rust の値 → Python の値 ) -> Result<Value, EvalError> # => True この変換は誰がやる? 基本的には PyO3 がやってくれます
PyO3 は Python の型と Rust の型の対応表を持っている
型が決まっていれば自動変換してくれる Rust 側 Python 側 引数: Python の値 → Rust
の値 import calculate_py fn add(a: i64, b: i64) -> i64 { a + b x = calculate_py.add(2, 3) print(x) PyO3 が i64 に自動変換 返り値: Rust の値 → Python の値 } この場合、変換コードは 1 行も書かない
一方、evaluate() の params は? Rust 側(core) Python 側 引数: Python
の値 → Rust の値 import evaluator fn evaluate( evaluator.evaluate(tree_json, { "amount": 7000, # int "tags": ["予約"], # list "first_purchase": True, # bool }) expr: &Expr, params: &Params, ) -> Result<Value, EvalError> 返り値: Rust の値 → Python の値 注文データ(int / list / bool が混ざる dict) この Params の型をどうするか?
固定の struct にはしたくない Rust 側(core) struct Params { amount: i64,
tags: Vec<String>, first_purchase: bool, // 項目が増えるたびに、ここを足すことに }
固定の struct にはしたくない Rust 側(core) struct Params { amount: i64,
tags: Vec<String>, first_purchase: bool, // 項目が増えるたびに、ここを足すことに } • • 注文データのどの項目を、どんな型で使うかは、式木が決める ◦ EC 事業者 A の式木 → amount だけ ◦ EC 事業者 B の式木 → amount と tags ◦ EC 事業者 C の式木 → struct に現状ない customer_rank 項目が増えた場合、評価器のコードも都度足していく必要がある 「変わるのは式木データだけ」が崩れてしまう
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; 実体 // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value に変換する関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value に変換する関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value に変換する関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } // Value(型を実行時に持つ enum) enum Value { ④ 7000 は Value::Int(7000) になる Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; // 最終的な Params の値 "amount": Int(7000) "tags": List([Str("予約")]) "first_purchase": Bool(true) ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value に変換する関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } ⑤ HashMap に入る // Value(型を実行時に持つ enum) enum Value { ④ 7000 は Value::Int(7000) になる Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; // 最終的な Params の値 "amount": Int(7000) "tags": List([Str("予約")]) "first_purchase": Bool(true) 変換層として自作するのは、この2つ ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value にする関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } ⑤ HashMap に入る // Value(型を実行時に持つ enum) enum Value { ④ 7000 は Value::Int(7000) になる Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
注意ポイント ② Value の型は、Python にも JavaScript にも寄せない 前ページの enum Value、中身の対応付けは以下のように決めた
注意ポイント ② Value の型は、Python にも JavaScript にも寄せない 前ページの enum Value、中身の対応付けは以下のように決めた
• int と float は ひとくくりにしない ◦ • JS の number(区別がない)に寄せない bool は int の サブタイプにしない ◦ Python の bool ⊂ int を持ち込まない どちらの言語にも寄せず、最大公約数で決める
注意ポイント ③ 変換関数では、bool を int より先に判定する • Python の bool
は int の サブタイプ ◦ → isinstance(True, int) は True になる ❌ 数値から先に判定すると... fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // int 判定 if let Ok(i) = obj.downcast::<PyInt>() { return Ok(Value::Int(i.extract::<i64>()?)); } // bool 判定 if let Ok(b) = obj.downcast::<PyBool>() { return Ok(Value::Bool(b.is_true())); } // float? str? list? … と続く }
注意ポイント ③ 変換関数では、bool を int より先に判定する • Python の bool
は int の サブタイプ ◦ → isinstance(True, int) は True になる ❌ 数値から先に判定すると... fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // int 判定 if let Ok(i) = obj.downcast::<PyInt>() { return Ok(Value::Int(i.extract::<i64>()?)); } // bool 判定 if let Ok(b) = obj.downcast::<PyBool>() { return Ok(Value::Bool(b.is_true())); } // float? str? list? … と続く } 先に int を見ると、True が Int(1) になって事故る
注意ポイント ③ 変換関数では、bool を int より先に判定する • Python の bool
は int の サブタイプ ◦ → isinstance(True, int) は True になる ❌ 数値から先に判定すると... ⭕ bool を先に見る fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // int 判定 if let Ok(i) = obj.downcast::<PyInt>() { return Ok(Value::Int(i.extract::<i64>()?)); } // bool 判定 if let Ok(b) = obj.downcast::<PyBool>() { return Ok(Value::Bool(b.is_true())); } // float? str? list? … と続く } fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool 判定 if let Ok(b) = obj.downcast::<PyBool>() { return Ok(Value::Bool(b.is_true())); } // int 判定 if let Ok(i) = obj.downcast::<PyInt>() { return Ok(Value::Int(i.extract::<i64>()?)); } // float? str? list? … と続く } 先に int を見ると、True が Int(1) になって事故る エラーは出ないので、答えがひっそりと変わる 落とし穴①(同じつもりのコードが違う答えを返す)を防ぐ
注意ポイント ④ 変換関数では、i64 に収まらない int を 境界で弾く >>> 10 **
30 # Python の int に上限はない 1000000000000000000000000000000 Rust の i64 は 9,223,372,036,854,775,807 まで 収まらない値は、黙って丸めずに OverflowError で返す
注意ポイント ④ 変換関数では、i64 に収まらない int を 境界で弾く >>> 10 **
30 # Python の int に上限はない 1000000000000000000000000000000 Rust の i64 は 9,223,372,036,854,775,807 まで 収まらない値は、黙って丸めずに OverflowError で返す ⭕ 変換関数の中(int を受けるところ) fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // … if let Ok(i) = obj.downcast::<PyInt>() { let v: i64 = i.extract()?; // 収まらなければ ? が OverflowError にして呼び出し側へ返す return Ok(Value::Int(v)); } // … }
前ページの OverflowError はどうやって Python に届くのか? • PyO3 が Python の例外にしてくれる
◦ to_value(obj: &Bound<PyAny>) -> PyResult<Value> = Result<Value, PyErr> ◦ PyErr = Python の例外を Rust 側で表す型 Rust に例外は無い。Err を返せば境界で Python の例外になる
STEP 4 maturin で Rust core を Python へ配る evaluator/
├─ core/ ✅ ├─ bindings-py/ Python への橋(PyO3) │ ├─ Cargo.toml │ ├─ src/lib.rs │ ├─ evaluator.pyi │ └─ pyproject.toml └─ bindings-wasm/ ←最後の一手間が必要
maturin とは? Rust を wheel に詰めるビルドツール • 前章までで Rust core
と Python binding を作ったが、実はまだ Python からは利用できない ◦ Rust のコードを .so にビルドし、Python パッケージの形にしないと import できない ◦ それをやるのが maturin
maturin とは? Rust を wheel に詰めるビルドツール • 前章までで Rust core
と Python binding を作ったが、実はまだ Python からは利用できない ◦ Rust のコードを .so にビルドし、Python パッケージの形にしないと import できない ◦ それをやるのが maturin core + bindings-py • • build 配れる一つのファイル maturin は setuptools と同じビルドバックエンドの位置 ◦ import evaluator wheel (.whl) Rust のコード cargo build 後に生成された .so を wheel に詰める maturin build --release コマンドで wheel を作れる pip install 配った先の Python サーバ
maturin の実行前に必要な設定ファイルは 2 つだけ ① pyproject.toml(ビルド方法の宣言) [build-system] requires = ["maturin>=1.7,<2"]
build-backend = "maturin" [project] name = "evaluator" version = "0.1.0" requires-python = ">=3.9" ② Cargo.toml(cdylib として吐く) [lib] name = "evaluator" crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.22", features = ["abi3-py39"] } evaluator-core = { path = "../core" } build-backend = "maturin" の 1 行が、pip と cargo をつないでくれる
注意ポイント ⑤ 型スタブ(.pyi)は、maturin build だけでは付いてこない
注意ポイント ⑤ 型スタブ(.pyi)は、maturin build だけでは付いてこない ※生成ツールもある: pyo3-stub-gen / maturin 1.13+
の --generate-stubs(PyO3 の experimental-inspect が必要・開発中)今回は関数 1 つなので手書き 型スタブは自分で用意する 置き場所さえ合っていれば同梱は maturin がやってくれる
おまけ Wasm への展開 evaluator/ ├─ core/ ✅ ├─ bindings-py/ ✅
└─ bindings-wasm/ ├─ Cargo.toml └─ src/lib.rs ブラウザへの橋(wasm-bindgen)
再掲
新しく足すのは bindings-wasm だけ
新しく足すのは bindings-wasm だけ 新しく書くのはこの1ファイルだけ // bindings-wasm/src/lib.rs use evaluator_core as core;
use wasm_bindgen::prelude::*; binding 追加 #[wasm_bindgen] pub fn evaluate(expr_json: &str, input_json: &str) -> Result<String, JsValue> { core::evaluate(..) // PyO3 と同様に Rust core を呼ぶ } Rust core に配布先(Python)の都合を入れなかったので、 Wasm 対応は binding を足すだけで済んだ
まとめ
各セクションの注意ポイントまとめ ① core 公開 • core に属性マクロを持ち込まない ② 変換層 •
• • 型は Python にも JavaScript にも寄せず、最大公約数で決める bool を int より先に判定する 収まらない値は、黙って丸めずに境界で弾く ③ 配布 • maturin build で型スタブは作られない
Rust core 化で守りたかったのは、速さより正本 • Rust の速さも魅力ではある ◦ ただ、今回いちばん重視したのは、評価ロジックの正本を 1 つに保てること
Rust core 化で守りたかったのは、速さより正本 • Rust の速さも魅力ではある ◦ • ただ、今回いちばん重視したのは、評価ロジックの正本を 1
つに保てること Python だけで完結するなら、Python のままでいい ◦ 同じロジックを Python 以外の言語でも動かしたいのであれば、 Rust core 化は有力な選択肢になる ◦ ただし、橋のコードは自分で書く(= 今日の注意ポイント 5 つ)
None
Thank you!