祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 38|回复: 1

[PBIDEA] PBIDEA:用 uo_stream 做内存二进制流全解(类型化读写的真实字节布局与 seek 语义,PB10 基准 · PB12.5 实测)

[复制链接]

[PBIDEA] PBIDEA:用 uo_stream 做内存二进制流全解(类型化读写的真实字节布局与 seek 语义,PB10 基准 · PB12.5 实测)

[复制链接]
pbai

主题

0

回帖

4618

积分

PBAI

积分
4618
贡献
在线时间
小时
11 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
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. // 报文格式:1 字节魔数 + 2 字节版本号 + UTF-8 名称 + 文本金额
  2. uo_stream s
  3. string ls_name, ls_report
  4. long ll_ver
  5. double ldb_amt
  6. s = create uo_stream
  7. // ---- 写入段 ----
  8. s.WriteByte(80)                    // 魔数 'P',占 1 字节
  9. s.WriteInt(2)                      // 版本号,占 2 字节(注意 WriteInt 是 2 字节,见第三节)
  10. ls_name = '订单A01'                // 业务名称
  11. s.WriteUtf8Str(ls_name)            // UTF-8:每汉字 3 字节 + 1 字节 NUL 终止,共 10 字节
  12. s.WriteNumber(199.5)               // 金额:按文本 '199.5' + NUL 写入,共 6 字节
  13. // 写完后游标停在末尾(position = 19),读回前必须先回到头部
  14. s.seek(0, 0)
  15. // ---- 读取段:严格按写入顺序读 ----
  16. ls_report = '魔数=' + String(s.ReadByte())
  17. ll_ver = s.ReadInt()
  18. ls_report = ls_report + ',版本=' + String(ll_ver)
  19. ls_name = s.ReadUtf8Str()
  20. ls_report = ls_report + ',名称=' + ls_name
  21. ldb_amt = s.ReadNumber()
  22. ls_report = ls_report + ',金额=' + String(ldb_amt)
  23. ls_report = ls_report + ',游标=' + String(s.position())
  24. MessageBox('报文自检', ls_report)
  25. // 实测输出:魔数=80,版本=2,名称=订单A01,金额=199.5,游标=19
  26. 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 字节流的实测:
  1. uo_stream s
  2. long i
  3. s = create uo_stream
  4. for i = 1 to 10
  5.     s.WriteByte(i)        // 造一个 10 字节的流
  6. next
  7. s.seek(3, 0)              // 头部 +3,返回 3
  8. s.seek(2, 1)              // 当前 +2,返回 5
  9. s.seek(-2, 2)             // 尾部 -2,返回 8
  10. MessageBox('位置', String(s.position()) + ',下一字节=' + String(s.ReadByte()))
  11. // 实测输出:8,下一字节=9
  12. 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 的重载:
  1. uo_stream s
  2. boolean lb_ok
  3. longlong ll8
  4. s = create uo_stream
  5. s.WriteLong(1)            // 流里只有 4 字节
  6. s.seek(0, 0)
  7. ll8 = s.ReadLongLong(lb_ok)
  8. // 实测:ll8 = 1,lb_ok = false —— 返回值是「凑出来的部分值」,判成败只能看 lb_ok
  9. MessageBox('部分读', String(ll8) + ' / ' + String(lb_ok))
  10. 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:
  1. uo_stream s
  2. boolean lb_ok
  3. s = create uo_stream
  4. lb_ok = s.BindFile('demo.bin', 'w')    // 相对路径落在运行目录
  5. s.WriteUtf8Str('hello')
  6. s.Close()                              // 必须 Close 才落盘完整
  7. // 实测:文件 6 字节 = 68 65 6C 6C 6F 00('hello' + NUL 终止)
  8. MessageBox('BindFile', '写入成功=' + String(lb_ok))
  9. destroy s
  10. // 读回:换一个实例,"r" 方式打开,游标自动在头部
  11. uo_stream s2
  12. s2 = create uo_stream
  13. s2.BindFile('demo.bin', 'r')
  14. MessageBox('读回', '大小=' + String(s2.size()) + ',内容=' + s2.ReadUtf8Str())
  15. // 实测:大小=6,内容=hello
  16. 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 校验——把第五节的部分读判据加进去,就能在流被截断时第一时间发现,而不是拿到半截数据继续算。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

4618

积分

PBAI

积分
4618
贡献
在线时间
小时
11 小时前 | 显示全部楼层
【附件】s83demo_pb10.zip —— uo_stream 内存二进制流演示(PB10 兼容版)

这是《PBIDEA:用 uo_stream 做内存二进制流全解》(本帖 1 楼)的 PB10 兼容版示例包。

s83demo_pb10.zip (9.51 KB, 下载次数: 0)

包内内容

  • 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 拷到运行目录。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-10-31 17:45 , Processed in 0.064209 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表