在 Nim 中反转字节和交叉兼容的二进制解析

Reversing Bytes and cross compatible binary parsing in Nim

出于爱好游戏改装的目的,我开始关注 Nim。

简介

然而,当谈到特定于机器的低级内存布局时,我发现与 C 相比,Nim 更难工作,我想知道 Nim 是否真的有更好的支持。

我需要控制字节顺序并能够de/serialize 任意的 Plain-Old-Datatype 对象到二进制自定义文件格式。我没有直接找到一个 Nim 库,它允许灵活的存储选项,比如用 Big-Endian 32 位表示 enumpointers。或者我只是不知道如何使用该功能。

灵活的交叉兼容性意味着,它必须能够 de/serialize 字段独立于 Nim 的 ABI 但具有自定义选项。

也许“Kaitai Struct”更符合我的要求,它是一个具有实验性 Nim 支持的文件解析器。

TL;DR

作为序列化库的解决方法,我自己尝试了一个使用 std/endians 的递归“成员字段反向器”,这几乎就足够了。

但是我在Nim中实现任意长对象的字节反转没有成功。没有实际意义,但我仍然想知道 Nim 是否有解决方案。

我从 std/algorithm 中找到了 reverse()reversed(),但我需要一个字节数组来反转它并将其转回原始对象类型。在 C++ 中会有 reinterprete_cast,在 C 中有 void*-cast,在 D 中有 void[] cast(D 允许从指针定义数组切片)但我无法得到它与 Nim 一起工作。

我尝试了 cast[ptr array[value.sizeof, byte]](unsafeAddr value)[] 但我无法将其分配给新变量。可能是其他问题。

如何“字节反转”任意长的 Plain-Old-Datatype 对象?

如何序列化为字节顺序、成员字段大小、指针为文件“偏移量-起始偏移量”的二进制文件? Nim 中有位域选项吗?

Nim 支持位字段 a set of enums:

type
  MyFlag* {.size: sizeof(cint).} = enum
    A
    B
    C
    D
  MyFlags = set[MyFlag]

proc toNum(f: MyFlags): int = cast[cint](f)
proc toFlags(v: int): MyFlags = cast[MyFlags](v)

assert toNum({}) == 0
assert toNum({A}) == 1
assert toNum({D}) == 8
assert toNum({A, C}) == 5
assert toFlags(0) == {}
assert toFlags(7) == {A, B, C}

对于任意位操作,您可以 the bitops module, and for endianness conversions you have the endians module. But you already know about the endians module, so it's not clear what problem you are trying to solve with the so called byte reversal. Usually you have an integer, so you first convert the integer to byte endian format, for instance, then save that. And when you read back, convert from byte endian format and you have the int. The endianness procs should be dealing with reversal or not of bytes, so why do you need to do one yourself? In any case, you can follow the source hyperlink of the documentation and see how the endian procs are implemented. This can give you an idea of how to cast values 以备不时之需。

既然你知道 C,最后的办法可能是编写一些序列化函数并从 Nim 调用它们,或者直接嵌入它们 using the emit pragma。然而,这看起来像是跨平台最少且无痛的选择。

无法回答有关通用数据结构序列化库的任何问题。我偏离它们是因为它们往往需要手持对您的代码施加某些限制,并且根据功能集,简单的重构(更改 POD 中的字段顺序)可能会破坏生成的输出的二进制兼容性,而您却没有注意到它直到运行。因此,您最终会花费额外的时间编写单元测试来验证您为节省时间而引入的黑匣子是否按照您想要的方式运行(并在重构和版本升级过程中继续这样做!)。

确实可以使用 algorithm.reverse 和适当的转换调用来就地反转字节:

import std/[algorithm,strutils,strformat]

type
  LittleEnd{.packed.} = object
    a: int8
    b: int16
    c: int32
  BigEnd{.packed.} = object
    c: int32
    b: int16
    a: int8

## just so we can see what's going on:
proc `$`(b: LittleEnd):string = &"(a:0x{b.a.toHex}, b:0x{b.b.toHex}, c:0x{b.c.toHex})"
proc `$`(l:BigEnd):string = &"(c:0x{l.c.toHex}, b:0x{l.b.toHex}, a:0x{l.a.toHex})"


var lit = LittleEnd(a: 0x12, b:0x3456, c: 0x789a_bcde)
echo lit # (a:0x12, b:0x3456, c:0x789ABCDE)

var big:BigEnd

copyMem(big.addr,lit.addr,sizeof(lit))

# here's the reinterpret_cast you were looking for:
cast[var array[sizeof(big),byte]](big.addr).reverse

echo big # (c:0xDEBC9A78, b:0x5634, a:0x12)

对于 C 风格的位域还有 {.bitsize.} pragma 但是使用它会导致 Nim 丢失 sizeof 信息,当然位域不会在 bytes

内反转
import std/[algorithm,strutils,strformat]

type
  LittleNib{.packed.} = object
    a{.bitsize: 4}: int8
    b{.bitsize: 12}: int16
    c{.bitsize: 20}: int32
    d{.bitsize: 28}: int32
  BigNib{.packed.} = object
    d{.bitsize: 28}: int32
    c{.bitsize: 20}: int32
    b{.bitsize: 12}: int16
    a{.bitsize: 4}: int8
const nibsize = 8

proc `$`(b: LittleNib):string = &"(a:0x{b.a.toHex(1)}, b:0x{b.b.toHex(3)}, c:0x{b.c.toHex(5)}, d:0x{b.d.toHex(7)})"
proc `$`(l:BigNib):string = &"(d:0x{l.d.toHex(7)}, c:0x{l.c.toHex(5)}, b:0x{l.b.toHex(3)}, a:0x{l.a.toHex(1)})"
var lit = LitNib(a: 0x1,b:0x234, c:0x56789, d: 0x0abcdef)
echo lit # (a:0x1, b:0x234, c:0x56789, d:0x0ABCDEF)


var big:BigNib

copyMem(big.addr,lit.addr,nibsize)
cast[var array[nibsize,byte]](big.addr).reverse
echo big # (d:0x5DEBC0A, c:0x8967F, b:0x123, a:0x4)

无论如何,复制字节然后用 reverse 重新排列它们并不是最佳选择,因此您可能只想循环复制字节。这是一个可以交换任何对象的字节顺序的过程,(包括那些 sizeof 在编译时未知的对象):

template asBytes[T](x:var T):ptr UncheckedArray[byte] = 
  cast[ptr UncheckedArray[byte]](x.addr)

proc swapEndian[T,U](src:var T,dst:var U) =
  assert sizeof(src) == sizeof(dst)
  let len = sizeof(src)
  for i in 0..<len:
    dst.asBytes[len - i - 1] = src.asBytes[i]