From 4e76b44391275b180f40574aea070f3701fe891d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 6 May 2026 22:46:02 -0400 Subject: [PATCH] [F52.1] mxaccess-codec: BytesMut output buffer for write encoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. 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) --- design/M6-bench-baseline.md | 46 +- design/followups.md | 2 +- rust/Cargo.lock | 1 + rust/crates/mxaccess-codec/Cargo.toml | 1 + .../mxaccess-codec/benches/alloc_count.rs | 15 +- .../mxaccess-codec/src/write_message.rs | 415 ++++++++++++++---- 6 files changed, 385 insertions(+), 95 deletions(-) diff --git a/design/M6-bench-baseline.md b/design/M6-bench-baseline.md index 7230dc6..cc95747 100644 --- a/design/M6-bench-baseline.md +++ b/design/M6-bench-baseline.md @@ -15,16 +15,17 @@ The bench gates on this: any `write_message::encode` scenario at ## Baseline (release profile, Windows x64) -| scenario | iters | allocs/op | bytes/op | deallocs/op | -|-------------------------------------------|--------:|----------:|---------:|------------:| -| `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` (Float64) | 10,000 | 2.00 | 52 | 2.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 | -| `MxReferenceHandle::from_names` | 10,000 | 2.00 | 22 | 2.00 | -| `NmxSubscriptionMessage::parse_inner` | 10,000 | 1.00 | 72 | 1.00 | -| (DataUpdate, Int32) | | | | | +| scenario | iters | allocs/op | bytes/op | deallocs/op | +|------------------------------------------------|--------:|----------:|---------:|------------:| +| `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` (Float64) | 10,000 | 2.00 | 52 | 2.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_to_bytes_mut` (Int32) | 10,000 | 2.00 | 44 | 2.00 | +| `MxReferenceHandle::from_names` | 10,000 | 2.00 | 22 | 2.00 | +| `NmxSubscriptionMessage::parse_inner` | 10,000 | 1.00 | 72 | 1.00 | +| (DataUpdate, Int32) | | | | | ## Read @@ -56,6 +57,31 @@ With the target already met, F39's scope tightens to: 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`. 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. + ## Reproducing ```powershell diff --git a/design/followups.md b/design/followups.md index d7ad3b6..a7af885 100644 --- a/design/followups.md +++ b/design/followups.md @@ -64,7 +64,7 @@ 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. **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. 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. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f4864ff..02bf168 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -693,6 +693,7 @@ dependencies = [ name = "mxaccess-codec" version = "0.0.0" dependencies = [ + "bytes", "thiserror 2.0.18", ] diff --git a/rust/crates/mxaccess-codec/Cargo.toml b/rust/crates/mxaccess-codec/Cargo.toml index 935cf72..a6b3e07 100644 --- a/rust/crates/mxaccess-codec/Cargo.toml +++ b/rust/crates/mxaccess-codec/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true authors.workspace = true [dependencies] +bytes = { workspace = true } thiserror = { workspace = true } [features] diff --git a/rust/crates/mxaccess-codec/benches/alloc_count.rs b/rust/crates/mxaccess-codec/benches/alloc_count.rs index 17963bf..2c7b156 100644 --- a/rust/crates/mxaccess-codec/benches/alloc_count.rs +++ b/rust/crates/mxaccess-codec/benches/alloc_count.rs @@ -39,7 +39,7 @@ use std::alloc::{GlobalAlloc, Layout, System}; use std::sync::atomic::{AtomicU64, Ordering}; use mxaccess_codec::{ - MxReferenceHandle, NmxSubscriptionMessage, write_message, write_message::WriteValue, + write_message, write_message::WriteValue, MxReferenceHandle, NmxSubscriptionMessage, }; // ---- counting allocator ------------------------------------------------- @@ -202,6 +202,18 @@ 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); + }) +} + fn bench_subscription_decode() -> Row { // Build a single-record DataUpdate body once; decode N times. let body = build_data_update_int32_body(42); @@ -262,6 +274,7 @@ fn main() { bench_write_double(), bench_write_bool(), bench_write_string(), + bench_write_int32_bytes_mut(), bench_handle_from_names(), bench_subscription_decode(), ]; diff --git a/rust/crates/mxaccess-codec/src/write_message.rs b/rust/crates/mxaccess-codec/src/write_message.rs index 0ae3483..95fcbbe 100644 --- a/rust/crates/mxaccess-codec/src/write_message.rs +++ b/rust/crates/mxaccess-codec/src/write_message.rs @@ -88,8 +88,10 @@ // Direct byte indexing — see reference_handle.rs / envelope.rs for rationale. #![allow(clippy::indexing_slicing)] -use crate::MxReferenceHandle; +use bytes::BytesMut; + use crate::error::CodecError; +use crate::MxReferenceHandle; /// Normal-write opcode (`NmxWriteMessage.cs:9`). pub const COMMAND: u8 = 0x37; @@ -253,6 +255,27 @@ pub fn encode( 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 { + let mut dst = BytesMut::new(); + encode_inner_into(handle, value, write_index, client_token, None, &mut dst)?; + Ok(dst) +} + /// Encode a `Write2` (timestamped) body. Mirrors `NmxWriteMessage.EncodeTimestamped` /// (`NmxWriteMessage.cs:36-56`). /// @@ -279,6 +302,30 @@ 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 { + let mut dst = BytesMut::new(); + encode_inner_into( + handle, + value, + write_index, + client_token, + Some(timestamp_filetime), + &mut dst, + )?; + Ok(dst) +} + fn encode_inner( handle: &MxReferenceHandle, value: &WriteValue, @@ -286,54 +333,82 @@ fn encode_inner( client_token: u32, timestamp: Option, ) -> Result, 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, + dst: &mut BytesMut, +) -> Result<(), CodecError> { + write_body_into_bytes_mut(handle, value, write_index, client_token, timestamp, dst) +} + +/// Resize `dst` (a `Vec`) to the encoded body size and fill it. Used by +/// the [`encode`] path so the existing `Vec`-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, + dst: &mut Vec, +) -> Result<(), CodecError> { let kind = value.kind(); match value { - WriteValue::Boolean(b) => Ok(encode_boolean( - handle, - *b, - write_index, - client_token, - timestamp, - )), + WriteValue::Boolean(b) => { + let size = boolean_body_size(timestamp); + resize_vec(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); - Ok(encode_fixed( + let size = fixed_body_size(value_bytes.len()); + resize_vec(dst, size); + write_fixed_body( handle, kind, &value_bytes, write_index, client_token, timestamp, - )) + dst, + ); } - WriteValue::String(s) => { + WriteValue::String(s) | WriteValue::DateTime(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, kind, &value_bytes, write_index, client_token, timestamp, - )) - } - 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, - )) + 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); - Ok(encode_array( + let size = array_body_size(value_bytes.len()); + resize_vec(dst, size); + write_array_body( handle, kind, &value_bytes, @@ -342,13 +417,16 @@ fn encode_inner( 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); - Ok(encode_array( + let size = array_body_size(value_bytes.len()); + resize_vec(dst, size); + write_array_body( handle, kind, &value_bytes, @@ -357,13 +435,16 @@ fn encode_inner( 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); - Ok(encode_array( + let size = array_body_size(value_bytes.len()); + resize_vec(dst, size); + write_array_body( handle, kind, &value_bytes, @@ -372,13 +453,16 @@ fn encode_inner( 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); - Ok(encode_array( + let size = array_body_size(value_bytes.len()); + resize_vec(dst, size); + write_array_body( handle, kind, &value_bytes, @@ -387,13 +471,16 @@ fn encode_inner( write_index, client_token, timestamp, - )) + dst, + ); } - WriteValue::StringArray(arr) => { + WriteValue::StringArray(arr) | WriteValue::DateTimeArray(arr) => { let count = value.array_count().ok_or_else(array_too_large)?; // Variable arrays hard-code element_width = 4 (`NmxWriteMessage.cs:30, 52`). 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, kind, &value_bytes, @@ -402,23 +489,162 @@ fn encode_inner( write_index, client_token, timestamp, - )) - } - 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, - )) + dst, + ); } } + 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, + 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, 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 { @@ -431,21 +657,53 @@ fn array_too_large() -> CodecError { // ---- 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) -> 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 /// (`NmxWriteMessage.cs:121-128`); the timestamped form uses a single-byte /// payload with the 14-byte timestamped suffix (`NmxWriteMessage.cs:130-137`). -fn encode_boolean( +fn write_boolean_body( handle: &MxReferenceHandle, value: bool, write_index: i32, client_token: u32, timestamp: Option, -) -> Vec { + body: &mut [u8], +) { if let Some(filetime) = timestamp { // Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index. // 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(&mut body, handle, WriteValueKind::Boolean); + write_common_prefix(body, handle, WriteValueKind::Boolean); body[KIND_OFFSET + 1] = if value { 0xff } else { 0x00 }; write_timestamped_suffix( &mut body[KIND_OFFSET + 2..], @@ -453,35 +711,31 @@ fn encode_boolean( write_index, client_token, ); - body } else { // 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 mut body = vec![0u8; KIND_OFFSET + 1 + value_bytes.len() + 11 + 4]; - write_common_prefix(&mut body, handle, WriteValueKind::Boolean); + write_common_prefix(body, handle, WriteValueKind::Boolean); body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(&value_bytes); write_boolean_suffix( &mut body[KIND_OFFSET + 1 + value_bytes.len()..], write_index, client_token, ); - body } } /// Fixed-size scalar (Int32, Float32, Float64). Mirrors `CreateFixed` / /// `CreateFixedTimestamped` (`NmxWriteMessage.cs:112-119, 139-146`). -fn encode_fixed( +fn write_fixed_body( handle: &MxReferenceHandle, kind: WriteValueKind, value_bytes: &[u8], write_index: i32, client_token: u32, timestamp: Option, -) -> Vec { - let mut body = vec![0u8; KIND_OFFSET + 1 + value_bytes.len() + 14 + 4]; - write_common_prefix(&mut body, handle, kind); + body: &mut [u8], +) { + write_common_prefix(body, handle, kind); body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(value_bytes); let suffix_start = KIND_OFFSET + 1 + value_bytes.len(); match timestamp { @@ -490,28 +744,26 @@ fn encode_fixed( } None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token), } - body } /// Variable-length payload (String, DateTime). Mirrors `CreateVariable` / /// `CreateVariableTimestamped` (`NmxWriteMessage.cs:148-168`). Total length /// is `44 + utf16_bytes_len`. -fn encode_variable( +fn write_variable_body( handle: &MxReferenceHandle, kind: WriteValueKind, value_bytes: &[u8], write_index: i32, client_token: u32, timestamp: Option, -) -> Vec { - // 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(&mut body, handle, kind); + body: &mut [u8], +) { + write_common_prefix(body, handle, kind); // body[18..22] = outer_length = N + 4 (`NmxWriteMessage.cs:152, 163`) 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`) - 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 + value_bytes.len()].copy_from_slice(value_bytes); let suffix_start = 26 + value_bytes.len(); @@ -521,13 +773,12 @@ fn encode_variable( } None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token), } - body } /// Array body. Mirrors `CreateArray` / `CreateArrayTimestamped` /// (`NmxWriteMessage.cs:170-205`). #[allow(clippy::too_many_arguments)] -fn encode_array( +fn write_array_body( handle: &MxReferenceHandle, kind: WriteValueKind, value_bytes: &[u8], @@ -536,16 +787,15 @@ fn encode_array( write_index: i32, client_token: u32, timestamp: Option, -) -> Vec { - // 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(&mut body, handle, kind); + body: &mut [u8], +) { + write_common_prefix(body, handle, kind); // 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`). - write_u16_le(&mut body, 24, element_width); - // body[18..22] and body[26..28] are zero-initialised by vec! and not - // written by the .NET reference either — they remain zero. + write_u16_le(body, 24, element_width); + // body[18..22] and body[26..28] are zero-initialised by the dispatcher's + // resize and not written by the .NET reference either — they remain zero. body[28..28 + value_bytes.len()].copy_from_slice(value_bytes); let suffix_start = 28 + value_bytes.len(); match timestamp { @@ -554,7 +804,6 @@ fn encode_array( } None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token), } - body } // ---- Prefix and suffix writers -------------------------------------------- @@ -1578,7 +1827,7 @@ mod tests { expected.extend_from_slice(&[0x01, 0x00]); // .cs:210 (version=1) expected.extend_from_slice(&projection); // .cs:211 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]); // 7-byte zero region of Boolean suffix (.cs:235) expected.extend_from_slice(&[0; 7]);