马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_blob 补齐 PowerBuilder 的二进制处理短板(hex/base64、按类型读写、CRC/MD5 与文件映射,PB10 基准 · PB12.5 实测)
【阅读说明】
- 适用版本:PBIDEA 组件,PowerBuilder 8 及以上通用(写法按 PB10 基准给出);PB 12.5 差异见文末单独说明,本文以顶部【版本声明】核验的版本为准。
- 支持数据库:本文不涉及数据库。
- 操作系统与环境要求:Windows 7 及以上(PBIDEA 本体支持 Windows XP+);需随应用分发 PbIdea.dll 运行库;无其它第三方依赖。
- 难度系数:★★★☆☆(需要一点字节序与字符编码的底子,示例都给全了照抄能跑)。
- 其它说明:文中所有读数(hex 串、CRC 值、字节数)均为实机运行输出,可直接对照复现;本文与《PowerBuilder 用 UPDATEBLOB 与 SELECTBLOB 存取图片等大字段》互补——那篇讲"blob 进出数据库",这篇讲"blob 在内存里怎么玩"。 【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-10-01;当次核验的 PBIDEA 核心运行库:18,105,344 字节 / 2544 个导出,md5 4f49718e4460…。
- 版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
- 适用平台:Windows XP 及以上(XP / 7 / 10 / 11 均可);PowerBuilder 8 及以上(PB 8/9/10/10.5/11.x/12.x 及以上通用)。本文的实机验证环境为 Windows 10 + PB 10 与 PB 12.5。
- PBIDEA 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。
一、PB 原生 Blob 到底缺什么
PowerBuilder 处理二进制数据的原生家底就几样:Blob()、String()、Len()、BlobMid()、BlobEdit(),再加数据库侧的 UPDATEBLOB/SELECTBLOB。够不够用?写一遍协议解析就知道了:
- 想把整数按字节写进报文?自己拆高低位,还得惦记大小端;
- 想 hex 字符串和二进制互转?没有,自己写循环;
- 想 CRC16/CRC32/MD5?没有,要么调 API 要么纯手搓;
- 想在 100MB 文件的第 800 万字节改一个值?FileRead 全量读进内存再写回,内存和速度都难看。
uo_blob 就是补这块短板的:一个非可视类用户对象,内部直接在二进制缓冲区上操作,hex/base64 编解码、按类型读写、查找替换、CRC 校验、文件映射读写一条龙。本文全部结论来自双版实机逐条运行,不少行为和注释字面意思不一致,坑都替你踩完了。
二、快速上手:set / get / size / hex
uo_blob 声明后必须 create(它不是 autoinstantiate 对象),用完 destroy。最小例子:
- // 演示:uo_blob 的创建、写入、取长度、hex 展示
- uo_blob lb // 声明 uo_blob 变量(非 autoinstantiate,需手工创建)
- string ls_hex // 接收 hex 展示串
- long ll_size // 接收字节数
- lb = create uo_blob // 创建对象,构造函数里已分配好内部缓冲区
- lb.set('ABC') // 写入字符串(默认按 UTF-8 编码,且追加 1 个 00 结束符)
- ll_size = lb.size() // 取当前字节数:实测 = 4(3 个字母 + 1 个 00)
- ls_hex = lb.hex() // 转 hex 字符串:实测 = 41424300
- MessageBox('uo_blob', 'size=' + String(ll_size) + ' hex=' + ls_hex)
- // 实测输出:size=4 hex=41424300 —— set(string) 会多出一个 00 结束符!
- destroy lb // 用完销毁
复制代码
第一坑当场就来了:set(string) 会在末尾追加一个 00 结束符。存 'ABC' 得到 4 字节。这个 00 会一路影响后面所有"按整块数据算"的功能(base64 串尾带 AA==、CRC/MD5 把 00 一起算进去),后面逐个说。
读取用 get,位置从 1 开始(全对象统一 1 基,实机验证 ReadByte(0) 返回 0、get(0,...) 返回 false):
- // 演示:按位置读取 + 按类型读取
- uo_blob lb // uo_blob 对象
- int li_v // 接 int(2 字节)
- int li_b // 接单字节(asByte 参数)
- long ll_v // 接 long(4 字节)
- lb = create uo_blob
- lb.set(16909060) // 写入 long 0x01020304,小端存放为 04 03 02 01
- lb.get(1, ll_v) // 从位置 1 读 4 字节 long:= 16909060
- lb.get(1, li_v) // 从位置 1 读 2 字节 int:= 0x0304 = 772
- lb.get(1, li_b, true) // 第三参 true:只读 1 个字节:= 0x04 = 4
- lb.ReadByte(1) // 等价写法,直接返回位置 1 的字节:= 4
- MessageBox('uo_blob', 'long=' + String(ll_v) + ' int=' + String(li_v) + ' byte=' + String(li_b))
- // 实测输出:long=16909060 int=772 byte=4
- destroy lb
复制代码
注意 get(pos, ref int) 读 2 字节,而 set(int) 写 4 字节(见下节)——这是本文第一个"不对称"。
三、字符编码三态:默认 UTF-8、true 也是 UTF-8、false 是宽字符
set(string ...) / append(string ...) 带一个可选的编码参数,帮助注释写的是"是否转 UTF-8",实测行为是三态:
| 写法 | 'ABC' 实测字节 | '中' 实测字节 | 结论 | | set('X')(默认) | 41 42 43 00 | E4 B8 AD 00 | UTF-8 + 1 字节 NUL | | set('X', true) | 同默认 | 同默认 | true = UTF-8 | | set('X', false) | 41 00 42 00 43 00 00 00 | 2D 4E 00 00 | UTF-16LE 宽字符 + 2 字节宽 NUL |
- // 演示:同一字符串三种编码形态
- uo_blob lb // uo_blob 对象
- lb = create uo_blob
- lb.set('中') // 默认:UTF-8,实测 E4B8AD00(4 字节)
- lb.clear() // 清空缓冲区(reset() 同义)
- lb.set('中', true) // true:UTF-8,实测 E4B8AD00(4 字节)
- lb.clear()
- lb.set('中', false) // false:宽字符,实测 2D4E0000(4 字节)
- // '中' 的 Unicode 码点 U+4E2D,小端即 2D 4E
- MessageBox('uo_blob', '默认=' + lb.hex())
- // 提示:把上一行放到想要观察的分支后面,hex() 就是内存的真实字节
- destroy lb
复制代码
想要 GBK 字节怎么办? uo_blob 的 string 入口给不了 GBK(这是和直觉最冲突的一点)。办法是绕道 PB 原生转换:Blob('中') 在 PB 里按系统 ANSI(中文系统即 GBK)转码,得 D6 D0,再交给 uo_blob 处理。但注意下一节的结论——带 readonly blob 参数的重载一律别用,所以 GBK 字节请用 FromHex('D6D0') 这类"纯字节"入口进对象。
四、按类型读写:写入宽度、读取宽度与大坑
把常用类型的实测宽度列成表(小端默认):
| 类型 | set 写入 | get 读取 | 往返 | | int | 4 字节(C int) | 2 字节 | ⚠️ 不对称 | | int + asByte(true) | 仍 4 字节(不生效) | 1 字节(生效) | ⚠️ 两侧不一致 | | long / ulong | 4 字节 | 4 字节 | ✓ 对称 | | longlong | 8 字节 | 8 字节 | ✓ 对称 | | double / real / decimal | 一律 4 字节 float32 | get(double) 读 8 字节 | ⚠️ 丢精度 | | IEEE754(专用函数) | double 8 字节 | 8 字节 | ✓ 精确往返 |
两个大坑:
坑一:set(3.14) 实际按 4 字节 float32 存。不管你传 double 变量还是字面量,实测 size 都是 4,读回来是 3.140000104904175(float32 的双精度投影)。要 8 字节 IEEE754 双精度,必须用专用的 SetIEEE754 / GetIEEE754(第 4 条,实测 3.14 精确往返)。
坑二:连续 append int 再逐个 get(int) 会错位——每个 int 实际占 4 字节,读取只走 2 字节。正道是:4 字节数值统一用 long 写、long 读;真正要单字节的场景(协议里的 BYTE 字段)用 get(pos, ref int, true) 读或 append(n, true) 写(append 的 asByte 实测生效,写 1 字节)。
大小端与字节交换:
- // 演示:大端写入与 SwapInt 字节对换
- uo_blob lb // uo_blob 对象
- lb = create uo_blob
- lb.SetAlign(1) // 1 = 大端,0 = 小端(默认),影响后续 int/long 写入
- lb.clear()
- lb.set(258) // int 0x0102 大端写入:实测 00000102
- lb.SetAlign(0) // 用完记得恢复小端
- lb.clear()
- lb.set(258) // 小端:02010000
- lb.SwapInt(1) // 把位置 1 起的 2 字节高低对换:实测 01020000
- MessageBox('uo_blob', '大端=' + lb.hex())
- // 实测输出:大端=01020000(SwapInt 后)
- destroy lb
复制代码
SwapInt(pos) 对换 2 字节、SwapLong(pos) 对换 4 字节,适合"收到的报文是网络字节序"这类场景:原样写进对象再 Swap 一次,就能按小端读。
五、报文构建:append / Write / replace / remove / fill
拼协议报文的主战场。append 把数据接到尾部并返回新数据的起始位置(1 基):
- // 演示:append 链式拼报文(注意 string 追加 NUL 的副作用)
- uo_blob lb // uo_blob 对象
- long ll_pos // 接收 append 返回的起始位置
- lb = create uo_blob
- ll_pos = lb.append('AB') // 接 41 42 00(string 带 NUL)→ 返回 1
- ll_pos = lb.append('CD') // 接 43 44 00 → 返回 4(因为上一段带 NUL 占了 3 字节)
- ll_pos = lb.append(258) // int 写 4 字节:02 01 00 00 → 返回 7
- ll_pos = lb.append(7, true) // asByte 生效:写 1 字节 07 → 返回 11
- // 最终 11 字节:4142004344000201000007(实测)
- MessageBox('uo_blob', 'hex=' + lb.hex() + ' size=' + String(lb.size()))
- // 实测输出:hex=4142004344000201000007 size=11
- destroy lb
复制代码
看清楚 4142 00 4344 00 0201 0000 07:两段字符串中间的 00 就是 NUL。拼二进制报文别用 append(string),用 append(int/long) 或 FromHex 构造,string 只适合"结尾本来就是 00 的 C 字符串"。
原地修改一族(都不扩容,写多少覆盖多少):
- Write(pos, ...):从 pos 覆盖写入。string 形态带 NUL(实测写 'XY' 进 'ABCDEF' 得 41 42 58 59 00 46,把位置 5 的 'F' 也覆盖成了 00);int 形态写 4 字节。
- replace(pos, len, string):这个会扩容——用 string+NUL(3 字节)替换 2 字节,实测 'ABCDEF' replace(3,2,'XY') 后变 7 字节 41425859004546。
- remove(pos, len) / Erase(pos, len):删除数据段,两者效果相同(实测都从 'ABCDEF' 删位置 3 起 2 字节得 41424546)。remove 返回对象自身(链式),Erase 返回 boolean。
- fill(...):填充。fill(65) 全填 'A';fill(2, 2, 66) 从位置 2 填 2 个 'B';fill('30') 按 hex 字节填。
- resize(n):重设大小,原数据保留、尾部补 00(实测 414243 resize(6) 后 414243000000)。
- edit(pos, value) / WriteByte(pos, hexChar):改单个字节,value 用十进制(0~255)或 hex 串(如 'FF')。
⚠️ 越界删除的坑:Erase(999, 5) 这种越界删除实测返回 true 但什么都不删。判断删除是否生效,删后用 size() 复核,别信返回值。
六、查找与比较:blob 重载是雷区
- // 演示:find 查找子串(hex 串形态与 uo_blob 形态)
- uo_blob lb // 被查找对象
- uo_blob lp // 查找目标
- long ll_pos // 查找结果位置
- lb = create uo_blob
- lp = create uo_blob
- lb.FromHex('414243444546') // 'ABCDEF',FromHex 不追加 NUL
- ll_pos = lb.find('4344') // 按 hex 串找 'CD':实测 = 3(1 基)
- lp.FromHex('4344')
- ll_pos = lb.find(lp) // 按 uo_blob 找:实测 = 3
- ll_pos = lb.find('FFFF') // 找不到:实测 = 0
- MessageBox('uo_blob', 'CD 在位置 ' + String(ll_pos))
- // 实测输出:CD 在位置 3(演示 find('4344') 分支)
- destroy lp
- destroy lb
复制代码
这里埋着本文最大的雷:uo_blob 有一批带 readonly blob 参数的重载,实测全部异常,且根因相同——DLL 把 PB 的 blob 参数按宽字符展开(每字节后插 00):
| 调用 | 预期 | 实测 | | set(Blob('AB')) | 41 42 | 41 00 42 00(宽字符化) | | find(Blob('CD')) | 3 | 恒 0(找的是 43 00 44 00) | | compare(pos, Blob(...)) | 三态 | 恒 -1 | | replace(pos, len, Blob(...)) | 58 59 | 58 00 59 00 |
正道很简单:凡传"要被读的数据",用 hex 字符串、string 或 uo_blob 重载;find(hex串)、find(uo_blob)、compare(pos, string) 实测全部正常。compare 的 string 形态是标准三态(等 0、小 -1、大 1)。
复制与结尾判断:
- CopyBlob(pos, len) 返回 PB blob、CopyObject(pos, len) 返回新 uo_blob(返回的对象要你 destroy)。要看 blob 内容别用 String(lblob)(实测显示为问号),丢回 uo_blob 看 hex,或直接用 CopyObject。
- EndWith(byte / hex串) 与 EndWithString(str, toUtf8):判断结尾。注意 set(string) 来源的数据尾部是 NUL,EndWith 永远判 false;FromHex 来源则一切正常。
七、编码转换:hex / base64 / ToBytes / dump
- // 演示:hex 与 base64 双向往返
- uo_blob lb // uo_blob 对象
- string ls_b64 // base64 串
- lb = create uo_blob
- lb.set('ABC') // 41424300(带 NUL)
- ls_b64 = lb.base64() // 实测 QUJDAA== —— 尾部 AA== 就是那个 NUL
- lb.clear()
- lb.FromBase64('QUJD') // 解码纯 'ABC'
- lb.FromHex('414243') // hex 解码,不追加 NUL(3 字节)
- lb.ToBytes() // 实测返回 [65,66,67] 十进制数组文字
- lb.dump() // 实测返回 '00000000 41 42 43 00 ... ABC.' 调试格式
- MessageBox('uo_blob', 'base64=' + ls_b64)
- // 实测输出:base64=QUJDAA==
- destroy lb
复制代码
要点:
- FromHex / FromBase64 不追加 NUL,是"纯字节"入口;CRC、MD5、EndWith 这类"整块算"的场景请一律从这两个入口构造数据。
- 坏输入不报错、产垃圾:FromHex('ZZ') 实测产出 1 字节 ff;FromBase64('!!!') 实测产出 2 字节垃圾。解码后请核对 size() 与预期长度。
- hex() 的大小写不统一:set 来源输出大写(41424300),FromHex 来源输出小写(e4b8ad)。要比较请先 Upper() 归一。
- toString() 只对 ASCII 内容可靠;UTF-8 中文实测还原成乱码,别拿它做解码。
- dump() 返回"偏移 + hex + ASCII"三列调试串,写日志很方便。
八、校验值:CRC16 全家族、CRC32、MD5
这是 uo_blob 最扎实的一块——八种 CRC16 变体在无 NUL 数据 '123456789' 上全部对上公开标准校验值:
| crc(类型) | 常量名 | 标准名 | '123456789' 实测值 | | 1 | CRC16_CCITT | Kermit | 8585(0x2189) | | 2 | CRC16_CCITT_FALSE | CCITT-FALSE | 10673(0x29B1) | | 3 | CRC16_XMODEM | XMODEM | 12739(0x31C3) | | 4 | CRC16_X25 | X.25 | 36974(0x906E) | | 5 | CRC16_MODBUS | Modbus | 19255(0x4B37) | | 6 | CRC16_IBM | IBM/ARC | 47933(0xBB3D) | | 7 | CRC16_MAXIM | Maxim | 17602(0x44C2) | | 8 | CRC16_USB | USB | 46280(0xB4C8) |
- // 演示:计算 Modbus 风格 CRC16 与 CRC32/MD5
- uo_blob lb // uo_blob 对象
- uint lui_crc // CRC16 结果(16 位)
- ulong lul_crc32 // CRC32 结果(32 位)
- string ls_md5 // MD5 结果(32 位 hex 小写)
- lb = create uo_blob
- lb.FromHex('313233343536373839') // '123456789' 的 hex,无 NUL —— 校验场景务必这样构造
- lui_crc = lb.crc(5) // CRC16_MODBUS:实测 19255(0x4B37,标准校验值)
- lul_crc32 = lb.crc32() // 实测 3421780262(0xCBF43926,标准校验值)
- ls_md5 = lb.md5() // 实测 25f9e794323b453885f5181f1b624d0b
- MessageBox('uo_blob', 'CRC16=' + String(lui_crc) + ' CRC32=' + String(lul_crc32))
- // 实测输出:CRC16=19255 CRC32=3421780262
- destroy lb
复制代码
NUL 入算坑:CRC/MD5/CRC32 按对象全部字节计算。set('123456789') 来源多一个 00,实测 CRC16-MODBUS = 54794(不是 19255)。协议校验前,先用 FromHex 或精确 append 构造好字节,别顺手 set 一个字符串就开算。
crc(类型, start, size) / crc32(start, size) / md5(start, size) 还能对指定区段校验,大报文分块算很方便。
浮点精度场景用 IEEE754 专用对:SetIEEE754(3.14) 写 8 字节双精度(返回起始位置),GetIEEE754(1) 读回,实测 3.14 精确往返(1.5 同样精确)。
九、文件映射:OpenFile 直接改磁盘文件
OpenFile 不是"把文件读进内存",而是文件映射——对象的读写直接落到磁盘文件上:
- // 演示:文件映射方式修改大文件中指定位置的字节
- // 前置:先造一个 3 字节文件 blob_rw.bin(内容 ABC),然后只改第 2 字节
- uo_blob lb // uo_blob 对象
- boolean lb_ok // 各步返回值
- int li_file // 文件句柄
- string ls_back // 回读内容
- lb = create uo_blob
- lb.FromHex('414243') // 'ABC'
- lb_ok = lb.SaveFile('blob_rw.bin') // 另存为文件:实测 true
- lb_ok = lb.OpenFile('blob_rw.bin', false) // false = 可写模式打开文件映射
- lb.WriteByte(2, '58') // 把位置 2 改成 0x58('X')—— 直接写穿到磁盘
- lb.CloseFile() // 关闭映射
- li_file = FileOpen('blob_rw.bin', LineMode!, Read!)
- FileRead(li_file, ls_back) // 回读磁盘文件
- FileClose(li_file)
- MessageBox('uo_blob', '文件内容=' + ls_back)
- // 实测输出:文件内容=AXC —— 磁盘上的文件真的被改了
- destroy lb
复制代码
实测完整闭环:SaveFile 写盘 → OpenFile(file, false) → WriteByte → CloseFile → 磁盘回读 AXC。OpenFile(file) 单参默认只读。这就是 hex 编辑器的做法:几百 MB 的文件改几个字节,不占内存、瞬时完成。SaveFile(file, true, true) 还能把字节流存成带空格的 hex 文本('ABC' 实测得 12 字节的 "41 42 43 ")。
十、API 速查表(实测行为标注)
| 分类 | 方法 | 一句话说明(实测口径) | | 基础 | set / get / clear / reset | 写入 / 读出;clear=reset 清空(保留容量) | | 基础 | size / resize | 当前字节数 / 重设大小(数据保留补 00) | | 基础 | hex / ptr / RefOf | hex 串 / 内存地址 / 引用地址 | | 类型 | set/get(int,long,ulong,longlong,double,real,decimal,blob,string) | 写入宽度见第四节表 | | 类型 | SetAlign / SwapInt / SwapLong | 大小端 / 2 字节对换 / 4 字节对换 | | 类型 | SetIEEE754 / GetIEEE754 | 8 字节双精度精确往返 | | 构建 | append / Write / replace | 追加(返回起始位)/ 覆盖 / 替换(扩容) | | 构建 | remove / Erase / fill / edit | 删除 / 删除 / 填充 / 改单字节 | | 查找 | find / compare / EndWith / EndWithString | hex 或 uo_blob 形态可用;blob 形态是雷 | | 复制 | CopyBlob / CopyObject | 返回 blob / 返回新对象(要 destroy) | | 编码 | hex / base64 / FromHex / FromBase64 | 双向编解码;From 系不追加 NUL | | 输出 | ToBlob / ToBytes / toString / dump | blob / 十进制数组文字 / ASCII 字符串 / 调试格式 | | 校验 | crc(1~8) / crc32 / md5 | 八种 CRC16 全对标准值;支持区段 | | 文件 | SaveFile / OpenFile / CloseFile | 另存 / 文件映射(false=可写)/ 关闭 | | 其它 | getline | 分行读取——实测行为不稳定,本文不推荐使用 |
十一、坑清单(全部实机踩出)
- set(string) 追加 1 个 NUL;string 参与的 append/Write/replace 同样带 NUL。纯二进制场景用 FromHex / 数值类型。
- set(string, false) 是 UTF-16LE 宽字符,不是"不转码保持 GBK"。GBK 字节绕道 PB 原生 Blob() 转换后经 hex 入口进对象。
- set(int) 写 4 字节、get(int) 读 2 字节,读写宽度不对称;4 字节数值用 long 全程对称。
- set(int, true) 的 asByte 不生效(仍写 4 字节);append 的 asByte 生效(写 1 字节);get(pos, ref int, true) 生效(读 1 字节)。
- set(double/real/decimal) 一律 4 字节 float32,double 精度用 SetIEEE754/GetIEEE754。
- 四大 blob 输入参数重载全坑(set/find/compare/replace 的 blob 形态):根因是 blob 参数被宽字符化。用 hex 串、string、uo_blob 重载替代。
- CRC/MD5 把 NUL 一起算:set 来源的校验值与标准值对不上,用 FromHex 构造。
- FromHex 坏串产 ff、FromBase64 坏串产垃圾,解码后核对 size。
- Erase 越界返回 true 但不删,删后用 size 复核。
- EndWith 对 set(string) 来源恒 false(尾部是 NUL)。
- String(blob) 显示问号,看内容走 uo_blob.hex() 或 CopyObject。
- toString 中文乱码,只对 ASCII 可靠。
- getline 实测行为不稳定,分行处理自己写循环。
- hex() 大小写不统一,比较前 Upper()。
- 越界读取不崩:get(999)/ReadByte(999)/ReadByte(0)/get(0) 都返回 false 或 0;CopyObject(999,...) 返回 size=0 的空对象。放心判空。
- CopyObject 返回的对象必须 destroy,内存泄漏不客气。
- hex 比较先统一大小写;find 找不到返回 0,与"位置 1"天然区分,不必用负数。
十二、实机验证情况
- 验证口径:PB10 基准(--pb-version 100 全量编译 0 错误)+ PB12.5 实机(PBVM 运行)双轨执行;测试对象 = uo_blob 源码 + 演示 NVO,四个测试用例(主体 66 读数 / 探针 40 读数 / 补充 19 读数 / 边界 12 读数)。
- 编译:PB10 与 PB12.5 全量编译均 0 错误。
- 运行:PB10 与 PB12.5 各跑 5 个用例全部 PASS;汇总断言 86/86 全中(两版一致)。
- 双版输出比对:三份诊断文件逐字节相同;主体诊断仅 2 处 decimal 显示格式差异(PB10 展开 18 位、PB12.5 最短表示,数值相等)。
- 静态检查:源码编码检查全 OK(GBK、CRLF、无 BOM),规则检查 0 错误 0 提醒。
- 未实测项:getline(行为不稳定,已列入不推荐)、RefOf/ptr(地址类功能与具体内存布局相关,无公开可比对的标准值,本文未展开)、多线程并发访问同一 uo_blob 实例(未验证,请勿跨线程共享实例)。
十三、PB12.5 差异
除 decimal 数值转字符串的显示格式不同(PB10 展开成 18 位小数、PB12.5 用最短表示,见第十二节;仅影响 String() 展示,不影响任何二进制行为)之外,PB12.5 与 PB10 行为一致:全部 137 条读数中其余 135 条逐字节相同。
十四、结语
uo_blob 的定位很清楚:PB 原生 blob 的"能力外挂"——原生只给了一根"字节吸管"(BlobMid/BlobEdit),它把吸管换成了带刻度的量杯:hex/base64、按类型读写、CRC 家族、文件映射,都是 PB 圈常年手搓的东西。用的时候记住三件事:string 带 NUL、blob 重载是雷、校验前用 FromHex 构造纯字节,剩下的放心用。文中全部读数来自双版实机,照抄即可复现。 |