iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
Rust

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

Day 15: 在 Zenoh 中橋接 Rust 與 C — 第 2 部分:使用模式與安全性

  • 分享至 

  • xImage
  •  

在 Zenoh 中橋接 Rust 與 C — 第 2 部分:使用模式與安全性

第 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:將 Ownership 帶入 C

Zenoh-c 透過一套謹慎設計的 type system,將 Rust 的所有權概念映射為 C 約定,這是現行帶入 memory safety 至 C API 的最複雜嘗試之一。

型別分類

Zenoh-c 將型別組織為多種 distinct categories,每個在所有權模型中擔任特定角色:

  • Owned types (z_owned_xxx_t):代表專有擁有權的資源,類似 Rust 的 owned 值
  • Loaned types (z_loaned_xxx_t):代表借用引用,等同於 Rust 的 &T
  • Moved types (z_moved_xxx_t):函式參數中表達所有權轉移
  • View types (z_view_xxx_t):堆疊分配的檢視物件,不須顯式清除
  • Option structures (z_xxx_options_t):操作的配置參數
  • Enums 及 plain data types (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 物件(無需清理)

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,離開作用域自動處理

函式呼叫中移動語義(Move Semantics)

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 參數中取回擁有值,令所有權轉移顯式。


從 Loaned 參考取得所有權

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的操作來說可謂是相當靈活的!


進階功能與範例

Closure 支援

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 的安全風險。

尚存安全性的問題

  1. Use After Free :C 端仍可能在呼叫 z_drop() 後存取資料,但物件會被設為特殊 “gravestone” 狀態來幫助 Lint 錯誤
  2. Invalid Drop :對未初始化物件呼叫 z_drop() 可能導致 undefined behavior,可惜的是C 無法區分未初始化與合法內容
  3. Resource Leaks :物件覆寫前未正確釋放,導致資源遺漏,因 C 並無無自動解構子

解法

zenoh-c 採用多項策略降低風險:

  • Gravestone Pattern:被釋放物件標示為可辨識的狀態,方便偵錯
  • Type-Based Safety:型別系統透過靜態檢查與明確所有權語意降低錯誤
  • Clear Ownership Transfer:利用 z_move() 明示所有權轉移
  • 驗證函式 (z_internal_check()):執行中檢驗物件有效性
  • 完善測試:覆蓋常見錯誤與一致性驗證
  • 清晰文件與範例:協助開發者遵守正確用法

C 語言安全的現實

這些手段顯著提升安全,但無法與 Rust 編譯期 borrow checker 相提並論。目標是使正確用法易於執行,錯誤用法顯而易見,而非完全防止錯誤。

Zenoh-c 實現務實平衡,在維持熟悉用法與高效能之餘,盡可能提供最大安全保障。


效能與人體工學

零額外開銷抽象

儘管型別系統複雜,zenoh-c 保持零執行時額外開銷:

  • 編譯時解析:泛型宏轉譯為直接函式調用
  • 內聯操作:move 與 loan 減少為指標算術
  • 不需 boxing:opaque types 與 Rust 原型大小和對齊相同

在 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!


上一篇
Day 14: 在 Zenoh 中橋接 Rust 與 C — 第 1 部分:架構與程式碼生成
下一篇
Day 16: 在 Zenoh 中橋接 Rust 與 C — 第 3 部分:現代 C++ API 與 Zenoh-CPP
系列文
30 天玩轉 Zenoh:Rust 助力物聯網、機器人與自駕的高速通訊19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言