Rust 特有语法糖
本章列出 Darra EtherCAT Rust SDK 在 crate::sugar 模块下提供的 Rust 特有语法糖.
这些 API 完全可选, 不引入任何破坏性变更, 不影响向后兼容. 只要在文件顶部加一行:
use darra_ethercat::sugar::prelude::*;
就能解锁所有便利写法. 本章按主题组织, 每节给出 标准 API ↔ 语法糖 的对照, 并说明何时该用语法糖, 何时直接用主 API.
1. 迭代器 (SlaveIterExt)
1.1 标准 API
主 SDK 已经为 [EtherCATMaster] 实现 IntoIterator, 可以直接 for s in &master:
let m = EtherCATMaster::new()?;
for slave in &m {
println!("{}", slave.name());
}
slaves() 方法也能把所有从站收成 Vec<Slave>:
let all: Vec<Slave> = m.slaves();
1.2 语法糖: 链式过滤
加上 use darra_ethercat::sugar::prelude::*; 后, 任何 Iterator<Item = Slave>
都自动获得四个领域感知方法:
| 方法 | 用途 |
|---|---|
.by_state(EcState::Operational) | 按 EtherCAT 状态过滤 |
.by_group(0) | 按从站组过滤 (0..=7) |
.by_vendor(0x00000002) | 按厂商 ID 过滤 |
.with_dc() | 仅保留 DC 同步从站 |
.filter_slave(|s| ...) | 任意自定义谓词, 签名比 Iterator::filter 明确 |
use darra_ethercat::sugar::prelude::*;
use darra_ethercat::EcState;
// 在 OP 状态的 Beckhoff 从站
let beckhoff_op: Vec<_> = m.iter()
.by_state(EcState::Operational)
.by_vendor(0x00000002)
.collect();
// 第 0 组里有 AL 错误的从站
let bad: Vec<_> = m.iter()
.by_group(0)
.filter_slave(|s| {
s.detailed_info()
.map(|d| d.al_status_code != 0)
.unwrap_or(false)
})
.collect();
1.3 何时用?
- 链式过滤多于 1 步 → 用语法糖, 一行成型
- 只过滤一次 → 直接
Iterator::filter也行, 习惯哪种用哪种 - 极致性能 (微秒级 PDO 回调内) → 不要用迭代器, 直接
for i in 1..=countmaster.slave(i)避免任何中间 trait 解糖开销
2. 索引 (Indexable)
2.1 标准 API
主 SDK 用 slave(i) 拿从站句柄:
let s1 = m.slave(1);
let s2 = m.slave(2);
2.2 语法糖: master.indexable()[i]
由于 Slave 是 Copy 句柄 (不持有资源, 仅是 (master_idx, slave_idx) 二元组),
SDK 可以为它提供 Index 风格的访问:
use darra_ethercat::sugar::prelude::*;
let view = m.indexable();
let s1 = &view[1]; // 等价 m.slave(1)
let s2 = &view[2u16]; // 同时支持 usize 和 u16 索引
println!("{}", view.len());
// 越界会 panic, 想要安全访问请用下文的 try_slave
2.3 实现说明
Indexable<'a> 内部缓存了 1..=N 的从站句柄, 与原 &'a EtherCATMaster 共享生命周期.
不缓存到主 EtherCATMaster 是为了避免破坏现有 &self 不可变方法的调用模式
(否则要全改 &mut self).
2.4 何时用?
- 演示/教学/REPL → 用
view[i]简洁直观 - 生产代码 → 推荐
try_slave(i)?(见 §4), 越界变错误而不是 panic - PDO 紧循环 → 仍然是
m.slave(i)直调最快
3. RAII (Drop / IomapGuard)
3.1 已经实现
主 SDK 自带的 RAII 不需要 sugar 模块, 列出供参考:
{
let m = EtherCATMaster::new()?;
m.set_network("\\Device\\NPF_{...}", "")?;
// ... 使用 ...
} // ← 此处 Drop 自动 Stop + Dispose, 无须手动调用
IomapGuard 同样是 RAII 包装:
{
let _guard = m.iomap_guard(); // 加锁
// ... 安全读写 IO ...
} // ← guard drop 时自动解锁
3.2 注意
- 不要
mem::forget(master)— 会泄漏 DLL 资源 MasterBuilder::build()返回的BuildResult实现Deref<Target = EtherCATMaster>, 也享受 RAII
3.3 sugar 模块未触碰的部分
Drop 由主类型保证, sugar 不重复实现. IomapGuard 同理.
4. 错误传播 (? 友好)
4.1 标准 API
let s = m.slave(1); // 永不失败 (越界拿到的是悬空句柄)
let count = m.slave_count();
if 1 > count {
return Err(/* 自己拼错误 */);
}
4.2 语法糖: try_slave / slave_opt
use darra_ethercat::sugar::prelude::*;
use darra_ethercat::Result;
fn use_first(m: &EtherCATMaster) -> Result<()> {
let s = m.try_slave(1)?; // 越界自动返回 Err
println!("vendor: 0x{:08X}", s.vendor_id());
Ok(())
}
// 或选择 Option 路径
if let Some(s) = m.slave_opt(1) {
println!("{}", s);
}
4.3 状态切换的 Result 包装
主 API set_state 要求 &mut self, 不利于在共享场景下直接 ?:
// 标准 API (需要 &mut)
master.set_state(EcState::Operational)?;
sugar 版用 &self 直接走 FFI, 配合 ?:
use darra_ethercat::sugar::prelude::*;
fn ramp_up(m: &EtherCATMaster) -> Result<()> {
m.try_set_state(EcState::PreOp, 5000)?;
m.try_set_state(EcState::SafeOp, 5000)?;
m.try_set_state(EcState::Operational, 5000)?;
Ok(())
}
4.4 注意
try_set_state 不会自动启动 PDO 线程 (副作用只在 &mut self 版的
set_state 中触发). 如果要进 SafeOp/OP 还想要 PDO 跑起来, 用主 API:
master.set_state(EcState::Operational)?; // 自动调用 Start()
5. 状态变化通道 (mpsc::channel)
5.1 标准 API: 回调
let m = EtherCATMaster::new()?;
let events = m.events();
events.on_slave_state_changed(|master, slave, old, new| {
println!("slave {}: {} -> {}", slave, old, new);
});
回调写法的痛点:
- 多线程消费要自己
Arc<Mutex<...>> - 想做事件历史回放 / 测试断言, 要自己缓存
- 跨 await 边界 (异步代码) 难处理
5.2 语法糖: state_stream() 返回 Receiver
use darra_ethercat::sugar::prelude::*;
let m = EtherCATMaster::new()?;
let rx = m.state_stream(); // mpsc::Receiver, 容量 1024
std::thread::spawn(move || {
for ev in rx {
println!("从站 {}: {} -> {}", ev.slave, ev.old_state, ev.new_state);
}
});
5.3 设计要点
- 容量 1024 (可配置), 满时丢弃最新事件 (try_send), 不阻塞实时线程
MasterStateStream实现IntoIterator, 直接for ev in rx- 也能
rx.recv()阻塞 /rx.try_recv()非阻塞 - 多次调用
state_stream()返回独立的 channel, 各自接收一份事件
5.4 紧急事件 (EMCY) 同款
let emcy = m.emergency_stream();
for ev in emcy {
println!("EMCY {}: code=0x{:04X} reg=0x{:02X}",
ev.slave, ev.error_code, ev.error_register);
}
EMCY 默认 unbounded — 这类事件量级低, 不会触发实时线程压力.
6. 异步流 (tokio, 仅 async-tokio feature)
6.1 启用方式
Cargo.toml:
[dependencies]
darra-ethercat-master = { version = "2.5", features = ["async-tokio"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync"] }
注意 tokio 必须开 sync feature (async-tokio 内部用 tokio::sync::mpsc; crate 已在 tokio 依赖中声明 sync,无需用户额外配置 crate 侧 feature)。
6.2 用法
use darra_ethercat::sugar::prelude::*;
use darra_ethercat::EtherCATMaster;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let m = EtherCATMaster::new()?;
let mut stream = m.state_stream_async();
while let Some(ev) = stream.recv().await {
println!("从站 {}: {} -> {}", ev.slave, ev.old_state, ev.new_state);
}
Ok(())
}
6.3 与同步版的关系
state_stream()(同步) 和state_stream_async()(异步) 互不影响, 可同时用- 异步版基于
tokio::sync::mpsc::unbounded_channel, 满时不会丢事件 (但会占内存) - 如果希望对接
futures::Streamtrait, 用into_inner()拿到UnboundedReceiver, 再tokio_stream::wrappers::UnboundedReceiverStream::new(rx)即可
6.4 何时用?
- 整个程序已经是 tokio runtime → 用异步版本, 不再起新线程
- 只有事件消费需要异步 → 同步
state_stream()+ tokiospawn_blocking也能凑合, 但不优雅 - 不用 tokio → 只用同步版, 不要启用
async-tokiofeature, 减少依赖
7. derive 派生 / 类型转换
7.1 已有派生
主 SDK 给绝大部分数据结构都派生了 Clone, Debug, Copy, PartialEq, Eq, Default.
如 SlaveIdentity / EsmTimeouts / RedundancyStatus / CommunicationStats 都是
#[derive(Debug, Clone, Copy)].
7.2 sugar 增补的 From 转换
| 源类型 | 目标类型 | 用途 |
|---|---|---|
&Slave | SlaveIdentity | let id: SlaveIdentity = (&slave).into(); 一键提身份 |
Slave | SlaveIdentity | 同上, owned 版本 |
u8 | EcState | 从 raw 字节直拿枚举 |
EcState | u8 / i32 | 写回 DLL 接口 |
u8 | LinkState | raw 字节直拿枚举 |
i32 | RedundancyState | DLL 返回值直拿枚举 |
i32 | CiA402State | 同上 |
EcState / LinkState / RedundancyState | &'static str | 日志/格式化 |
use darra_ethercat::sugar::prelude::*;
let s = m.slave(1);
// 一键提身份, 用于配置比对
let id: SlaveIdentity = (&s).into();
println!("{}", id); // Vendor=0x00000002 Product=0x044C2C52 Rev=...
// raw -> 枚举 (例如解析 PDO 帧字节)
let st: EcState = 0x08u8.into();
assert_eq!(st, EcState::Operational);
// 枚举 -> 静态字符串 (无堆分配)
let label: &'static str = EcState::PreOp.into();
assert_eq!(label, "PreOp");
7.3 何时用?
- 写日志 / metrics 标签 →
&'static str转换零成本, 优于format! - DLL 边界转换 →
EcState::from(raw_byte)比EcState::from_raw(raw_byte).unwrap_or(...)短
8. Display 增强
8.1 sugar 补齐的 Display
主 SDK 已为 EcState / DarraError / EcPortType 等实现 Display. sugar 补齐:
| 类型 | 输出例 |
|---|---|
LinkState | Connected / Redundancy |
RedundancyState | PrimaryOnly / Both |
CiA402State | OperationEnabled / Fault |
CiA402Mode | CSP / PP |
EcALState | NoError / InvalidStateChange / AL(0x00XX) 兜底 |
SlaveIdentity | Vendor=0x... Product=0x... Rev=0x... Serial=0x... |
8.2 用法
use darra_ethercat::sugar::prelude::*;
use darra_ethercat::data::error::CiA402State;
use darra_ethercat::data::types::EcALState;
println!("链路: {}", m.link_status()); // 链路: Connected
println!("驱动: {}", CiA402State::OperationEnabled); // 驱动: OperationEnabled
println!("ALSC: {}", EcALState::SyncError); // ALSC: SyncError
8.3 与 Debug 的区别
| trait | 输出风格 | 何时用 |
|---|---|---|
Debug ({:?}) | 完整字段, 适合排错 | 单元测试断言 / 打印整个结构 |
Display ({}) | 简短可读, 适合最终用户 | 日志 / GUI / 报警消息 |
9. Builder 模式
9.1 已有 Builder
主 SDK 已经实现了 MasterBuilder, sugar 模块不重复造:
let result = EtherCATMaster::builder()
.set_eni("config.xml")
.enable_auto_startup()
.build()?;
println!("从站数量: {}", result.slave_count());
BuildResult 实现 Deref<Target = EtherCATMaster>, 直接当 master 用.
9.2 sugar 模块未触碰
我们认为现有 builder 已经覆盖大部分场景, 不必重新造一个 sugar 版本. 若需要把
builder 链式继续拓展 (例如 with_redundancy(...) / with_callback(...) 等),
应该在主 master/core.rs 里加方法, 而不是 sugar.
10. 完整示例: 综合用法
use darra_ethercat::sugar::prelude::*;
use darra_ethercat::{EtherCATMaster, EcState, Result};
fn main() -> Result<()> {
let mut m = EtherCATMaster::builder()
.set_eni("config.xml")
.enable_auto_startup()
.build()?
.master;
// 启动状态流 (后台线程消费)
let rx = m.state_stream();
std::thread::spawn(move || {
for ev in rx {
log::info!("[state] slave {} {} -> {}", ev.slave, ev.old_state, ev.new_state);
}
});
// 进 OP
m.set_state(EcState::Operational)?;
// 索引 + 错误传播
let s = m.try_slave(1)?;
println!("{}", s);
// 链式过滤: 列出所有 OP 状态的 Beckhoff 从站
let beckhoff_op: Vec<_> = m.iter()
.by_state(EcState::Operational)
.by_vendor(0x00000002)
.collect();
println!("Beckhoff OP 从站数: {}", beckhoff_op.len());
// RAII 自动 Stop + Dispose
Ok(())
}
附录: 模块文件索引
| 文件 | 职责 |
|---|---|
src/sugar/mod.rs | 模块入口, re-export 所有子模块 |
src/sugar/prelude.rs | 一行 use 引入全部语法糖 |
src/sugar/index.rs | Indexable + MasterIndexExt |
src/sugar/conversions.rs | 跨类型 From / Into |
src/sugar/display.rs | LinkState / RedundancyState / CiA402State 等 Display 补齐 |
src/sugar/state_stream.rs | mpsc 状态流 + 紧急流 |
src/sugar/iter_ext.rs | SlaveIterExt (by_state / by_group / by_vendor / with_dc) |
src/sugar/try_slave.rs | try_slave / slave_opt / try_set_state |
src/sugar/async_stream.rs | tokio 异步流 (async-tokio feature) |
附录: 与其它语言 SDK 的差异
| SDK | 等价于本章语法糖的能力 |
|---|---|
| C# | LINQ (.Where(...).Select(...)), IDisposable, event |
| Java | Stream API, AutoCloseable, Listener interface |
| Python | 内置迭代, context manager (with), callback |
| Rust | 本章 sugar 模块: Iterator + adapter / Drop / mpsc::channel |
Rust 的语法糖建立在 零成本抽象 上 — 上述所有 adapter 在编译期都被 inline 展开, 运行期开销与手写循环等价. C# / Java / Python 的对应物多少有装箱/分配/虚调用开销, 而 Rust sugar 不引入任何额外指令.