PBIDEA:用 uo_stream 做内存二进制流全解(类型化读写的真实字节布局与 seek 语义,PB10 基准 · PB12.5 实测)
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 / Closeopen 再建一条新流(见第六节的坑)、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 之类的值后磁盘上是关键结论
WriteByte141单字节
WriteInt204 03只写低 16 位!0x01020304 静默截断成 0x0304
WriteLong4FF FF FF 7F32 位小端
WriteLongLong801 00 00 00 00 00 00 0064 位小端
WriteBool201 002 字节,false 是 00 00
WriteNumber331 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 校验——把第五节的部分读判据加进去,就能在流被截断时第一时间发现,而不是拿到半截数据继续算。 【附件】s83demo_pb10.zip —— uo_stream 内存二进制流演示(PB10 兼容版)
这是《PBIDEA:用 uo_stream 做内存二进制流全解》(本帖 1 楼)的 PB10 兼容版示例包。
包内内容
[*]s83demo.pbl —— 演示库(应用对象 a_s83demo + 演示对象 nvo_art83)
[*]s83demo.pbt —— 工程文件(库列表为相对路径,打开后按本机路径重挂)
[*]使用说明.txt —— 环境要求、部署步骤、常见坑(GBK 编码)
[*]src 目录 —— 两份源码(.sra / .sru),可直接 import
环境要求
[*]PB10 基准(PB12.5 及以上同样可用);PBIDEA 运行库 PbIdea.dll / Pbidea_cs.dll 放运行目录
[*]编译需挂与 PB 大版本匹配的 websuite.pbl(10 格式库挂 12.5 工程会报 C0101)
[*]Windows 7+,32 位;本文与示例不依赖任何数据库
运行方式
打开 s83demo.pbt 直接运行,open 事件会执行原文第二、四、五、七节的四个示例并弹出全部读数。预期输出:
b1 size=19,magic=80,ver=2,name=订单A01,nameLen=5,amt=199.5,pos=19
b2 s3=3,s5=5,s8=8,b=9
b3 r8=1,ok=false
b4 w=true,len=6,rd=hello
其中 b4 会在运行目录生成 f83_art.bin(6 字节 = hello + NUL)。
实机验证情况
PB10:import 0 错 + 全量编译 0 错 + 对象齐全 + 运行 PASS,读数与 PB12.5 逐字一致。
PB12.5:同样 0 错 + PASS(原文 23 组用例)。未实测项:BindBuffer 的 rw 写入(原文已标注)、ReadStru 实测返回 false。
提示:uo_stream 在 websuite.pbl 里,自己的工程要用,挂库即可;Play 时如果报 Error opening DLL library pbidea.dll,把 PbIdea.dll 拷到运行目录。
页:
[1]