iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Rust

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

Day 16: 在 Zenoh 中橋接 Rust 與 C — 第 3 部分:現代 C++ API 與 Zenoh-CPP

  • 分享至 

  • xImage
  •  

在 Zenoh 中橋接 Rust 與 C — 第 3 部分:現代 C++ API 與 Zenoh-CPP

第 1 部分中,我們探討了 zenoh-c 自動綁定生成系統背後的複雜架構;在第 2 部分則分析了 C 綁定的實務使用模式和安全性考量。雖然 zenoh-c 相較於傳統 C API 有明顯改進,但依舊受限於 C 記憶體模型的基本制約。

本篇第三部分要介紹 zenoh-cpp 如何在 zenoh-c 穩固基礎上,帶來現代、安全與人體工學兼具的 C++ API。
善用 C++ 的 RAII、模板和型別系統,zenoh-cpp 不僅減少了第二部裡討論的許多安全顧慮,還讓 C++ 開發更直觀自然。

zenoh-cpp 方法論:現代 C++ 立基於穩固的 C 基礎

架構概覽

zenoh-cpp 是現代綁定策略的進化體,以 header-only C++ 包裝 既有的 zenoh-c 實作。這種設計有幾個明顯優勢:

  • 零建構依賴:僅需鏈結 zenoh-c
  • 大量模板:充分利用 C++17 模板以達型別安全
  • 編譯期最佳化:極大化內聯與模板特化
  • 標頭檔即用、易於整合:能輕鬆加入既有 C++ 專案

RAII 革命

zenoh-cpp 最大的改變,就是全面淘汰了 C API 下必須自行管理記憶體的痛點。以往 zenoh-c 需明確呼叫 z_drop() 並追蹤所有權,zenoh-cpp 則藉由 RAII(資源取得即初始化)自動管理資源。

C API - 手動資源管理:

// C 需明確釋放
z_owned_session_t session;
z_open(&session, z_move(config));
z_owned_subscriber_t sub;
z_declare_subscriber(&sub, z_loan(session), z_keyexpr("demo/**"),
                    z_move(callback), NULL);
// 釋放順序很容易搞錯
z_drop(z_move(sub));
z_drop(z_move(session));

C++ API - 自動資源管理:

// C++ 全程自動
{
    auto session = Session::open(std::move(config));
    auto subscriber = session.declare_subscriber("demo/**", on_sample);
    // 離開區塊自動釋放,無須手動管理、不會漏釋放
}

模板為核心的架構

zenoh-cpp 的設計以高階模板類別為中心,對 C API 提供型別安全與零額外負擔的包裝。

主要基礎類別

函式庫內有兩種主要的基礎模板類別:

// 可複製的 C 結構(配置、選項)
template <typename ZC_COPYABLE_TYPE>
class Copyable {
protected:
    ZC_COPYABLE_TYPE _0;
    // 提供安全存取原始 C 結構
};
// 具所有權(sessions, publishers, subscribers)
template <typename ZC_OWNED_TYPE>
class Owned {
protected:
    ZC_OWNED_TYPE _0;
    // 自動 RAII 與移動語意
    ~Owned() { ::z_drop(::z_move(_0)); }
};

這些模板自動確保所有 Zenoh 物件能正確被生命週期管理,防止各類記憶體安全問題。

型別安全的互通層

進階互通層實現 C/C++ 物件間的自動轉換:

// C++ 物件自動轉為 C API
template <class OwnedType>
const auto* as_loaned_c_ptr(const Owned<OwnedType>& cpp_obj) {
    return ::z_loan(*as_owned_c_ptr(cpp_obj));
}
// 用法自然且透明
auto session = Session::open(config);
auto publisher = session.declare_publisher("demo/topic");
// 內部自動處理型別轉換

現代 C++ 特性與開發者體驗

例外機制與明確錯誤碼

zenoh-cpp 提供 C++ 風格(預設)與明確錯誤回傳兩種錯誤處理形式:

// 例外處理(推薦)
try {
    auto session = Session::open(std::move(config));
    auto publisher = session.declare_publisher("demo/topic");
    publisher.put("Hello, World!");
} catch (const ZException& e) {
    std::cerr << "Zenoh error: " << e.what() << std::endl;
}
// 明確錯誤回傳(如偏好顯式流程)
ZResult err;
auto session = Session::open(std::move(config), SessionOptions::create_default(), &err);
if (err != Z_OK) {
    std::cerr << "Failed to open session: " << static_cast<int>(err) << std::endl;
    return -1;
}

