PHP 雖以 C 語言開發,但未原生支援 C 結構體(`struct`)。透過 `unpack()`/`pack()` 可實現二進位資料與 PHP 陣列/字串的雙向轉換。本文深度解析格式字元對照表、位序/位寬/對齊陷阱、字串終止符處理,並提供完整讀取 C 結構體二進位檔案的實戰範例。
string pack(string $format, mixed ...$values): string
array unpack(string $format, string $data): array
| 字元 | 說明 | 位寬 | 位序/備註 |
|---|---|---|---|
| a | NUL 填充字串 | - | 不足補 `\0`,解包保留尾部 `\0` |
| A | SPACE 填充字串 | - | 不足補空格,解包移除尾部空格 |
| h / H | 十六進位字串 | - | h: 低半位組優先 (LSB),H: 高半位組優先 (MSB) |
| c / C | 有號/無號 char | 1 byte | -128~127 / 0~255 |
| s / S | 有號/無號 short | 2 bytes | 機器位序 (Little/Big Endian) |
| n | 無號 short (Big Endian) | 2 bytes | 網路位序,固定 Big Endian |
| v | 無號 short (Little Endian) | 2 bytes | 固定 Little Endian |
| i / I | 有號/無號 int | 機器相關 | 通常 4 bytes,視平台而定 |
| l / L | 有號/無號 long | 4 bytes | 機器位序 |
| N | 無號 long (Big Endian) | 4 bytes | 網路位序 |
| V | 無號 long (Little Endian) | 4 bytes | 固定 Little Endian |
| f / d | float / double | 4/8 bytes | IEEE 754,機器位序 |
| x | 忽略 1 byte (NUL) | 1 byte | 跳過位移,解包不產生鍵值 |
| X | 倒退 1 byte | 1 byte | 指標後移 |
| @ | 絕對位移填充 NUL | - | 移動到指定絕對位移 |
C 編譯器為效能在成員間插入填充位元組。PHP `unpack` 不自動處理,需手動插入 `x` 跳過。
// C: struct { char c; int i; } // 可能佔 8 bytes (1+3 padding+4)
// PHP 格式:
$format = 'c c3pad i'; // c3pad 跳過 3 bytes 填充
// 或使用 @ 絕對定位
$format = 'c @4 i'; // 位移 4 讀取 int
C 字串以 `\0` 結尾,PHP 字串可包含 `\0`。`unpack('a10', ...)` 會保留尾部 `\0`,導致 `"hello\0world\0"`。
解決: 解包後用 `strtok($str, "\0")` 截取第一個 `\0` 前內容。
PHP 整數為有號。`unpack('C')` 返回 0-255,`unpack('c')` 返回 -128~127。大數值(如 `V` 無號長整數)可能超出 PHP 整數範圍(32 位系統),返回浮點數,需用 `sprintf('%u', $val)` 或 GMP/BCMath 處理。
// struct_def.h
struct UserRecord {
char username[20];
char password[32];
int user_id;
unsigned char status;
}; // 共 20+32+4+1 = 57 bytes (無額外 padding)
// write.c
struct UserRecord user = {"alice", "secret123", 1001, 1};
fwrite(&user, sizeof(user), 1, fp);
<?php
$filename = 'users.dat';
$recordSize = 20 + 32 + 4 + 1; // 57 bytes
// 格式字串:對應 C 結構體成員順序
$format = 'a20username a32password Vuser_id Cstatus';
// V: Little Endian 無號長整數 (4 bytes)
// C: 無號字元 (1 byte)
$data = file_get_contents($filename);
$fileSize = strlen($data);
$recordCount = $fileSize / $recordSize;
if ($fileSize % $recordSize !== 0) {
die("檔案大小非記錄大小整數倍,可能損壞");
}
$users = [];
for ($offset = 0; $offset < $fileSize; $offset += $recordSize) {
// 解包單筆記錄
$record = unpack($format, substr($data, $offset, $recordSize));
// 處理字串終止符
foreach ($record as $key => &$val) {
if (is_string($val)) {
$val = strtok($val, "\0");
}
}
unset($val); // 解除引用
$users[] = $record;
}
// 輸出
print_r($users);
/*
Array
(
[0] => Array
(
[username] => alice
[password] => secret123
[user_id] => 1001
[status] => 1
)
)
*/
$user = [
'username' => 'bob',
'password' => 'newpass456',
'user_id' => 1002,
'status' => 1,
];
// pack 格式需與 unpack 一致
$binary = pack(
'a20 a32 V C',
$user['username'],
$user['password'],
$user['user_id'],
$user['status']
);
file_put_contents('users.dat', $binary, FILE_APPEND | LOCK_EX);
| 方案 | 優點 | 缺點 |
|---|---|---|
| `unpack`/`pack` | 內建、無依賴、靈活 | 格式字串易錯、大量資料較慢 |
| FFI (PHP 7.4+) | 直接映射 C 類型、效能高 | 需 PHP 7.4+、學習曲線、安全限制 |
| Protobuf/MessagePack | 跨語言、Schema 驅動、壓縮率高 | 需預定義 .proto、額外依賴 |
| C 擴展 | 極致效能、原生存取 | 開發維護成本高 |
`unpack`/`pack` 是 PHP 處理二進位資料的核心工具。掌握格式字元語義、位序指定、結構體對齊、字串終止符處理四大關鍵點,即可可靠解析 C 結構體檔案、網路封包、二進位協定。實務上建議封裝為 `BinaryParser` 類別,管理格式定義、位移計算、錯誤處理,提高可維護性。