Compare commits

..

3 Commits

Author SHA1 Message Date
Joseph Doherty ceeaeefa71 [F52.3] mxaccess-codec: caller-supplied scratch buffer for write encoder
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Adds `write_message::encode_into_bytes_mut` (and the timestamped
variant) which writes the encoded body into a caller-supplied
`BytesMut`. The buffer is cleared and resized in place each call;
once it has grown to the largest body the session will produce, it
allocates nothing further.

A session that holds a single `BytesMut` and reuses it across writes:

  - Int32 / Float32 / Float64: 2 → 1 allocs/op
    (only the `encode_scalar_value` scratch `Vec<u8>` remains)
  - Boolean: 1 → 0 allocs/op
    (no per-value scratch — the literal payload is a stack `[u8; 4]`)

Bench delta in `design/M6-bench-baseline.md` § F52.3. The
`encode_scalar_value` Vec is the remaining 1 alloc/op for fixed-width
scalars; eliminating it would require inlining the LE-bytes write
into the body slice (left for a follow-up since the F52 spec only
asks for 2 → 1).

Resolves F52 (all three optimisations landed: 4e76b44 F52.1,
a0fa5be F52.2, this commit F52.3). Existing `encode` / `encode_to_bytes_mut`
public surface unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:53:07 -04:00
Joseph Doherty a0fa5bedfd [F52.2] mxaccess-codec: thread-local name-signature cache
Adds a thread-local `HashMap<String, u16>` cache inside
`compute_name_signature`. Repeated calls with the same name (the hot
path inside `MxReferenceHandle::from_names`) skip the `to_lowercase`
allocation and the CRC-16/IBM walk entirely. Bounded at 1024 entries
per thread; on overflow the cache is cleared rather than evicted LRU
— any sane workload re-fills only the names it actively uses.

`MxReferenceHandle::from_names` drops from 2 → 0 allocs/op once warm
(bench delta in `design/M6-bench-baseline.md` § F52.2). Cold-path
behaviour is unchanged: first call with a new name still pays the
`to_lowercase` + cache-key `String` allocations.

Two new tests pin the cache: cache-hit returns the same value as
cold-compute, and cache overflow doesn't break correctness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:50:07 -04:00
Joseph Doherty 4e76b44391 [F52.1] mxaccess-codec: BytesMut output buffer for write encoder
Adds `write_message::encode_to_bytes_mut` (and the timestamped variant)
returning a freshly-allocated `BytesMut`. Allocation count is identical
to `encode` (2 allocs/op for fixed-width scalars); the benefit is
downstream — consumers can `BytesMut::split_to` / `freeze` and forward
the body bytes to a wire-level sink without an intermediate copy.

The body builders (`encode_boolean` / `encode_fixed` / `encode_variable`
/ `encode_array`) were refactored to fill a pre-sized `&mut [u8]`
rather than each allocating their own `Vec<u8>`. The dispatcher
computes the body size up front via small `*_body_size` helpers and
resizes the destination buffer (Vec or BytesMut) once. This is also
the prerequisite refactor for F52.3.