編譯器型別檢查的 Builder 寫法

函式庫可透過 API 與編譯期檢查防止誤用:

Session::GetOptions options;
options.target = QueryTarget::Z_QUERY_TARGET_ALL;
options.timeout_ms = 5000;
options.consolidation = ConsolidationMode::Z_CONSOLIDATION_MODE_NONE;
session.get(selector, std::move(options), on_reply, on_done);

Lambda 與現代回呼機制

zenoh-cpp 支援 lambda 與各式回呼整合:

// 支援帶捕獲的 Lambda
std::atomic<size_t> counter{0};
auto on_sample = [&counter](const Sample& sample) {
    counter++;
    std::cout << "Received #" << counter << ": "
              << sample.get_payload().as_string() << std::endl;
};
auto subscriber = session.declare_subscriber("sensor/**", on_sample);

// 也可用函式物件與成員指標
class MessageHandler {
public:
    void handle_sample(const Sample& sample) { /* ... */ }
};
MessageHandler handler;
auto subscriber2 = session.declare_subscriber("control/**",
    [&handler](const Sample& sample) { handler.handle_sample(sample); });

基於 Channel 的串流界面

// FIFO channel 處理有序訊息
auto [sender, receiver] = channels::FifoChannel<Sample>::create(1000);
auto subscriber = session.declare_subscriber("data/**",
    channels::closure(sender));
