Rust 中的编译时数据包构造
Compile-time packet construction in Rust
我有一个 C++ 程序,我在其中使用模板元编程生成要发送的小型二进制格式数据包 "over the wire",与分配固定大小的更天真的方法相比,它提供了更好的安全性和清晰度缓冲区并使用手动计算的偏移量将各种数据项复制到其中。
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());
}
这将发送一个包含两个字节 0xAA
和 0xBB
的数据包,(例如)来自 data1
的 8 个字节,来自 f1
的 4 个字节,以及4 个字节组成 555
。 (int
等的实际大小当然取决于 compiler/architecture 细节,但我也可以使用例如 uint64_t
类型进行精确控制。
(注意:membuf
的完整实现与问题并不相关,但如果有兴趣,您可以在此处查看:https://godbolt.org/z/sr0Cuu)
这种情况下的重要特征是:
- 不涉及堆分配,并且
- 数据包的大小和每个值的偏移量是在编译时计算的
- 都是标准的C++,没有扩展,没有实验性的特性(实际上是C++11)
碰巧,这被编译成一个非常有效的指令序列,它简单地在堆栈上分配缓冲区并将每个值写入其中的正确位置:
foo(int, long long, float):
subq , %rsp
movl $-17494, %eax
movl , %edx
movq %rsi, 2(%rsp)
movq %rsp, %rsi
movw %ax, (%rsp)
movl 5, 14(%rsp)
movd %xmm0, 10(%rsp)
call write
addq , %rsp
ret
我正在寻找的是一个 Rust 解决方案来实现同样的事情。如果 Rust 编译器目前不能生成与上述一样高效的代码,我并不介意,但满足上述要求很重要:没有堆分配,没有数据包大小或数据偏移的动态计算,没有使用实验性/"unstable" 语言功能。
我一直在阅读 the Rust book 并试图了解我是否可以以及如何在 Rust 中做到这一点,但到目前为止我一无所获:
- 泛型类型似乎没有帮助,因为它们更像是 "templates" 这个词的原始含义而不是 C++ 含义。他们似乎也不允许除类型之外的任何参数化。
- 宏似乎是 Rust 中选择的元编程工具,但除非我没有正确理解它们在令牌流上运行,而且除非我遗漏了一种方法,否则它们不能做那种事情
membuf
示例确实如此。
本质上:我想要一个通用类型,由缓冲区大小参数化,它可以接受一个值和 return 一个更大的固定大小的缓冲区,最后附加数据。但也许该规范过于以 C++ 为中心,在 Rust 中可以采取另一种策略——我只需要弄清楚它是什么!
用实际代码稍微放大我的评论:restruct
-crate 可以做你要求的;但是,它确实需要 nightly 截至目前,因为打包和解包是 const
函数,还不稳定。
给定您的示例,在将 restruct
和 restruct_derive
添加到依赖项之后:
#![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);
}
我有一个 C++ 程序,我在其中使用模板元编程生成要发送的小型二进制格式数据包 "over the wire",与分配固定大小的更天真的方法相比,它提供了更好的安全性和清晰度缓冲区并使用手动计算的偏移量将各种数据项复制到其中。
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());
}
这将发送一个包含两个字节 0xAA
和 0xBB
的数据包,(例如)来自 data1
的 8 个字节,来自 f1
的 4 个字节,以及4 个字节组成 555
。 (int
等的实际大小当然取决于 compiler/architecture 细节,但我也可以使用例如 uint64_t
类型进行精确控制。
(注意:membuf
的完整实现与问题并不相关,但如果有兴趣,您可以在此处查看:https://godbolt.org/z/sr0Cuu)
这种情况下的重要特征是:
- 不涉及堆分配,并且
- 数据包的大小和每个值的偏移量是在编译时计算的
- 都是标准的C++,没有扩展,没有实验性的特性(实际上是C++11)
碰巧,这被编译成一个非常有效的指令序列,它简单地在堆栈上分配缓冲区并将每个值写入其中的正确位置:
foo(int, long long, float):
subq , %rsp
movl $-17494, %eax
movl , %edx
movq %rsi, 2(%rsp)
movq %rsp, %rsi
movw %ax, (%rsp)
movl 5, 14(%rsp)
movd %xmm0, 10(%rsp)
call write
addq , %rsp
ret
我正在寻找的是一个 Rust 解决方案来实现同样的事情。如果 Rust 编译器目前不能生成与上述一样高效的代码,我并不介意,但满足上述要求很重要:没有堆分配,没有数据包大小或数据偏移的动态计算,没有使用实验性/"unstable" 语言功能。
我一直在阅读 the Rust book 并试图了解我是否可以以及如何在 Rust 中做到这一点,但到目前为止我一无所获:
- 泛型类型似乎没有帮助,因为它们更像是 "templates" 这个词的原始含义而不是 C++ 含义。他们似乎也不允许除类型之外的任何参数化。
- 宏似乎是 Rust 中选择的元编程工具,但除非我没有正确理解它们在令牌流上运行,而且除非我遗漏了一种方法,否则它们不能做那种事情
membuf
示例确实如此。
本质上:我想要一个通用类型,由缓冲区大小参数化,它可以接受一个值和 return 一个更大的固定大小的缓冲区,最后附加数据。但也许该规范过于以 C++ 为中心,在 Rust 中可以采取另一种策略——我只需要弄清楚它是什么!
用实际代码稍微放大我的评论:restruct
-crate 可以做你要求的;但是,它确实需要 nightly 截至目前,因为打包和解包是 const
函数,还不稳定。
给定您的示例,在将 restruct
和 restruct_derive
添加到依赖项之后:
#![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);
}