仓颉标准库 std.binary 完全指南:大端/小端字节序转换与字节反转实战
仓颉标准库 std.binary 完全指南:大端/小端字节序转换与字节反转实战
本篇技术指南围绕仓颉(Cangjie)标准库中的 std.binary 包展开,系统讲解如何通过 BigEndianOrder、LittleEndianOrder、SwapEndianOrder 三个接口在仓颉数据类型与二进制字节序列之间进行双向转换,以及如何反转多字节数值的字节顺序。读完本文,你将掌握端序(Endianness)的核心概念、三个接口的全部 API 与参数约定、各内置类型(整数/浮点/布尔)的实现差异、缓冲区不足时的异常行为,并能结合仓库源码理解底层实现原理,在二进制协议解析、网络报文收发、文件格式读写等场景中直接落地使用。
一、背景:为什么需要端序转换
一般来说,多字节对象在存储器或数字通信链路中被存储为连续的字节序列。字节在序列中的排列顺序称为端序(Endianness),又称字节顺序或尾序。字节有两种排列方式:
- 小端序(Little-endian):将一个多位数的低位存储在内存的低地址端,高位存储在内存的高地址端。例如数值
0x1234存储为0x34 0x12。 - 大端序(Big-endian):与上述相反,高位存储在内存的低地址端,低位存储在内存的高地址端。例如数值
0x1234存储为0x12 0x34。
不同的硬件架构、网络协议与文件格式对端序的偏好各不相同(例如 TCP/IP 网络协议栈采用大端序,而多数 x86 主机内存采用小端序)。因此,在跨端序环境交换二进制数据时,显式、可移植的端序转换能力是标准库必须具备的基础设施。仓颉标准库的 std.binary 包正是为此设计。
二、std.binary 包提供的两大能力
依据 binary_package_overview.md 的说明,std.binary 包当前提供以下两类功能:
- 仓颉数据类型和二进制字节序列间的互相转换接口,分为大端序(Big-endian)和小端序(Little-endian)两种转换类型;
- 仓颉数据类型自身大小端序转换(字节顺序反转)的接口。
也就是说,包内既提供了"数值 → 字节数组"与"字节数组 → 数值"的双向读写,也提供了不经过字节数组、直接在数值内部完成字节反转的能力。前者适合做序列化/反序列化,后者适合做内存数据就地转换。
三、核心接口 API 一览
std.binary 包通过三个泛型接口抽象端序操作,具体接口定义与功能如下(详见 binary_package_interfaces.md):
| 接口名 | 功能 |
|---|---|
BigEndianOrder<T> |
大端序字节序列转换接口。 |
LittleEndianOrder<T> |
小端序字节序列转换接口。 |
SwapEndianOrder<T> |
反转字节顺序接口。 |
3.1 BigEndianOrder<T>:大端序字节序列转换
public interface BigEndianOrder<T> {
func writeBigEndian(buffer: Array<Byte>): Int64
static func readBigEndian(buffer: Array<Byte>): T
}
| 方法 | 签名 | 功能 | 参数说明 | 返回值 |
|---|---|---|---|---|
readBigEndian |
static func readBigEndian(buffer: Array<Byte>): T |
从字节数组中以大端序的方式读取一个 T 值 |
buffer: Array<Byte>:缓冲区,用于存放待读取的数据 |
T:读取得到的 T 值 |
writeBigEndian |
func writeBigEndian(buffer: Array<Byte>): Int64 |
将 T 值以大端序的方式写入字节数组 |
buffer: Array<Byte>:缓冲区,用于存放待写入的数据 |
Int64:实际写入的数据字节数 |
3.2 LittleEndianOrder<T>:小端序字节序列转换
public interface LittleEndianOrder<T> {
func writeLittleEndian(buffer: Array<Byte>): Int64
static func readLittleEndian(buffer: Array<Byte>): T
}
| 方法 | 签名 | 功能 | 参数说明 | 返回值 |
|---|---|---|---|---|
readLittleEndian |
static func readLittleEndian(buffer: Array<Byte>): T |
从字节数组中以小端序的方式读取一个 T 值 |
buffer: Array<Byte>:缓冲区,用于存放待读取的数据 |
T:读取得到的 T 值 |
writeLittleEndian |
func writeLittleEndian(buffer: Array<Byte>): Int64 |
将 T 值以小端序的方式写入字节数组 |
buffer: Array<Byte>:缓冲区,用于存放待写入的数据 |
Int64:实际写入的数据字节数 |
3.3 SwapEndianOrder<T>:反转字节顺序
public interface SwapEndianOrder<T> {
func swapBytes(): T
}
| 方法 | 签名 | 功能 | 返回值 |
|---|---|---|---|
swapBytes |
func swapBytes(): T |
反转 T 值的字节顺序 |
T:字节顺序反转后的 T 值 |
四、各内置类型的接口实现
在仓颉标准库中,上述接口通过 extend 扩展机制为具体类型提供实现(源码位于 binary.cj)。以下表格汇总了当前实现所覆盖的类型:
| 类型 | BigEndianOrder |
LittleEndianOrder |
SwapEndianOrder |
|---|---|---|---|
Int8 |
支持 | 支持 | 支持(单字节反转后不变) |
Int16 |
支持 | 支持 | 支持 |
Int32 |
支持 | 支持 | 支持 |
Int64 |
支持 | 支持 | 支持 |
UInt8 |
支持 | 支持 | 支持(单字节反转后不变) |
UInt16 |
支持 | 支持 | 支持 |
UInt32 |
支持 | 支持 | 支持 |
UInt64 |
支持 | 支持 | 支持 |
Float16 |
支持 | 支持 | — |
Float32 |
支持 | 支持 | — |
Float64 |
支持 | 支持 | — |
Bool |
支持 | 支持 | — |
从上表可以看出:
- **整数类型(有符号/无符号)**实现了全部三个接口;
- **浮点类型(
Float16/Float32/Float64)**实现了大小端读写接口,但没有实现SwapEndianOrder; Bool只实现了大小端读写接口;- 单字节类型(
Int8/UInt8)的swapBytes()恒等于自身,因为单字节不存在字节顺序问题。
各类型的读写均返回实际写入/读取的字节数:Int8/UInt8/Bool 返回 1,Int16/UInt16/Float16 返回 2,Int32/UInt32/Float32 返回 4,Int64/UInt64/Float64 返回 8。
4.1 大端序读写示例(Bool)
import std.binary.*
main() {
let buffer: Array<Byte> = [0x1]
let n = Bool.readBigEndian(buffer)
println(n)
}
运行结果:
true
写入示例:
import std.binary.*
main() {
let buffer = Array<Byte>(8, repeat: 0)
let n = true.writeBigEndian(buffer)
println(n == 1)
println(buffer[0] == 0x01u8)
}
运行结果:
true
true
4.2 大端序读写示例(Float16)
以 12.5 为例,其 IEEE 754 半精度表示为 0x4A40:
import std.binary.*
main() {
/* 12.5的IEEE 754表示为0x4A40 */
let buffer: Array<Byte> = [0x4A, 0x40]
let n = Float16.readBigEndian(buffer)
println(n == 12.5)
}
运行结果:
true
写入并观察字节内容(0x4A 的十进制为 74,0x40 的十进制为 64):
import std.binary.*
main() {
let buffer = Array<Byte>(8, repeat: 0)
let len = 12.5f16.writeBigEndian(buffer)
println(len)
/* 0x4A的十进制表示为74,0x40的十进制表示为64 */
println(buffer[0..len])
}
运行结果:
2
[74, 64]
4.3 大端序读写示例(Float32)
12.5 的 IEEE 754 单精度表示为 0x41480000,字节为 [0x41, 0x48, 0x00, 0x00]:
import std.binary.*
main() {
let buffer: Array<Byte> = [0x41, 0x48, 0x00, 0x00]
let n = Float32.readBigEndian(buffer)
println(n)
}
运行结果:
12.500000
4.4 大端序读写示例(Int16 / Int32 / Int64 / UInt 系列)
文档中以 std.unittest 断言形式给出了各整数类型的大端序读写验证,例如:
import std.binary.*
import std.unittest.*
import std.unittest.testmacro.*
main() {
let buffer: Array<Byte> = [0x12, 0x34]
let n = Int16.readBigEndian(buffer)
@Assert(n, 0x1234)
}
写入方向同样成立,例如 Int32 的写入验证:
import std.binary.*
import std.unittest.*
import std.unittest.testmacro.*
main() {
var buffer: Array<Byte> = Array<Byte>(8, repeat: 0)
let n = 0x12345678i32.writeBigEndian(buffer)
@Assert(n, 4)
@Assert(buffer[..n] == [0x12u8, 0x34u8, 0x56u8, 0x78u8])
}
Int64/UInt64 同理,以 8 字节大端序读写 0x1234567890123456:
import std.binary.*
import std.unittest.*
import std.unittest.testmacro.*
main() {
let buffer: Array<Byte> = [0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56]
let n = Int64.readBigEndian(buffer)
@Assert(n, 0x1234567890123456)
}
小端序的读写方向与字节排列完全相反,例如 Int16.readLittleEndian([0x34, 0x12]) 同样得到 0x1234,Int32.writeLittleEndian 则把 0x12345678 写为 [0x78, 0x56, 0x34, 0x12],Float32 的小端字节为 [0x00, 0x00, 0x48, 0x41],Float64 的小端字节为 [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x40]。上述大小端字节排列的镜像关系,正是端序定义的直接体现。
4.5 字节顺序反转示例(SwapEndianOrder)
swapBytes() 直接反转数值内部的字节顺序,不经过字节数组。文档示例(以 Int16 为例):
import std.binary.*
import std.unittest.*
import std.unittest.testmacro.*
main() {
let n = 0x1234i16
let m = n.swapBytes()
@Assert(m, 0x3412)
}
更多验证(来自文档示例):
0x12345678i32.swapBytes()得到0x78563412;0x1234567890123456i64.swapBytes()得到0x5634129078563412;0x12i8.swapBytes()与0x12u8.swapBytes()均为0x12(单字节不变)。
五、异常行为:缓冲区不足的处理约定
std.binary 的读写方法对缓冲区长度有明确校验。依据接口文档与 binary.cj 实现,异常规则如下:
- 当
buffer太小,不足以读出或存储一个Bool、Int8、UInt8值时,抛出 IllegalArgumentException; - 当
buffer太小,不足以读出或存储一个Int16/Int32/Int64/UInt16/UInt32/UInt64/Float16/Float32/Float64值时,抛出 IndexOutOfBoundsException。
从实现看,Int16 及以上的多字节类型统一使用 IndexOutOfBoundsException,错误信息中会携带当前缓冲区大小与所需最小字节数,例如 "Buffer size 4 is too small, need at least 8 bytes.";而单字节的 Bool/Int8/UInt8 在缓冲区为空时抛出 IllegalArgumentException,错误信息为 "Buffer size is 0, need at least 1 byte."。write* 系列在写入前校验,read* 系列在读取前校验,保证边界安全。
六、源码级实现原理
6.1 大端序写入的本质是位移与截断
以 UInt64.writeBigEndian 为例(binary.cj):
@OverflowWrapping
public func writeBigEndian(buffer: Array<Byte>): Int64 {
if (buffer.size < 8) {
throw IndexOutOfBoundsException("Buffer size ${buffer.size} is too small, need at least 8 bytes.")
}
buffer[0] = UInt8(this >> 56)
buffer[1] = UInt8(this >> 48)
buffer[2] = UInt8(this >> 40)
buffer[3] = UInt8(this >> 32)
buffer[4] = UInt8(this >> 24)
buffer[5] = UInt8(this >> 16)
buffer[6] = UInt8(this >> 8)
buffer[7] = UInt8(this)
return 8
}
大端序写入通过"高位先出"的位移序列完成:最高有效字节(第 56~63 位)先写入 buffer[0],最低有效字节最后写入 buffer[7]。UInt64.readBigEndian 则是逆操作,通过按位或(|)与左移把字节重新拼装为 64 位数值。UInt32/UInt16 的实现模式与此完全一致,只是字节数与位移步长按位宽缩减。
6.2 有符号整数委托无符号实现
Int64/Int32/Int16 的写入方法直接委托给对应的无符号类型实现(如 UInt64(this).writeLittleEndian(buffer)),读取方法则从无符号拼装结果转换为有符号类型。字节层面的位模式一致,因此可以安全复用。
6.3 浮点类型通过位模式复用整数实现
Float64/Float32/Float16 并没有独立实现位移逻辑,而是通过 toBits() 取出 IEEE 754 位模式,委托给对应位宽的整数类型的读写方法,读取时再用 Float64.fromBits(bits) 等构造方法还原:
extend Float64 <: BigEndianOrder<Float64> & LittleEndianOrder<Float64> {
public func writeLittleEndian(buffer: Array<Byte>): Int64 {
let bits = this.toBits()
bits.writeLittleEndian(buffer)
}
public static func readBigEndian(buffer: Array<Byte>): Float64 {
let bits = UInt64.readBigEndian(buffer)
return Float64.fromBits(bits)
}
// ...
}
因此浮点数的二进制序列完全遵循 IEEE 754 标准布局,网络协议与文件格式中常见的浮点二进制编码可以据此直接互通。
6.4 字节反转由 FastNative 内建函数加速
swapBytes() 的核心并不在纯仓颉代码中实现,而是声明为 @FastNative 的外部函数(binary.cj 末尾):
@FastNative
foreign func CJ_Bswap64(n: UInt64): UInt64
@FastNative
foreign func CJ_Bswap32(n: UInt32): UInt32
@FastNative
foreign func CJ_Bswap16(n: UInt16): UInt16
@FastNative 将函数调用内联为编译器内建操作,对应硬件层面的字节交换指令(如 bswap),从而避免逐字节搬运的开销。UInt64/UInt32/UInt16 的 swapBytes() 分别调用 CJ_Bswap64/CJ_Bswap32/CJ_Bswap16,有符号类型则先转换为无符号类型再委托,最后转换回原类型。
6.5 Bool 的编码约定
Bool 写入时把 true 编码为字节 0x01、false 编码为 0x00,读取时以"非零即真"(buffer[0] != 0)还原,无论大小端序均占用 1 字节。这也是网络布尔标志位最常用的编码方式。
七、构建与依赖说明
std.binary 包源码位于 stdlib/libs/std/binary/binary.cj,其构建目标通过 CMakeLists.txt 中的 BINARY_SRCS 声明为 binary.cj,随标准库一起编译。使用该包无需额外安装第三方依赖,在仓颉源码中直接导入即可:
import std.binary.*
八、仓库内真实应用案例
std.binary 并非孤立工具,它已被仓颉标准库自身的多个模块作为基础设施依赖。通过观察这些调用点,可以更直观地理解其典型使用场景。
8.1 IPv4 地址的字节化
ipv4_address.cj 中通过 UInt32 的位模式与大端序读写实现 IP 地址与 4 字节序列的互转:
import std.binary.*
// ...
bits.writeBigEndian(_octets)
// ...
return UInt32.readBigEndian(_octets)
8.2 128 位无符号整数的复合读写
uint128.cj 展示了如何在标准库未直接提供 UInt128 端序实现的情况下,组合两次 UInt64 的大端/小端读写完成 128 位数据转换:
public static func readBigEndian(buf: Array<UInt8>): UInt128 {
return UInt128(UInt64.readBigEndian(buf), UInt64.readBigEndian(buf[8..]))
}
public func writeBigEndian(buf: Array<UInt8>): Int64 {
_hi.writeBigEndian(buf)
_lo.writeBigEndian(buf[8..])
}
public func swapBytes(): UInt128 {
UInt128(this._lo.swapBytes(), this._hi.swapBytes())
}
其内部 toU16() 还借助 UInt16.readBigEndian 将大端字节序列切分为 16 位数组,用于 IP 地址的字组表示。
8.3 时区数据库(TZif)文件解析
zone_info_read.cj 使用 Int32.readBigEndian 解析 TZif 二进制时区文件中的头部位偏移与数据偏移。TZif 文件格式以 32 位大端整数记录表头信息,这与本包的大端读取能力精确匹配:
import std.binary.*
// ...
var indexOffset = Int32.readBigEndian(indexOffsetArr)
var dataOffset = Int32.readBigEndian(dataOffsetArr)
8.4 单元测试传输协议的帧长编码
transport.cj 中导入 std.binary.BigEndianOrder,用 Int64.readBigEndian 解析结果长度、用 writeBigEndian 写出载荷大小,体现了"长度前缀 + 载荷"这一经典二进制帧协议的标准做法:
import std.binary.BigEndianOrder
// ...
let resultSize = Int64.readBigEndian(resultSizeBytes)
// ...
payload.size.writeBigEndian(payloadSizeBytes)
以上案例说明:只要涉及跨进程/跨机器的二进制字节流(网络地址、大整数、时区文件、测试协议帧),std.binary 就能提供统一、可移植的端序处理能力。
九、小结
std.binary 是仓颉标准库中处理二进制数据序列化的基础模块,它以三个泛型接口统一了大端序读写、小端序读写与就地字节反转三类操作,并为全部内置整数、浮点与布尔类型提供了实现:
- 读写方向:
BigEndianOrder/LittleEndianOrder分别在字节数组与数值之间建立大端/小端双向转换,write*返回写入字节数; - 就地反转:
SwapEndianOrder.swapBytes()通过@FastNative内建函数直接反转数值字节顺序; - 边界安全:缓冲区不足时按类型抛出
IndexOutOfBoundsException或IllegalArgumentException; - 实现策略:位移拼装、无符号委托、浮点位模式复用与硬件级字节交换指令,兼顾正确性与性能;
- 生态印证:标准库的 net、time、unittest 等模块已将其作为二进制协议解析的底层依赖。
当你在仓颉项目中需要解析网络协议头、读取二进制文件、或与异构系统交换字节序数据时,std.binary 即开即用。