在第 1 部分中,我們探討了 zenoh-c 自動綁定生成系統背後的複雜架構,了解此專案如何利用 compilation-based introspection、cbindgen 與 generic macros 從 Rust 程式碼自動生成 C API。
本篇重點在於實務:生成的綁定如何實際運作,Zenoh-c 獨特的 type system 如何將 Rust 的 ownership semantics 帶入 C,以及這套先進系統無法完全消除的安全挑戰。
第 3 部分將展示 zenoh-cpp 如何建基於這套 C 基礎,為現代 C++ 開發者提供透過 RAII 及 C++ 型別系統設計的安全且人體工學的 API。
Zenoh-c 透過一套謹慎設計的 type system,將 Rust 的所有權概念映射為 C 約定,這是現行帶入 memory safety 至 C API 的最複雜嘗試之一。
Zenoh-c 將型別組織為多種 distinct categories,每個在所有權模型中擔任特定角色:
z_owned_xxx_t
):代表專有擁有權的資源,類似 Rust 的 owned 值z_loaned_xxx_t
):代表借用引用,等同於 Rust 的 &T
z_moved_xxx_t
):函式參數中表達所有權轉移z_view_xxx_t
):堆疊分配的檢視物件,不須顯式清除z_xxx_options_t
):操作的配置參數z_xxx_t
):基礎值型別這套型別系統可讓 C 開發者顯式表示所有權關係,讓記憶體管理模式於 API 層級清晰揭露。
基本模式是建立與銷毀資源:
// 初始化與銷毀範例
z_owned_string_t s; // 未初始化記憶體
z_string_copy_from_str(&s, "Hello, world!"); // 寫入資料
z_drop(z_move(s)); // z_move 轉移所有權
// z_drop 呼叫 Rust 的 destructor
z_move()
將所有權從變數自動轉移至 z_drop()
,確保資源僅清理一次。
不同型別依底層 Rust 行為有不同 clone 語意:
// 字串深層複製範例
z_owned_string_t s1, s2;
z_string_copy_from_str(&s1, "Hello, world!");
z_string_clone(&s2, z_loan(s1)); // 透過 z_loan 進行深拷貝
z_drop(z_move(s1)); // 釋放 s1
z_drop(z_move(s2)); // 釋放 s2
// 引用計數類型淺複製範例
z_owned_bytes_t b1, b2;
z_bytes_from_static_str(&b1, "Hello, world!");
z_bytes_clone(&b2, z_loan(b1)); // 淺層複製,Rust 使用 Arc
z_drop(z_move(b1)); // 釋放 b1
z_drop(z_move(b2)); // 最後擁有者釋放資源
z_loan()
函式產生借用引用,適用唯讀或 clone 的來源。
View types 以堆疊配置方式呈現,使用後離開作用域自動清除:
// View 物件不需清理
z_owned_string_t owned;
z_string_copy_from_str(&owned, "Hello, world!");
z_view_string_t view;
z_view_string_from_str(&view, "Hello, another world!"); // stack-allocated
// view 不需呼叫 drop,離開作用域自動處理
Moved types 使函式間安全且明確地轉移所有權:
// 取得所有權的函式
void consume_string(z_moved_string_t* ps) {
z_owned_string_t s;
z_take(&s, ps); // 從 moved type 取得所有權
printf("%.*s\n", z_string_len(z_loan(s)), z_string_data(z_loan(s)));
z_drop(z_move(s));
}
// 呼叫端轉移所有權
z_owned_string_t s;
z_string_copy_from_str(&s, "Hello, world!");
consume_string(z_move(s));
// 不需後續清理,所有權移轉給 consume_string
z_take()
從 moved 參數中取回擁有值,令所有權轉移顯式。
callback 場景常需求借用轉 owned:
// callback 轉 owned
void sub_callback(z_loaned_sample_t* sample, void* arg) {
z_owned_sample_t s;
z_take_from_loaned(&s, sample); // 從借用的sample中克隆(Arc +1)
// 可將 s 儲存或移往其他執行緒
}
z_owned_closure_sample_t callback;
z_closure(&callback, sub_callback, NULL, NULL);
z_owned_subscriber_t sub;
if (z_declare_subscriber(&sub, z_loan(session), z_loan(keyexpr),
z_move(callback), NULL) < 0) {
printf("宣告 subscriber 失敗。\n");
exit(-1);
}
此種用法對於callback的操作來說可謂是相當靈活的!
Zenoh-c 支援 C 端注冊 closure:
// C 回呼函式
void my_callback(const z_loaned_sample_t* sample, void* context) {
printf("收到資料: %.*s\n",
(int)z_bytes_len(z_sample_payload(sample)),
z_bytes_data(z_sample_payload(sample)));
}
z_owned_closure_sample_t callback;
z_closure(&callback, my_callback, NULL, my_context);
z_subscriber_declare(&subscriber, z_loan(session), z_keyexpr("key"),
z_move(callback), NULL);
利用 loan/borrow 方式保留 Zenoh 零拷貝特性:
// 借用資料無複製
const z_loaned_bytes_t* payload = z_sample_payload(z_loan(sample));
z_bytes_slice_t slice = z_bytes_get_slice(payload);
// 直接使用 slice.start 與 slice.len,無須複製
泛型巨集系統提供跨型別的 type-safe 操作:
// 適用任何 owned 型別
z_drop(z_move(session)); // 內部呼叫 z_session_drop
z_drop(z_move(subscriber)); // 內部呼叫 z_subscriber_drop
z_drop(z_move(publisher)); // 內部呼叫 z_publisher_drop
// Loan 操作同理
const z_loaned_session_t* s = z_loan(session);
const z_loaned_keyexpr_t* k = z_loan(keyexpr);
#include "zenoh.h"
#include <stdio.h>
// 資料處理的 callback,接收 sample 並從中提取 key 及 payload,然後印出
void data_handler(const z_loaned_sample_t* sample, void* context) {
z_owned_string_t key_str; // 宣告用於接收 key 的 owned string
// 將 sample 中的 keyexpr 轉成字串,存入 key_str
z_keyexpr_to_string(z_sample_keyexpr(sample), &key_str);
// 取得 sample payload 的借用參考
const z_loaned_bytes_t* payload = z_sample_payload(sample);
// 印出接收到的 key 與 payload 的內容
printf("Received: %.*s => %.*s\n",
(int)z_string_len(z_loan(key_str)), z_string_data(z_loan(key_str)),
(int)z_bytes_len(payload), z_bytes_data(payload));
// 釋放 key_str 所擁有的資源:用 z_move 將所有權轉給 z_drop,確保資料正確釋放
z_drop(z_move(key_str));
}
// 當需要清理 context 時呼叫此函式(此範例中未使用任何 context)
void drop_handler(void* context) {
// 如需清理 context,可在此實作
}
int main() {
// 宣告並初始化 config,適用於建立 Session
z_owned_config_t config;
z_config_default(&config); // 建立預設設定
// 建立一個 owned session,使用 z_move 把 config 的所有權轉出
z_owned_session_t session;
if (z_open(&session, z_move(config)) < 0) {
printf("Unable to open session!\n"); // 開啟失敗顯示錯誤
return -1;
}
// 建立一個 owned closure sample,將 data_handler 與 drop_handler 註冊為回呼
z_owned_closure_sample_t callback;
z_closure(&callback, data_handler, drop_handler, NULL);
// 宣告 subscriber 並建立 key 表達式,用於訂閱的查詢
z_owned_subscriber_t sub;
z_view_keyexpr_t keyexpr;
z_view_keyexpr_from_str(&keyexpr, "demo/example"); // 從字串產生檢視型 keyexpr
// 建立訂閱者,使用 z_loan 取得 session 和 keyexpr 的借用參考,
// 用 z_move 轉移 callback 的所有權到訂閱者
if (z_declare_subscriber(&sub, z_loan(session), z_loan(keyexpr),
z_move(callback), NULL) < 0) {
printf("Unable to declare subscriber!\n"); // 建立失敗時錯誤輸出
z_drop(z_move(session)); // 釋放 session,避免資源洩漏
return -1;
}
// 宣告發布者
z_owned_publisher_t pub;
// 產生發布者,使用借用參考 session 和 keyexpr
if (z_declare_publisher(&pub, z_loan(session), z_loan(keyexpr), NULL) < 0) {
printf("Unable to declare publisher!\n");
z_drop(z_move(sub)); // 釋放訂閱者
z_drop(z_move(session)); // 釋放 session
return -1;
}
// 建立 payload 並從字串初始化,不需自己管理記憶體
z_owned_bytes_t payload;
z_bytes_from_static_str(&payload, "Hello, Zenoh!");
// 透過 publisher 發布資料,z_move 轉移 payload 所有權,函式會自動管理釋放
z_publisher_put(z_loan(pub), z_move(payload), NULL);
// 程式暫停一秒,保持運作,確保訊息送出
z_sleep_s(1);
// 清理資源,釋放順序與建立相反
z_drop(z_move(pub));
z_drop(z_move(sub));
z_drop(z_move(session));
return 0;
}
儘管擁有複雜型別系統與自動生成技術,zenoh-c 在 C 記憶體模型的根本限制中仍面臨挑戰,其策略坦誠面對此問題,也顯著改善傳統 C API 的安全風險。
z_drop()
後存取資料,但物件會被設為特殊 “gravestone” 狀態來幫助 Lint 錯誤z_drop()
可能導致 undefined behavior,可惜的是C 無法區分未初始化與合法內容zenoh-c 採用多項策略降低風險:
z_move()
明示所有權轉移z_internal_check()
):執行中檢驗物件有效性這些手段顯著提升安全,但無法與 Rust 編譯期 borrow checker 相提並論。目標是使正確用法易於執行,錯誤用法顯而易見,而非完全防止錯誤。
Zenoh-c 實現務實平衡,在維持熟悉用法與高效能之餘,盡可能提供最大安全保障。
儘管型別系統複雜,zenoh-c 保持零執行時額外開銷:
泛型宏系統(generic macro system) 在 C++ 可結合函式多載用法:
// C++ 可用函式多載,語法更簡潔
z_drop(std::move(session)); // 無須使用 z_move()
auto loaned = z_loan(keyexpr); // 類型自動推導
zenoh-c 綁定系統展示透過精心設計與自動化工具可在 C API 中大幅提升記憶體安全。儘管無法消除 C 本質的所有安全缺陷,但可讓正確使用更自然,錯誤明確。
專案坦誠安全挑戰與實務緩解策略,為現代系統程式語言與傳統 C 程式碼庫橋接提供可行示範。
核心啟示是:徹底的安全也許無法完美達成,但透過明確所有權、工具自動化與慎密設計能大幅提升。
C 語言根本限制仍存在部分安全挑戰,待第 3 部分將論及 zenoh-cpp 藉由 RAII、模板與 C++ 型別系統,消除更多安全風險,同時保有 Rust 實作效能。
敬請期待第 3 部分,我們將探討Zenoh 中如何靈活運用現代化的 C++ API!