PHP 8.6 的 pack()/unpack() 能写端序了:s、l、q 与 f 的用法和 ValueError 边界
PHP 8.6 给 pack() / unpack() 的格式码加了端序修饰符:在 s、S、l、L、q、Q 之后可以写 ``(大端),f、d 同样支持。修饰符紧跟在格式码后面,位于数量和键名之前。
旧写法:移位拼高低字
以前要往报文里塞一个大端有符号 64 位整数,只能自己拆:
> 32) & 0xFFFFFFFF, $v & 0xFFFFFFFF);
}
// 新写法
$bin = pack('q>', -2);
var_dump(bin2hex($bin)); // fffffffffffffffe
var_dump(bin2hex(be64_old(-2))); // fffffffffffffffe
移位写法依赖算术右移加掩码,负数容易写错;strrev(pack('q', $v)) 更省事,但它假设了机器是小端,换到大端机器上结果就反了。有了修饰符之后,字节序写进格式串本身,和平台无关。
支持的格式码
pack()/unpack() 沿用 Perl 语义:
| 码 | 含义 |
|---|---|
s / S | 有符号 / 无符号 16 位 |
l / L | 有符号 / 无符号 32 位 |
q / Q | 有符号 / 无符号 64 位 |
f / d | float / double |
v n V N P J | 自带固定端序 |
g G e E | 自带固定端序的浮点 |
宽度是格式码定的,跟平台的 long 大小无关:l 恒为 32 位,q 恒为 64 位。这点和 C 里 long 在 LP64 / LLP64 下宽度不同要区分开。
报文字段:头长度、序号、金额
一个典型支付/硬件协议头:2 字节总长度(大端无符号)、4 字节序号(大端无符号)、8 字节金额(大端有符号,单位分,退款为负)。
Nq>', $len, $seq, $cents);
echo bin2hex($frame), PHP_EOL;
// 002400002766fffffffffffffb1e
小端对端则写成 `Slen/Nseq/q>amount', $frame);
// ['len' => 36, 'seq' => 10086, 'amount' => -1250]
// 只解尾部金额:第三个参数是起始偏移
$tail = unpack('q>amount', $frame, 6);
// ['amount' => -1250]
键名写在修饰符之后(`q>amount`)。不写键名时,返回数组的键是 1 起始的数字下标,跨版本改动字段顺序时容易错位,建议一律显式命名。偏移量按字节算,等于前面所有字段宽度之和。
## 有符号还是无符号
`q` / `Q` 只决定这 8 个字节怎么解释,PHP 侧拿到的都是 `int`,不会因为选了 `Q` 就多出一个更大的数值类型。取舍看协议本身:长度、序号一定是非负的,用 `S>` / `N`;金额、余额这类可能为负的字段用 `q>`,别用 `Q>` 然后自己判断符号位。16 位和 32 位同理,`s` 与 `S`、`l` 与 `L` 的区分只在解包时体现,打包时负值写进无符号码会按补码截断。
## 跨语言对端
Java `ByteBuffer` 默认大端,`putShort` / `putInt` / `putLong` 正好对上 `S>` / `N` / `q>`:
```java
ByteBuffer b = ByteBuffer.allocate(14);
b.putShort((short) 36).putInt(10086).putLong(-1250L);
小端对端要显式 b.order(ByteOrder.LITTLE_ENDIAN),对应 `S', 'V', 'P', 'C', 'g', 'e'] as $fmt) {
try {
pack($fmt, 1);
} catch (ValueError $e) {
echo $fmt, ': ValueError', PHP_EOL;
}
}
浮点这边 `f` 等价于 `G`、`d` 等价于 `E`,所以 `g`/`G`/`e`/`E` 上再叠修饰符属于重复指定,同样报错。已有 `g`/`G`/`e`/`E` 的代码不用改,新代码统一用 `f` 只是让格式串读起来和其他字段一致。
需要兼容 PHP 8.5 及更早版本的库,格式串里不能出现这些修饰符。