Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compile-time packet construction in Rust

Tags:

I have a C++ program where I've used template metaprogramming to generate small binary-format packets to be sent "over the wire", which gives better safety and clarity than a more naive approach of allocating a fixed-size buffer and copying various data items into it using calculated-by-hand offsets.

int foo(int fd, long long data1, float f1)
{
    auto m = membuf()
             .append<char>(0xAA).append<char>(0xBB)  // header
             .append(data1)
             .append(f1)
             .append(555); // in the pipe (arbitary extra data)
    return write(fd, m.data(), m.size());
}

This would send a packet consisting of the two bytes 0xAA and 0xBB, (for example) 8 bytes from data1, 4 bytes from f1, and 4 bytes forming 555. (Actual sizes of int and so on would depend on compiler/architecture details of course, but I could also use eg uint64_t type for precise control).

(Note: The full implementation of membuf isn't really relevant to the question, but you can see it here if interested: https://godbolt.org/z/sr0Cuu)

The important characteristics in this case are that:

  • no heap allocation is involved, and
  • the size of the data packet and the offsets of each of the values are calculated at compile time
  • it's all standard C++, no extensions, no experimental features (in fact it's C++11)

As it happens, this is compiled into a very efficient sequence of instructions which simply allocates the buffer on the stack and writes each value to the correct place within it:

foo(int, long long, float):
        subq    $40, %rsp
        movl    $-17494, %eax
        movl    $18, %edx
        movq    %rsi, 2(%rsp)
        movq    %rsp, %rsi
        movw    %ax, (%rsp)
        movl    $555, 14(%rsp)
        movd    %xmm0, 10(%rsp)
        call    write
        addq    $40, %rsp
        ret

What I am looking for is a Rust solution to achieve the same thing. I don't mind necessarily if the Rust compiler can't currently produce code that is quite as efficient as above, but it is important that the above requirements are met: no heap allocation, no dynamic calculation of packet size or data offsets, no use of experimental/"unstable" language features.

I've been reading the Rust book and trying to understand if and how I could do this in Rust, but so far I've gotten nowhere:

  • Generic types don't seem to help, as they are more like "templates" in the original sense of the word than in the C++ sense. They also don't seem to allow parameterising by anything but types.
  • Macros seem to be the metaprogramming tool of choice in Rust, but unless I'm not understanding correctly they operate on token streams and, unless there's a way I'm missing, they can't do the kind of thing that the membuf example does.

Essentially: I want a generic type, parameterised by buffer size, which can take a value and return a larger, fixed-size buffer, with the data appended at the end. But maybe that specification is too C++-centric and there's another tack that can be taken in Rust - I just need to figure out what it is!

like image 561
davmac Avatar asked Jul 30 '19 10:07

davmac


1 Answers

Slightly enlarging my comment with actual code: The restruct-crate can do what you ask for; it does, however, require nightly as of now, as packing und unpacking are const-functions, which are not stable yet.

Given your example, after adding restruct and restruct_derive to the dependencies:

#![feature(const_int_conversion)]
#![feature(const_fn)]
#![feature(const_slice_len)]
#![feature(const_transmute)]


/// A packer/unpacker for two unsigned bytes, a group of eight unsigned bytes, a group of
/// four unsigned bytes and four padding bytes; all in little endian.
#[derive(restruct_derive::Struct)]
#[fmt="< 2B 8s 4s 4x"]
struct Foo;


fn main() {
    let data = (0xAA, 0xBB, [1,2,3,4,5,6,7,8], [4,3,2,1]);

    println!("The buffer has a size of {}", Foo::SIZE);

    let buf: [u8; Foo::SIZE] = Foo::pack(data);
    println!("Packed as {:?}", buf);

    let unpacked: <Foo as restruct::Struct>::Unpacked = Foo::unpack(buf);
    println!("Unpacked as {:?}", unpacked);
}
like image 79
user2722968 Avatar answered Nov 15 '22 04:11

user2722968