[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:
+36
-10
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Generated
+1
@@ -693,6 +693,7 @@ dependencies = [
|
||||
name = "mxaccess-codec"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user