while (running) {
    auto result = receiver.try_recv();
    if (result.has_value()) {
        process_sample(result.value());
    } else if (result.error() == channels::RecvError::Z_DISCONNECTED) {
        break;
    } else {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

進階功能:序列化與型別安全

通用序列化框架

zenoh-cpp 內建泛型序列化機制,直接支援 STL 容器型別:

// 複雜物件自動序列化
std::unordered_map<uint64_t, std::string> sensor_data = {
    {1001, "temperature: 23.5°C"},
    {1002, "humidity: 45%"},
    {1003, "pressure: 1013.25 hPa"}
};
auto payload = ext::serialize(sensor_data);
publisher.put(std::move(payload));
// 另一端可自動解出
auto on_sample = [](const Sample& sample) {
    try {
        auto data = ext::deserialize<std::unordered_map<uint64_t, std::string>>(
            sample.get_payload());
        for (const auto& [id, reading] : data) {
            std::cout << "Sensor " << id << ": " << reading << std::endl;
        }
    } catch (const std::exception& e) {
        std::cerr << "Deserialization error: " << e.what() << std::endl;
    }
};

STL 相容與零複製

zenoh-cpp 自然支援 STL 容器並維持 zero-copy:

std::vector<double> measurements = get_sensor_readings();
publisher.put(ext::serialize(measurements));
auto on_sample = [](const Sample& sample) {
    auto slice = sample.get_payload().as_slice();
    process_raw_data(slice.start, slice.len);
};

實用範例:C 與 C++ API 對照

簡易發布/訂閱

C API:

#include "zenoh.h"
void data_handler(const z_loaned_sample_t* sample, void* context) {
    z_owned_string_t key_str;
    z_keyexpr_to_string(z_sample_keyexpr(sample), &key_str);
    printf("Received: %.*s => %.*s\n",
           (int)z_string_len(z_loan(key_str)), z_string_data(z_loan(key_str)),
           (int)z_bytes_len(z_sample_payload(sample)),
           z_bytes_data(z_sample_payload(sample)));
    z_drop(z_move(key_str));
}
int main() { ... }

C++ API:

#include "zenoh.hxx"
using namespace zenoh;
int main() {
    try {
        auto session = Session::open(Config::create_default());
        auto subscriber = session.declare_subscriber("demo/**",
            [](const Sample& sample) {
                std::cout << sample.get_keyexpr().as_string_view() << " => "
                          << sample.get_payload().as_string() << std::endl;
            });
        session.put(KeyExpr("demo/test"), "Hello from C++!");
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return 0;
    } catch (const ZException& e) {
        std::cerr << "Zenoh error: " << e.what() << std::endl;
        return -1;
    }
}

Query/Queryable Pattern

C API(簡化):

void query_handler(const z_loaned_query_t* query, void* context) {
    z_owned_string_t response;
    z_string_copy_from_str(&response, "Response from C");
    z_query_reply(query, z_query_keyexpr(query), z_move(response), NULL);
}
// ...完整設置與手動管理

C++ API:

auto queryable = session.declare_queryable("demo/service/**",
    [](const Query& query) {
        query.reply(query.get_keyexpr(), "Response from C++");
    });
session.get("demo/service/info", "",
    [](const Reply& reply) {
        if (reply.is_ok()) {
            std::cout << "Reply: " << reply.get_ok().get_payload().as_string()
                      << std::endl;
        }
    }, std::move(options));

對 C API 的安全性改善

zenoh-cpp 針對第二部分析的所有重大安全挑戰都提出了解方:

1. 消除 Use After Free

// C++: 被 std::move 過後再用會編譯錯誤
auto session = Session::open(config);
auto moved_session = std::move(session);
// session.put(...);  // 編譯錯誤
moved_session.put("data", "value");

2. 防止 Double Drop

// C++: RAII 保證每物件只會析構一次
{
    auto session = Session::open(config);
    // 範圍結束自動釋放
}

3. 杜絕資源外洩

try {
    auto session = Session::open(config);
    auto subscriber = session.declare_subscriber("key", callback);
    if (some_condition) {
        throw std::runtime_error("Error occurred");
    }
} catch (...) {
    // 例外發生仍自動清理資源
    throw;
}

4. 編譯期管理生命週期,防止懸掛參考

class SafeSubscriber {
    Session session_;
    std::unique_ptr<Subscriber> subscriber_;
public:
    SafeSubscriber() : session_(Session::open(Config::create_default())) {
        subscriber_ = std::make_unique<Subscriber>(
            session_.declare_subscriber("data/**", callback));
    }
    // 析構順序自動正確:先 subscriber_,後 session_
};

效能:零額外負擔的抽象

模板驅動的零成本包裝

// 此 C++ ...
auto session = Session::open(config);
session.put(KeyExpr("demo/test"), "Hello");
// 產生的組合語碼與 ...
z_owned_session_t session;
z_open(&session, z_move(config));
z_owned_bytes_t payload;
z_bytes_from_static_str(&payload, "Hello");
z_put(z_loan(session), z_keyexpr("demo/test"), z_move(payload), NULL);
z_drop(z_move(session));
// ... 完全相同

編譯期最佳化

大量模板與 inline 實作意謂著:

  • 泛型操作全於編譯期展開
  • 單純操作不會有任何函式調用額外負擔
  • 特化能最佳化特定型別的效能
  • 移動語意省去一切不必要複製

記憶體效率

// C++ 物件與 C 結構完全同尺寸
static_assert(sizeof(Session) == sizeof(z_owned_session_t));
static_assert(alignof(Session) == alignof(z_owned_session_t));
// 只是介面不同,底層同一份資料

雙後端支援

zenoh-cpp 支援 zenoh-czenoh-pico 兩種後端(條件編譯):

#ifdef ZENOHCXX_ZENOHC
    auto session = Session::open(config);
    auto advanced_subscriber = session.ext().declare_advanced_subscriber(keyexpr);
#elif ZENOHCXX_ZENOHPICO
    auto session = Session::open(config);  // 同樣 API, 但底層不同
    auto subscriber = session.declare_subscriber(keyexpr, callback);
#endif

同份 C++ 代碼可部署於全功能後端或嵌入式場景。

小結

zenoh-cpp 結合 Rust 核心效能、C 綁定的成熟穩定,以及 C++ 的現代安全性和表達力。從 Rust-C 綁定自動生成、C API 安全設計到現代 C++ 抽象,展現出層層技術疊加的力量。

安全無慮:

  • RAII 根絕資源外洩與重複釋放
  • 移動語意防止 use-after-free
  • 模板定義於編譯期抓錯
  • 例外處理也能自動回收資源

效能無傷:

  • 零成本模板抽象
  • 編譯期展開與內聯
  • 操作直接對應 C API
  • 無多餘runtime 開銷

體驗升級:

  • C++ 標準慣用法與 STL 整合
  • 型別安全,自動資源管理
  • Lambda 友好,程式簡潔可讀

zenoh-cpp 證明了系統語言的控制與效能、現代高階語言的安全性與表達力,完全可以兼得。對追求高效能分散式系統開發的 C++ 工程師而言,zenoh-cpp 是結合 Rust 動力、C 穩定、C++ 優雅設計的理想選擇。

下一個系列,我們將進入Rust/Python之間相互合作的探討,敬請期待!


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

尚未有邦友留言

立即登入留言