v1.2.0
rust-ethernet-ip v1.2.0
A behavioral-fix, hardening, and additive-surface release. No breaking changes
to the public Rust API (verified by cargo-semver-checks: 195/195 checks pass
against the 1.1.0 baseline, across the main crate and all four sibling crates).
MSRV remains Rust 1.96. Upgrading from 1.1.0 is a drop-in version bump for
Rust, C#, and Python consumers.
C FFI ABI note: the ABI version moves to 2 — three unusable
*mut EipClient raw-pointer exports were removed and eip_abi_version() was
bumped. The shipped C#/Python packages are unaffected (they import only the
_by_id exports and move in lockstep with the native library); a hypothetical
third-party direct C consumer fails fast at the version handshake instead of
misbehaving. The new include/rust_ethernet_ip.h header is the supported C/C++
contract.
Validated end-to-end across all four bindings against CompactLogix hardware
(5069-L330ERM fw38): 2304/2304 reads, 2285/2285 writes, 2285/2285 verify,
0 anomalies — the full-coverage gate now writes and verifies STRING tags,
including STRING members inside UDTs and UDT array elements.
Install
# Rust — Cargo.toml
rust-ethernet-ip = "1.2.0"<!-- C# -->
<PackageReference Include="RustEtherNetIp" Version="1.2.0" /># Python
pip install rust-ethernet-ip==1.2.0/* C/C++ — link the cdylib and include the shipped header */
#include "rust_ethernet_ip.h"✨ Highlights
- Custom Logix string types write and read. Studio 5000 users routinely
define their own string types (own name and length, e.g.Str82,Str400).
Previous releases hardcoded the built-inSTRINGstructure handle (0x0FCE),
so writes to custom string types were rejected with CIP0x2107and were
misdocumented as firmware-blocked. The library now discovers the target tag's
real structure handle and sizes the payload to the target type — the normal
write_tag/WriteString/write_tag/write_stringAPIs work for
built-in and custom string types alike, standalone or inside UDTs and UDT
array elements, in controller and program scope. Newread_string_tag(Rust)
andClient.read_string(Python) decode any string-shaped structure to text.
Seedocs/STRING_HANDLING.md. - CIP fragmentation (Read/Write Tag Fragmented,
0x52/0x53). Strings and
structures larger than one CIP packet (~490-byte request / ~500-byte reply on
the validated target) are now read by reassembling fragments and written by
splitting into fragments — including whole large-UDT array-element reads. - Packet-size-aware batch grouping. Batch operations now split by both
operation count and the actual packet-size budget (measured from the real
Multiple Service Packet + Unconnected Send framing). Fixes large batch reads
failing wholesale with EtherNet/IP status0x65(Invalid Length). - First-class C/C++ consumer support. A checked-in C99 header
(include/rust_ethernet_ip.h, CI-enforced parity with the exported symbol
table), a CMake RAII example (examples/cpp/), a manifest-driven C++
full-coverage exerciser, and a Qt integration guide (docs/CPP_INTEGRATION.md). - Transport & session hardening. Timeout desynchronization fixes,
sender-context correlation, shared session handles, and lag-tolerant
subscription/fleet event forwarding (stop()stops, abandoned consumers can't
wedge polling). - Tag-addressing correctness. Member-suffix preservation, bit-syntax fixes,
batch BOOL-array handling,.DATA[i]segments, and program-tag discovery. - Honest diagnostics. Operation/error counters are real per-client atomics;
placeholder system metrics are explicitly flagged; never-worked public
STRING/UDT paths return explicit unsupported errors instead of failing
mysteriously (deprecated, removal queued for 2.0).
🔧 Fixed
- STRING wire format: writes emit the documented Logix structure encoding
(0x02A0+ handle + padded payload); standard STRING reads decode to
PlcValue::String. The "firmware blocks STRING writes" claim is retired —
the historical0x2107was a structure-handle/type mismatch. - Scalar UDT-array-element member writes (e.g.
MyUdtArray[0].Speed) write
directly when the full member path is preserved. read_array_rangeandget_tag_attributeswire fixes; the in-process
simulator was made an oracle rather than a mirror of library bugs.- C# wrapper:
WriteUdtMemberdeadlock, UTF-8 marshalling, keep-alive
serialization; native P/Invoke integration tests. - UDT crate strictness: truncated member ranges and missing member values now
fail closed instead of silently skipping/zero-filling.
📦 Release contents
- crates.io:
rust-ethernet-ip1.2.0 plus sibling crates
(-types,-tag-path,-protocol,-udt) at 1.2.0. - NuGet:
RustEtherNetIp1.2.0 (win-x64 / linux-x64 / osx-arm64). - PyPI:
rust-ethernet-ip1.2.0 (Windows / macOS / manylinux wheels + sdist).
Known limits
- A single string read/write must fit one CIP packet unless the fragmented path
applies; practical custom-string maximum is ~400–450 bytes ofDATA
depending on tag-path length (Str400works in every scope on the validated
target). Details and per-scope table:docs/STRING_HANDLING.md. - Whole-UDT array-element writes as a single structure are not supported —
update members individually. Whole-element reads work (fragmented when
large). read_tagreturns custom string types asPlcValue::Udt; use
read_string_tagwhen the tag is known to be a string.
Validation
- Cross-binding full-coverage hardware run (Rust/C#/Python/C++), CompactLogix
5069-L330ERM fw38: 2304 reads / 2285 writes / 2285 verify / 0 anomalies, with
STRING members (built-in and custom types) written and verified. - Evidence:
docs/validation/2026-07-08_cross-binding_full-coverage_5069-L330ERM_fw38.md.
Full Changelog: v1.1.0...v1.2.0