Skip to content

Feature/zcopy dser - #10

Closed
alambert-lanl wants to merge 1 commit into
lanl:mainfrom
alambert-lanl:feature/zcopy-dser
Closed

Feature/zcopy dser#10
alambert-lanl wants to merge 1 commit into
lanl:mainfrom
alambert-lanl:feature/zcopy-dser

Conversation

@alambert-lanl

@alambert-lanl alambert-lanl commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

The flag to enable this is --zero-copy

@alambert-lanl
alambert-lanl marked this pull request as draft June 3, 2026 00:27
@alambert-lanl
alambert-lanl force-pushed the feature/zcopy-dser branch 12 times, most recently from 07d3818 to 63ea40c Compare June 8, 2026 15:22
Comment thread xdr_codegen/src/validate.rs Outdated
let decl_size = size_tab
.get(tn)
.expect("could not find size information for type \"{tn}\"");
let decl_size = size_tab.get(tn).expect(&format!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this change should be pulled out into a separate PR and merged in first, since it's a bug fix of a pre-existing bug that doesn't depend on the rest of the code here

@bertschinger bertschinger left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this needs to be broken up into smaller PRs, it's a little too large to review all at once.

Can you start with a PR that only handles non-recursive structs? Unions, linked lists, can just be skipped, you can just throw a todo!() on them for now.

I want to focus on getting the simplest cases right first and that'll be easier with a much more focused PR.

Comment thread xdr_codegen/src/codegen/deserialize.rs Outdated
}
}

pub(super) fn deserialize_inline_zcopy(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would probably pull all of the new zcopy functions into a new file, somethng like deserialize_zc.rs to help keep things organized

#[test]
fn test_structs_basic() {
#[rustfmt::skip]
let data: Vec<u8> = vec![

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's pretty hard to read these byte vectors. In general I'd rather define a Rust struct that contains the data, use the allocating serialization APIs to convert it into an array of bytes, and then test the zcopy functions on that byte array.

Admittedly that doesn't work for testing error cases where we want to ensure that trying to read an invalid message returns an error. For those cases, you can either hand-write the byte vector, or serialize the rust struct into a byte vector which you then modify (for example by overwriting the length of an array with a too long length or something).

@alambert-lanl
alambert-lanl force-pushed the feature/zcopy-dser branch 3 times, most recently from 70eef47 to 51ddd12 Compare June 10, 2026 16:09
Comment thread xdr_codegen/src/main.rs
#[arg(short, long)]
no_alloc: bool,

/// Whether to generate zero-copy serdes routines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This flag should probably be specific to deserialization - if a zero copy serialization API is later added, that can be a separate flag

Comment thread xdr_codegen/src/main.rs
if args.no_alloc {
compiler.enable_no_alloc().disable_alloc().run()
} else if args.zero_copy {
compiler.disable_alloc().enable_zcopy().run()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Enabling zero copy shouldn't prevent compiling the allocating (or non-allocating) routines. Right now, this is problematic because compiling the zero-copy deserialize routines doesn't produce any serialization code so there's no way to use this in a real app that needs to do both sides.

It should be possible to generate:

  • just alloc or just no-alloc
  • zcopy + alloc
  • zcopy + no-alloc

Comment thread xdr_lib/src/lib.rs
Comment thread xdr_lib/src/lib.rs
where
T: Reader<'a>,
{
type Item = T;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see that the iterator has an err field - I guess this is meant to indicate that the message was corrupt and the user of this API is expected to check that field after None is returned to see whether iteration finished normally or abnormally.

That API seems a bit easy to misuse. Will users of the API know that they have to check iter.err?

Maybe the type should be xdr_lib::Result<T> and when the message is found to be invalid, Some(Err(_)) is returned. This is still a bit inelegant because then one more next() call will be required to return None. Unfortunately fallible iterators seem a bit clunky but making it return Result<T> seems harder to misuse by forgetting to check that a message was corrupt.

Comment thread xdr_codegen/src/codegen/mod.rs
&format!("impl<'a> xdr_lib::Reader<'a> for {}Reader<'a>", self.name),
|buf| {
buf.code_block(
"fn from_buf(buf: &'a [u8]) -> Result<Self, xdr_lib::DeserializeError>",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These signatures should return the less verbose xdr_lib::Result<Self>

include!(concat!(env!("OUT_DIR"), "/structs.rs"));

use crate::structs::*;
use xdr_lib::Reader;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think users of the API should have to import this trait. There should be a constructor that doesn't depend on a trait. It can always just call from_buf() if that's the implementation that makes the most sense:

    impl<'a> FooReader<'a> {
          pub fn new(buf: &'a [u8]) -> xdr_lib::Result<Self> {
              Self::from_buf(buf)
          }
    ....

Comment thread xdr_lib/src/lib.rs

pub err: Option<DeserializeError>,
// DEFAULT INIT BELOW
pub off: usize,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

most of these members don't seem like they should be public

buf.code_block(&format!("pub struct {}Reader <'a>", self.name), |buf| {
buf.add_line("buf: &'a [u8],");
for dep in deps.iter() {
buf.add_line(&format!("{}_width: std::cell::OnceCell<usize>,", dep));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the OnceCell pattern makes this code trickier and more complex than it needs to be.

From looking at a simple struct case:

struct foo {
    opaque str<>;
    int a;
};

it seems that the width is always filled in at construction time:

from_buf()
  -> validate()
    -> get_str_width() // sets width

later, the width is read in

get_a()
  -> get_str_width() // gets width

Soget_str_width() is effectively used as both a getter and a setter.

I would rather the struct be defined as

    pub struct fooReader <'a> {
        buf: &'a [u8],
        str_width: usize,
    }

And the constructor could be something like:

        fn from_buf(buf: &'a [u8]) -> xdr_lib::Result<Self> {
            let str_width = Self::initialize_str_width(buf)?;
            let me = Self {
                buf,
                str_width,
            };
            me.validate()
        }

then later, instead of calling self.get_str_width(), simply access the field directly:

        pub fn get_a(&self) -> i32 {
            xdr_lib::get_i32_immut(&self.buf[self.str_width])
        }

Sure, the OnceCell pattern prevents redefining the value once it's initialized - but actually, it's not possible to do that anyways because there are no methods on fooReader that take a mutable reference. That makes fooReader effectively an immutable object, so the OnceCell isn't providing a useful service here.

@alambert-lanl

Copy link
Copy Markdown
Contributor Author

closing in favor of #21

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.

2 participants