在 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 是現代綁定策略的進化體,以 header-only C++ 包裝 既有的 zenoh-c 實作。這種設計有幾個明顯優勢:
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");
// 內部自動處理型別轉換
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;
}
函式庫可透過 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);
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); });
// 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;
}
};
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 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;
}
}
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));
zenoh-cpp 針對第二部分析的所有重大安全挑戰都提出了解方:
// C++: 被 std::move 過後再用會編譯錯誤
auto session = Session::open(config);
auto moved_session = std::move(session);
// session.put(...); // 編譯錯誤
moved_session.put("data", "value");
// C++: RAII 保證每物件只會析構一次
{
auto session = Session::open(config);
// 範圍結束自動釋放
}
try {
auto session = Session::open(config);
auto subscriber = session.declare_subscriber("key", callback);
if (some_condition) {
throw std::runtime_error("Error occurred");
}
} catch (...) {
// 例外發生仍自動清理資源
throw;
}
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-c 與 zenoh-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++ 抽象,展現出層層技術疊加的力量。
安全無慮:
效能無傷:
體驗升級:
zenoh-cpp 證明了系統語言的控制與效能、現代高階語言的安全性與表達力,完全可以兼得。對追求高效能分散式系統開發的 C++ 工程師而言,zenoh-cpp 是結合 Rust 動力、C 穩定、C++ 優雅設計的理想選擇。
下一個系列,我們將進入Rust/Python之間相互合作的探討,敬請期待!