iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Rust

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

Day 14: 在 Zenoh 中橋接 Rust 與 C — 第 1 部分:架構與程式碼生成

  • 分享至 

  • xImage
  •  

在 Zenoh 中橋接 Rust 與 C — 第 1 部分:架構與程式碼生成

Zenoh 是一個高效能、零額外開銷(zero-overhead)的 pub/sub(發布/訂閱)、儲存/查詢(store/query)與計算(compute)協定,統一了動態資料(data in motion)、靜態資料(data at rest)與計算工作。核心實作採用 Rust 以確保效能與記憶體安全,而 Zenoh 透過 zenoh-c 專案提供完整的 C 綁定,以實現與 C/C++ 應用程序的無縫整合。

接下來的三篇系列文章探討使此實現成真的複雜綁定生成系統:

  • 第 1 部分(本文):技術架構與自動程式碼生成
  • 第 2 部分:C 綁定中的使用模式與安全性考量
  • 第 3 部分zenoh-cpp 如何建立在這些基礎上,提供現代 C++ API

挑戰:安全地橋接 Rust 與 C

為 Rust 函式庫創建 C 綁定面臨獨特挑戰:

  • 記憶體安全(Memory Safety):Rust 的所有權系統(ownership system)和借用檢查器(borrow checker)能於編譯時防止記憶體錯誤,但 C 並無此保護。
  • 型別系統不匹配(Type System Mismatch):Rust 豐富的型別系統,包括泛型(generics)、trait 和生命週期參數(lifetime parameters)無法直接映射至 C。
  • ABI 穩定性(ABI Stability):Rust 的內部表示可能因編譯器版本不同而改變。
  • 效能(Performance):綁定 (binding) 必須保留 Rust 的零成本抽象(zero-cost abstractions)。

Zenoh-c 透過 多層架構(multi-layered architecture) 自動從 Rust 程式碼產生安全且高效的 C API 來克服這些挑戰。


架構概覽

zenoh-c 專案以結構化方式實現,包含五大主要組件:

  1. 不透明型別生成(Opaque Type Generation) — 自動從 Rust 結構生成相容 C 的不透明型別
  2. 標頭檔案生成(Header Generation) — 使用 cbindgen 自動產生 C 標頭檔
  3. 泛型巨集系統(Generic Macro System) — 為 C 及 C++ 提供型別安全的泛型巨集
  4. 基於功能的條件編譯(Feature-Based Compilation) — 根據啟用的功能條件產生 API
  5. 建置系統整合(Build System Integration) — 基於 CMake 的建置系統協調整個流程

建置流程管線

建置腳本指揮(build.rs

整個流程的核心是 build.rs,負責指揮整個建置管線:

fn main() {
    buildrs::opaque_types_generator::generate_opaque_types();
    buildrs::cbindgen_generator::generate_c_headers();
    // 額外的設定與驗證
}

此建置腳本於 Rust 編譯階段執行,完成多項關鍵任務:

  • 生成與 Rust 結構對齊的 C 相容不透明型別
  • 產生 C 標頭檔(header files) via cbindgen
  • 處理含 Cargo 功能的 條件編譯(conditional compilation)
  • 選擇性輸出已生成程式碼以供檢視

不透明型別生成系統

zenoh-c 最具創新性的一環是其 不透明型別生成系統buildrs/opaque_types_generator.rs),解決 Rust 至 C FFI 的核心問題:如何在不破壞安全性(safety)與 ABI 穩定性(ABI stability)的情況下expose Rust 型別。

問題

直接暴露 Rust 結構體給 C 將會:

  • 破壞 Rust 的記憶體安全保證
  • 揭露內部實作細節
  • 在編譯器版本間造成 ABI 不穩定性

解決方案:基於編譯的內省(Compilation-Based Introspection)

Zenoh 使用 基於編譯的內省 方式抽取型別metadata:

  1. 產生帶有metadata的錯誤訊息 — 以巨集(macro)get_opaque_type_data! 生成人為錯誤,其中包涵對齊(alignment)和大小(size)資訊。
  2. 解析錯誤訊息 — 利用正規表示式(regex)抽取資料。
  3. 產生不透明的 C 型別 — 依據正確大小與對齊產生 opaque C types。

錯誤產生巨集

#[macro_export]
macro_rules! get_opaque_type_data {
    ($src_type:ty, $name:ident) => {
        const _: () = {
            const ALIGN: usize = std::mem::align_of::<$src_type>();
            const SIZE: usize = std::mem::size_of::<$src_type>();
            panic!("type: {}, align: {}, size: {}", stringify!($name), ALIGN, SIZE);
        };
    };
}

啟用 panic feature 時,該巨集會產生類似錯誤訊息:

type: z_owned_bytes_t, align: 8, size: 32

錯誤解析實作

建置系統會用正則表達式解析這些故意產生的錯誤:

// 計算編譯錯誤總數以作驗證
let total_error_count = data_in
    .lines()
    .filter(|line| line.starts_with("error[E"))
    .count();

// 利用 regex 抽出型別資訊
let re = Regex::new(r"type: (\w+), align: (\d+), size: (\d+)").unwrap();

for (_, [type_name, align, size]) in re.captures_iter(&data_in).map(|c| c.extract()) {
    // 依據抽取的大小與對齊生成 C 相容結構
    // ...
}

// 確保所有錯誤皆有解析成功
if good_error_count != total_error_count {
    panic!("Failed to parse {} out of {} compilation errors",
           total_error_count - good_error_count, total_error_count);
}

此驗證確保每一則編譯錯誤皆成功對應到型別定義,確保生成綁定的完整性。


範例:產生的不透明型別

Rust 中的宣告

