iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Rust

30 天玩轉 Zenoh:Rust 助力物聯網、機器人與自駕的高速通訊系列 第 17

Day 17: Zenoh 如何實作高效能 Python 綁定 - 第一部分

  • 分享至 

  • xImage
  •  

Zenoh 如何實作高效能 Python 綁定 - 第一部分

探索 zenoh-python 在 Rust 與 Python 互通上的架構基礎


為高效能 Rust 函式庫建立 Python 綁定是一項相當複雜的工程挑戰。
該如何在維持效能與安全的前提下,將 Rust 的零成本抽象(zero-cost abstractions)暴露給 Python,同時還能保有 Pythonic 的使用體驗?
zenoh-python 專案提供了一個教科書級的範例,展現了如何在系統程式設計與動態腳本語言之間架起橋樑。

在這個兩部曲系列中,我們將深入剖析 zenoh-python 如何實作 Python 綁定。本文將從架構基礎與設計原則談起,說明它如何讓 Python 開發者能夠使用 Zenoh 的進階訊息功能。


基礎:PyO3 與 Maturin

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(())
    }
}

這個結構展現了幾個關鍵模式:

  1. 階層式組織:模組結構符合 Python 套件慣例,同時維持清晰的責任分工
  2. 條件編譯:像 zenoh-ext 這樣的功能,會依 Cargo features 決定是否納入
  3. 執行期模組註冊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,確保綁定層有一致的介面。


Wrapper 模式:型別安全與易用性的結合

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 { /* ... */ }
    };
}

這個巨集會生成:

  • PyClass 定義:讓 Rust 型別能被 Python 使用
  • 雙向轉換:自動生成 From 實作
  • Trait 整合:無縫接入型別轉換系統

應用範例如下:

wrapper!(zenoh::bytes::ZBytes: Clone, Default);
wrapper!(zenoh::bytes::Encoding: Clone, Default);
wrapper!(zenoh::Session);

想複習如何使用巨集的讀者,可以參考這篇Day 10: Rust Macro 熱身:從 macro_rules! 到 Derive Macro


Enum 對應:保留 Rust 語意

在 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,具備以下特性:

  • 正確表示:明確控制記憶體配置 (memory layout)
  • Python 慣例:符合 Python 的 snake_case 命名規範
  • 值的保留:保留 Rust 中的 discriminator 數值
  • 相等語意:正確實作 __eq__

範例:QoS Priority

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;
}

這段程式碼會產生:

  • 一個 Python 的 enum 類別,並保留明確的數值
  • 符合 Python 命名:REAL_TIMEINTERACTIVE_HIGH
  • Rust 與 Python enum 的雙向轉換
  • 常見常數 (constants) 作為類別屬性

Python 使用方式:

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 管理:API 的核心

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()
    }
}

這個實作展示了:

  • Context Manager 支援:透過 __enter__/__exit__ 相容 Python 的 with 語法
  • 資源釋放:顯式 close() 方法與 Drop 實作
  • 非同步橋接:透過 wait() 將 Rust async 與 Python sync 接軌
  • GIL 管理:正確處理 Python 全域直譯器鎖

Builder Pattern 轉譯

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 的建造者 API:同時適配 Python 的關鍵字參數
  • 型別安全:所有轉換都透過 trait 系統完成
  • 便利性 :Python 開發者可用熟悉的關鍵字參數語法

記憶體管理哲學

Rust-Python 綁定中最具挑戰性的部分之一是記憶體管理。zenoh-python 採用了多種策略:

1. 透過 Context Manager 的 RAII

當 Python 物件被銷毀或 context manager (with) 區塊結束時,資源會自動清理。

2. 用 Option Wrapper 來管理生命週期

// 定義一個名為 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 代表未宣告或已關閉的資源。

3. 感知 GIL 的資源清理

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 錯誤
  • 自動轉換:任何 Rust 錯誤型別都會變成 Python 例外
  • 錯誤上下文保留:錯誤訊息會跨越邊界被保留下來

效能哲學

zenoh-python 的每個設計決策都以效能為優先考量:

  1. 盡可能零拷貝(Zero-Copy):像 ZBytes 這類型別提供對底層緩衝區的直接存取
  2. 最小化配置:包裝型別為編譯期結構,不增加執行期負擔
  3. 釋放 GIL:長時間運行的操作會透過 py.allow_threads() 釋放 Python GIL
  4. 靜態快取:常用的 Python 物件會透過 py_static! 巨集進行快取

第二部分 中,我們將深入探討實作細節:
zenoh-python 如何處理非同步操作、如何實作複雜的回呼與通道處理系統、如何管理 Python 的全域直譯器鎖(GIL)以達到最佳效能,敬請期待!


上一篇
Day 16: 在 Zenoh 中橋接 Rust 與 C — 第 3 部分:現代 C++ API 與 Zenoh-CPP
下一篇
Day 18: Zenoh 如何實作高效能的 Python 綁定 - 第二部
系列文
30 天玩轉 Zenoh:Rust 助力物聯網、機器人與自駕的高速通訊19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言