探索 zenoh-python 在 Rust 與 Python 互通上的架構基礎
為高效能 Rust 函式庫建立 Python 綁定是一項相當複雜的工程挑戰。
該如何在維持效能與安全的前提下,將 Rust 的零成本抽象(zero-cost abstractions)暴露給 Python,同時還能保有 Pythonic 的使用體驗?zenoh-python
專案提供了一個教科書級的範例,展現了如何在系統程式設計與動態腳本語言之間架起橋樑。
在這個兩部曲系列中,我們將深入剖析 zenoh-python
如何實作 Python 綁定。本文將從架構基礎與設計原則談起,說明它如何讓 Python 開發者能夠使用 Zenoh 的進階訊息功能。
zenoh-python
的核心是 PyO3 —— Rust 與 Python 互通最成熟的框架。但 zenoh-python
並不只是單純使用 PyO3,它更展現了如何透過進階設計模式,打造可用於生產環境的 Python 擴充模組。
專案結構清楚揭示了其架構選擇:
# Cargo.toml
[lib]
name = "zenoh"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.25.1", features = [
"extension-module",
"abi3-py38", # 適用於 Python 3.8+ 的穩定 ABI
] }
其中 crate-type = ["cdylib"]
指示 Rust 編譯成 C 相容的動態函式庫,而 abi3-py38
則啟用 Python 穩定 ABI,確保從 Python 3.8 開始的跨版本二進位相容性 —— 這對發行套件至關重要。
綁定架構從 src/lib.rs
中一個精心設計的模組階層開始:
#[pymodule]
pub(crate) mod zenoh {
use pyo3::prelude::*;
#[pymodule_export]
use crate::{
bytes::{Encoding, ZBytes},
config::{Config, WhatAmI, WhatAmIMatcher, ZenohId},
handlers::Handler,
session::{open, Session, SessionInfo},
// ... 更多匯出
};
#[pymodule_init]
fn init(m: &Bound<'_, PyModule>) -> PyResult<()> {
let sys_modules = m.py().import("sys")?.getattr("modules")?;
sys_modules.set_item("zenoh.handlers", m.getattr("handlers")?)?;
Ok(())
}
}
這個結構展現了幾個關鍵模式:
zenoh-ext
這樣的功能,會依 Cargo features 決定是否納入init
函式動態註冊子模組至 Python 的 sys.modules
zenoh-python
最精巧的部分之一,是如何橋接 Rust 嚴格的型別系統與 Python 動態型別的特性。這是透過 utils.rs
中定義的一組完整 trait 系統實現的:
pub(crate) trait IntoRust: 'static {
type Into;
fn into_rust(self) -> Self::Into;
}
pub(crate) trait IntoPython: Sized + Send + Sync + 'static {
type Into: for<'py> IntoPyObject<'py>;
fn into_python(self) -> Self::Into;
fn into_pyobject(self, py: Python) -> PyObject {
self.into_python().into_py_any(py).unwrap()
}
}
這些 traits 建立了一個 雙向轉換系統,讓型別能在 Rust 與 Python 間無縫轉換,同時保持編譯期安全性。任何跨越 Rust-Python 邊界的型別都會實作這些 traits,確保綁定層有一致的介面。
zenoh-python
使用一個基於巨集的 wrapper 系統,自動為 Rust 型別生成 Python 綁定:
macro_rules! wrapper {
($($path:ident)::* $(<$arg:lifetime>)? $(:$($derive:ty),*)?) => {
#[pyo3::pyclass]
#[derive($($($derive),*)?)]
pub(crate) struct $ty(pub(crate) $path);
// 自動生成 From 實作
impl From<$ty> for $path { /* ... */ }
impl From<$path> for $ty { /* ... */ }
// 自動生成 trait 實作
impl IntoRust for $ty { /* ... */ }
impl IntoPython for $path { /* ... */ }
};
}
這個巨集會生成:
From
實作應用範例如下:
wrapper!(zenoh::bytes::ZBytes: Clone, Default);
wrapper!(zenoh::bytes::Encoding: Clone, Default);
wrapper!(zenoh::Session);
想複習如何使用巨集的讀者,可以參考這篇Day 10: Rust Macro 熱身:從
macro_rules!
到 Derive Macro
在 Python 綁定中,Rust 的 enum 帶來了特別的挑戰,因為 Python 缺乏與 Rust 相同語意的原生 enum 支援。
Zenoh-python 透過另一個精巧的 巨集 (macro) 來解決這個問題:
macro_rules! enum_mapper {
($($path:ident)::*: $repr:ty { $($variant:ident $(= $discriminator:literal)?,)* }) => {
#[pyo3::pyclass(eq)]
#[repr($repr)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum $ty {$(
#[pyo3(name = $variant:snake:upper)]
$variant $(= $discriminator)?,
)*}
// 雙向轉換...
};
}
這會建立一個 Python 可存取的 enum,具備以下特性:
snake_case
命名規範__eq__
enum_mapper!(zenoh::qos::Priority: u8 {
RealTime = 1,
InteractiveHigh = 2,
InteractiveLow = 3,
DataHigh = 4,
Data = 5,
DataLow = 6,
Background = 7,
});
#[pymethods]
impl Priority {
#[classattr]
const DEFAULT: Self = Self::Data;
#[classattr]
const MIN: Self = Self::Background;
#[classattr]
const MAX: Self = Self::RealTime;
#[classattr]
const NUM: usize = 1 + Self::MIN as usize - Self::MAX as usize;
}
REAL_TIME
、INTERACTIVE_HIGH
等import zenoh
# 使用 enum 值
priority = zenoh.Priority.REAL_TIME
print(priority) # Priority.REAL_TIME
# 保留來自 Rust 的數值
assert priority.value == 1
assert zenoh.Priority.DATA.value == 5
# 使用類別常數
default_priority = zenoh.Priority.DEFAULT
assert default_priority == zenoh.Priority.DATA
Session
型別展示了 zenoh-python
如何處理複雜的物件生命週期:
// 使用 PyO3 將 Rust 結構體暴露為 Python 類別
// 這裡將 `zenoh::Session` 包裝成可在 Python 中使用的型別
#[pyclass]
pub(crate) struct Session(pub(crate) zenoh::Session);
#[pymethods] // 標記以下方法可供 Python 呼叫
impl Session {
// 實作 Python 的 context manager `__enter__` 方法
// 這讓它能在 Python 中這樣使用:
// with zenoh.Session(...) as s:
// ...
fn __enter__<'a, 'py>(this: &'a Bound<'py, Self>) -> &'a Bound<'py, Self> {
this
}
// 實作 Python 的 context manager `__exit__` 方法
// 在 `with` 區塊結束時會自動呼叫
// 確保 session 能被正確關閉
#[pyo3(signature = (*_args, **_kwargs))] // 接受任意參數以符合 Python API
fn __exit__(
&mut self,
py: Python,
_args: &Bound<PyTuple>,
_kwargs: Option<&Bound<PyDict>>,
) -> PyResult<PyObject> {
self.close(py)?; // 呼叫 close() 優雅地關閉 session
Ok(py.None()) // 回傳 None,表示正常結束
}
// 提供給 Python 直接呼叫的顯式 close 方法
// 使用 `wait()` 將 Rust 的非同步 close 轉換為 Python 同步呼叫
fn close(&self, py: Python) -> PyResult<()> {
wait(py, self.0.close())
}
}
// Rust 的解構子 (Drop trait)
// 確保即使 Python 沒有呼叫 `close()`,資源也能被釋放
// 使用 `Python::with_gil` 來安全地取得 GIL 並執行清理
impl Drop for Session {
fn drop(&mut self) {
Python::with_gil(|gil| self.close(gil)).unwrap()
}
}
這個實作展示了:
__enter__
/__exit__
相容 Python 的 with
語法close()
方法與 Drop
實作wait()
將 Rust async 與 Python sync 接軌Zenoh 的 Rust API 大量使用建造者(Builder)模式,但這種模式並不能直接套用到 Python。zenoh-python 透過一套精巧的巨集系統來解決這個問題:
// 定義一個名為 build! 的巨集,用於簡化 Builder 模式的參數組裝
macro_rules! build {
// 接受一個初始 builder 表達式,後面跟著一串可選參數 (識別子)
($builder:expr, $($value:ident),* $(,)?) => {{
// 建立一個可變的 builder 變數,從傳入的初始 builder 開始
let mut builder = $builder;
$(
// 對於傳入的每個可選參數 (Option),
// 如果有值 (Some),則透過 IntoRust trait 轉換成 Rust 型別
if let Some(value) = $value.map($crate::utils::IntoRust::into_rust) {
// 呼叫對應的 builder 方法來設置參數
builder = builder.$value(value);
}
)*
// 返回完成設置的 builder
builder
}};
}
這樣便能支援如下的方法:
#[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None))]
fn put(
&self,
py: Python,
key_expr: KeyExpr,
payload: ZBytes,
encoding: Option<Encoding>,
// ... 更多可選參數
) -> PyResult<()> {
let build = build!(
self.0.put(key_expr, payload),
encoding,
congestion_control,
priority,
express,
);
wait(py, build)
}
這種模式的特點:
Rust-Python 綁定中最具挑戰性的部分之一是記憶體管理。zenoh-python 採用了多種策略:
當 Python 物件被銷毀或 context manager (with
) 區塊結束時,資源會自動清理。
// 定義一個名為 option_wrapper! 的巨集
// 主要用途:將一個 Rust 型別包裝成 Python 可用的類別,並用 Option 來追蹤生命週期
macro_rules! option_wrapper {
// 接受一個型別路徑 (例如 zenoh::Session),以及一個錯誤訊息字串
($($path:ident)::*, $error:literal) => {
// 產生一個 Python 可見的類別 (透過 PyO3 的 #[pyclass])
// 內部實際上存放的是 Option<$path>
// - Some(...) 表示資源有效
// - None 表示資源尚未初始化或已經關閉
#[pyclass]
pub(crate) struct $ty(pub(crate) Option<$path>);
impl $ty {
// 提供一個檢查函式,確保當前的 Python 對象內部還有有效的資源
// 如果是 None,就會回傳錯誤
fn check<'a, 'py>(this: &'a Bound<'py, Self>) -> PyResult<&'a Bound<'py, Self>> {
// 呼叫 get_ref() 驗證 Option 內部狀態是否為 Some
this.borrow().get_ref()?;
Ok(this)
}
}
};
}
這個模式將物件生命週期狀態編碼進型別系統中: None
代表未宣告或已關閉的資源。
impl Drop for $ty {
fn drop(&mut self) {
Python::with_gil(|gil| gil.allow_threads(|| drop(self.0.take())));
}
}
即便資源在非 Python 執行緒中被釋放,也能正確清理,並在需要時取得 GIL。
zenoh-python 建立了一套完整的錯誤處理策略,將 Rust 的 Result
型別橋接到 Python 的例外系統:
pub(crate) trait IntoPyErr {
fn into_pyerr(self) -> PyErr;
}
impl<E: ToString> IntoPyErr for E {
fn into_pyerr(self) -> PyErr {
ZError::new_err(self.to_string())
}
}
pyo3::create_exception!(zenoh, ZError, pyo3::exceptions::PyException);
這樣能提供:
zenoh-python 的每個設計決策都以效能為優先考量:
ZBytes
這類型別提供對底層緩衝區的直接存取py.allow_threads()
釋放 Python GILpy_static!
巨集進行快取在 第二部分 中,我們將深入探討實作細節:
zenoh-python 如何處理非同步操作、如何實作複雜的回呼與通道處理系統、如何管理 Python 的全域直譯器鎖(GIL)以達到最佳效能,敬請期待!