Skip to content

[FEAT] Zero-allocation Linkable::encode_into(&mut [u8]) #177

Description

@lxsaah

Background

Linkable is the byte-oriented codec seam for connector links:
to_bytes(&self) -> Result<Vec<u8>, String> and
from_bytes(&[u8]) -> Result<Self, String> (aimdb-data-contracts/src/lib.rs:252).
The .linked_to(url) verb wires the outbound path straight onto it —
with_serializer(|_, v| v.to_bytes()...) (aimdb-data-contracts/src/linkable.rs:41).

Because to_bytes returns an owned Vec<u8>, every outbound message allocates a
fresh heap buffer, then the connector copies it into the transport. On a no_std /
embedded target (Embassy, thumbv7em-none-eabihf) the connector typically already
owns a bounded, preallocated TX buffer, so that per-message allocation is exactly
the heap traffic we otherwise design out. postcard makes this avoidable —
emit_linkable_postcard (aimdb-codegen/src/rust.rs:996) currently calls
postcard::to_allocvec, but postcard::to_slice serializes with no intermediate
Vec at all.

Proposal

Add an optional encode-into-slice method to Linkable, with a default impl so
existing implementations keep compiling unchanged:

/// Serialize into a caller-provided buffer, returning bytes written.
/// Default impl falls back to `to_bytes()` + copy.
fn encode_into(&self, buf: &mut [u8]) -> Result<usize, CodecError> {
    let bytes = self.to_bytes().map_err(CodecError::from)?;
    if bytes.len() > buf.len() {
        return Err(CodecError::BufferTooSmall);
    }
    buf[..bytes.len()].copy_from_slice(&bytes);
    Ok(bytes.len())
}
  • Codegen emits a true zero-alloc override (postcard::to_slice) for
    serialization = "postcard" records.
  • .linked_to prefers encode_into into the connector's transport buffer when
    one is exposed, falling back to to_bytes otherwise.

This should be done together with the CodecError alignment already recorded
in design-041 §7 (docs/design/041-data-contracts-integration.md:378): today
Linkable's String error is dropped lossily to SerializeError::InvalidData
(aimdb-data-contracts/src/linkable.rs:44). Both changes touch the same
Linkable ↔ connector-serializer signatures and both remove an embedded
allocation, so they belong in one pass rather than two.

Why it's interesting

  • Removes the per-message heap allocation on the embedded outbound hot path.
  • postcard already supports to_slice, so the win is real with no new dependency.
  • Backwards-compatible: the default impl means no existing Linkable impl breaks.

Constraints / risks

  • Requires settling CodecError's shape first (coordinate with design-041 §7);
    the seam and the error type move together.
  • encode_into must stay optional — hand-written JSON/binary impls that don't want
    a zero-alloc path keep the default.

Scope

Trait method + codegen override + connector wiring, gated on the CodecError
decision. No change to the self-describing (AimX/MCP/CLI) paths.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions