马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_stream 做内存二进制流全解(类型化读写的真实字节布局与 seek 语义,PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(本文所有代码两版编译 0 错、23 组运行读数逐字一致,无版本差异)
2. 支持数据库:本文不涉及数据库
3. 操作系统与环境要求:Windows 7+;需部署 PBIDEA 运行库(PbIdea.dll,websuite.pbl 内含 uo_stream);编译需挂对应大版本的 websuite.pbl
4. 难度系数:★★☆☆☆(API 直白,坑集中在字节布局与游标位置,按本文清单规避即可)
5. 其它阅读说明:建议先读第 21 类《用 uo_blob 补齐 PowerBuilder 的二进制处理短板》——那篇讲「静态字节数组」怎么操作,本篇讲「带游标的流」怎么读写,两者互补
PowerBuilder 原生的 blob 是一整块字节,想在里面「当前位置读 4 字节、再往后跳 8 字节」全靠自己维护下标。PBIDEA 的 uo_stream 把这件事封装成了带游标的流对象:按类型写入、按类型读出、seek 任意定位,还能绑定文件或内存块。它是拼二进制协议报文、读写自定义格式文件最顺手的工具。
本文所有结论都来自本机实机探测:PB10 与 PB12.5 双轨各 23 组用例,编译 0 错、运行全过、两版读数逐字一致。凡是没有实测到的能力,文中会明确标注「未实测」,不猜。
一、API 全貌
uo_stream 从 nonvisualobject 继承,需要 create 使用(不是 autoinstantiate)。构造时自动创建内部流句柄,析构时自动销毁。全部方法按功能分七组:
| 分组 | 方法 | 说明 | | 生命周期 | open / Close | open 再建一条新流(见第六节的坑)、Close 关闭当前流 | | 位置 | size / position / seek | 数据大小、当前游标、三锚点定位 | | 块读写 | Read / Write / ReadBlob / WriteBlob | 按块搬运字节 | | 类型化读 | ReadByte / ReadInt / ReadLong / ReadLongLong / ReadBool / ReadNumber / ReadDate / ReadTime / ReadDateTime | 每个都有「无参」与「ref boolean succ」两个重载 | | 类型化写 | WriteByte / WriteInt / WriteLong / WriteLongLong / WriteBool / WriteNumber / WriteDate / WriteTime / WriteDateTime | 返回写入字节数 | | 字符串 | WriteUtf16Str / WriteUtf8Str / WriteAnsiStr / ReadUtf16Str / ReadUtf8Str / ReadAnsiStr | 三种编码,NUL 终止 | | 绑定与结构 | BindFile / BindBuffer / WriteStru / ReadStru / WriteDatas | 绑定文件或内存块、结构读写、变参写入 |
二、最小可运行示例:拼一个二进制报文
下面是文章主线示例,完整演示「写 → seek 回头部 → 读」的标准循环。要求:PbIdea.dll 在运行目录、工程挂 websuite.pbl。
- // 报文格式:1 字节魔数 + 2 字节版本号 + UTF-8 名称 + 文本金额
- uo_stream s
- string ls_name, ls_report
- long ll_ver
- double ldb_amt
- s = create uo_stream
- // ---- 写入段 ----
- s.WriteByte(80) // 魔数 'P',占 1 字节
- s.WriteInt(2) // 版本号,占 2 字节(注意 WriteInt 是 2 字节,见第三节)
- ls_name = '订单A01' // 业务名称
- s.WriteUtf8Str(ls_name) // UTF-8:每汉字 3 字节 + 1 字节 NUL 终止,共 10 字节
- s.WriteNumber(199.5) // 金额:按文本 '199.5' + NUL 写入,共 6 字节
- // 写完后游标停在末尾(position = 19),读回前必须先回到头部
- s.seek(0, 0)
- // ---- 读取段:严格按写入顺序读 ----
- ls_report = '魔数=' + String(s.ReadByte())
- ll_ver = s.ReadInt()
- ls_report = ls_report + ',版本=' + String(ll_ver)
- ls_name = s.ReadUtf8Str()
- ls_report = ls_report + ',名称=' + ls_name
- ldb_amt = s.ReadNumber()
- ls_report = ls_report + ',金额=' + String(ldb_amt)
- ls_report = ls_report + ',游标=' + String(s.position())
- MessageBox('报文自检', ls_report)
- // 实测输出:魔数=80,版本=2,名称=订单A01,金额=199.5,游标=19
- destroy s
复制代码
这段代码在 PB10 与 PB12.5 双轨实测,输出完全一致。size 是 19:1 + 2 + 10 + 6,一分不差。
三、类型化写入的真实字节布局(本文最值钱的一节)
帮助与直觉都会骗人,下面是逐字节 dump 出来的真相。测试值:WriteByte(65)、WriteInt(0x01020304)、WriteLong(0x7FFFFFFF)、WriteLongLong(1)、WriteBool(true)、WriteNumber(1.0),连续写入后全量 dump:
| 方法 | 实测字节数 | 写入 0x01020304 之类的值后磁盘上是 | 关键结论 | | WriteByte | 1 | 41 | 单字节 | | WriteInt | 2 | 04 03 | 只写低 16 位!0x01020304 静默截断成 0x0304 | | WriteLong | 4 | FF FF FF 7F | 32 位小端 | | WriteLongLong | 8 | 01 00 00 00 00 00 00 00 | 64 位小端 | | WriteBool | 2 | 01 00 | 2 字节,false 是 00 00 | | WriteNumber | 3 | 31 2E 00 | 是文本「1.0」加 NUL,不是 IEEE 754! |
四个最容易被坑的点:
第一,WriteInt 是 2 字节。 参数声明虽然叫 uint,但 PB 的 integer 本来就是 16 位,写 16909060(0x01020304)落盘的只有低 16 位 0x0304,读回 772,全程零报错。要写 32 位整数必须用 WriteLong,要写 64 位用 WriteLongLong。
第二,WriteNumber 写的是十进制文本。 WriteNumber(2.5) 落盘的是「2.5」五个字符加一个 NUL,共 6 字节——不是 8 字节 double。好处是 ReadNumber 读回无精度损失;代价是体积大、且不能拿它和 C 语言的二进制浮点互操作。要按 IEEE 754 写浮点,请用第 21 类 uo_blob 的 SetIEEE754。
第三,日期时间也是文本。 WriteDate 落盘「2026-10-31」加 NUL 共 11 字节,WriteTime 落盘「14:30:05」加 NUL 共 9 字节,WriteDateTime 落盘「2026-10-31 14:30:05」加 NUL 共 20 字节。读回按同样格式解析,往返无损。
第四,字符串三种编码都是 NUL 终止,没有长度前缀。 对「A中文」实测:WriteUtf16Str 落盘 41 00 2D 4E 87 65 00 00(UTF-16LE 每字 2 字节 + NUL);WriteUtf8Str 落盘 41 E4 B8 AD E6 96 87 00(UTF-8 每汉字 3 字节 + NUL);WriteAnsiStr 落盘 41 D6 D0 CE C4 00(GBK 每汉字 2 字节 + NUL)。因为靠 NUL 定界,字符串内容里不能有 0x00 字节;要先写长度再写原始字节,用 WriteBlob。
WriteBlob 的格式是「4 字节长度前缀 + 原样字节」:WriteBlob(Blob('AB')) 落盘 04 00 00 00 41 00 42 00 共 8 字节(注意 PB 的 Blob('AB') 本身是 UTF-16LE 的 4 字节)。ReadBlob 先读长度再读数据,往返对称。
WriteDatas 是变参写入,实测规则:整数按 4 字节 long 写、字符串按 ANSI 加 NUL 写、blob 按「长度前缀 + 原样字节」写。WriteDatas(65, 'ab', Blob('xy')) 落盘 15 字节:41 00 00 00、61 62 00、04 00 00 00、78 00 79 00。读回没有对应的 ReadDatas,要按写入顺序用 ReadLong、ReadAnsiStr、ReadBlob 逐个对上。
四、seek 三锚点与越界行为
seek 的第二参:0 从头部算、1 从当前位置算、2 从尾部算,返回新位置。对象里预定义了 SEEK_SET / SEEK_CUR / SEEK_END 三个常量。对 10 字节流的实测:
- uo_stream s
- long i
- s = create uo_stream
- for i = 1 to 10
- s.WriteByte(i) // 造一个 10 字节的流
- next
- s.seek(3, 0) // 头部 +3,返回 3
- s.seek(2, 1) // 当前 +2,返回 5
- s.seek(-2, 2) // 尾部 -2,返回 8
- MessageBox('位置', String(s.position()) + ',下一字节=' + String(s.ReadByte()))
- // 实测输出:8,下一字节=9
- destroy s
复制代码
三条越界规则也全部实测过:
- seek 允许越过末尾。 对 10 字节的流 seek(100, 0) 返回 100 不报错。此时再 WriteByte(255),流长直接变成 101,中间 90 个字节全部填 0——利用这个特性可以「跳着写」,但也要小心游标算错时静默制造空洞。
- 负数 offset 静默不动。 seek(-5, 0) 返回当前的原位置,不报错、不回绕。游标计算出错时它不会提醒你。
- 读越界:类型化读返回 0 且 succ 为 false;Read 返回 0 字节块。详见第五节。
五、读越界与「部分读」的真实行为
4 字节的流(WriteLong(1)),从头部用 ReadLongLong 读 8 字节——只有 4 字节可读,实测返回值是 1(把剩下的 4 字节拼成了部分值)、succ 为 false、游标停在 4。也就是说:部分读会给出「能凑出来的一部分」值,成败只能看 succ,不能看返回值是不是 0。完全无字节可读时返回 0 且 succ 为 false。无 succ 参数的重载在越界时同样只返回 0,所以解析关键数据一律用带 succ 的重载:
- uo_stream s
- boolean lb_ok
- longlong ll8
- s = create uo_stream
- s.WriteLong(1) // 流里只有 4 字节
- s.seek(0, 0)
- ll8 = s.ReadLongLong(lb_ok)
- // 实测:ll8 = 1,lb_ok = false —— 返回值是「凑出来的部分值」,判成败只能看 lb_ok
- MessageBox('部分读', String(ll8) + ' / ' + String(lb_ok))
- destroy s
复制代码
六、生命周期三个坑:游标、open、Close
坑一:写完后游标停在末尾,读回前必须 seek(0, 0)。 这是最容易踩的坑——写完直接 ReadUtf8Str 会得到空串(游标已在 EOF,读到的是「空」而不是数据)。第二节示例里 seek(0, 0) 那行删掉就会复现。BindFile 只读模式打开时游标自动在头部,所以只有「先写后读」的场景需要显式 seek。
坑二:对同一个实例再调 open(),旧数据直接丢。 open 的内部实现是「再建一条新流」,旧流的句柄被覆盖、里面已写的数据全部作废,size 变回 0。实测写入 4 字节后调 open(),返回一个新的句柄值,size 归零。一条实例一条流,想要第二条流就再 create 一个 uo_stream。
坑三:Close 之后写入静默失败。 Close 后调 WriteByte 返回 0、size 恒 0,没有任何错误提示。Close 主要用于 BindFile 场景(把缓冲刷进文件并解除绑定),纯内存流直接 destroy 即可。
七、BindFile:把流落到文件
BindFile(文件名, 方式) 三种方式:"w" 从零写、"r" 只读、"rw" 读写(读写方式的实际可用性见下文 BindBuffer 部分)。三参版多了 autoDeleteFile:
- uo_stream s
- boolean lb_ok
- s = create uo_stream
- lb_ok = s.BindFile('demo.bin', 'w') // 相对路径落在运行目录
- s.WriteUtf8Str('hello')
- s.Close() // 必须 Close 才落盘完整
- // 实测:文件 6 字节 = 68 65 6C 6C 6F 00('hello' + NUL 终止)
- MessageBox('BindFile', '写入成功=' + String(lb_ok))
- destroy s
- // 读回:换一个实例,"r" 方式打开,游标自动在头部
- uo_stream s2
- s2 = create uo_stream
- s2.BindFile('demo.bin', 'r')
- MessageBox('读回', '大小=' + String(s2.size()) + ',内容=' + s2.ReadUtf8Str())
- // 实测:大小=6,内容=hello
- destroy s2
复制代码
autoDeleteFile 参数的实测行为比直觉狠:传 true 时,Close 一执行文件就没了——不管你写没写进东西。「自动删除」的时点在 Close,不是在程序退出。想保留文件就传 false(或不传,两参版默认保留)。
八、BindBuffer:只能当只读源
BindBuffer(blob, 方式) 把一块内存当流读,实测结论很干脆:
- "r" 方式:正常工作。BindBuffer(Blob('ABC'), 'r') 后 size=6(UTF-16LE),ReadUtf16Str 读回「ABC」。
- "w" 方式:形同虚设。绑空 blob 返回 false;绑非空 blob 返回 true 但之后所有 Write 都不生效——WriteByte(65) 后 size 不变、dump 原字节原样,写进去的 65 无影无踪。
- "rw" 方式:绑定返回 true、size 等于原 blob 长度,写入是否生效未实测(失败形态已在 "w" 处复现,建议直接当只读用)。
结论:要写流,用默认内存流或 BindFile;BindBuffer 只用来把现成 blob 变成可按类型读的游标视图。
九、WriteStru / ReadStru:写得出,读不回(如实标注)
WriteStru 能把一个结构体写入流(实测含 long id、string name、double amt 三个字段的结构写入了 12 字节),但 ReadStru 实测返回 false、目标结构体三个字段全是默认值,读不回来。换成「逐字段按类型写、按类型读」的顺序化写法(即第二节示例的模式)完全可靠。此段结论两版一致,属于当前版本的能力缺口,读者不要在结构体通道上花时间。
十、坑清单(发布前逐条实测核对)
- WriteInt 是 2 字节,写 32 位值静默截断,用 WriteLong / WriteLongLong。
- WriteNumber / WriteDate / WriteTime / WriteDateTime 全是「文本 + NUL」,不是二进制格式;跨语言交换二进制浮点用 uo_blob 的 SetIEEE754。
- 三种字符串写入都是 NUL 终止无长度前缀,字符串内容不能含 0x00。
- WriteBlob 是「4 字节长度前缀 + 原样字节」,与字符串格式不同,读回必须配对。
- 写完游标在末尾,读回前必须 seek(0, 0),否则读到空。
- seek 允许越过末尾,随后写入会把空洞填 0。
- seek 负数 offset 静默不动,游标算错无提醒。
- 类型化读越界返回 0 加 succ=false,部分读会给出「凑出来的部分值」,判成败只看 succ。
- 解析关键数据一律用带 succ 重载,无 succ 重载越界只返回 0。
- 二次 open() 抛弃旧数据另起新流,别用它当「清空」用。
- Close 后写入静默失败(返回 0)。
- BindFile 的 autoDeleteFile=true 在 Close 时就删文件。
- BindBuffer 写模式不可用(绑空 blob 直接 false,绑非空 blob 写入不生效),只当只读视图。
- WriteStru 写得出、ReadStru 读不回(实测 false),结构化数据用逐字段读写。
- WriteDatas 读回无对应方法,须按写入顺序逐个类型化读,且整数按 4 字节、字符串按 ANSI 加 NUL。
十一、PB12.5 差异
本文全部 23 组用例在 PB10 与 PB12.5 双轨编译 0 错、运行 PASS、读数逐字一致,唯一出现差异的字段是第六节 open() 返回的句柄数值——那本来就是一个随内存分配变化的指针值,不属于行为差异。结论:uo_stream 在两版上行为一致。
十二、选型:什么时候用 uo_stream
- 拼自定义二进制协议报文(类型化写入 + 游标回读):uo_stream 最顺手。
- 对现成 blob 做按类型的随机读取(先 BindBuffer 成只读视图)。
- 读写自定义格式的小文件(BindFile 落盘)。
- 需要按位置改字节、算 CRC / MD5、十六进制编解码:用第 21 类的 uo_blob,它没有游标但字节级操作更全。
- 需要 IEEE 754 二进制浮点:uo_blob.SetIEEE754,不要用 WriteNumber 的文本格式。
示例工程里还有一个可直接照抄的完整报文工具类思路:写入端按「魔数、版本、长度、正文」顺序落盘,读取端逐字段带 succ 校验——把第五节的部分读判据加进去,就能在流被截断时第一时间发现,而不是拿到半截数据继续算。 |