C++ 特有语法糖
C++ SDK 在 C++17/C++20 标准之上,提供了一组完全可选的"语法糖"组件,集中放在 include/sugar/ 子目录、命名空间 darra::sugar 下。它们不修改主 SDK 任何头文件,完全是 free function / 独立类,按需引入即可。
设计原则
- 不修改主 SDK: 所有 sugar 都是 free function 或独立类,主 hpp 字节不变。
- 零强制依赖: 不引入新第三方库,仅用 STL(
<future><thread><optional><tuple><string_view><concepts>)。 - C++17 兼容: 默认 C++17 可用,C++20 特性(concepts)通过宏开关条件编译。
- 按需引入: 一文件一主题,也可一行
#include "sugar/sugar.hpp"全收。 - 不破坏现有 API: 所有原有 PascalCase 属性方法保留不变。
一行引入全部
#include "ethercat.hpp"
#include "sugar/sugar.hpp"
using namespace darra::ethercat;
using namespace darra::sugar;
也可以按主题单独引入:
#include "sugar/scoped.hpp" // 仅 RAII
#include "sugar/iter.hpp" // 仅 range-based for / STL 算法
#include "sugar/snapshot.hpp" // 仅快照视图
模块概览
| 头文件 | 主题 | 关键符号 |
|---|---|---|
sugar/scoped.hpp | RAII 守护 | ScopedStart / ScopedState / ScopedLockIO / MakeMaster() |
sugar/iter.hpp | 迭代视图 | SlavesView / slaves(master) |
sugar/index.hpp | 索引访问 | at() / slave_opt() / find_by_id() / find_by_name() / idx() |
sugar/snapshot.hpp | 数据快照 | SlaveSnapshot / slaves_snapshot() / master_snapshot() |
sugar/async.hpp | 异步操作 | StartAsync() / StopAsync() / BuildAsync() / WaitForState() |
sugar/tuple.hpp | 结构化绑定 | identity_tuple() / pdo_size() / wkc_stats() |
sugar/string_view.hpp | string_view 入参 | set_network() / set_eni() |
sugar/concepts.hpp | C++20 类型约束 | SlaveLike / MasterLike |
RAII 作用域守护 (scoped.hpp)
现状
EtherCATMaster 自身已实现 RAII:析构函数会自动调用 Dispose(),依次 Stop() → dll_.Dispose() → 释放从站缓存。普通用户已经不需要手动调用 Stop/Dispose。
{
EtherCATMaster master(dll);
master.SetNetwork("\\Device\\NPF_{...}").Build();
master.SetState(EcState::OP);
master.Start();
// ... 周期通信 ...
} // 离开作用域: 析构 → Dispose() → Stop() → dll_.Dispose()
ScopedStart:进入 Start, 离开 Stop
适合"临时启动一段周期通信"的场景:
master.Build();
master.SetState(EcState::OP);
{
darra::sugar::ScopedStart guard(master);
if (!guard) {
std::cerr << "Start 失败\n";
return -1;
}
// 周期通信进行中
std::this_thread::sleep_for(std::chrono::seconds(5));
}
// 自动 Stop()
如果不希望离开作用域时 Stop(例如转交给另一个对象),调用 release():
ScopedStart guard(master);
if (!guard) return;
guard.release(); // 放弃守护, 之后由用户负责 Stop
ScopedState:进入切换状态, 离开恢复
master.SetState(EcState::PreOp);
{
darra::sugar::ScopedState op(master, EcState::OP);
if (!op) {
std::cerr << "无法切到 OP\n";
return;
}
// 临时 OP, 做一段事
}
// 离开作用域自动切回 PreOp(即 ScopedState 构造时记录的 old_)
ScopedLockIO:IOmap 互斥锁守护
MutexProtection=false 的高性能模式下,用户必须手动 LockIOmap/UnlockIOmap 配对。ScopedLockIO 替代手写:
master.MutexProtection(false); // 关闭自动锁
void HighFreqCallback(uint16_t mi) {
darra::sugar::ScopedLockIO lk(master);
auto* in = master.GetSlave(1).InputDataPointer();
auto* out = master.GetSlave(1).OutputDataPointer();
// 临界区 - 离开自动 Unlock
}
MakeMaster:一行工厂
避免 Builder + Validate + Build 三步重复样板:
try {
auto master = darra::sugar::MakeMaster(dll, "\\Device\\NPF_{...}");
master.SetState(EcState::OP);
master.Start();
} catch (const darra::ethercat::DarraException& e) {
std::cerr << "MakeMaster 失败: " << e.what() << "\n";
}
冗余模式:
auto master = darra::sugar::MakeMaster(dll,
"\\Device\\NPF_{primary}",
"\\Device\\NPF_{redundant}");
range-based for / STL 算法 (iter.hpp)
内置迭代器
EtherCATMaster::SlaveIterator 已经支持 range-based for:
for (auto s : master) {
std::cout << s.SlaveNum() << " " << s.Name() << "\n";
}
注意 *it 返回 Slave 按值(轻量句柄)。
SlavesView 视图
sugar::SlavesView 提供更明确的视图语义,适合与 STL 算法配合:
#include <algorithm>
auto v = darra::sugar::slaves(master);
// 计算支持 DC 的从站数量
auto dc_count = std::count_if(v.begin(), v.end(),
[](Slave& s) { return s.HasDC(); });
// 查找首个丢失的从站
auto it = std::find_if(v.begin(), v.end(),
[](Slave& s) { return s.IsLost(); });
if (it != v.end()) {
std::cout << "丢失从站: " << it->Name() << " idx=" << it.index() << "\n";
}
// 视图大小
std::cout << "size=" << v.size() << " empty=" << v.empty() << "\n";
SlavesView::iterator 实现了 input_iterator 要求:operator* / operator-> / operator++ / operator== / operator!=,并提供 STL 期望的 value_type / iterator_category 等 typedef。
索引访问 (index.hpp)
越界检查
EtherCATMaster::GetSlave(idx) 是 lazy-create 的,越界时行为取决于底层。sugar::at() 提供显式越界检查:
try {
auto& s = darra::sugar::at(master, 99); // 越界抛 DarraException
} catch (const darra::ethercat::DarraException& e) {
std::cerr << e.what() << "\n";
// sugar::at 越界: idx=99 当前从站数=8
}
std::optional null safety
if (auto opt = darra::sugar::slave_opt(master, 1)) {
Slave& s = opt->get();
std::cout << s.Name() << "\n";
} else {
std::cout << "从站不存在\n";
}
按 ID 或名称查找
// 按 vendor:product 查找首个匹配
auto found = darra::sugar::find_by_id(master, 0x000000ABu, 0x12345678u);
if (found) std::cout << "在槽位 " << found->get().Index() << "\n";
// 按名称查找
auto el2008 = darra::sugar::find_by_name(master, "EL2008");
if (el2008) {
el2008->get().GetCoE(); // 链式调用
}
简短调用糖
auto& s = darra::sugar::idx(master, 1); // 等价 master.GetSlave(1)
数据快照 (snapshot.hpp)
Slave 类持有 dll_t& 引用,不可拷贝/不可序列化。当用户需要:
- 把当前从站状态发到 HMI / 写入日志
- 在另一个线程异步处理 (避免持有 dll_t)
- 单元测试断言
需要先"快照成纯数据"。
SlaveSnapshot POD
struct SlaveSnapshot {
uint16_t index;
std::string name;
uint32_t vendor_id;
uint32_t product_id;
uint32_t revision;
uint32_t serial_number;
uint16_t config_addr;
uint16_t alias_addr;
uint16_t input_bits;
uint16_t output_bits;
bool has_dc;
bool is_lost;
EcState state;
};
auto snaps = darra::sugar::slaves_snapshot(master);
for (auto& s : snaps) {
std::cout << "[" << s.index << "] " << s.name
<< " VID=0x" << std::hex << s.vendor_id
<< " PID=0x" << s.product_id
<< " state=" << static_cast<int>(s.state) << "\n";
}
// 跨线程传递 - 拷贝/move 安全
std::async(std::launch::async, [snaps]() {
upload_to_hmi(snaps);
});
MasterSnapshot
auto m = darra::sugar::master_snapshot(master);
log("WKC=%u/%u err_cnt=%u dc=%lld",
m.wkc, m.expected_wkc, m.error_cnt, (long long)m.dc_time_ns);
std::future 异步 (async.hpp)
EtherCATMaster::SetStateAsync 已内置返回 std::future<bool>:
auto fut = master.SetStateAsync(EcState::OP, 10000);
// ... 同时做其他工作 ...
bool ok = fut.get(); // 阻塞等待
sugar/async.hpp 补充更多异步包装。
StartAsync / StopAsync / BuildAsync
auto fut = darra::sugar::BuildAsync(master);
// UI 主线程继续刷新,避免阻塞
while (fut.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready) {
pump_ui_messages();
}
bool ok = fut.get();
SlaveSetStateAsync:单从站异步切状态
auto& s = master.GetSlave(1);
auto fut = darra::sugar::SlaveSetStateAsync(s, EcState::SafeOp, 3000);
if (!fut.get()) std::cerr << s.Name() << " 切 SafeOp 失败\n";
WaitForState:被动轮询等待
适合"由其他线程发起切状态,本线程被动等待"的场景:
auto fut = darra::sugar::WaitForState(master, EcState::OP, 5000, 50);
if (fut.get()) std::cout << "已进入 OP\n";
else std::cerr << "超时\n";
structured binding (tuple.hpp)
C++17 结构化绑定让多返回值更清晰:
// 从站身份
auto [vendor, product, rev] = darra::sugar::identity_tuple(slave);
// PDO 尺寸
auto [in_bytes, out_bytes] = darra::sugar::pdo_size(slave);
// PDO 偏移 (字节, 起始位)
auto [io, ib, oo, ob] = darra::sugar::pdo_offsets(slave);
// 主站 WKC 三件套
auto [wkc, ewkc, errs] = darra::sugar::wkc_stats(master);
// 冗余 WKC
auto [pri_wkc, sec_wkc] = darra::sugar::redundancy_wkc(master);
// 主站状态三件套
auto [st, al_err, link] = darra::sugar::state_tuple(master);
写日志/格式化时不用再频繁 master.WKC() master.ExpectedWKC() 串调一长串。
std::string_view 入参 (string_view.hpp)
SetNetwork / SetENI 原型接收 const std::string&。如果用户已经持有 std::string_view(例如配置解析得到子串),需要构造临时 std::string,写起来啰嗦。
using namespace std::string_view_literals;
darra::sugar::set_network(master, "\\Device\\NPF_{primary}"sv);
darra::sugar::set_eni(master, "C:/cfg/site.eni"sv);
Tradeoff:DLL 边界依然要 c_str() 终止符,所以 sugar 内部仍然 std::string(view) 一次拷贝;价值在调用风格统一,调用者无需手动转换。
C++20 Concepts (concepts.hpp)
仅
__cplusplus >= 202002L时启用,C++17 模式下concepts.hpp体内是空文件,不报错。
提供"鸭子类型"约束,方便用户写适配 mock / 测试桩:
#include "sugar/concepts.hpp"
template<darra::sugar::SlaveLike S>
void log_slave(const S& s) {
std::cout << s.Name() << " VID=" << s.VendorId() << "\n";
}
// 真实从站
log_slave(master.GetSlave(1));
// 用户自定义 mock
struct FakeSlave {
std::string Name() const { return "fake"; }
uint32_t VendorId() const { return 0x42; }
uint32_t ProductId() const { return 0x1234; }
};
log_slave(FakeSlave{});
SlaveLike 要求类型暴露 Name() VendorId() ProductId() 三个方法(返回值可转 std::string / uint32_t)。MasterLike 类似要求 SlaveCount() / Build()。
constexpr 枚举常量
EcState / EcALState / EcLinkState 是普通枚举,自带 constexpr 语义,无需额外语法糖:
constexpr EcState target = EcState::OP;
static_assert(target == EcState::OP);
如果需要 switch 全枚举,建议 #include <type_traits> 配合 static_cast<int> 显式转:
switch (master.State()) {
case EcState::Init: ...; break;
case EcState::PreOp: ...; break;
case EcState::SafeOp: ...; break;
case EcState::OP: ...; break;
default: ...;
}
std::function 回调 (现状)
C++ SDK 已经全面采用 std::function 作为回调载体, 通过 On*() / SetPDOCallback*() 注册:
master.Events().SetPDOCallbackSync([&](uint16_t mi) {
// Lambda 捕获 - 完全 OK
auto& s = master.GetSlave(1);
/* ... */
});
master.OnStateChanged([](EcState old_st, EcState new_st) {
std::cout << "state " << (int)old_st << " -> " << (int)new_st << "\n";
});
MasterEvents 每个事件是 多订阅者 的 std::vector<std::function<...>>, 用 On*() /
SetPDOCallbackSync() 注册 (可注册多个回调), 不能用 = 直接赋值。
回调可以是 Lambda、std::bind 结果、函数指针、Functor。这是已有特性,sugar 中无重复包装。
完整示例:所有糖组合用
#include "ethercat.hpp"
#include "sugar/sugar.hpp"
#include <algorithm>
#include <iostream>
using namespace darra::ethercat;
using namespace darra::sugar;
int main() {
dll_t dll;
if (!load_darra_dll(dll, "Darra.Core.dll")) return -1;
try {
// 1. 一行工厂 + RAII
auto master = MakeMaster(dll, "\\Device\\NPF_{...}");
// 2. structured binding 取状态
auto [st, err, link] = state_tuple(master);
std::cout << "init state=" << (int)st << "\n";
// 3. 异步切 OP
auto fut_op = master.SetStateAsync(EcState::OP, 10000);
if (!fut_op.get()) {
std::cerr << "OP 失败\n";
return -2;
}
// 4. 作用域 Start
ScopedStart run(master);
if (!run) return -3;
// 5. STL 找 DC 从站
auto v = slaves(master);
auto dc_count = std::count_if(v.begin(), v.end(),
[](Slave& s) { return s.HasDC(); });
std::cout << dc_count << " 个 DC 从站\n";
// 6. 快照上报
auto snaps = slaves_snapshot(master);
std::async(std::launch::async, [snaps]() {
// 在 worker 线程把快照发给 HMI
upload(snaps);
});
// 7. 周期回调
master.Events().SetPDOCallbackSync([&master](uint16_t mi) {
auto& s = master.GetSlave(1);
(void)s.InputDataPointer();
});
std::this_thread::sleep_for(std::chrono::seconds(10));
} catch (const DarraException& e) {
std::cerr << "异常: " << e.what() << " code=" << e.code() << "\n";
return -1;
}
// master 析构 → Stop → Dispose
return 0;
}
不在 sugar 中的内容
为避免误解,以下不属于 sugar,由主 SDK 直接提供:
- 移动语义(
EtherCATMaster(EtherCATMaster&&)):主类已实现 - 拷贝禁止:主类已
= delete std::optional<EtherCATMaster>工厂:InitializeSpecificMaster已返回- range-based for 基础版:
master.begin()/end()已可直接用 - 异常类:
DarraException在utils/dll.hpp - 自动 Dispose:
~EtherCATMaster已实现
sugar 是附加层,所有原生用法保持完整有效。
编译要求
| 头文件 | 最低标准 |
|---|---|
scoped.hpp / iter.hpp / index.hpp / snapshot.hpp / tuple.hpp | C++17 |
async.hpp | C++17 (<future> <thread> <chrono>) |
string_view.hpp | C++17 (<string_view>) |
concepts.hpp | C++20 (__cplusplus >= 202002L) — C++17 下退化为空文件 |
sugar.hpp (伞形) | C++17 起,自动跳过 concepts |
CMake 示例:
target_compile_features(my_app PRIVATE cxx_std_17)
# 如需 concepts:
# target_compile_features(my_app PRIVATE cxx_std_20)
target_include_directories(my_app PRIVATE ${SDK_PATH}/include)
引用关系图
ethercat.hpp (主 SDK 伞形, 原状态)
└── master/core.hpp (EtherCATMaster - 已 RAII / 已 begin-end / 已 SetStateAsync)
└── slave/core.hpp (Slave - 已 PascalCase 属性)
│
│ (sugar 是叠加层, 不修改任何上述文件)
▼
sugar/sugar.hpp (sugar 伞形)
├── scoped.hpp ──┐
├── iter.hpp ──┤
├── index.hpp ──┼─ 仅 #include 主 SDK 头文件 + STL
├── snapshot.hpp ──┤
├── async.hpp ──┤
├── tuple.hpp ──┤
├── string_view.hpp ──┤
└── concepts.hpp ──┘ (条件编译)
常见问答
Q: sugar 是否影响二进制 ABI? A: 不影响。所有 sugar 是 inline free function / class template,不导出符号到 DLL。
Q: 不引 sugar 是否完整可用?
A: 完整可用。ethercat.hpp 单独引就是全功能 SDK。sugar 只是写法层的便利。
Q: 跨编译器兼容性? A: MSVC 19.20+ / GCC 9+ / Clang 10+ 的 C++17 模式下均测试通过;C++20 concepts 部分需要 MSVC 19.30+ / GCC 10+ / Clang 13+。
Q: 与 C# / Java SDK 的对应关系?
A: sugar 是 C++ 独有特性。其他语言 SDK 通过本身语言机制提供等价能力(C# using / Java try-with-resources / Python with / Rust Drop / Go defer)。