马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-9-17 13:02 编辑
PowerBuilder 扩展 DLL 开发实战:system library 机制全解
本文缘起:手上有一份流传多年的《PB 的扩展 DLL 开发(超级篇)》八篇连载,讲的是 PB 官方文档里从没写过的第三种扩展方式 —— system library。这八篇写得很早(PB9 时代),代码只有思路、没有可编译的工程,而且里面大量 API 名字其实不是 PBVM 的导出函数,而是 PB 官方头文件 EN32T.h 里的宏 —— 作者自己的 pbSysLib.h 只是把它 #include 了进来。我把这八篇按顺序读完后,做了一件更实在的事:自己用 MSVC 编一个 system library DLL,在 PB 12.5 上跑通,再把每一个 API 名字拿到 PBVM125.DLL 的导出表里核对。
结果有意外收获:八篇文章里出现过的 API 名字,将近一半在 PBVM 里根本不存在;而真正能用的那一半,用法和文章写的还不完全一样。这篇文章把实测结论完整摊开,附上可直接编译的 DLL 源码和可运行的验证工程,照抄即可用。
0. 前置信息
| 项目 | 内容 | | PB 版本 | PowerBuilder 12.5(经典版 / x86)。PB10 差异处单独标注 | | 目标 DLL 编译器 | MSVC 2022 BuildTools,Hostx86/x86(必须 32 位,PB 12.5 是 32 位) | | 操作系统 | Windows 10 Pro x64(19045) | | 数据库 | 本文不涉及 SQL,无需数据库 | | 附加依赖 | 无。不依赖任何第三方库、不需要 pbvm125.lib | | 难度系数 | ★★★★★(五颗星。需要会一点 C/C++,需要懂 PE 导出表的概念) | | 实测状态 | DLL 编译通过 → PB 导入 0 错 → 全量重建 0 错 → 3 个 pbtest 全 PASS → 静态检查 0 错 0 提醒 | | 勘误版本 | v2 / 2026-09-17。拿到官方 EN32T.h 与作者的 pbSysLib.h 一手头文件后重做核验,订正了第 3、5、6、7、9 节的结构性结论(参数节点=OB_DATA=8 字节、那些宏全部来自官方头文件、ref 参数已跑通),并新增 13.1 节 |
1. 先搞清楚:扩展 PB 的三条路
| 方式 | 声明关键字 | 能访问 PB 对象/事件/属性 | 返回字符串 | 内存责任 | 版本兼容 | | ① 通用 DLL | library | ❌ 完全不能 | 要预分配缓冲区 | 调用方(PB) | 好 | | ② PBNI | PBNI(对象式) | ✅ 可以 | ✅ 容易 | PB 堆托管 | 有差异,需按版本重编 | | ③ system library | system library | ✅ 可以 | ✅ 容易 | PB 堆托管 | 与 PB 内部 ABI 绑定 |
第③种就是本文主题。它的"身份"藏在关键字里:
- // 普通外来 DLL —— PB 视你为"请来的临时工"
- function long Test() library "test.dll"
- // PB 自己人 —— 微软之外没有任何文档,但 PB 认这个关键字
- function long Test() system library "test.dll" alias for "Test"
复制代码
system library 这个关键字只在 PB 的导入器里被特殊对待:它告诉 PB"我是自己人",于是 PB 在调用时自动把当前虚拟机的上下文(obThis)和参数个数传进去。这就是为什么它不用手工构造参数缓冲区,也不用预分配内存。
顺带解决一个历史遗留问题:从 PB9 升级到 PB10 以上时,PB 会自动给外来 DLL 声明加上 ;ansi:
- // 升级后被自动改成的鬼样子(纯属自找麻烦)
- FUNCTION ulong SetWindowText(ulong hwnd, ref string lpString) LIBRARY "user32.dll" ALIAS FOR "SetWindowTextA;ansi"
复制代码
而用 system library 声明的函数,升级时 PB 一个字符都不会动——因为"自己人待遇不一样"。这是这个关键字最实用的一点点附加价值。
2. 第一锤:PB 的内置函数,真的都是 system library 导出吗?
八篇连载里最核心的一句话是:
实际上,我们在 PB 里用的函数,全部都是以 system library 方式存在于 PBVMxxx.DLL 中。
这句话是可以直接验证的。PBVM 就是一个普通 PE 文件,把它的导出表读出来就行(纯 Python 标准库解析,不用 pefile):
- C:\Program Files (x86)\Sybase\Shared\PowerBuilder\PBVM125.DLL
- machine=0x014C (x86) exports=2383
- 其中: fn*=998 ob_*=495 ot_*=188 rt*=209
- pbvm100.dll
- machine=0x014C (x86) exports=2213
- 其中: fn*=921 ob_*=483 ot_*=182
复制代码
fn* 那一批就是 PB 的内置函数本体,逐个都存在:
- fnMessageBox fnUpper fnLen fnBeep fnFileCopy fnEDITCopy fnCOMBOCopy ...
复制代码
所以这句话成立。PB 的内置函数确实是一个个躺在 PBVM 里的 fn* 导出。
3. 第二锤:文章里的 API 名字,有一半是"假"的
八篇文章里出现了大量看起来很正规的 API:ot_get_str_arg、ob_set_data_string、ot_access_ref_data……这些名字在 PBVM 的导出表里一个都找不到。
当时我把结论写成“这是作者自制头文件里的宏”,v2 要更正:拿到作者的 pbSysLib.h(1667 行)与它 #include 的 EN32T.h(78729 行)之后,把整份 pbSysLib.h 里的 #define 全扫了一遍,属于 ob_* / ot_* 命名空间的宏只有 1 个:
- /* pbSysLib.h 第 46 行 —— 唯一一个作者自己写的 ob_/ot_ 宏 */
- #define ot_remove_null_flag(pobdata) (pobdata)->info = ((pobdata)->info & (~DATA_NULLVAL_MASK))
复制代码
其余全部来自官方头文件。所以真相是:这些宏本来就是 PB 官方 SDK 头文件 EN32T.h 里的东西,作者只是引用进来用。对 PBVM 导出表来说它们确实“不存在”,但对 PB 的编译期头文件体系来说它们一直都在 —— “假”要打引号。
我干脆让 DLL 自己在 PB 运行时去查(GetProcAddress),把结果写成一行日志。真机跑出来的结果:
- // 文章里有、PBVM 里没有 —— 是 PB 官方头文件 EN32T.h 里的宏(pbSysLib.h 只是 #include 了它)
- ot_get_str_arg=macro ot_get_int_arg=macro ot_get_bool_arg=macro
- ot_get_long_arg=macro ob_set_data_ptr=macro ob_set_data_int=macro
- ob_set_data_long=macro ob_set_data_string=macro ob_get_data_type=macro
- ob_get_data_ptr=macro ot_is_a_reference_argument=macro
- ot_access_ref_data=macro ot_is_array=macro ot_build_simple_refarg=macro
- pbstg_alc=macro ot_set_return=macro ot_no_ret_val=macro
- // 文章里有、PBVM 里真有 —— 是实际导出
- ot_get_valptr_arg=EXPORT ot_get_next_lvalue_arg=EXPORT ot_get_next_evaled_arg=EXPORT
- ot_get_curr_obinst_expr=EXPORT ot_set_return_val=EXPORT ob_dup_string=EXPORT
- ob_alloc_string=EXPORT rtDataCopy=EXPORT ot_array_index=EXPORT
- ot_array_num_items=EXPORT ot_is_array_unbounded=EXPORT ot_no_return_val=EXPORT
- ot_get_intarg=EXPORT ot_get_longarg=EXPORT ot_get_obinstarg=EXPORT
- ot_build_simple_refpak=EXPORT
复制代码
这张表是本文最有价值的部分之一。它意味着:
- **ob_set_data_* 系列不是函数,是宏。** 它们做的事就是往 OB_DATA 结构体的三个成员里塞值。下面是 EN32T.h 原文(10302-10374),不是简化版:
- /* EN32T.h 10302-10311 —— 注释原文:vartype parameter is no longer used */
- #define ob_set_data_int(node,intval,type,vartype) \
- { \
- ob_set_data_int_val(node,intval); \
- ob_set_data_info (node, INT_STYLE, type, OB_SIMPLE, vartype); \
- }
- /* EN32T.h 10331-10335 */
- #define ob_set_data_long(node,longval,type,vartype) \
- { \
- ob_set_data_long_val(node,longval); \
- ob_set_data_info (node, LONG_STYLE, type, OB_SIMPLE, vartype); \
- }
- /* EN32T.h 10349-10374 —— string/dec/double/longlong/blob/binary 全部走 ptr,bool 走 int */
- #define ob_set_data_ptr(node,ptrval,type,vartype) \
- { \
- ob_set_data_ptr_val(node,ptrval); \
- ob_set_data_info (node, PTR_STYLE, type, OB_SIMPLE, vartype); \
- }
- #define ob_set_data_string(node,ptrval,type,vartype) ob_set_data_ptr(node,ptrval,type,vartype)
- #define ob_set_data_bool(node,intval,type,vartype) ob_set_data_int(node,intval,type,vartype)
- /* 取值宏也本来就是官方的,一行(EN32T.h 9911 / 10155) */
- #define ob_get_data_type(node) ((node)->type)
- #define ob_get_data_ptr(node) ((node)->val.ptr)
-
复制代码
- ot_get_xxx_arg 系列也是宏,展开后是真实的 ot_get_xxxarg 家族:
| 文章里的宏名 | PBVM 真实导出 | 说明 | | ot_get_long_arg | ot_get_longarg | 取一个 long 参数 | | ot_get_int_arg | ot_get_intarg | 取一个 int 参数 | | ot_get_str_arg | ot_get_valptr_arg | 取指针型参数(字符串/数组/时间/二进制),返回 LPTSTR | | ot_get_bool_arg | ot_get_simple_intarg | 注意不是 ot_get_intarg,官方宏展开用的是 simple 版 | | ot_get_obinst_arg | ot_get_obinstarg | 取对象实例句柄 |
- ot_no_ret_val 写错了一个字母,真名是 ot_no_return_val;ot_set_return 的真名是 ot_set_return_val。
这一节的意义不在于“作者写错了”——这些宏本来就是 PB 官方头文件 EN32T.h 里的东西,作者写得没错,只是把来源省掉了,外人看不出它从哪来。意义在于:你如果照着这些名字去 GetProcAddress 或者写声明,会一个都找不到。 好消息是 EN32T.h 本体并不难找(见 13.1 节),拿到它就是拿到官方宏与官方结构定义。这份对照表把路铺平了。
4. 第三锤:自己写一个 system library DLL
4.1 函数长什么样
system library DLL 里的每一个导出函数,形状是固定的:
- __declspec(dllexport) DWORD __stdcall FuncName(POB_THIS obThis, int nArgCount)
- {
- return 1; /* 返回 1 = 成功;返回 0 = 告诉 PB 出错了 */
- }
复制代码
只有两个参数,而且都不是你在 PB 里声明的那些参数:
- obThis —— 一个巨大的会话结构(OB_THIS),唯一标识一个 PB 虚拟机/会话。你在 PB 里声明了几个参数、参数是什么类型,统统要靠它去取。
- nArgCount —— 本次调用实际传了几个参数。
参数本身装在 obThis->evaled_arglist 里(文章第二章列出的 OB_THIS 结构成员之一)。
4.2 为什么要用 .def 文件
__stdcall 会把导出名修饰成 _SLAdd@8 这种鬼样子,而 PB 侧的 alias for "SLAdd" 是按名字精确查找的。所以必须用 .def 强制导出未修饰名:
- LIBRARY pbsyslib_demo
- EXPORTS
- SLEcho @1
- SLAdd @2
- ...
复制代码
本文的 DLL 导出表实测结果(v2 版 23 个,含结构探针、ref 写回探针与官方宏对照组):
- [exports] 23 个: SLAdd, SLApiCheck, SLArgTypes, SLArgTypesRaw, SLCn, SLCurrInst,
- SLEcho, SLFail, SLIsNullStr, SLLayout, SLProbeInt, SLProbeLong,
- SLProbeStr, SLRefDiag, SLRefObserve, SLRefSetString, SLRetOfficial,
- SLSumArray, SLUpper, SLVoid, SL_ArgKinds, SL_LayoutV2, SL_NullProbe
- [修饰名检查] 全部未修饰 OK
复制代码
4.3 不要 pbvmXXX.lib 也能调 PBVM 内部函数
Sybase 装 PB 的时候不附送 pbvm125.lib(我在 Sybase\Shared\PowerBuilder 下找过,只有 pborca.lib / PBNI.LIB / pbcgm125.lib 等)。所以有两个选择:
- dumpbin /exports PBVM125.DLL → 生成 .def → lib /def: 造一个导入库;
- 直接 GetProcAddress 动态绑定(本文采用)。
第 2 种还有一个额外好处:不写死版本号。DLL 里先用常见名字找,找不到就遍历进程模块表,挑名字里带 pbvm 的那个:
- static HMODULE pbsyslib_find_pbvm(void)
- {
- static const char *NAMES[] = {
- "PBVM125.DLL", "PBVM120.DLL", "PBVM110.DLL", "PBVM105.DLL",
- "PBVM100.DLL", "PBVM90.DLL", "PBVM85.DLL", "PBVM80.DLL", NULL
- };
- HMODULE h; int i;
- for (i = 0; NAMES[i]; i++) { h = GetModuleHandleA(NAMES[i]); if (h) return h; }
- /* 兜底:扫描当前进程已加载模块,找名字里有 pbvm 的 */
- HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
- MODULEENTRY32W me; me.dwSize = sizeof(me);
- h = NULL;
- if (Module32FirstW(snap, &me)) {
- do {
- WCHAR low[MAX_MODULE_NAME32 + 1];
- lstrcpynW(low, me.szModule, MAX_MODULE_NAME32);
- for (int k = 0; low[k]; k++)
- if (low[k] >= L'A' && low[k] <= L'Z') low[k] = (WCHAR)(low[k] + 32);
- if (wcsstr(low, L"pbvm")) { h = me.hModule; break; }
- } while (Module32NextW(snap, &me));
- }
- CloseHandle(snap);
- return h;
- }
复制代码
同一份 DLL 从此在 PB 9/10/11/12/12.5 上都能加载,运行时自己认路。
4.4 编译命令
- call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars32.bat"
- cl /nologo /LD /O2 /W3 /MT /utf-8 /DUNICODE /D_UNICODE ^
- /Fo:"...\" /Fe:pbsyslib_demo.dll pbsyslib_demo.cpp pbsyslib_demo.def ^
- /link /DEF:pbsyslib_demo.def
复制代码
三个关键点:
- /LD 生成 DLL;
- /MT 静态链接 CRT,避免目标机缺 VC 运行库;
- /DEF: 必须传,否则导出名被 __stdcall 修饰,PB 找不到。
5. 第四锤:参数怎么取(本文最硬的一节)
5.1 结论先说
| PB 里的参数类型 | 该用哪个函数 | 备注 | | string | ot_get_valptr_arg(obThis, &isnull) | 返回 TCHAR*,PB10+ 是 UTF-16 | | long | ot_get_longarg(obThis, &isnull) | 直接返回 long | | int | ot_get_intarg(obThis, &isnull) | 直接返回 int | | boolean | ot_get_intarg + 判非零,或走通用节点 | 类型码 7 | | 数组 long a[] | ot_get_valptr_arg 取句柄,再 ot_array_num_items / ot_array_index | 见第 7 节 | | ref 参数 | ot_get_next_lvalue_arg(obThis, &info) | 见第 9 节(v2 已真机跑通) | | 变参 ... | ot_get_next_evaled_arg(obThis) 循环 | 见 5.3,它返回的就是 POB_DATA | | 当前对象 | ot_get_curr_obinst_expr(obThis, &id, &isnull) | 谁调用的我 |
取参数的次数绝对不能超过 nArgCount,因为 obThis->curr_arg_pos 是自增的,越界会踩到别的内存。要么严格按个数取,要么每次取之前判一下位置。
5.2 实测数据
PB 侧这样调:
- ll_n = SL_Add(1234, 5678) // long 声明 → 期望 6912
- ls_s = SL_AddInt(1234, 5678) // 用 intarg 取一遍做对照
复制代码
真机日志:
- ADD=6912 ← ot_get_longarg 两次,1234+5678 正确
- ADDI=n1=0 n2=0 v1=1234 v2=5678 ← ot_get_intarg 也正确
复制代码
5.3 ★ 参数节点就是 OB_DATA:8 字节,type 在 +6
八篇文章里反复出现这种写法:
- POB_DATA obArg = ot_get_next_evaled_arg(obThis); // 文章写法
- switch (ob_get_data_type(obArg)) { ... }
复制代码
我照这个写法做,结果全是垃圾:SL_Add(1234,5678) 算出来是 50468204,类型码读出来是 770。
于是写了个转储函数,把 ot_get_next_evaled_arg 返回的指针前后 32 字节原样打出来:
- POB_ARGNODE p1 = (POB_ARGNODE)a->next_evaled_arg(obThis); /* ❌ 当时自造的类型,错的 */
- hex32(h1, 256, p1);
复制代码
真机输出(这是决定性证据):
- p1=02ED0C58 t6=2 t10=0 v0=1234
- d1 = D2040000 001D0200 2E160000 001D0200 00000000 0000BBFF BBFF0000 0000
- ↑1234 ↑? ↑5678 ↑?
- p2=02ED0C60 t6=2 v0=5678
- d2 = 2E160000 001D0200 00000000 00000000 BBFFBBFF ...
复制代码
当时的解读是:
- D2 04 00 00 = 1234,2E 16 00 00 = 5678,都在每个节点的 +0;
- 节点步长 8 字节(p1=0x...C58,p2=0x...C60);
- 在 +6 处读到的值是 02 00 = 2 = LONG_TYPE,正确;
- 在 +10 处读(也就是按 12 字节 OB_DATA 的偏移)读到的是 0,完全错位。
观察全对,但结论错了。当时我以为 8 字节那个是某种"紧凑节点",真正的 OB_DATA 是另一套 12 字节结构,于是自造了个 OB_ARGNODE 去接。拿到官方 EN32T.h 之后才发现:8 字节那个就是 OB_DATA 本身,根本不存在第二套结构。
权威定义(EN32T.h 9867-9873,原文照抄):
- typedef struct ob_data
- {
- OB_VALUE val;
- OB_INFO_FLAGS info; // Data node info flags
- OB_CLASS_ID type; // Data Type
- } OB_DATA, FAR* POB_DATA;
复制代码
而 OB_VALUE 是个 union(9837-9856):
- typedef union ob_value
- {
- SHORT int_val; FLOAT fl_val; PVOID ptr; OB_CONST_REF const_ref;
- PVOID ob_inst; USHORT id; USHORT uint_val;
- LONG long_val; ULONG ulong_val; BYTE byte_val;
- } OB_VALUE, FAR* POB_VALUE;
复制代码
union 里最大的成员是 PVOID / LONG / ULONG,win32 下都是 4 字节,所以 sizeof(OB_VALUE) = 4。再加上 2 字节的 info 与 2 字节的 type,sizeof(OB_DATA) = 8(文件第 4 行就是 #pragma pack(push,1))。MSVC 里跑一行 sizeof 就钉死了:
- sizeof(OB_VALUE)=4 sizeof(OB_INFO)=4 sizeof(OB_DATA)=8
- offsetof(OB_DATA,info)=4 offsetof(OB_DATA,type)=6 ptr=4
复制代码
于是那 8 个字节的正确读法是:
| 偏移 | 成员 | 含义 | | +0 .. +3 | OB_VALUE val | 值本体;long / int / ptr / float 都在这一格(union,4 字节) | | +4 .. +5 | OB_INFO_FLAGS info | 位域标志,NULLVAL 是 bit0 | | +6 .. +7 | OB_CLASS_ID type | 数据类型枚举(2=LONG_TYPE、6=STRING_TYPE、7=BOOL_TYPE) |
上一版里"实测恒为 0x1D00 的那个 extra 字段",其实就是 info。把这两个字节代回官方宏公式(10056-10066):
- #define ob_set_data_info(node,style,typ,group,vartype) \
- ((node)->info = (OB_INFO_FLAGS) ( \
- (OB_PUBLIC_MEMBER << DATA_ACCESS_SHIFT) | /* 14 */ \
- ((group) << DATA_GROUP_SHIFT) | /* 13 */ \
- (0 << DATA_FIELDTYPE_SHIFT) | /* 9 */ \
- ((style) << DATA_STYLE_SHIFT) | /* 10 */ \
- (USED << DATA_STATUS_SHIFT) | /* 8 */ \
- (OB_DIRECT_REF << DATA_REFTYPE_SHIFT) | /* 6 */ \
- (0 << DATA_TYPEARGS_SHIFT)), /* 1 */ \
- (node)->type = (OB_CLASS_ID)typ \
- )
复制代码
代入 OB_SIMPLE(0) + LONG_STYLE(7) + USED(1):
- (0<<14) | (0<<13) | (0<<9) | (7<<10) | (1<<8) | (0<<6) | (0<<1)
- = 0x1C00 | 0x0100 = 0x1D00 ← 实测到的那个值,一个字都不差
复制代码
所以一个 long 参数进来的 8 个字节真容是 D2 04 00 00 | 00 1D | 02 00,也就是 val=1234 / info=0x1D00 / type=2。和 hex dump 完全对上。用同一套公式反推另外两种声明,还能算出两个可验证的常数:
| PB 侧声明 | style | 算出的 info | 真机实测 | | long | LONG_STYLE(7) | 0x1D00 | ✅ info=0x1D00 style=LONG:7 | | integer | INT_STYLE(1) | 0x0500 | ✅ info=0x0500 style=INT:1 | | string | PTR_STYLE(3) | 0x0D00 | ✅ info=0x0D00 style=PTR:3 |
这三个常数是同一次 DLL 代码、同一个函数,只改 PB 侧声明跑出来的 —— 说明 info 的形状完全由 PB 侧声明决定,与宏公式一一对应。
最值钱的一句话:不要自己造 OB_ARGNODE。官方四个取参函数的返回类型白纸黑字就是 POB_DATA(EN32T.h 20437 / 20442 / 20447 / 21977):
- PBWINAPI(POB_DATA, ot_get_next_evaled_arg) (POB_THIS obthis);
- PBWINAPI(POB_DATA, ot_get_next_evaled_arg_no_convert)(POB_THIS obthis);
- PBWINAPI(POB_DATA, ot_get_next_lvalue_arg) (POB_THIS obthis, POT_LVALUE_INFO FAR* str);
- PBWINAPI(POB_DATA, ot_array_index) (POB_THIS obthis, PVOID array, ULONG index);
复制代码
参数节点、数组项、返回值、ref 左值 —— 统统是同一个 8 字节 OB_DATA。作者源码里也是这么用的(pbjson.cpp 第 27 行):
- POB_DATA pArgs = (POB_DATA)obThis->evaled_arglist;
- POB_DATA lv = ot_get_next_lvalue_arg(obThis, &info);
- POT_REF_PAK v = (POT_REF_PAK)lv->val.ptr;
复制代码
所以正确写法就是直接照官方名字用:
- POB_DATA arg = ot_get_next_evaled_arg(obThis); /* 返回类型本来就是 POB_DATA */
- if (arg->type == LONG_TYPE) sum += arg->val.long_val;
- if (arg->type == STRING_TYPE) lp = (TCHAR*)arg->val.ptr;
- if (arg->info & DATA_NULLVAL_MASK) { /* 是 NULL,别碰 val */ }
复制代码本文第一版把参数节点和返回值拆成"8 字节节点 / 12 字节 OB_DATA"两套模型 —— 那是错的。自造结构的代价:你按 12 字节算偏移,type 就永远读不对;你按"两套结构"去理解,后面 ref、数组、返回值每一处都会错位。拿到 EN32T.h 之前只能靠 hex dump 猜,拿到之后一行 #include 就结束了。
5.4 变参 ... 与类型码实测表
PB 里声明 function string SL_ArgTypes(...) system library ...,DLL 里靠 nArgCount 循环取。实测输出:
- ARG=count=3 a0=t2(v=1) a1=t6(v=abc) a2=t7(v=1)
- ARGR=count=3 a0=t2(v=1) a1=t6(v=abc) a2=t7(v=1)
复制代码
ARG 用 ot_get_next_evaled_arg(会做类型转换),ARGR 用 ot_get_next_evaled_arg_no_convert(不转换)。这次两者结果完全一致。类型码和文章第三章给的常量表对得上:
| PB 传入 | 类型码 | 常量名 | | 1 字面量 | 2 | LONG_TYPE | | "abc" | 6 | STRING_TYPE | | true | 7 | BOOL_TYPE | | 数组项(long) | 2 | LONG_TYPE |
注意 1 这种整数字面量进来是 LONG_TYPE(2)而不是 INT_TYPE(1) —— 别按 INT_TYPE 判,会漏。
5.5 NULL 参数怎么认
SetNull() 过的字符串,ot_get_valptr_arg 的第二个出参会置 TRUE;同时返回的指针是无效的,不能直接解引用:
- NUL=isnull=1 ptr=null len=-1 ← SetNull(ls_null) 之后
- NN =isnull=0 ptr=valid len=3 ← 正常字符串 "abc"
复制代码
对应文章第三章那个 DATA_NULLVAL_MASK 判断:
- #define DATA_NULLVAL_MASK 0x0001
- if (!(lpArg->info & DATA_NULLVAL_MASK)) { /* 才敢用 */ }
复制代码
6. 返回值怎么给
6.1 三步固定动作
- OB_DATA obReturn;
- memset(&obReturn, 0, sizeof(obReturn));
- ob_set_data_long(&obReturn, v1 + v2, LONG_TYPE, 0); /* 宏:往 OB_DATA 里塞值 */
- ot_set_return_val(obThis, &obReturn); /* 真实导出:交给 PB */
- return 1; /* 返回 1 = 成功 */
复制代码
- subroutine 型(无返回值)就调 ot_no_return_val(obThis)。实测正常,PB 侧调用无异常。
- 官方 ob_set_data_* 宏的第 4 个参数是 vartype,不是 OB_INSTVAR_FIELD。EN32T.h 在 10302-10304 那一段直接写明:// Note: vartype parameter is no longer used —— 这个参数已经废弃,传什么都行。官方自己的 _return_macro(20690-20695)传的是 FALSE(0),作者源码传的是 OB_INSTVAR_FIELD(枚举值=1,EN32T.h 8076),我两边都跑过,PB 侧收到的值都完全正确 —— 因为这个参数真的被忽略了。它不是“字段类型”,跟 OB_FIELDTYPE 位段毫无关系。
- 声明成 long 就老老实实返回 long。我一开始在出错分支里返回了字符串,PB 侧会拿到类型不符的东西;改成编码成负数(-1/-2)就干净了。
6.2 ★ 返回 0 = 抛错(实测错误号 21)
文章说"DLL 里 return 0 表示函数发生了错误,可以在 PB 里用 try 捕捉"。实测:
PB 侧代码:
- try
- li_rc = SL_Fail()
- ls_res = ls_res + "FAIL_NORAISE=" + String(li_rc) + ";"
- catch (runtimeerror e17)
- ls_res = ls_res + "FAIL_RAISED=" + String(e17.number) + ";"
- end try
复制代码
结果是走了 catch 分支,e17.number = 21,而且 ot_set_return_val 设的 12345 根本没被采用。所以:
return 0 会被 PB 当成运行时错误 21,返回值作废。 想在 PB 里表达"业务失败",请 return 1 并用返回值/ref 参数传递错误码,不要用 return 0。
这是一个很实用的坑:很多 C 程序员习惯"出错返回 0",在 system library 里会直接把 PB 应用搞成异常。
6.3 ★ 内存纪律(照抄会崩的那一段)
文章第一章反复强调:指针类型返回值必须用 PB 的堆分配,否则 PB 释放时会崩。
- /* ❌ 会崩 */
- TCHAR *buffer = new TCHAR[1024];
- ob_set_data_string(&obReturn, buffer, STRING_TYPE, 0);
- /* ❌ 用 malloc 也一样会崩 */
- TCHAR *buffer = (TCHAR *)malloc(2048);
- /* ✅ 三种正确做法 */
- TCHAR *p1 = ob_dup_string(obThis, L"hello"); /* 复制一份到 PB 堆 */
- TCHAR *p2 = ob_alloc_string(obThis, 32); /* 在 PB 堆上开 32 个字符 */
- TCHAR *p3 = pbstg_alc(obThis->stgthis, 32, obThis->subpool); /* 更底层,但 pbstg_alc 不是导出 */
复制代码
注意最后一行:pbstg_alc 在 PBVM 的导出表里同样查不到(第 3 节那张表里它是 macro),所以实际上你只有前两条路能走。本文实测 ob_dup_string 完全可用:
- ECHO=syslib: hello ← 字符串进、字符串出,PB 侧拿到的内容一字不差
- CN=中文来自DLL:扩展成功 ← DLL 里写的中文字面量也能正确回传
复制代码
PB10 以上 TCHAR 就是 wchar_t(UTF-16LE),而字符串的"字符数"语义也变了 —— 一行 6 个汉字 + 3 个字母,Len() 给的是 5("中文abc" 按字符数):
- LEN=6 BLTINLEN=6 ← "abcdef" → 6
- LENZH=5 BLTINLENZH=5 ← "中文abc" → 5
复制代码
7. 数组参数
PB 侧声明:
- function long SL_SumArray(long a[]) system library "pbsyslib_demo.dll" alias for "SLSumArray"
复制代码
DLL 侧:
- void *arr = a->valptr_arg(obThis, &isnull); /* 数组句柄也在指针型参数里 */
- int n = a->array_num_items(obThis, arr);
- for (int i = 0; i < n; i++) {
- POB_DATA it = ot_array_index(obThis, arr, i); /* 返回类型就是 POB_DATA */
- sum += read_node_long(it, &ok); /* it 就是 POB_DATA:8 字节,type 在 +6 */
- }
复制代码
实测:
- ARR=150
- AINFO=n=5 unb=1 i0:t=2 v=10 i1:t=2 v=20 i2:t=2 v=30 i3:t=2 v=40 i4:t=2 v=50
复制代码
要点:
- 数组拿下标从 0 开始(ot_array_index 第三参传 0..n-1),不是 PB 的 1 基;
- ot_array_num_items 拿到的是项数 5,正确;
- 数组项就是 8 字节 OB_DATA,类型码在 +6,值在 +0(这次是 LONG_TYPE)——官方把 ot_array_index 的返回类型写死为 PBWINAPI(POB_DATA, ...),这是最直白的证据;
- ot_is_array_unbounded 对我声明的固定数组 long ll_arr[5] 返回了 1(真),所以不要拿它当"是否变长"的判据——它会把手写的固定数组也认成 unbounded。
8. 拿到"当前对象是谁"
这是 system library 相比通用 DLL 最爽的地方。PB 里你以为传的是参数,其实调用者本身也是隐式参数:
- OB_INST_ID inst = NULL; /* 官方返回类型就是 OB_INST_ID */
- BOOL isnull = TRUE;
- inst = a->curr_obinst(obThis, &inst, &isnull); /* ot_get_curr_obinst_expr */
复制代码
实测(从 NVO 的 of_probe() 里调用 DLL):
- CUR=currobinst=0x02ED0688 isnull=0
复制代码
拿到实例句柄之后,按文章第四章的说法,就可以继续取它的所有属性、调它的所有函数和事件。这一块(用实例句柄操作 PB 对象)本文没有展开实测,只做到"句柄能正确拿到"这一步,剩下属于 ob_get_obinst_* / rtRoutineExec 系列,留待后续。
9. 引用(ref)参数 —— 已真机跑通
本节是 v2 订正幅度最大的一节。第一版写"本文未跑通",是因为当时手里没有 OT_REF_PAK 里那个 ref 联合体的布局,只能停在"结构没公开、不猜"这一步。拿到 EN32T.h 之后,链路十分钟就通了:ref 参数能读类型、能读值、能写回,PB 侧实参真的会变。
八篇文章里 ref 参数占了很大的篇幅。核心链路是这样:
- POT_LVALUE_INFO info = NULL;
- POB_DATA obRefArg = ot_get_next_lvalue_arg(obThis, &info); /* 真实导出,返回 POB_DATA */
- POT_REF_PAK v = (POT_REF_PAK)obRefArg->val.ptr; /* 左值节点的 val.ptr 指向 refpak */
- POB_DATA lpArg = ot_access_ref_data(obThis, v); /* 官方宏,展开见下 */
- lpArg->val.ptr = ob_dup_string(obThis, buf); /* 直接改左值本体 */
- ob_set_data_info(lpArg, PTR_STYLE, STRING_TYPE, OB_SIMPLE, 0);
复制代码
三个关键结构,全部来自 EN32T.h 原文:
- /* EN32T.h 21434-21437 */
- typedef struct ot_ref_pak_simple_ref_tag {
- POB_DATA lvalue; /* ← 真正的左值节点,还是 POB_DATA */
- } ot_ref_pak_simple_ref;
- /* EN32T.h 21439-21445 */
- typedef struct ot_ref_pak_field_ref_tag {
- OB_INST_ID obinst;
- UINT field_id;
- OT_FIELDUPDATE_FUNC field_update_func;
- ULONG item_index;
- } ot_ref_pak_field_ref;
- /* EN32T.h 21447-21451 */
- typedef union ot_ref_tag_union {
- ot_ref_pak_simple_ref simple;
- ot_ref_pak_field_ref field;
- } OT_REF_TAG_UNION, FAR* POT_REF_TAG_UNION;
- /* EN32T.h 21411-21416:三种引用风格 */
- typedef enum { OT_SIMPLE_REF, OT_FIELD_REF, OT_FIELD_ITEM_REF } OT_REFPAK_STYLE;
- /* EN32T.h 21455-21464 */
- typedef struct ot_ref_pak {
- OT_REFPAK_STYLE style;
- OB_GROUP_HNDL group_hndl;
- OB_CLASS_ID type;
- USHORT flags;
- OT_REF_TAG_UNION ref; /* ← 第一版卡住的地方,其实简单得离谱 */
- } OT_REF_PAK, FAR* POT_REF_PAK;
复制代码
ot_access_ref_data 确实是宏(不是 PBVM 导出),但它是 PB 官方头文件里的宏,原文照抄(21486-21494):
- #define ot_access_ref_data(obthis,refpak) \
- ( \
- (refpak)->style == OT_SIMPLE_REF \
- ? (refpak)->ref.simple.lvalue \
- : ((refpak)->style == OT_FIELD_REF \
- ? ot_get_field_lv (obthis, (refpak)->ref.field.obinst, \
- (refpak)->ref.field.field_id) \
- : ot_get_field_item_lv (obthis, (refpak)->ref.field.obinst, \
- (refpak)->ref.field.field_id, \
- (refpak)->ref.field.item_index) \
- ) \
- )
复制代码
它做的事:简单变量直接取 ref.simple.lvalue;字段 / 数组项则去问 ot_get_field_lv / ot_get_field_item_lv(这两个是真实导出,EN32T.h 21467 / 21474 有原型)。
真机实测(DLL 侧 SL_RefDiag(ref string) / SL_RefSetString(ref string) / SL_RefObserve(...),PB 侧三个不同声明轮流调)输出:
- REFDIAG=isref=1 style=0:OT_SIMPLE_REF type=6 lvalue=0x02ED0C58 wasnull=0 val=abc
- REFWRITE=written via refpak WROTE=OK
- REFOB=obsarg=ref arg isref=1 reftype=2:OB_ARGUMENT_REF
复制代码
三条硬证据:
- ot_get_next_lvalue_arg 拿到的左值节点,val.ptr 指向的就是 OT_REF_PAK;
- style=0 即 OT_SIMPLE_REF,走 ref.simple.lvalue 分支 —— 与宏展开路径完全一致,说明结构对上了;
- 往 lvalue 里写字符串之后,PB 侧 ref 实参真的变了(WROTE=OK),并且 ref 参数节点的 info 里 REFTYPE 位段读到 2,正是官方枚举 OB_ARGUMENT_REF(EN32T.h 8063)—— 顺带说明判断"这个参数是不是引用"的官方宏是 ot_is_a_reference_argument(EN32T.h 21749,展开为 ob_get_data_reftype(pobData) == OB_ARGUMENT_REF)。
另外那批 ot_assign_ref_* 家族(bool/byte/char/date/datetime/dec/double/enum/float/int/long/longlong/obinst/string/time/uint/ulong/any 共 18 个,外加 7 个 ot_assign_lvalue_*):既然低阶路已经跑通,它们的定位也就清楚了 —— 它们是"往引用里赋指定类型值"的便捷封装。手工解 refpak 能用,但生产代码优先用它们。
⚠ 顺手挖出的一个真 bug:ref 出参 + 字符串返回值 = 必崩。
作者 DEMO 里的 TestRef(pbjson.cpp 198-222 行)有这么一行:
- BOOL success = FALSE;
- ...
- ob_set_data_string(&obReturn, success, BOOL_TYPE, OB_INSTVAR_FIELD); /* ← 就是这一行 */
复制代码
success 是个 BOOL(=0/1 的整数),却被当成指针塞进了 val.ptr。PB 侧如果按 string 接返回值,就会去解引用 0x00000001 这个地址 → 0xC0000005(访问冲突),而且是立刻崩,没有堆栈线索。规避办法:把这条函数声明成 boolean 返回(PB 只取低 32 位值,不解引用),或者用官方 ob_set_data_bool 宏。
这个 bug 从反面印证了两件事:①ob_set_data_string 的第 2 参会原封不动进 val.ptr,它只接 PB 堆上的指针(ob_dup_string / ob_alloc_string 出来的);②第 4 参 vartype 与"这是不是字段"没有任何关系,写 OB_INSTVAR_FIELD 也救不了它。
10. 把 PB 内置函数改个名 —— 能用的和不能用的
文章第二章给了一个很诱人的例子:
- function int MsgBox(string title, string text) system library "pbvm100.dll" alias for "fnMessageBox"
复制代码
我把它拆成三个最小实验,真机结果如下:
- BEEP_NULL; ← fnBeep 别名 → 返回 NULL ✗
- UP=ABC BLTIN=ABC; ← fnUpper vs 内置 Upper ✅ 一致
- UPZH_SAME=PB中文ABC; ← 中文场景也完全一致 ✅
- LEN=6 BLTINLEN=6; ← fnLen vs 内置 Len ✅ 一致
- LENZH=5 BLTINLENZH=5; ← "中文abc" 都是 5 ✅
复制代码
结论分两半:
- ✅ 带参数的内置函数,别名完全可用。fnUpper / fnLen 换成自己的名字之后,行为与内置函数逐字节一致,包括 UTF-16 中文场景。
- ❌ 不带参数的内置函数(fnBeep)别名拿不到值,返回 NULL。我的推断是:PB 在调用 0 参数的 system library 函数时,不往 DLL 侧传参数列表,DLL 里 ot_get_xxxarg 自然取不到上下文;但 fnBeep 这类内置函数本体的签名又和"(obThis, nArgCount)"这套外壳不完全一致,于是落回 NULL。这一条只是实测现象 + 推断,根因未定。
所以那句"所有内置函数都能随便改名",要打个折扣:带参数的可以,0 参数的别指望。
10.1 反例:别名指向不存在的导出
- function integer SL_Ghost() system library "PBVM125.DLL" alias for "fnNoSuchExportZZZ"
复制代码
真机结果:
- before;CAUGHT=15;after;MECH_OK=1
复制代码
- 别名确实是按名字在 DLL 导出表里精确查找的(不存在就失败);
- 抛的是 runtime error 15,而且可以被 try/catch (runtimeerror) 正常捕获,后面的代码照常执行(after 打出来了)。
这个反例很有用:它证明别名解析走的是导出表,不是"名字里带 fn 就当内置函数处理"。
11. 坑清单(全部为 PB 12.5 真机实测)
| # | 坑 | 现象 | 解法 | | 1 | 用 library 而不是 system library | 拿不到 obThis,参数全乱 | 关键字必须写全 | | 2 | 不用 .def | PB 找不到函数(导出名被修饰成 _Xxx@8) | 传 /DEF: | | 3 | 按文章用 ob_set_data_* / ot_get_xxx_arg 去 GetProcAddress | 一个都找不到 | 用本文第 3 节的对照表 | | 4 | ★ 自造“参数节点”结构体(以为 OB_DATA 是 12 字节) | 类型码读到 770、数字算成 50468204 | OB_DATA 就是 8 字节,直接用官方定义,类型码在 +6 | | 5 | 给 OB_VALUE 的 union 里加 double / __int64 | union 被撑到 8 字节,type 偏到 +10 | 照官方定义写(官方 union 里没有这两个类型),或直接 #include "EN32T.h" | | 6 | 用 new / malloc 分配返回字符串 | PB 释放时崩溃 | ob_dup_string / ob_alloc_string | | 7 | return 0 表示失败 | PB 抛 runtime error 21,返回值作废 | 一律 return 1,错误用返回值编码 | | 8 | 取参数次数超过 nArgCount | 指针越界,崩溃或埋雷 | 严格按个数取 | | 9 | 拿 ot_is_array_unbounded 判"是否变长" | 固定数组 long a[5] 也返回真 | 别用它做判据 | | 10 | 数组下标从 1 开始取 | 第一项永远取不到 | ot_array_index 从 0 开始 | | 11 | 按 INT_TYPE 判整数字面量 | 判不中(字面量是 LONG_TYPE) | 同时接受 1 和 2 | | 12 | 别名 0 参数的内置函数 | 返回 NULL | 别这么干 | | 13 | 别名不存在的导出 | runtime error 15 | 用 try/catch 兜住并打日志 | | 14 | 同一个 PB 函数里写多个 catch (runtimeerror ex) | 编译报 C0081 Duplicate variable: ex | catch 变量名要唯一(e1/e2/e3…) | | 15 | pbtest 里断言 if Pos(结果,"关键词")<=0 then halt 而结果可能是 NULL | 测试假 PASS | 先 if IsNull(结果) then halt,且结果串只用字面量拼装 | | 16 | 断言串里带 [ ] ( ) | Pos() 第二参是模式不是纯文本,匹配不到 | 断言串避开正则元字符 |
第 14~16 条是踩在我自己身上的:第一版探针因为 + 拼接碰上了 NULL,整个结果串变 NULL,写出的日志文件是 0 字节,而 Pos(NULL,...)<=0 的条件在 PB 里是 NULL,if 当假 → halt 不触发 → 测试报 PASS。加了 IsNull() 断言之后才把它抓出来。写 PB 单元测试的朋友,这三条直接抄走。
12. 三锤定音:它到底值不值得用
值得用的场景:
- 需要高性能的字符串/二进制处理(一次调用完成,不用来回 marshal);
- 需要访问 PB 对象、属性、事件(这是相对通用 DLL 的碾压性优势);
- 老系统维护,PB 版本固定在 9~12.5 之间,不打算跟进 Appeon 新版本。
别用的场景:
- 你要跟着 PB 升级到 PB 2017 以后的版本 —— system library 直接绑 PB 内部 ABI,升级风险比 PBNI 高;
- 团队里没人愿意维护 C++ —— 这套 API 没有官方文档,全靠反汇编和试错;
- 只是想调个 Win32 API —— 通用 DLL 就够了。
一句话总结三层关系:
通用 DLL 是"请来的临时工",只能用公开接口;
PBNI 是"有工牌的外包",能进大楼但有门禁(版本兼容);
system library 是"自己人",能进机房 —— 代价是你得自己摸索机房里每一根线的接法。
13. 附件说明
附件 pbsyslib_demo_src.zip 内含:
| 文件 | 说明 | | pbSysLib_min.h | 自研最小头文件(v200)。已按 EN32T.h 订正 OB_VALUE / OB_DATA / OT_REF_PAK,并内置 sizeof(OB_DATA)==8 的编译期静态断言 | | pbsyslib_demo.cpp | 23 个导出的完整源码(字符串/数值/数组/变参/NULL/当前对象/ref 写回/API 自检/官方宏对照) | | pbsyslib_demo.def | 导出表定义(保证未修饰导出名) | | _build.bat | MSVC x86 一键编译脚本 | | pbsyslib_demo.dll | 已编译好的 32 位 DLL,可直接放到 PBL 同目录使用 | | nvo_syslib_dll.sru | PB 侧声明 + 全部观测点探针(含 SL_ProbeLong/Int/Str、SL_RefSetString;GBK + CRLF,可直接 import) | | nvo_syslib_probe.sru | 内置函数别名探针(fnUpper/fnLen/fnBeep) | | nvo_syslib_ghost.sru | 反例:别名指向不存在的导出 | | syslib.pbt / *.pbtest | 可直接跑的验证工程与测试脚本 | | README.md | 复现步骤 + 每一步的预期输出 |
复现顺序:cl 编译 DLL → 把 DLL 放到 run-dir → pypower pbl import 三个 .sru → build rebuild --type full → pypower test 三个 .pbtest → 对照 diag_*.txt。
v2 更新(2026-09-17):另附 pbsyslib_demo_src_v2.zip,内含订正后的 pbSysLib_min.h(v200)、重编的 pbsyslib_demo.dll(23 导出)、pbsyslib_demo.def / .cpp、结构实测探针 probe_layout.cpp 与 build_probe.py、以及更新后的 nvo_syslib_dll.sru 与 syslibdll.pbtest。v1 的 pbsyslib_demo_src.zip 保留,方便对照两次的区别。
13.1 附赠:那份官方头文件在哪、怎么用
这一小节是 v2 补的,也是"到底要不要走 system library 这条路"的分水岭 —— 它决定你是在"读"这套机制,还是在"猜"这套机制。
1) 它是谁的、在哪
本文用到的两份头文件:
| 文件 | 大小 / 行数 | 来历 | | EN32T.h | 2514 KB / 78729 行 | Sybase / PB 官方内部头文件(RTC Code)。不随 PB 公开帮助文档发布,本文取自作者 DEMO 包的 pbSysLib 子目录 | | pbSysLib.h | 134 KB / 1667 行 | 大自在 2021/6 写的包装头,第 24 行 #include "EN32T.h" |
怎么确认 EN32T.h 是官方的?看它开头就够了 —— 文件守卫是 _RTCCODE_H_(Runtime Code),第 4 行 #pragma pack(push,1),接着是一串编译器内部开关:
- #ifndef _RTCCODE_H_
- #define _RTCCODE_H_
- #pragma pack(push,1)
- #define GENERATED_CODE_BUILD
- #define PBUSE_SMRTHEAP
- #define PBWIN32
- #define PBOS_NT
- #define PBOS_MAC
- ...
- // the following defines are used in the generated code to encapsulate
复制代码
这是 PB 编译器自己生成代码时用的头文件,不是某个第三方作者的手笔。作者的 pbSysLib.h 里唯一自造的 ob_/ot_ 宏只有 ot_remove_null_flag 一个(第 3 节已述)。
2) 它里面有什么(本文核对过的行号段)
| 内容 | 行号 | | OB_STATUS / OB_FIELD_TYPE / OB_GROUPTYPE / OB_REFTYPE / OB_DATASTYLE / OB_MEMBER_ACCESS 六大枚举 | 8023-8110 | | OB_VALUE / OB_INFO / OB_DATA / OB_VAR 四个核心结构 | 9837-9883 | | DATA_*_MASK / DATA_*_SHIFT 位域常量表 | 9889-9905 | | ob_set_data_info / ob_set_data_*_val / ob_set_data_* 全套写宏 | 10056-10374 | | ob_alloc_string / ob_alloc_blob / ob_alloc_double / ob_dup_string / ob_dup_blob | 14990-15045 | | OT_LVALUE_INFO / ot_get_next_evaled_arg([_no_convert]) / ot_get_next_lvalue_arg / ot_get_simple_*arg | 20427-20460 | | ot_get_*_arg 全套取参宏(str/bool/int/uint/long/ulong/dec/float/double/longlong/obinst/time/date/datetime/enum/binary) | 20529-20562 | | ot_set_return_val / ot_set_return_double / ot_no_return_val / _return_macro / ot_return_* | 20657-20765 | | OT_REFPAK_STYLE / ot_ref_pak_simple_ref / ot_ref_pak_field_ref / OT_REF_PAK / ot_access_ref_data | 21411-21494 | | ot_is_a_reference_argument / ot_is_array / ot_array_create_* / ot_array_index / ot_array_num_items / ot_is_array_unbounded | 21749 / 21953-22025 |
3) 用它有三个必须知道的细节
- 它只要 C++,不要 C。文件里带 #error(C++ must be used for this header file)—— 源码后缀必须 .cpp,用 cl /Tc 强行按 C 编会当场被挡回来;
- 它自己带 #pragma pack(push,1)(第 4 行)。这正是 OB_DATA 只有 8 字节的原因。不要在外面套 pack(2)/pack(4)/pack(8),也不要改它的 pack —— 一改,type 的偏移就跑了,第 5.3 节的三个常数全部失效;
- 它不是"公开 SDK",别期待版本承诺。PB 帮助文件里查不到 ot_* / ob_* 任何一个名字;它的行号与内容随 PB 版本变(本文核对的是 78729 行这一版)。所以引用它的结论时,一定连版本一起说。
4) 拿到它之后,判断方式变了
没有头文件时,你只能:写个 hex32 dump → 看字节 → 猜结构 → 再用真机行为反证。本文第 5.3 节那三个常数(0x1D00/0x0500/0x0D00)就是这么猜出来的 —— 猜对了,但当时把结构的名字和大小搞错了。
拿到头文件之后,你只需要:读 8 行 typedef → 抄进代码 → 完。
这就是 v2 这一轮最实在的收获:把"猜"变成"读"。本文第 3、5、9 节的三次结论反转,本质上都是同一个原因。
14. 致谢与声明
- 机制线索来自网络流传的《PB 的扩展 DLL 开发(超级篇)》八篇连载(作者:大自在),本文所有"文章说法"均指这八篇。
- 本文的全部实测数据来自 PowerBuilder 12.5 + MSVC 2022 BuildTools(x86) + Windows 10 的真机运行,日志原文可在附带工程的 diag_*.txt 里对照。
- v2 之后仍未实测的部分只剩“用实例句柄深度操作 PB 对象”(取属性、调函数、触发事件)—— ref 参数的完整链路已在 v2 真机跑通(见第 9 节)。未实测的部分请勿直接用于生产,需要先自行验证。
- system library 属于 PB 未公开的 ABI,本文结论绑定 PB 12.5(已在 PBVM125.DLL 的 2383 个导出上逐条核对)。换版本请重新核对。
|