/// 一個 Zenoh 資料型
get_opaque_type_data!(ZBytes, z_owned_bytes_t);
get_opaque_type_data!(ZBytes, z_loaned_bytes_t);
decl_c_type! {
    owned(z_owned_bytes_t, ZBytes),
    loaned(z_loaned_bytes_t),
}

相容 C 的結構

// 產生於 zenoh-c/src/opaque_types/mod.rs
#[repr(C, align(8))]
#[rustfmt::skip]
pub struct z_owned_bytes_t {
    _0: [u8; 32],  // 由編譯內省決定的大小
}

FFI 函式實作

/// 在提供的未初始化記憶體位置建構所擁有的淺層複本
#[no_mangle]
extern "C" fn z_bytes_clone(dst: &mut MaybeUninit<z_owned_bytes_t>, this: &z_loaned_bytes_t) {
    dst.as_rust_type_mut_uninit()
        .write(this.as_rust_type_ref().clone());
}
/// 釋放資源,重設為墓碑值
#[no_mangle]
extern "C" fn z_bytes_drop(this_: &mut z_moved_bytes_t) {
    let _ = this_.take_rust_type();
}
/// 借用資料
#[no_mangle]
unsafe extern "C" fn z_bytes_loan(this: &z_owned_bytes_t) -> &z_loaned_bytes_t {
    this.as_rust_type_ref().as_loaned_c_type_ref()
}

此作法確保 C 端可為 Rust 型別正確分配大小並對齊的記憶體,且不暴露實作細節。


移動語意(Move Semantics)與記憶體管理

為支援 Rust 的移動語意,zenoh-c 生成「已移動(moved)」的變種:

#[repr(C)]
pub struct z_moved_bytes_t {
    _this: z_owned_bytes_t,
}
impl TakeCType for z_moved_bytes_t {
    type CType = z_owned_bytes_t;
    fn take_c_type(&mut self) -> Self::CType {
        std::mem::replace(&mut self._this, z_owned_bytes_t::gravestone())
    }
}

使 Rust 與 C 間的所有權轉移安全。


使用 cbindgen 產生標頭檔

本專案使用 cbindgen 來自動生成 C 標頭檔。以下為範例 cbindgen.toml 配置:

language = "C"
style = "both"
usize_is_size_t = true

[fn]
prefix = "ZENOHC_API"

[enum]
rename_variants = "ScreamingSnakeCase"
prefix_with_name = true

管線步驟

  1. cbindgen 生成原始標頭檔
  2. 進行後製處理(如透過 fix_cbindgen() 修正格式及瑕疵)
  3. 根據 Cargo 啟用的功能套用條件編譯旗標
  4. 將標頭拆分為不同模組(common、concrete、opaque 等)

泛型巨集系統

C 語言不支持模板(templates)或泛型(generics),但 Zenoh 需提供如 z_drop()z_move()z_clone() 等多型操作。解決方式是 C11 的 _Generic(C)和函式多載(function overloading,C++)。

C11 泛型巨集示例

#define z_drop(x) \
    _Generic((x), \
        z_owned_session_t* : z_session_drop, \
        z_owned_subscriber_t* : z_subscriber_drop, \
        z_owned_publisher_t* : z_publisher_drop \
    )(x)

C++ 函式多載示例

inline void z_drop(z_owned_session_t* x) { z_session_drop(x); }
inline void z_drop(z_owned_subscriber_t* x) { z_subscriber_drop(x); }
inline void z_drop(z_owned_publisher_t* x) { z_publisher_drop(x); }

函式自動發現

巨集產生器透過正則表達式解析生成的標頭,如:

let re = Regex::new(r"(\w+)_drop\(struct (\w+) *(\w+)\);").unwrap();
for (_, [func_name_prefix, arg_type, arg_name]) in re.captures_iter(&bindings) {
    // 產生對應的 move 和 take 函式
}

確保所有必要函式皆被包含,並檢驗 API 一致性。


建置系統整合

CMake 整合

專案內建完整的 CMake 支援,可處理:

  • 跨平台建置(Cross-compilation)
  • 功能(Features)選擇
  • 靜態與動態庫
  • 標頭與庫的安裝

範例 CMake 指令

# 啟用特定功能
cmake -DZENOHC_BUILD_WITH_UNSTABLE_API=true ../zenoh-c

# 交叉編譯 Windows 版
cmake ../zenoh-c \
  -DCMAKE_SYSTEM_NAME="Windows" \
  -DZENOHC_CUSTOM_TARGET="x86_64-pc-windows-gnu"

測試與驗證

測試套件涵蓋:

  • 函式簽名一致性
  • 記憶體布局相容性
  • 功能旗標組合測試
  • 跨平台建置驗證

效能考量

零額外開銷設計(Zero-Overhead Design)

  • 編譯時生成:所有程式碼生成均於建置時完成
  • 內聯操作:移動與借用操作編譯成簡單記憶體操作
  • 直接 FFI 呼叫:無中介包裝層

記憶體效能

  • 精確大小:不透明型別佔用正確記憶體空間
  • 移動語意:避免大型結構不必要複製
  • 借用模式(loan patterns):實現零拷貝訪問內部資料

後續內容

本文深度探討 Zenoh C 綁定的架構與程式碼生成

明後兩天我們將探索:

  • 第 2 部分將聚焦使用模式安全性考量
  • 第 3 部分將示範 zenoh-cpp 如何基於這些基礎打造具 RAII、模板與零成本抽象的現代 C++ API。

敬請期待~


上一篇
Day 13: Zenoh 如何在 Rust 中用現代 Trait-Based API 統一同步與非同步
下一篇
Day 15: 在 Zenoh 中橋接 Rust 與 C — 第 2 部分:使用模式與安全性
系列文
30 天玩轉 Zenoh:Rust 助力物聯網、機器人與自駕的高速通訊19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言