Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PyO3 で既存 Python 評価器を Rust core 化する ー wasm-bindg...

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for 𝕂' 𝕂'
August 21, 2026

PyO3 で既存 Python 評価器を Rust core 化する ー wasm-bindgen でブラウザにも配るための設計

PyCon JP 2026.8.21(DAY1) 16:15 – 16:45

https://2026.pycon.jp/ja/talks/FXFNLY

Avatar for 𝕂'

𝕂'

August 21, 2026

More Decks by 𝕂'

Other Decks in Programming

Transcript

  1. 自己紹介 古川 祐希 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 の型ヒントと共に進化するコード
  2. 発表内容のスコープ お話すること ✅ お話しないこと ❌ Python のロジックを Rust に 1

    本化して配る話 • • • • Rust 言語そのものの入門・文法解説 • PyO3 の全機能の網羅 • 高速化・ベンチマークの話 なぜ、書き直しではなく Rust core 化なのか 題材: Python 評価器の紹介 Rust core 化から、Python へ配るまでの過程を紹介 → 主目的は速度ではなく実装の一本化 ◦ • 注意ポイント 5 つ どんなときに Rust core 化に踏み込む価値があるか
  3. ただし、2 つのロジックで答えがズレると困る このシチュエーションにおける前提 • フロントエンドは、あくまでも先読み(UX の向上) • バックエンドが最終判定 → 永続化

    ◦ 移植元と移植先のロジックは完全に同一の挙動でないといけない この前提を置くと、 単純に移植する手法にはいくつかの落とし穴があります →
  4. core ロジックを Rust にして両方に配る • Rust には、1 つの実装を他の実行環境へ持ち出す 橋 が揃っている

    PyO3 Rust の実装を Python から import できるようにする wasm-bindgen Rust の実装をブラウザ (Wasm) で動かせるようにする 書き直して 2 つ持つのではなく、1 つを両側へ配ることができる
  5. core ロジックを任せる言語としても Rust は堅い 例えば、Rust には暗黙の型変換が無い。1 + "2" を実行すると… null

    が無い(Option で明示)。例外も無い(Result で明示) 「知らぬ間に暗黙の挙動が入り込む」が構造的に起きにくい
  6. 弊社 Recustomer について • EC 事業者向けに、 購入〜購入後の体験(配送追跡・返品・交換・キャンセル)を向上させる プラットフォームを提供 プラットフォームを提供 バックエンド

    EC 事業者 ストアで購入 返品・配送追跡などの 画面を提供 買い物客 今日の話は、この Python バックエンドの中で始まります
  7. EC 事業者ごとに違うのは、値ではなく条件の形 EC 事業者A の条件 注文金額が 5,000 円以上 なら発行する 違うのは

    5,000 という数値だけ。この場合、事業者ごとの設定値 1 個で足りる EC事業者 B の条件 注文金額が 10,000 円以上 かつ 「予約」タグがない なら発行する 条件が 2 つになり、AND で繋がった。タグの名前も EC 事業者が自由に決める 設定値をいくつ足しても、条件の形は持てない
  8. 愚直に 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
  9. 愚直に 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 を壊すリスク
  10. 条件を 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)だけ
  11. 式木の読み方 {"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)」も、同じ形の入れ子で書ける ノードの組み合わせで、どんな条件の形も表現できる
  12. ある日、この判定を Recustomer の外でも動かしたくなった • 今の判定は Recustomer の Python バックエンドの中で完結している ◦

    • 発行するかどうかを返すだけ(即時性は求められない処理) EC 事業者のサイトは、外部の EC サイトプラットフォームの上で動いている ◦ そのカート画面に「クーポンがもらえるか」をその場で表示したい ◦ カートの中身が変わるたびに再判定 → 買い物客のブラウザの中で動かしたい
  13. つまり、以下のようなことをしたい Recustomer 内 Recustomer の外 EC 事業者ごとの条件(式木) + 注文データ 評価ロジック

    Recustomer のバックエンドと 同じ評価ロジックを動かしたい EC 事業者が利用する EC サイトプラットフォーム ブラウザ 評価ロジック
  14. JavaScript で書くとロジックが 2 つになってズレ始める Recustomer 内 Recustomer の外 EC 事業者ごとの条件(式木)

    + 注文データ 評価ロジック Recustomer のバックエンドと 同じ評価ロジックを動かしたい EC 事業者が利用する EC サイトプラットフォーム ブラウザ 評価ロジック 同じ 評価するコードが 2つになる
  15. 最終形がこちら 最終形のディレクトリ構成 evaluator/ ├─ core/ 評価ロジック(純 Rust) ├─ bindings-py/ Python

    への橋(PyO3) └─ bindings-wasm/ ブラウザへの橋(wasm-bindgen) 以降、ステップごとに作っていきます
  16. STEP 1 評価ロジックを Rust core として定義する evaluator/ ├─ core/ │

    ├─ Cargo.toml │ └─ src/lib.rs ├─ bindings-py/ └─ bindings-wasm/ 判定ロジック(純 Rust) ←まずはここを作る
  17. 式木のノード 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
  18. 式木のノード 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 }, }
  19. 評価ロジックの本体も定義する これも 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 を評価して分岐 */ }
  20. 評価ロジックの本体も定義する これも 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 つならズレる余地がそもそも無い
  21. STEP 2 Rust core ロジックを Python から呼ぶ evaluator/ ├─ core/

    ✅ ├─ bindings-py/ │ ├─ Cargo.toml │ └─ src/lib.rs └─ bindings-wasm/ Python への橋(PyO3) ←次はここを作る
  22. PyO3: 書いた Rust を Python から import して利用できる Rust core

    calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b }
  23. 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)
  24. 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) を呼ぶ
  25. 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 が返る
  26. 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 と関数
  27. 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 と関数
  28. 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> { }
  29. 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 を呼んで返す(薄いラッパー) }
  30. 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 できない
  31. 関数は、モジュールに載せて初めて 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
  32. 関数は、モジュールに載せて初めて 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) これで橋は開通
  33. 注意ポイント ① 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 に片側の都合を混ぜないのが鉄則
  34. STEP 3 言語の壁を越える「変換層」を用意する evaluator/ ├─ core/ ✅ ├─ bindings-py/ │

    ├─ Cargo.toml │ └─ src/lib.rs └─ bindings-wasm/ Python への橋(PyO3) ←引き続き、この中の話
  35. 引数と返り値は、言語の境界を往復する 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>
  36. 引数と返り値は、言語の境界を往復する 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 がやってくれます
  37. 型が決まっていれば自動変換してくれる 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 行も書かない
  38. 一方、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 の型をどうするか?
  39. 固定の struct にはしたくない Rust 側(core) struct Params { amount: i64,

    tags: Vec<String>, first_purchase: bool, // 項目が増えるたびに、ここを足すことに }
  40. 固定の 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 項目が増えた場合、評価器のコードも都度足していく必要がある 「変わるのは式木データだけ」が崩れてしまう
  41. 変換層を自作して橋渡しする(変換関数と 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>), } // 中身の決め方は後述
  42. 変換層を自作して橋渡しする(変換関数と 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>), } // 中身の決め方は後述
  43. 変換層を自作して橋渡しする(変換関数と 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>), } // 中身の決め方は後述
  44. 変換層を自作して橋渡しする(変換関数と 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>), } // 中身の決め方は後述
  45. 変換層を自作して橋渡しする(変換関数と 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>), } // 中身の決め方は後述
  46. 変換層を自作して橋渡しする(変換関数と 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>), } // 中身の決め方は後述
  47. 変換層を自作して橋渡しする(変換関数と 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>), } // 中身の決め方は後述
  48. 変換層を自作して橋渡しする(変換関数と 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>), } // 中身の決め方は後述
  49. 注意ポイント ② Value の型は、Python にも JavaScript にも寄せない 前ページの enum Value、中身の対応付けは以下のように決めた

    • int と float は ひとくくりにしない ◦ • JS の number(区別がない)に寄せない bool は int の サブタイプにしない ◦ Python の bool ⊂ int を持ち込まない どちらの言語にも寄せず、最大公約数で決める
  50. 注意ポイント ③ 変換関数では、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? … と続く }
  51. 注意ポイント ③ 変換関数では、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) になって事故る
  52. 注意ポイント ③ 変換関数では、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) になって事故る エラーは出ないので、答えがひっそりと変わる 落とし穴①(同じつもりのコードが違う答えを返す)を防ぐ
  53. 注意ポイント ④ 変換関数では、i64 に収まらない int を 境界で弾く >>> 10 **

    30 # Python の int に上限はない 1000000000000000000000000000000 Rust の i64 は 9,223,372,036,854,775,807 まで 収まらない値は、黙って丸めずに OverflowError で返す
  54. 注意ポイント ④ 変換関数では、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)); } // … }
  55. 前ページの OverflowError はどうやって Python に届くのか? • PyO3 が Python の例外にしてくれる

    ◦ to_value(obj: &Bound<PyAny>) -> PyResult<Value> = Result<Value, PyErr> ◦ PyErr = Python の例外を Rust 側で表す型 Rust に例外は無い。Err を返せば境界で Python の例外になる
  56. STEP 4 maturin で Rust core を Python へ配る evaluator/

    ├─ core/ ✅ ├─ bindings-py/ Python への橋(PyO3) │ ├─ Cargo.toml │ ├─ src/lib.rs │ ├─ evaluator.pyi │ └─ pyproject.toml └─ bindings-wasm/ ←最後の一手間が必要
  57. maturin とは? Rust を wheel に詰めるビルドツール • 前章までで Rust core

    と Python binding を作ったが、実はまだ Python からは利用できない ◦ Rust のコードを .so にビルドし、Python パッケージの形にしないと import できない ◦ それをやるのが maturin
  58. 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 サーバ
  59. 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 をつないでくれる
  60. 注意ポイント ⑤ 型スタブ(.pyi)は、maturin build だけでは付いてこない ※生成ツールもある: pyo3-stub-gen / maturin 1.13+

    の --generate-stubs(PyO3 の experimental-inspect が必要・開発中)今回は関数 1 つなので手書き 型スタブは自分で用意する 置き場所さえ合っていれば同梱は maturin がやってくれる
  61. おまけ Wasm への展開 evaluator/ ├─ core/ ✅ ├─ bindings-py/ ✅

    └─ bindings-wasm/ ├─ Cargo.toml └─ src/lib.rs ブラウザへの橋(wasm-bindgen)
  62. 新しく足すのは 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 を足すだけで済んだ
  63. 各セクションの注意ポイントまとめ ① core 公開 • core に属性マクロを持ち込まない ② 変換層 •

    • • 型は Python にも JavaScript にも寄せず、最大公約数で決める bool を int より先に判定する 収まらない値は、黙って丸めずに境界で弾く ③ 配布 • maturin build で型スタブは作られない
  64. Rust core 化で守りたかったのは、速さより正本 • Rust の速さも魅力ではある ◦ • ただ、今回いちばん重視したのは、評価ロジックの正本を 1

    つに保てること Python だけで完結するなら、Python のままでいい ◦ 同じロジックを Python 以外の言語でも動かしたいのであれば、 Rust core 化は有力な選択肢になる ◦ ただし、橋のコードは自分で書く(= 今日の注意ポイント 5 つ)