仓颉标准库 std.binary 完全指南:大端/小端字节序转换与字节反转实战

原创2026-10-13 13:28:471,448 阅读
文章标签:编程语言语言运行时标准库内存管理OpenHarmony

仓颉标准库 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 包当前提供以下两类功能:

  1. 仓颉数据类型和二进制字节序列间的互相转换接口,分为大端序(Big-endian)和小端序(Little-endian)两种转换类型;
  2. 仓颉数据类型自身大小端序转换(字节顺序反转)的接口。

也就是说,包内既提供了"数值 → 字节数组"与"字节数组 → 数值"的双向读写,也提供了不经过字节数组、直接在数值内部完成字节反转的能力。前者适合做序列化/反序列化,后者适合做内存数据就地转换。

三、核心接口 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 即开即用。

登录后查看全文
cangjie_runtime