[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>
This commit is contained in:
Joseph Doherty
2026-05-06 22:46:02 -04:00
parent c7505f9570
commit 4e76b44391
6 changed files with 385 additions and 95 deletions
+36 -10
View File
@@ -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<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.
## Reproducing
```powershell
+1 -1
View File
@@ -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.
+1
View File
@@ -693,6 +693,7 @@ dependencies = [
name = "mxaccess-codec"
version = "0.0.0"
dependencies = [
"bytes",
"thiserror 2.0.18",
]
+1
View File
@@ -9,6 +9,7 @@ rust-version.workspace = true
authors.workspace = true
[dependencies]
bytes = { workspace = true }
thiserror = { workspace = true }
[features]
@@ -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(),
];
+332 -83
View File
@@ -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<BytesMut, CodecError> {
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<BytesMut, CodecError> {
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<i64>,
) -> 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();
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<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 {
@@ -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<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
/// (`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<i64>,
) -> Vec<u8> {
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<i64>,
) -> Vec<u8> {
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<i64>,
) -> Vec<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(&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<i64>,
) -> Vec<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(&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]);