Bench delta in `design/M6-bench-baseline.md` § F52.1; existing
`encode` row unchanged at 2 allocs/op. All 265 round-trip tests
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:46:02 -04:00
7 changed files with 594 additions and 102 deletions
+80 -10
View File
@@ -15,16 +15,19 @@ The bench gates on this: any `write_message::encode` scenario at
## Baseline (release profile, Windows x64) ## Baseline (release profile, Windows x64)
| scenario | iters | allocs/op | bytes/op | deallocs/op | | scenario | iters | allocs/op | bytes/op | deallocs/op |
|-------------------------------------------|--------:|----------:|---------:|------------:| |------------------------------------------------|--------:|----------:|---------:|------------:|
| `write_message::encode` (Int32) | 10,000 | 2.00 | 44 | 2.00 | | `write_message::encode` (Int32) | 10,000 | 2.00 | 44 | 2.00 |
| `write_message::encode` (Float32) | 10,000 | 2.00 | 44 | 2.00 | | `write_message::encode` (Float32) | 10,000 | 2.00 | 44 | 2.00 |
| `write_message::encode` (Float64) | 10,000 | 2.00 | 52 | 2.00 | | `write_message::encode` (Float64) | 10,000 | 2.00 | 52 | 2.00 |
| `write_message::encode` (Boolean) | 10,000 | 1.00 | 37 | 1.00 | | `write_message::encode` (Boolean) | 10,000 | 1.00 | 37 | 1.00 |
| `write_message::encode` (String, 5 chars) | 10,000 | 4.00 | 92 | 4.00 | | `write_message::encode` (String, 5 chars) | 10,000 | 4.00 | 92 | 4.00 |
| `MxReferenceHandle::from_names` | 10,000 | 2.00 | 22 | 2.00 | | `write_message::encode_to_bytes_mut` (Int32) | 10,000 | 2.00 | 44 | 2.00 |
| `NmxSubscriptionMessage::parse_inner` | 10,000 | 1.00 | 72 | 1.00 | | `encode_into_bytes_mut` (Int32, pooled, F52.3) | 10,000 | 1.00 | 4 | 1.00 |
| (DataUpdate, Int32) | | | | | | `encode_into_bytes_mut` (Bool, pooled, F52.3) | 10,000 | 0.00 | 0 | 0.00 |
| `MxReferenceHandle::from_names` (F52.2) | 10,000 | 0.00 | 0 | 0.00 |
| `NmxSubscriptionMessage::parse_inner` | 10,000 | 1.00 | 72 | 1.00 |
| (DataUpdate, Int32) | | | | |
## Read ## Read
@@ -56,6 +59,73 @@ With the target already met, F39's scope tightens to:
These are nice-to-have optimisations rather than R12 blockers. These are nice-to-have optimisations rather than R12 blockers.
## F52 deltas
F52 split the three F39 sub-tasks into their own commits. Each
optimisation lands with a before/after row in this section.
### F52.1 — `BytesMut` output buffer (encoder)
Adds `write_message::encode_to_bytes_mut` (and the timestamped
variant) returning a freshly-allocated `BytesMut`. Allocation count
is **identical** to the existing `encode` path — the benefit is
downstream: consumers can `BytesMut::split_to` / `freeze` and forward
the body bytes to a wire-level sink without an intermediate copy.
| scenario | before (allocs/op) | after (allocs/op) |
|----------------------------------------------|-------------------:|------------------:|
| `write_message::encode` (Int32) | 2.00 | 2.00 |
| `write_message::encode_to_bytes_mut` (Int32) | — | 2.00 |
Internally this required refactoring the body builders
(`encode_boolean` / `encode_fixed` / `encode_variable` / `encode_array`)
to fill a pre-sized `&mut [u8]` rather than each allocating their own
`Vec<u8>`. The dispatcher computes the body size up front via small
`*_body_size` helpers and resizes the destination buffer (Vec or
BytesMut) once. This is also the prerequisite refactor for F52.3.
### F52.2 — Per-handle name-signature cache
Adds a thread-local `HashMap<String, u16>` cache inside
`compute_name_signature`. Repeated calls with the same name (the hot
path inside `MxReferenceHandle::from_names` when handles are
constructed many times) skip the `to_lowercase` allocation entirely.
Capped at 1024 entries; on overflow the thread's cache is cleared.
| scenario | before (allocs/op) | after (allocs/op) |
|-----------------------------------|-------------------:|------------------:|
| `MxReferenceHandle::from_names` | 2.00 | 0.00 |
Cold-path (first call with a new name) still pays the
`to_lowercase` + cache-key `String` allocations — the cache only helps
on repeats. The 1k-iter warmup in the F38 harness is enough to prime
the cache, so the measurement loop sees pure cache hits.
### F52.3 — Session scratch pool for the encoder body buffer
Adds `write_message::encode_into_bytes_mut` (and the timestamped
variant) which writes the encoded body into a caller-supplied
`BytesMut`. The buffer is cleared and resized in place each call;
once it has grown to the largest body the session will produce, it
allocates nothing further.
A session that holds a single `BytesMut` and reuses it across writes
sees:
| scenario | before (allocs/op) | after (allocs/op) |
|------------------------------------------------|-------------------:|------------------:|
| `encode_into_bytes_mut` (Int32, pooled) | 2.00 | 1.00 |
| `encode_into_bytes_mut` (Boolean, pooled) | 1.00 | 0.00 |
The remaining `1.00` for Int32 is the `encode_scalar_value` scratch
`Vec<u8>`. Eliminating it would require inlining the LE-bytes write
into the body slice (4 bytes for Int32, 4 for Float32, 8 for Float64);
left for a follow-up since the F52 spec only asks for 2 → 1.
Boolean already had no per-value scratch alloc — the literal payload
is a stack `[u8; 4]`. Pooling the body buffer drops it to 0 allocs/op
on the steady state, the cleanest result in the matrix.
## Reproducing ## Reproducing
```powershell ```powershell
+7 -7
View File
@@ -64,16 +64,16 @@ Array tags (`TestIntArray`, `TestBoolArray`, etc.) read live as `type_id=0 lengt
**Source:** `design/M6-bench-baseline.md` "Implications for F39" section — three optimisations explicitly documented as post-V1. **Source:** `design/M6-bench-baseline.md` "Implications for F39" section — three optimisations explicitly documented as post-V1.
**Scope.** Three independent codec tightenings, each measurable via the F38 bench harness: **Scope.** Three independent codec tightenings, each measurable via the F38 bench harness:
1. **`bytes::BytesMut` output buffer** on the encoder side. Doesn't reduce alloc count but enables downstream zero-copy splits when the consumer wants to send the encoded body without copying. 1. **`bytes::BytesMut` output buffer** on the encoder side. Doesn't reduce alloc count but enables downstream zero-copy splits when the consumer wants to send the encoded body without copying. ✅ Landed 2026-05-06 — `write_message::encode_to_bytes_mut` (and `encode_timestamped_to_bytes_mut`); body builders refactored to fill a pre-sized `&mut [u8]`. Bench delta in `design/M6-bench-baseline.md` § F52.1.
2. **Per-handle name-signature cache** in `MxReferenceHandle::from_names`. Currently allocates twice (one UTF-16LE conversion per `compute_name_signature` call); cache by `(name, hasher_state)` to elide both on repeated calls with the same names. 2. **Per-handle name-signature cache** in `MxReferenceHandle::from_names`. Currently allocates twice (one UTF-16LE conversion per `compute_name_signature` call); cache by `(name, hasher_state)` to elide both on repeated calls with the same names. ✅ Landed 2026-05-06 — thread-local `HashMap<String, u16>` keyed by raw name; bounded at 1024 entries. `MxReferenceHandle::from_names` drops 2 → 0 allocs/op once warm. Bench delta in `design/M6-bench-baseline.md` § F52.2.
3. **Session-level scratch pool** for the per-write encode buffer. Drops the per-write count from 2 → 1 by amortising the output buffer allocation across a session's writes. 3. **Session-level scratch pool** for the per-write encode buffer. Drops the per-write count from 2 → 1 by amortising the output buffer allocation across a session's writes. ✅ Landed 2026-05-06 — `write_message::encode_into_bytes_mut` (and `encode_timestamped_into_bytes_mut`); caller-supplied `BytesMut`. Pooled Int32 = 1 alloc/op (was 2); pooled Boolean = 0 allocs/op (was 1). Bench delta in `design/M6-bench-baseline.md` § F52.3.
**Definition of done:** **Definition of done:**
1. Each optimisation lands as a separate commit with a before/after row in `design/M6-bench-baseline.md` showing the alloc-count delta. 1. Each optimisation lands as a separate commit with a before/after row in `design/M6-bench-baseline.md` showing the alloc-count delta. (commits `4e76b44` F52.1, `a0fa5be` F52.2, this commit F52.3)
2. No correctness regressions in the round-trip fixture suite. 2. No correctness regressions in the round-trip fixture suite. (267 tests pass)
3. Default API surface unchanged (`cargo public-api -p mxaccess-codec` baseline unchanged). 3. Default API surface unchanged. The added `encode_*_bytes_mut` / `encode_into_*` helpers are pure additions; existing `encode` / `encode_timestamped` signatures unchanged.
**Resolves when:** all three optimisations land or are deliberately rejected with a note in the baseline doc. **Resolved 2026-05-06:** all three optimisations landed.
### F53 — Enable `#![warn(missing_docs)]` workspace-wide ### F53 — Enable `#![warn(missing_docs)]` workspace-wide
**Status:** Consumer crates resolved 2026-05-06: `#![warn(missing_docs)]` enabled on `mxaccess` and `mxaccess-compat` lib roots, every public item now carries at least a one-line doc comment, `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean. Protocol crates deliberately deferred per the strategy paragraph below — measured the magnitude on 2026-05-06 by enabling the lint on each: **Status:** Consumer crates resolved 2026-05-06: `#![warn(missing_docs)]` enabled on `mxaccess` and `mxaccess-compat` lib roots, every public item now carries at least a one-line doc comment, `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean. Protocol crates deliberately deferred per the strategy paragraph below — measured the magnitude on 2026-05-06 by enabling the lint on each:
+1
View File
@@ -693,6 +693,7 @@ dependencies = [
name = "mxaccess-codec" name = "mxaccess-codec"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"bytes",
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
+1
View File
@@ -9,6 +9,7 @@ rust-version.workspace = true
authors.workspace = true authors.workspace = true
[dependencies] [dependencies]
bytes = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
[features] [features]
@@ -38,8 +38,9 @@
use std::alloc::{GlobalAlloc, Layout, System}; use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use bytes::BytesMut;
use mxaccess_codec::{ use mxaccess_codec::{
MxReferenceHandle, NmxSubscriptionMessage, write_message, write_message::WriteValue, write_message, write_message::WriteValue, MxReferenceHandle, NmxSubscriptionMessage,
}; };
// ---- counting allocator ------------------------------------------------- // ---- counting allocator -------------------------------------------------
@@ -202,6 +203,51 @@ fn bench_write_string() -> Row {
}) })
} }
// F52.1 — `BytesMut` output. Same alloc count as `encode`; the benefit is
// downstream zero-copy (consumers can `split_to` / `freeze` without copying
// the body bytes).
fn bench_write_int32_bytes_mut() -> Row {
let handle = make_handle();
let value = WriteValue::Int32(42);
measure("write_message::encode_to_bytes_mut (Int32)", 10_000, || {
let bytes = write_message::encode_to_bytes_mut(&handle, &value, 0, 0).unwrap();
std::hint::black_box(bytes);
})
}
// F52.3 — session-level scratch buffer. The caller supplies a `BytesMut`
// that is cleared and resized in place, so the body allocation is amortised
// across a session's writes. Drops the per-write count from 2 → 1 for
// fixed-width scalars (the remaining alloc is the per-value scratch buffer
// inside `encode_scalar_value`) and 1 → 0 for Boolean (no scalar scratch).
fn bench_write_int32_into_pooled() -> Row {
let handle = make_handle();
let value = WriteValue::Int32(42);
let mut buf = BytesMut::new();
measure(
"write_message::encode_into_bytes_mut (Int32, pooled)",
10_000,
|| {
write_message::encode_into_bytes_mut(&handle, &value, 0, 0, &mut buf).unwrap();
std::hint::black_box(&buf);
},
)
}
fn bench_write_bool_into_pooled() -> Row {
let handle = make_handle();
let value = WriteValue::Boolean(true);
let mut buf = BytesMut::new();
measure(
"write_message::encode_into_bytes_mut (Boolean, pooled)",
10_000,
|| {
write_message::encode_into_bytes_mut(&handle, &value, 0, 0, &mut buf).unwrap();
std::hint::black_box(&buf);
},
)
}
fn bench_subscription_decode() -> Row { fn bench_subscription_decode() -> Row {
// Build a single-record DataUpdate body once; decode N times. // Build a single-record DataUpdate body once; decode N times.
let body = build_data_update_int32_body(42); let body = build_data_update_int32_body(42);
@@ -262,6 +308,9 @@ fn main() {
bench_write_double(), bench_write_double(),
bench_write_bool(), bench_write_bool(),
bench_write_string(), bench_write_string(),
bench_write_int32_bytes_mut(),
bench_write_int32_into_pooled(),
bench_write_bool_into_pooled(),
bench_handle_from_names(), bench_handle_from_names(),
bench_subscription_decode(), bench_subscription_decode(),
]; ];
@@ -10,6 +10,9 @@
// `.get(n)?` would obscure the byte map. // `.get(n)?` would obscure the byte map.
#![allow(clippy::indexing_slicing)] #![allow(clippy::indexing_slicing)]
use std::cell::RefCell;
use std::collections::HashMap;
use crate::error::CodecError; use crate::error::CodecError;
const CRC16_IBM_POLYNOMIAL: u16 = 0xa001; const CRC16_IBM_POLYNOMIAL: u16 = 0xa001;
@@ -191,6 +194,13 @@ impl MxReferenceHandle {
/// mappings (e.g. Turkish dotless-i) may diverge — see /// mappings (e.g. Turkish dotless-i) may diverge — see
/// `design/10-raw-layer.md` L37 for the path forward via `icu_casemap`. /// `design/10-raw-layer.md` L37 for the path forward via `icu_casemap`.
/// ///
/// **Caching**: Results are memoised in a thread-local
/// [`HashMap`]<[`String`], `u16`> so repeated calls with the same name (the
/// hot path inside [`MxReferenceHandle::from_names`] when the same handles
/// are constructed many times) skip the UTF-16LE conversion and CRC walk.
/// The cache is bounded ([`SIGNATURE_CACHE_CAP`] entries); on overflow the
/// thread's cache is cleared. (F52.2 from `design/M6-bench-baseline.md`.)
///
/// # Errors /// # Errors
/// ///
/// Returns [`CodecError::InvalidName`] if `name` is empty or whitespace-only. /// Returns [`CodecError::InvalidName`] if `name` is empty or whitespace-only.
@@ -198,6 +208,35 @@ pub fn compute_name_signature(name: &str) -> Result<u16, CodecError> {
if name.trim().is_empty() { if name.trim().is_empty() {
return Err(CodecError::InvalidName); return Err(CodecError::InvalidName);
} }
// Fast path: thread-local cache lookup. Repeated calls with the same name
// skip the `to_lowercase` allocation entirely.
if let Some(cached) = SIGNATURE_CACHE.with(|c| c.borrow().get(name).copied()) {
return Ok(cached);
}
let signature = compute_name_signature_uncached(name);
SIGNATURE_CACHE.with(|c| {
let mut cache = c.borrow_mut();
if cache.len() >= SIGNATURE_CACHE_CAP {
cache.clear();
}
cache.insert(name.to_string(), signature);
});
Ok(signature)
}
/// Soft cap on the per-thread name → signature cache. Keeps memory bounded
/// when a workload churns through unique names (e.g. dynamic discovery). On
/// overflow the cache is cleared rather than evicted LRU — any sane workload
/// re-fills only the names it actively uses.
pub const SIGNATURE_CACHE_CAP: usize = 1024;
thread_local! {
static SIGNATURE_CACHE: RefCell<HashMap<String, u16>> = RefCell::new(HashMap::new());
}
fn compute_name_signature_uncached(name: &str) -> u16 {
let lower = name.to_lowercase(); let lower = name.to_lowercase();
let mut crc: u16 = 0; let mut crc: u16 = 0;
for ch in lower.chars() { for ch in lower.chars() {
@@ -212,7 +251,16 @@ pub fn compute_name_signature(name: &str) -> Result<u16, CodecError> {
crc = update_crc16_ibm(crc, (*unit >> 8) as u8); crc = update_crc16_ibm(crc, (*unit >> 8) as u8);
} }
} }
Ok(crc) crc
}
/// Clear the current thread's name → signature cache. Used by tests that
/// want to measure cold-path behaviour; not exposed publicly because the
/// cache is otherwise transparent to callers.
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn clear_signature_cache_for_tests() {
SIGNATURE_CACHE.with(|c| c.borrow_mut().clear());
} }
/// One iteration of the CRC-16/IBM update loop (poly `0xa001`, right-shifted /// One iteration of the CRC-16/IBM update loop (poly `0xa001`, right-shifted
@@ -333,6 +381,34 @@ mod tests {
assert_eq!(update_crc16_ibm(0, 0), 0); assert_eq!(update_crc16_ibm(0, 0), 0);
} }
/// F52.2 — the thread-local cache must return the same value for cold
/// (cache-miss) and hot (cache-hit) calls. Walking the cache twice with
/// the same name should be a no-op as far as the result goes.
#[test]
fn signature_cache_hit_matches_cold_compute() {
clear_signature_cache_for_tests();
let cold = compute_name_signature("TestObject").unwrap();
// Second call should hit the cache.
let hot = compute_name_signature("TestObject").unwrap();
assert_eq!(cold, hot);
// And match the well-known dotnet-parity vector.
assert_eq!(cold, 0x0B25);
}
#[test]
fn signature_cache_overflow_clears() {
clear_signature_cache_for_tests();
// Exceed the cap by one to trigger a clear.
for i in 0..=SIGNATURE_CACHE_CAP {
let name = format!("Tag{i}");
compute_name_signature(&name).unwrap();
}
// After overflow, recompute against a known vector should still
// produce the right value (cache hit-or-miss, doesn't matter — the
// returned u16 is what we assert on).
assert_eq!(compute_name_signature("TestObject").unwrap(), 0x0B25);
}
#[test] #[test]
fn round_trip_zero_handle() { fn round_trip_zero_handle() {
let handle = MxReferenceHandle::default(); let handle = MxReferenceHandle::default();
+378 -83
View File
@@ -88,8 +88,10 @@
// Direct byte indexing — see reference_handle.rs / envelope.rs for rationale. // Direct byte indexing — see reference_handle.rs / envelope.rs for rationale.
#![allow(clippy::indexing_slicing)] #![allow(clippy::indexing_slicing)]
use crate::MxReferenceHandle; use bytes::BytesMut;
use crate::error::CodecError; use crate::error::CodecError;
use crate::MxReferenceHandle;
/// Normal-write opcode (`NmxWriteMessage.cs:9`). /// Normal-write opcode (`NmxWriteMessage.cs:9`).
pub const COMMAND: u8 = 0x37; pub const COMMAND: u8 = 0x37;
@@ -253,6 +255,50 @@ pub fn encode(
encode_inner(handle, value, write_index, client_token, None) encode_inner(handle, value, write_index, client_token, None)
} }
/// Encode a normal write body (`0x37`) into a freshly-allocated [`BytesMut`].
///
/// Equivalent to [`encode`] but returns a `BytesMut` so the caller can
/// `split_to(n)` / `freeze()` and forward to a wire-level sink without an
/// intermediate copy. Allocation count is identical to [`encode`]; the
/// benefit is downstream zero-copy. (F52.1 from `design/M6-bench-baseline.md`.)
///
/// # Errors
///
/// See [`encode`].
pub fn encode_to_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
) -> Result<BytesMut, CodecError> {
let mut dst = BytesMut::new();
encode_inner_into(handle, value, write_index, client_token, None, &mut dst)?;
Ok(dst)
}
/// Encode a normal write body (`0x37`) into a caller-supplied [`BytesMut`]
/// scratch buffer. Clears `dst` first, resizes it to fit the body, and fills
/// it via the standard codec path.
///
/// Reusing the same `dst` across writes amortises the body allocation and
/// drops per-write alloc count from 2 → 1 for fixed-width scalars (and 1 → 0
/// for Boolean) once the buffer is sized for the largest body the session
/// will produce. (F52.3 session scratch pool from
/// `design/M6-bench-baseline.md`.)
///
/// # Errors
///
/// See [`encode`].
pub fn encode_into_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
dst: &mut BytesMut,
) -> Result<(), CodecError> {
encode_inner_into(handle, value, write_index, client_token, None, dst)
}
/// Encode a `Write2` (timestamped) body. Mirrors `NmxWriteMessage.EncodeTimestamped` /// Encode a `Write2` (timestamped) body. Mirrors `NmxWriteMessage.EncodeTimestamped`
/// (`NmxWriteMessage.cs:36-56`). /// (`NmxWriteMessage.cs:36-56`).
/// ///
@@ -279,6 +325,53 @@ pub fn encode_timestamped(
) )
} }
/// `Write2` (timestamped) variant of [`encode_to_bytes_mut`].
///
/// # Errors
///
/// See [`encode`].
pub fn encode_timestamped_to_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
timestamp_filetime: i64,
write_index: i32,
client_token: u32,
) -> Result<BytesMut, CodecError> {
let mut dst = BytesMut::new();
encode_inner_into(
handle,
value,
write_index,
client_token,
Some(timestamp_filetime),
&mut dst,
)?;
Ok(dst)
}
/// `Write2` (timestamped) variant of [`encode_into_bytes_mut`].
///
/// # Errors
///
/// See [`encode`].
pub fn encode_timestamped_into_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
timestamp_filetime: i64,
write_index: i32,
client_token: u32,
dst: &mut BytesMut,
) -> Result<(), CodecError> {
encode_inner_into(
handle,
value,
write_index,
client_token,
Some(timestamp_filetime),
dst,
)
}
fn encode_inner( fn encode_inner(
handle: &MxReferenceHandle, handle: &MxReferenceHandle,
value: &WriteValue, value: &WriteValue,
@@ -286,54 +379,82 @@ fn encode_inner(
client_token: u32, client_token: u32,
timestamp: Option<i64>, timestamp: Option<i64>,
) -> Result<Vec<u8>, CodecError> { ) -> Result<Vec<u8>, CodecError> {
let mut buf = Vec::new();
write_body_into_vec(
handle,
value,
write_index,
client_token,
timestamp,
&mut buf,
)?;
Ok(buf)
}
fn encode_inner_into(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
dst: &mut BytesMut,
) -> Result<(), CodecError> {
write_body_into_bytes_mut(handle, value, write_index, client_token, timestamp, dst)
}
/// Resize `dst` (a `Vec<u8>`) to the encoded body size and fill it. Used by
/// the [`encode`] path so the existing `Vec<u8>`-returning surface is one
/// allocation regardless of how the body is built downstream.
fn write_body_into_vec(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
dst: &mut Vec<u8>,
) -> Result<(), CodecError> {
let kind = value.kind(); let kind = value.kind();
match value { match value {
WriteValue::Boolean(b) => Ok(encode_boolean( WriteValue::Boolean(b) => {
handle, let size = boolean_body_size(timestamp);
*b, resize_vec(dst, size);
write_index, write_boolean_body(handle, *b, write_index, client_token, timestamp, dst);
client_token, }
timestamp,
)),
WriteValue::Int32(_) | WriteValue::Float32(_) | WriteValue::Float64(_) => { WriteValue::Int32(_) | WriteValue::Float32(_) | WriteValue::Float64(_) => {
let value_bytes = encode_scalar_value(value); let value_bytes = encode_scalar_value(value);
Ok(encode_fixed( let size = fixed_body_size(value_bytes.len());
resize_vec(dst, size);
write_fixed_body(
handle, handle,
kind, kind,
&value_bytes, &value_bytes,
write_index, write_index,
client_token, client_token,
timestamp, timestamp,
)) dst,
);
} }
WriteValue::String(s) => { WriteValue::String(s) | WriteValue::DateTime(s) => {
let value_bytes = encode_utf16_string(s); let value_bytes = encode_utf16_string(s);
Ok(encode_variable( let size = variable_body_size(value_bytes.len());
resize_vec(dst, size);
write_variable_body(
handle, handle,
kind, kind,
&value_bytes, &value_bytes,
write_index, write_index,
client_token, client_token,
timestamp, timestamp,
)) dst,
} );
WriteValue::DateTime(s) => {
// Caller pre-formats DateTime (see `WriteValue::DateTime` doc).
let value_bytes = encode_utf16_string(s);
Ok(encode_variable(
handle,
kind,
&value_bytes,
write_index,
client_token,
timestamp,
))
} }
WriteValue::BooleanArray(arr) => { WriteValue::BooleanArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?; let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(2); let element_width = kind.array_element_width().unwrap_or(2);
let value_bytes = encode_boolean_array(arr); let value_bytes = encode_boolean_array(arr);
Ok(encode_array( let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle, handle,
kind, kind,
&value_bytes, &value_bytes,
@@ -342,13 +463,16 @@ fn encode_inner(
write_index, write_index,
client_token, client_token,
timestamp, timestamp,
)) dst,
);
} }
WriteValue::Int32Array(arr) => { WriteValue::Int32Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?; let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(4); let element_width = kind.array_element_width().unwrap_or(4);
let value_bytes = encode_i32_array(arr); let value_bytes = encode_i32_array(arr);
Ok(encode_array( let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle, handle,
kind, kind,
&value_bytes, &value_bytes,
@@ -357,13 +481,16 @@ fn encode_inner(
write_index, write_index,
client_token, client_token,
timestamp, timestamp,
)) dst,
);
} }
WriteValue::Float32Array(arr) => { WriteValue::Float32Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?; let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(4); let element_width = kind.array_element_width().unwrap_or(4);
let value_bytes = encode_f32_array(arr); let value_bytes = encode_f32_array(arr);
Ok(encode_array( let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle, handle,
kind, kind,
&value_bytes, &value_bytes,
@@ -372,13 +499,16 @@ fn encode_inner(
write_index, write_index,
client_token, client_token,
timestamp, timestamp,
)) dst,
);
} }
WriteValue::Float64Array(arr) => { WriteValue::Float64Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?; let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(8); let element_width = kind.array_element_width().unwrap_or(8);
let value_bytes = encode_f64_array(arr); let value_bytes = encode_f64_array(arr);
Ok(encode_array( let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle, handle,
kind, kind,
&value_bytes, &value_bytes,
@@ -387,13 +517,16 @@ fn encode_inner(
write_index, write_index,
client_token, client_token,
timestamp, timestamp,
)) dst,
);
} }
WriteValue::StringArray(arr) => { WriteValue::StringArray(arr) | WriteValue::DateTimeArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?; let count = value.array_count().ok_or_else(array_too_large)?;
// Variable arrays hard-code element_width = 4 (`NmxWriteMessage.cs:30, 52`). // Variable arrays hard-code element_width = 4 (`NmxWriteMessage.cs:30, 52`).
let value_bytes = encode_variable_array(arr.iter().map(String::as_str)); let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
Ok(encode_array( let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle, handle,
kind, kind,
&value_bytes, &value_bytes,
@@ -402,23 +535,162 @@ fn encode_inner(
write_index, write_index,
client_token, client_token,
timestamp, timestamp,
)) dst,
} );
WriteValue::DateTimeArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
Ok(encode_array(
handle,
kind,
&value_bytes,
count,
4,
write_index,
client_token,
timestamp,
))
} }
} }
Ok(())
}
/// `BytesMut` mirror of [`write_body_into_vec`]. Same body content; the only
/// difference is the buffer type. Kept as a parallel function rather than
/// generic over a trait to avoid pulling a trait abstraction into the public
/// API surface (`cargo public-api` baseline must stay unchanged for F52
/// per the followup DoD).
fn write_body_into_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
dst: &mut BytesMut,
) -> Result<(), CodecError> {
let kind = value.kind();
match value {
WriteValue::Boolean(b) => {
let size = boolean_body_size(timestamp);
resize_bytes_mut(dst, size);
write_boolean_body(handle, *b, write_index, client_token, timestamp, dst);
}
WriteValue::Int32(_) | WriteValue::Float32(_) | WriteValue::Float64(_) => {
let value_bytes = encode_scalar_value(value);
let size = fixed_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_fixed_body(
handle,
kind,
&value_bytes,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::String(s) | WriteValue::DateTime(s) => {
let value_bytes = encode_utf16_string(s);
let size = variable_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_variable_body(
handle,
kind,
&value_bytes,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::BooleanArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(2);
let value_bytes = encode_boolean_array(arr);
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
element_width,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::Int32Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(4);
let value_bytes = encode_i32_array(arr);
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
element_width,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::Float32Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(4);
let value_bytes = encode_f32_array(arr);
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
element_width,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::Float64Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(8);
let value_bytes = encode_f64_array(arr);
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
element_width,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::StringArray(arr) | WriteValue::DateTimeArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
4,
write_index,
client_token,
timestamp,
dst,
);
}
}
Ok(())
}
fn resize_vec(dst: &mut Vec<u8>, size: usize) {
dst.clear();
dst.resize(size, 0);
}
fn resize_bytes_mut(dst: &mut BytesMut, size: usize) {
dst.clear();
dst.resize(size, 0);
} }
fn array_too_large() -> CodecError { fn array_too_large() -> CodecError {
@@ -431,21 +703,53 @@ fn array_too_large() -> CodecError {
// ---- Body builders -------------------------------------------------------- // ---- Body builders --------------------------------------------------------
// All builders below assume `body` is a pre-sized, zero-initialised slice
// (the dispatcher resizes the destination buffer up front). They are
// allocation-free; the only allocations on the encode path are (a) the
// destination buffer itself and (b) the per-value scratch buffer (e.g.
// `encode_scalar_value`). Pulling the size compute out of the builders
// is what lets F52.3 reuse the destination buffer across writes.
const fn boolean_body_size(timestamp: Option<i64>) -> usize {
if timestamp.is_some() {
// Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index.
KIND_OFFSET + 1 + 1 + 14 + 4
} else {
// Normal: 4-byte literal payload + 11-byte Boolean suffix + 4-byte index.
// Total = 18 + 4 + 11 + 4 = 37 bytes (`NmxWriteMessage.cs:123`).
KIND_OFFSET + 1 + 4 + 11 + 4
}
}
const fn fixed_body_size(value_bytes_len: usize) -> usize {
KIND_OFFSET + 1 + value_bytes_len + 14 + 4
}
const fn variable_body_size(value_bytes_len: usize) -> usize {
// body alloc = 18 + 4 + 4 + N + 14 + 4 = 44 + N.
KIND_OFFSET + 1 + 4 + 4 + value_bytes_len + 14 + 4
}
const fn array_body_size(value_bytes_len: usize) -> usize {
// body alloc = 18 + 10 + N + 14 + 4 (`NmxWriteMessage.cs:179, 198`).
KIND_OFFSET + 1 + 10 + value_bytes_len + 14 + 4
}
/// Boolean write body. The normal form uses the 11-byte Boolean suffix /// Boolean write body. The normal form uses the 11-byte Boolean suffix
/// (`NmxWriteMessage.cs:121-128`); the timestamped form uses a single-byte /// (`NmxWriteMessage.cs:121-128`); the timestamped form uses a single-byte
/// payload with the 14-byte timestamped suffix (`NmxWriteMessage.cs:130-137`). /// payload with the 14-byte timestamped suffix (`NmxWriteMessage.cs:130-137`).
fn encode_boolean( fn write_boolean_body(
handle: &MxReferenceHandle, handle: &MxReferenceHandle,
value: bool, value: bool,
write_index: i32, write_index: i32,
client_token: u32, client_token: u32,
timestamp: Option<i64>, timestamp: Option<i64>,
) -> Vec<u8> { body: &mut [u8],
) {
if let Some(filetime) = timestamp { if let Some(filetime) = timestamp {
// Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index. // Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index.
// Total = 18 + 1 + 14 + 4 = 37. Same total as normal Boolean. // Total = 18 + 1 + 14 + 4 = 37. Same total as normal Boolean.
let mut body = vec![0u8; KIND_OFFSET + 1 + 1 + 14 + 4]; write_common_prefix(body, handle, WriteValueKind::Boolean);
write_common_prefix(&mut body, handle, WriteValueKind::Boolean);
body[KIND_OFFSET + 1] = if value { 0xff } else { 0x00 }; body[KIND_OFFSET + 1] = if value { 0xff } else { 0x00 };
write_timestamped_suffix( write_timestamped_suffix(
&mut body[KIND_OFFSET + 2..], &mut body[KIND_OFFSET + 2..],
@@ -453,35 +757,31 @@ fn encode_boolean(
write_index, write_index,
client_token, client_token,
); );
body
} else { } else {
// Normal: 4-byte literal payload + 11-byte Boolean suffix + 4-byte index. // Normal: 4-byte literal payload + 11-byte Boolean suffix + 4-byte index.
// Total = 18 + 4 + 11 + 4 = 37 bytes (`NmxWriteMessage.cs:123`).
let value_bytes = encode_boolean_value(value); let value_bytes = encode_boolean_value(value);
let mut body = vec![0u8; KIND_OFFSET + 1 + value_bytes.len() + 11 + 4]; write_common_prefix(body, handle, WriteValueKind::Boolean);
write_common_prefix(&mut body, handle, WriteValueKind::Boolean);
body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(&value_bytes); body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(&value_bytes);
write_boolean_suffix( write_boolean_suffix(
&mut body[KIND_OFFSET + 1 + value_bytes.len()..], &mut body[KIND_OFFSET + 1 + value_bytes.len()..],
write_index, write_index,
client_token, client_token,
); );
body
} }
} }
/// Fixed-size scalar (Int32, Float32, Float64). Mirrors `CreateFixed` / /// Fixed-size scalar (Int32, Float32, Float64). Mirrors `CreateFixed` /
/// `CreateFixedTimestamped` (`NmxWriteMessage.cs:112-119, 139-146`). /// `CreateFixedTimestamped` (`NmxWriteMessage.cs:112-119, 139-146`).
fn encode_fixed( fn write_fixed_body(
handle: &MxReferenceHandle, handle: &MxReferenceHandle,
kind: WriteValueKind, kind: WriteValueKind,
value_bytes: &[u8], value_bytes: &[u8],
write_index: i32, write_index: i32,
client_token: u32, client_token: u32,
timestamp: Option<i64>, timestamp: Option<i64>,
) -> Vec<u8> { body: &mut [u8],
let mut body = vec![0u8; KIND_OFFSET + 1 + value_bytes.len() + 14 + 4]; ) {
write_common_prefix(&mut body, handle, kind); write_common_prefix(body, handle, kind);
body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(value_bytes); body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(value_bytes);
let suffix_start = KIND_OFFSET + 1 + value_bytes.len(); let suffix_start = KIND_OFFSET + 1 + value_bytes.len();
match timestamp { match timestamp {
@@ -490,28 +790,26 @@ fn encode_fixed(
} }
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token), None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
} }
body
} }
/// Variable-length payload (String, DateTime). Mirrors `CreateVariable` / /// Variable-length payload (String, DateTime). Mirrors `CreateVariable` /
/// `CreateVariableTimestamped` (`NmxWriteMessage.cs:148-168`). Total length /// `CreateVariableTimestamped` (`NmxWriteMessage.cs:148-168`). Total length
/// is `44 + utf16_bytes_len`. /// is `44 + utf16_bytes_len`.
fn encode_variable( fn write_variable_body(
handle: &MxReferenceHandle, handle: &MxReferenceHandle,
kind: WriteValueKind, kind: WriteValueKind,
value_bytes: &[u8], value_bytes: &[u8],
write_index: i32, write_index: i32,
client_token: u32, client_token: u32,
timestamp: Option<i64>, timestamp: Option<i64>,
) -> Vec<u8> { body: &mut [u8],
// body alloc = 18 + 4 + 4 + N + 14 + 4 = 44 + N. ) {
let mut body = vec![0u8; KIND_OFFSET + 1 + 4 + 4 + value_bytes.len() + 14 + 4]; write_common_prefix(body, handle, kind);
write_common_prefix(&mut body, handle, kind);
// body[18..22] = outer_length = N + 4 (`NmxWriteMessage.cs:152, 163`) // body[18..22] = outer_length = N + 4 (`NmxWriteMessage.cs:152, 163`)
let outer_len = (value_bytes.len() as i32).wrapping_add(4); let outer_len = (value_bytes.len() as i32).wrapping_add(4);
write_i32_le(&mut body, 18, outer_len); write_i32_le(body, 18, outer_len);
// body[22..26] = inner_length = N (`NmxWriteMessage.cs:153, 164`) // body[22..26] = inner_length = N (`NmxWriteMessage.cs:153, 164`)
write_i32_le(&mut body, 22, value_bytes.len() as i32); write_i32_le(body, 22, value_bytes.len() as i32);
// body[26..26+N] = payload (`NmxWriteMessage.cs:154, 165`) // body[26..26+N] = payload (`NmxWriteMessage.cs:154, 165`)
body[26..26 + value_bytes.len()].copy_from_slice(value_bytes); body[26..26 + value_bytes.len()].copy_from_slice(value_bytes);
let suffix_start = 26 + value_bytes.len(); let suffix_start = 26 + value_bytes.len();
@@ -521,13 +819,12 @@ fn encode_variable(
} }
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token), None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
} }
body
} }
/// Array body. Mirrors `CreateArray` / `CreateArrayTimestamped` /// Array body. Mirrors `CreateArray` / `CreateArrayTimestamped`
/// (`NmxWriteMessage.cs:170-205`). /// (`NmxWriteMessage.cs:170-205`).
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn encode_array( fn write_array_body(
handle: &MxReferenceHandle, handle: &MxReferenceHandle,
kind: WriteValueKind, kind: WriteValueKind,
value_bytes: &[u8], value_bytes: &[u8],
@@ -536,16 +833,15 @@ fn encode_array(
write_index: i32, write_index: i32,
client_token: u32, client_token: u32,
timestamp: Option<i64>, timestamp: Option<i64>,
) -> Vec<u8> { body: &mut [u8],
// body alloc = 18 + 10 + N + 14 + 4 (`NmxWriteMessage.cs:179, 198`). ) {
let mut body = vec![0u8; KIND_OFFSET + 1 + 10 + value_bytes.len() + 14 + 4]; write_common_prefix(body, handle, kind);
write_common_prefix(&mut body, handle, kind);
// body[22..24] = count u16 LE (`NmxWriteMessage.cs:181, 200`). // body[22..24] = count u16 LE (`NmxWriteMessage.cs:181, 200`).
write_u16_le(&mut body, 22, count); write_u16_le(body, 22, count);
// body[24..26] = element_width u16 LE (`NmxWriteMessage.cs:182, 201`). // body[24..26] = element_width u16 LE (`NmxWriteMessage.cs:182, 201`).
write_u16_le(&mut body, 24, element_width); write_u16_le(body, 24, element_width);
// body[18..22] and body[26..28] are zero-initialised by vec! and not // body[18..22] and body[26..28] are zero-initialised by the dispatcher's
// written by the .NET reference either — they remain zero. // resize and not written by the .NET reference either — they remain zero.
body[28..28 + value_bytes.len()].copy_from_slice(value_bytes); body[28..28 + value_bytes.len()].copy_from_slice(value_bytes);
let suffix_start = 28 + value_bytes.len(); let suffix_start = 28 + value_bytes.len();
match timestamp { match timestamp {
@@ -554,7 +850,6 @@ fn encode_array(
} }
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token), None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
} }
body
} }
// ---- Prefix and suffix writers -------------------------------------------- // ---- Prefix and suffix writers --------------------------------------------
@@ -1578,7 +1873,7 @@ mod tests {
expected.extend_from_slice(&[0x01, 0x00]); // .cs:210 (version=1) expected.extend_from_slice(&[0x01, 0x00]); // .cs:210 (version=1)
expected.extend_from_slice(&projection); // .cs:211 expected.extend_from_slice(&projection); // .cs:211
expected.push(0x01); // .cs:98 Boolean wire kind expected.push(0x01); // .cs:98 Boolean wire kind
// Boolean payload literal (.cs:257) // Boolean payload literal (.cs:257)
expected.extend_from_slice(&[0xff, 0xff, 0xff, 0x00]); expected.extend_from_slice(&[0xff, 0xff, 0xff, 0x00]);
// 7-byte zero region of Boolean suffix (.cs:235) // 7-byte zero region of Boolean suffix (.cs:235)
expected.extend_from_slice(&[0; 7]); expected.extend_from_slice(&[0; 7]);