Skip to content

codec: replace codec! with a Codec derive (SFT-8112) - #1

Merged
badicsalex merged 5 commits into
mainfrom
SFT-8112-codec-derive
Oct 15, 2026
Merged

badicsalex merged 5 commits into
mainfrom
SFT-8112-codec-derive

Conversation

@badicsalex

Copy link
Copy Markdown
Contributor

Added a new derive that can create ql-codec Encode and Decode implementations for structs and enums, as well as a Ref version of the type, where applicable fields (Vec, String and friends) are typed as references instead.

Decode can decode to both owned and ref types, and convert from ref to owned. (I originally wrote decode_ref().to_owned() for every owned decode, but that's inefficient for Vec: it does two conversions)

The rest of the types were rewritten to use the derive if possible. Mostly mechanical. Wire format is exactly the same.
Some types that took buffer types and Bytes stuff as generics were simplified, without losing zero-copy. This results in one hacky solution, but IMO that's better than having to implement a whole decode_mut suite. I'll flag this as a review comment.

One interesting thing this allows: I marked all stucts as frozen, to make the wire format same as on main, but we can actually remove that for non-hot messages, allowing us to later easily extend the structs.

A corrupt block count made the receiver reserve 16 bytes per block up
front, which overflows the capacity on 32-bit targets and panics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@badicsalex
badicsalex force-pushed the SFT-8112-codec-derive branch 3 times, most recently from 19a5e26 to 04271a0 Compare October 10, 2026 20:42
Comment thread codec/src/codec.rs
}
}

/// `usize` is a varint, because its width differs between targets

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a design choice. There are no good solutions (having it fixed 64 is wasteful, fixed 32 is incompatible across architectures, not allowing usize would mean we have to modify some 60 fields in keyos)

Comment thread codec/src/lib.rs
fn decode_bytes(bytes: B) -> Result<Self, Error> {
let mut reader = Reader::new(bytes);
Self::decode(&mut reader)
/// Decodes `Self` from the start of `bytes`, ignoring whatever follows it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ignore part here is important, nothing checks the "junk after packet" case, which is different from rkyv and many serde implementations.

Could theoretically affect accidentally modifier frozen structs, but those should be flagged during review.

Comment thread codec/src/lib.rs

/// Marks every encodable type except `u8`
///
/// A `Vec<u8>` borrows as `&[u8]`, because its encoded bytes already are the values. Any other

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt the need for this clunky documentation, this is an unfortunate workaround

Comment thread fsm/src/fsm.rs
let ciphertext_address = record.payload.ciphertext.as_ptr() as usize;
let ciphertext_len = record.payload.ciphertext.len();

// XXX: The reader only lends shared slices, so the ciphertext is located in

@badicsalex badicsalex Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hacky workaround so that we don't need a decode_mut with MutReader or whatever that would be. A lot of unsafe code.

Comment thread fsm/src/fsm.rs
let bytes: Bytes = bytes.into();
let plaintext = bytes.slice(bytes.len() - decrypt_len..);
let frames = wire::parse_session_frames(plaintext);
let bytes = Bytes::from_owner(bytes);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Vec, this is practically equivalent to .into()

Comment thread wire/src/encrypted/mod.rs Outdated
Comment thread fsm/src/session/mod.rs Outdated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@badicsalex
badicsalex force-pushed the SFT-8112-codec-derive branch from 04271a0 to 487a9b6 Compare October 10, 2026 20:53
Only the last byte can overflow, so it's the only one checked.

Redundant encodings such as [0x80, 0x00] decode fine. Rejecting them is
not really necessary, as nothing else makes encodings unique either:
non-frozen structs skip unknown trailing fields. And the check costs
measurably, 6-19 ns per multi-byte varint on the device.

On the device, this cuts the time to decode small messages into their
borrowed form by a third to a half.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@badicsalex
badicsalex force-pushed the SFT-8112-codec-derive branch from 3e76017 to 02ec722 Compare October 11, 2026 14:17
The derive is meant for keyos IPC, so by default it prefixes structs and
enum variants with their length, which lets old and new versions of a
type decode each other. QuantumLink's own wire types use it frozen,
which keeps the codec! layout byte for byte.

Decode no longer abstracts over the byte container. The borrowed form
points into a &[u8] instead, and the frame parser slices stream payloads
out of the record's Bytes itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@badicsalex
badicsalex force-pushed the SFT-8112-codec-derive branch from 02ec722 to 8e29374 Compare October 11, 2026 15:06
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread codec/src/varint.rs
}

if byte & 0x80 == 0 {
if index > 0 && payload == 0 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the payload == 0 check was removed, because it's harmless, but costs some 5% in performance.

@badicsalex
badicsalex merged commit 0b247f4 into main Oct 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant