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:
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:
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!
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With