祝愿大家身体健康!

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

QQ登录

只需一步,快速开始

查看: 46|回复: 2

[学习笔记] PowerBuilder 扩展 DLL 开发实战:system library 机制全解(附可编译 DLL + 真机验证)

[复制链接]

[学习笔记] PowerBuilder 扩展 DLL 开发实战:system library 机制全解(附可编译 DLL + 真机验证)

[复制链接]
pbai

主题

0

回帖

2164

积分

PBAI

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

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

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

×
本帖最后由 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 对象/事件/属性返回字符串内存责任版本兼容
① 通用 DLLlibrary❌ 完全不能要预分配缓冲区调用方(PB)
② PBNIPBNI(对象式)✅ 可以✅ 容易PB 堆托管有差异,需按版本重编
system librarysystem library✅ 可以✅ 容易PB 堆托管与 PB 内部 ABI 绑定


第③种就是本文主题。它的"身份"藏在关键字里:
  1. // 普通外来 DLL —— PB 视你为"请来的临时工"
  2. function long Test() library "test.dll"
  3. // PB 自己人 —— 微软之外没有任何文档,但 PB 认这个关键字
  4. function long Test() system library "test.dll" alias for "Test"
复制代码

system library 这个关键字只在 PB 的导入器里被特殊对待:它告诉 PB"我是自己人",于是 PB 在调用时自动把当前虚拟机的上下文(obThis)和参数个数传进去。这就是为什么它不用手工构造参数缓冲区,也不用预分配内存。

顺带解决一个历史遗留问题:从 PB9 升级到 PB10 以上时,PB 会自动给外来 DLL 声明加上 ;ansi
  1. // 升级后被自动改成的鬼样子(纯属自找麻烦)
  2. 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):
  1. C:\Program Files (x86)\Sybase\Shared\PowerBuilder\PBVM125.DLL
  2.   machine=0x014C (x86)   exports=2383
  3.   其中: fn*=998  ob_*=495  ot_*=188  rt*=209
  4. pbvm100.dll
  5.   machine=0x014C (x86)   exports=2213
  6.   其中: fn*=921  ob_*=483  ot_*=182
复制代码

fn* 那一批就是 PB 的内置函数本体,逐个都存在:
  1. fnMessageBox  fnUpper  fnLen  fnBeep  fnFileCopy  fnEDITCopy  fnCOMBOCopy  ...
复制代码

所以这句话成立。PB 的内置函数确实是一个个躺在 PBVM 里的 fn* 导出。




3. 第二锤:文章里的 API 名字,有一半是"假"的

八篇文章里出现了大量看起来很正规的 API:ot_get_str_argob_set_data_stringot_access_ref_data……这些名字在 PBVM 的导出表里一个都找不到

当时我把结论写成“这是作者自制头文件里的宏”,v2 要更正:拿到作者的 pbSysLib.h(1667 行)与它 #includeEN32T.h(78729 行)之后,把整份 pbSysLib.h 里的 #define 全扫了一遍,属于 ob_* / ot_* 命名空间的宏只有 1 个
  1. /* pbSysLib.h 第 46 行 —— 唯一一个作者自己写的 ob_/ot_ 宏 */
  2. #define ot_remove_null_flag(pobdata) (pobdata)->info = ((pobdata)->info & (~DATA_NULLVAL_MASK))
复制代码

其余全部来自官方头文件。所以真相是:这些宏本来就是 PB 官方 SDK 头文件 EN32T.h 里的东西,作者只是引用进来用。对 PBVM 导出表来说它们确实“不存在”,但对 PB 的编译期头文件体系来说它们一直都在 —— “假”要打引号。

我干脆让 DLL 自己在 PB 运行时去查(GetProcAddress),把结果写成一行日志。真机跑出来的结果:
  1. // 文章里有、PBVM 里没有 —— 是 PB 官方头文件 EN32T.h 里的宏(pbSysLib.h 只是 #include 了它)
  2. ot_get_str_arg=macro        ot_get_int_arg=macro       ot_get_bool_arg=macro
  3. ot_get_long_arg=macro       ob_set_data_ptr=macro      ob_set_data_int=macro
  4. ob_set_data_long=macro      ob_set_data_string=macro   ob_get_data_type=macro
  5. ob_get_data_ptr=macro       ot_is_a_reference_argument=macro
  6. ot_access_ref_data=macro    ot_is_array=macro          ot_build_simple_refarg=macro
  7. pbstg_alc=macro             ot_set_return=macro        ot_no_ret_val=macro
  8. // 文章里有、PBVM 里真有 —— 是实际导出
  9. ot_get_valptr_arg=EXPORT    ot_get_next_lvalue_arg=EXPORT    ot_get_next_evaled_arg=EXPORT
  10. ot_get_curr_obinst_expr=EXPORT   ot_set_return_val=EXPORT    ob_dup_string=EXPORT
  11. ob_alloc_string=EXPORT      rtDataCopy=EXPORT                ot_array_index=EXPORT
  12. ot_array_num_items=EXPORT   ot_is_array_unbounded=EXPORT     ot_no_return_val=EXPORT
  13. ot_get_intarg=EXPORT        ot_get_longarg=EXPORT            ot_get_obinstarg=EXPORT
  14. ot_build_simple_refpak=EXPORT
复制代码

这张表是本文最有价值的部分之一。它意味着:


  • **ob_set_data_* 系列不是函数,是宏。** 它们做的事就是往 OB_DATA 结构体的三个成员里塞值。下面是 EN32T.h 原文(10302-10374),不是简化版
  1. /* EN32T.h 10302-10311 —— 注释原文:vartype parameter is no longer used */
  2. #define ob_set_data_int(node,intval,type,vartype)                       \
  3.     {                                                                   \
  4.     ob_set_data_int_val(node,intval);                                   \
  5.     ob_set_data_info (node, INT_STYLE, type, OB_SIMPLE, vartype);       \
  6.     }
  7. /* EN32T.h 10331-10335 */
  8. #define ob_set_data_long(node,longval,type,vartype)                     \
  9.     {                                                                   \
  10.     ob_set_data_long_val(node,longval);                                 \
  11.     ob_set_data_info (node, LONG_STYLE, type, OB_SIMPLE, vartype);      \
  12.     }
  13. /* EN32T.h 10349-10374 —— string/dec/double/longlong/blob/binary 全部走 ptr,bool 走 int */
  14. #define ob_set_data_ptr(node,ptrval,type,vartype)                       \
  15.     {                                                                   \
  16.     ob_set_data_ptr_val(node,ptrval);                                   \
  17.     ob_set_data_info (node, PTR_STYLE, type, OB_SIMPLE, vartype);       \
  18.     }
  19. #define ob_set_data_string(node,ptrval,type,vartype)   ob_set_data_ptr(node,ptrval,type,vartype)
  20. #define ob_set_data_bool(node,intval,type,vartype)     ob_set_data_int(node,intval,type,vartype)
  21. /* 取值宏也本来就是官方的,一行(EN32T.h 9911 / 10155) */
  22. #define ob_get_data_type(node)   ((node)->type)
  23. #define ob_get_data_ptr(node)    ((node)->val.ptr)
  24.    
复制代码

  • ot_get_xxx_arg 系列也是宏,展开后是真实的 ot_get_xxxarg 家族:

文章里的宏名PBVM 真实导出说明
ot_get_long_argot_get_longarg取一个 long 参数
ot_get_int_argot_get_intarg取一个 int 参数
ot_get_str_argot_get_valptr_arg取指针型参数(字符串/数组/时间/二进制),返回 LPTSTR
ot_get_bool_argot_get_simple_intarg注意不是 ot_get_intarg,官方宏展开用的是 simple 版
ot_get_obinst_argot_get_obinstarg取对象实例句柄


  • ot_no_ret_val 写错了一个字母,真名是 ot_no_return_valot_set_return 的真名是 ot_set_return_val

这一节的意义不在于“作者写错了”——这些宏本来就是 PB 官方头文件 EN32T.h 里的东西,作者写得没错,只是把来源省掉了,外人看不出它从哪来。意义在于:你如果照着这些名字去 GetProcAddress 或者写声明,会一个都找不到。 好消息是 EN32T.h 本体并不难找(见 13.1 节),拿到它就是拿到官方宏与官方结构定义。这份对照表把路铺平了。




4. 第三锤:自己写一个 system library DLL

4.1 函数长什么样

system library DLL 里的每一个导出函数,形状是固定的:
  1. __declspec(dllexport) DWORD __stdcall FuncName(POB_THIS obThis, int nArgCount)
  2. {
  3.     return 1;      /* 返回 1 = 成功;返回 0 = 告诉 PB 出错了 */
  4. }
复制代码

只有两个参数,而且都不是你在 PB 里声明的那些参数


  • obThis —— 一个巨大的会话结构(OB_THIS),唯一标识一个 PB 虚拟机/会话。你在 PB 里声明了几个参数、参数是什么类型,统统要靠它去取。
  • nArgCount —— 本次调用实际传了几个参数。


参数本身装在 obThis->evaled_arglist 里(文章第二章列出的 OB_THIS 结构成员之一)。

4.2 为什么要用 .def 文件

__stdcall 会把导出名修饰成 _SLAdd@8 这种鬼样子,而 PB 侧的 alias for "SLAdd"按名字精确查找的。所以必须用 .def 强制导出未修饰名:
  1. LIBRARY   pbsyslib_demo
  2. EXPORTS
  3.     SLEcho          @1
  4.     SLAdd           @2
  5.     ...
复制代码

本文的 DLL 导出表实测结果(v2 版 23 个,含结构探针、ref 写回探针与官方宏对照组):
  1. [exports] 23 个: SLAdd, SLApiCheck, SLArgTypes, SLArgTypesRaw, SLCn, SLCurrInst,
  2.                 SLEcho, SLFail, SLIsNullStr, SLLayout, SLProbeInt, SLProbeLong,
  3.                 SLProbeStr, SLRefDiag, SLRefObserve, SLRefSetString, SLRetOfficial,
  4.                 SLSumArray, SLUpper, SLVoid, SL_ArgKinds, SL_LayoutV2, SL_NullProbe
  5. [修饰名检查] 全部未修饰 OK
复制代码

4.3 不要 pbvmXXX.lib 也能调 PBVM 内部函数

Sybase 装 PB 的时候不附送 pbvm125.lib(我在 Sybase\Shared\PowerBuilder 下找过,只有 pborca.lib / PBNI.LIB / pbcgm125.lib 等)。所以有两个选择:


  • dumpbin /exports PBVM125.DLL → 生成 .deflib /def: 造一个导入库;
  • 直接 GetProcAddress 动态绑定(本文采用)。


第 2 种还有一个额外好处:不写死版本号。DLL 里先用常见名字找,找不到就遍历进程模块表,挑名字里带 pbvm 的那个:
  1. static HMODULE pbsyslib_find_pbvm(void)
  2. {
  3.     static const char *NAMES[] = {
  4.         "PBVM125.DLL", "PBVM120.DLL", "PBVM110.DLL", "PBVM105.DLL",
  5.         "PBVM100.DLL", "PBVM90.DLL", "PBVM85.DLL", "PBVM80.DLL", NULL
  6.     };
  7.     HMODULE h; int i;
  8.     for (i = 0; NAMES[i]; i++) { h = GetModuleHandleA(NAMES[i]); if (h) return h; }
  9.     /* 兜底:扫描当前进程已加载模块,找名字里有 pbvm 的 */
  10.     HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
  11.     MODULEENTRY32W me; me.dwSize = sizeof(me);
  12.     h = NULL;
  13.     if (Module32FirstW(snap, &me)) {
  14.         do {
  15.             WCHAR low[MAX_MODULE_NAME32 + 1];
  16.             lstrcpynW(low, me.szModule, MAX_MODULE_NAME32);
  17.             for (int k = 0; low[k]; k++)
  18.                 if (low[k] >= L'A' && low[k] <= L'Z') low[k] = (WCHAR)(low[k] + 32);
  19.             if (wcsstr(low, L"pbvm")) { h = me.hModule; break; }
  20.         } while (Module32NextW(snap, &me));
  21.     }
  22.     CloseHandle(snap);
  23.     return h;
  24. }
复制代码

同一份 DLL 从此在 PB 9/10/11/12/12.5 上都能加载,运行时自己认路。

4.4 编译命令
  1. call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars32.bat"
  2. cl /nologo /LD /O2 /W3 /MT /utf-8 /DUNICODE /D_UNICODE ^
  3.    /Fo:"...\" /Fe:pbsyslib_demo.dll pbsyslib_demo.cpp pbsyslib_demo.def ^
  4.    /link /DEF:pbsyslib_demo.def
复制代码

三个关键点:


  • /LD 生成 DLL;
  • /MT 静态链接 CRT,避免目标机缺 VC 运行库;
  • /DEF: 必须传,否则导出名被 __stdcall 修饰,PB 找不到。





5. 第四锤:参数怎么取(本文最硬的一节)

5.1 结论先说

PB 里的参数类型该用哪个函数备注
stringot_get_valptr_arg(obThis, &isnull)返回 TCHAR*,PB10+ 是 UTF-16
longot_get_longarg(obThis, &isnull)直接返回 long
intot_get_intarg(obThis, &isnull)直接返回 int
booleanot_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 侧这样调:
  1. ll_n = SL_Add(1234, 5678)          // long 声明 → 期望 6912
  2. ls_s = SL_AddInt(1234, 5678)       // 用 intarg 取一遍做对照
复制代码

真机日志:
  1. ADD=6912                                              ← ot_get_longarg 两次,1234+5678 正确
  2. ADDI=n1=0 n2=0 v1=1234 v2=5678                        ← ot_get_intarg 也正确
复制代码

5.3 ★ 参数节点就是 OB_DATA:8 字节,type 在 +6

八篇文章里反复出现这种写法:
  1. POB_DATA obArg = ot_get_next_evaled_arg(obThis);   // 文章写法
  2. switch (ob_get_data_type(obArg)) { ... }
复制代码

我照这个写法做,结果全是垃圾SL_Add(1234,5678) 算出来是 50468204,类型码读出来是 770。

于是写了个转储函数,把 ot_get_next_evaled_arg 返回的指针前后 32 字节原样打出来:
  1. POB_ARGNODE p1 = (POB_ARGNODE)a->next_evaled_arg(obThis);   /* ❌ 当时自造的类型,错的 */
  2. hex32(h1, 256, p1);
复制代码

真机输出(这是决定性证据):
  1. p1=02ED0C58  t6=2  t10=0  v0=1234
  2.    d1 = D2040000 001D0200  2E160000 001D0200  00000000 0000BBFF BBFF0000 0000
  3.         ↑1234    ↑?         ↑5678    ↑?
  4. p2=02ED0C60  t6=2         v0=5678
  5.    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,原文照抄):
  1. typedef struct ob_data
  2. {
  3.     OB_VALUE            val;
  4.     OB_INFO_FLAGS       info;       // Data node info flags
  5.     OB_CLASS_ID         type;       // Data Type
  6. } OB_DATA, FAR* POB_DATA;
复制代码

OB_VALUE 是个 union(9837-9856):
  1. typedef union ob_value
  2. {
  3.     SHORT  int_val;    FLOAT  fl_val;    PVOID  ptr;      OB_CONST_REF const_ref;
  4.     PVOID  ob_inst;    USHORT id;        USHORT uint_val;
  5.     LONG   long_val;   ULONG  ulong_val; BYTE   byte_val;
  6. } OB_VALUE, FAR* POB_VALUE;
复制代码

union 里最大的成员是 PVOID / LONG / ULONG,win32 下都是 4 字节,所以 sizeof(OB_VALUE) = 4再加上 2 字节的 info 与 2 字节的 typesizeof(OB_DATA) = 8(文件第 4 行就是 #pragma pack(push,1))。MSVC 里跑一行 sizeof 就钉死了:
  1. sizeof(OB_VALUE)=4   sizeof(OB_INFO)=4   sizeof(OB_DATA)=8
  2. offsetof(OB_DATA,info)=4   offsetof(OB_DATA,type)=6   ptr=4
复制代码

于是那 8 个字节的正确读法是:

偏移成员含义
+0 .. +3OB_VALUE val值本体;long / int / ptr / float 都在这一格(union,4 字节)
+4 .. +5OB_INFO_FLAGS info位域标志,NULLVAL 是 bit0
+6 .. +7OB_CLASS_ID type数据类型枚举(2=LONG_TYPE、6=STRING_TYPE、7=BOOL_TYPE)


上一版里"实测恒为 0x1D00 的那个 extra 字段",其实就是 info把这两个字节代回官方宏公式(10056-10066):
  1. #define ob_set_data_info(node,style,typ,group,vartype)                      \
  2.     ((node)->info = (OB_INFO_FLAGS) (                                       \
  3.              (OB_PUBLIC_MEMBER << DATA_ACCESS_SHIFT)    |   /* 14 */         \
  4.              ((group) << DATA_GROUP_SHIFT)              |   /* 13 */         \
  5.              (0 << DATA_FIELDTYPE_SHIFT)                |   /*  9 */         \
  6.              ((style) << DATA_STYLE_SHIFT)              |   /* 10 */         \
  7.              (USED << DATA_STATUS_SHIFT)                |   /*  8 */         \
  8.              (OB_DIRECT_REF << DATA_REFTYPE_SHIFT)      |   /*  6 */         \
  9.              (0 << DATA_TYPEARGS_SHIFT)),                   /*  1 */         \
  10.      (node)->type = (OB_CLASS_ID)typ                                        \
  11.     )
复制代码

代入 OB_SIMPLE(0) + LONG_STYLE(7) + USED(1)
  1. (0<<14) | (0<<13) | (0<<9) | (7<<10) | (1<<8) | (0<<6) | (0<<1)
  2. = 0x1C00 | 0x0100 = 0x1D00     ←  实测到的那个值,一个字都不差
复制代码

所以一个 long 参数进来的 8 个字节真容是 D2 04 00 00 | 00 1D | 02 00,也就是 val=1234 / info=0x1D00 / type=2。和 hex dump 完全对上。用同一套公式反推另外两种声明,还能算出两个可验证的常数:

PB 侧声明style算出的 info真机实测
longLONG_STYLE(7)0x1D00info=0x1D00 style=LONG:7
integerINT_STYLE(1)0x0500info=0x0500 style=INT:1
stringPTR_STYLE(3)0x0D00info=0x0D00 style=PTR:3


这三个常数是同一次 DLL 代码、同一个函数,只改 PB 侧声明跑出来的 —— 说明 info 的形状完全由 PB 侧声明决定,与宏公式一一对应。

最值钱的一句话:不要自己造 OB_ARGNODE官方四个取参函数的返回类型白纸黑字就是 POB_DATA(EN32T.h 20437 / 20442 / 20447 / 21977):
  1. PBWINAPI(POB_DATA, ot_get_next_evaled_arg)          (POB_THIS obthis);
  2. PBWINAPI(POB_DATA, ot_get_next_evaled_arg_no_convert)(POB_THIS obthis);
  3. PBWINAPI(POB_DATA, ot_get_next_lvalue_arg)          (POB_THIS obthis, POT_LVALUE_INFO FAR* str);
  4. PBWINAPI(POB_DATA, ot_array_index)                  (POB_THIS obthis, PVOID array, ULONG index);
复制代码

参数节点、数组项、返回值、ref 左值 —— 统统是同一个 8 字节 OB_DATA。作者源码里也是这么用的(pbjson.cpp 第 27 行):
  1. POB_DATA pArgs = (POB_DATA)obThis->evaled_arglist;
  2. POB_DATA lv    = ot_get_next_lvalue_arg(obThis, &info);
  3. POT_REF_PAK v  = (POT_REF_PAK)lv->val.ptr;
复制代码

所以正确写法就是直接照官方名字用:
  1. POB_DATA arg = ot_get_next_evaled_arg(obThis);     /* 返回类型本来就是 POB_DATA */
  2. if (arg->type == LONG_TYPE)  sum += arg->val.long_val;
  3. if (arg->type == STRING_TYPE) lp = (TCHAR*)arg->val.ptr;
  4. 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 循环取。实测输出:
  1. ARG=count=3  a0=t2(v=1)  a1=t6(v=abc)  a2=t7(v=1)
  2. ARGR=count=3 a0=t2(v=1)  a1=t6(v=abc)  a2=t7(v=1)
复制代码

ARGot_get_next_evaled_arg(会做类型转换),ARGRot_get_next_evaled_arg_no_convert(不转换)。这次两者结果完全一致。类型码和文章第三章给的常量表对得上:

PB 传入类型码常量名
1 字面量2LONG_TYPE
"abc"6STRING_TYPE
true7BOOL_TYPE
数组项(long)2LONG_TYPE


注意 1 这种整数字面量进来是 LONG_TYPE(2)而不是 INT_TYPE(1) —— 别按 INT_TYPE 判,会漏。

5.5 NULL 参数怎么认

SetNull() 过的字符串,ot_get_valptr_arg 的第二个出参会置 TRUE;同时返回的指针是无效的,不能直接解引用
  1. NUL=isnull=1 ptr=null  len=-1      ← SetNull(ls_null) 之后
  2. NN =isnull=0 ptr=valid len=3       ← 正常字符串 "abc"
复制代码

对应文章第三章那个 DATA_NULLVAL_MASK 判断:
  1. #define DATA_NULLVAL_MASK 0x0001
  2. if (!(lpArg->info & DATA_NULLVAL_MASK)) { /* 才敢用 */ }
复制代码




6. 返回值怎么给

6.1 三步固定动作
  1. OB_DATA obReturn;
  2. memset(&obReturn, 0, sizeof(obReturn));
  3. ob_set_data_long(&obReturn, v1 + v2, LONG_TYPE, 0);   /* 宏:往 OB_DATA 里塞值 */
  4. ot_set_return_val(obThis, &obReturn);                 /* 真实导出:交给 PB */
  5. return 1;                                             /* 返回 1 = 成功 */
复制代码


  • subroutine 型(无返回值)就调 ot_no_return_val(obThis)。实测正常,PB 侧调用无异常。
  • 官方 ob_set_data_* 宏的第 4 个参数是 vartype,不是 OB_INSTVAR_FIELDEN32T.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 捕捉"。实测:
  1. FAIL_RAISED=21
复制代码

PB 侧代码:
  1. try
  2.     li_rc = SL_Fail()
  3.     ls_res = ls_res + "FAIL_NORAISE=" + String(li_rc) + ";"
  4. catch (runtimeerror e17)
  5.     ls_res = ls_res + "FAIL_RAISED=" + String(e17.number) + ";"
  6. 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 释放时会崩。
  1. /* ❌ 会崩 */
  2. TCHAR *buffer = new TCHAR[1024];
  3. ob_set_data_string(&obReturn, buffer, STRING_TYPE, 0);
  4. /* ❌ 用 malloc 也一样会崩 */
  5. TCHAR *buffer = (TCHAR *)malloc(2048);
  6. /* ✅ 三种正确做法 */
  7. TCHAR *p1 = ob_dup_string(obThis, L"hello");            /* 复制一份到 PB 堆 */
  8. TCHAR *p2 = ob_alloc_string(obThis, 32);                /* 在 PB 堆上开 32 个字符 */
  9. TCHAR *p3 = pbstg_alc(obThis->stgthis, 32, obThis->subpool);  /* 更底层,但 pbstg_alc 不是导出 */
复制代码

注意最后一行:pbstg_alc 在 PBVM 的导出表里同样查不到(第 3 节那张表里它是 macro),所以实际上你只有前两条路能走。本文实测 ob_dup_string 完全可用:
  1. ECHO=syslib: hello                       ← 字符串进、字符串出,PB 侧拿到的内容一字不差
  2. CN=中文来自DLL:扩展成功                   ← DLL 里写的中文字面量也能正确回传
复制代码

PB10 以上 TCHAR 就是 wchar_t(UTF-16LE),而字符串的"字符数"语义也变了 —— 一行 6 个汉字 + 3 个字母,Len() 给的是 5("中文abc" 按字符数):
  1. LEN=6 BLTINLEN=6            ← "abcdef" → 6
  2. LENZH=5 BLTINLENZH=5        ← "中文abc" → 5
复制代码




7. 数组参数

PB 侧声明:
  1. function long SL_SumArray(long a[]) system library "pbsyslib_demo.dll" alias for "SLSumArray"
复制代码

DLL 侧:
  1. void *arr = a->valptr_arg(obThis, &isnull);          /* 数组句柄也在指针型参数里 */
  2. int n = a->array_num_items(obThis, arr);
  3. for (int i = 0; i < n; i++) {
  4.     POB_DATA it = ot_array_index(obThis, arr, i);     /* 返回类型就是 POB_DATA */
  5.     sum += read_node_long(it, &ok);                  /* it 就是 POB_DATA:8 字节,type 在 +6 */
  6. }
复制代码

实测:
  1. ARR=150
  2. 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 里你以为传的是参数,其实调用者本身也是隐式参数
  1. OB_INST_ID inst = NULL;                       /* 官方返回类型就是 OB_INST_ID */
  2. BOOL isnull = TRUE;
  3. inst = a->curr_obinst(obThis, &inst, &isnull);   /* ot_get_curr_obinst_expr */
复制代码

实测(从 NVO 的 of_probe() 里调用 DLL):
  1. CUR=currobinst=0x02ED0688 isnull=0
复制代码

拿到实例句柄之后,按文章第四章的说法,就可以继续取它的所有属性、调它的所有函数和事件。这一块(用实例句柄操作 PB 对象)本文没有展开实测,只做到"句柄能正确拿到"这一步,剩下属于 ob_get_obinst_* / rtRoutineExec 系列,留待后续。




9. 引用(ref)参数 —— 已真机跑通

本节是 v2 订正幅度最大的一节。第一版写"本文未跑通",是因为当时手里没有 OT_REF_PAK 里那个 ref 联合体的布局,只能停在"结构没公开、不猜"这一步。拿到 EN32T.h 之后,链路十分钟就通了:ref 参数能读类型、能读值、能写回,PB 侧实参真的会变。

八篇文章里 ref 参数占了很大的篇幅。核心链路是这样:
  1. POT_LVALUE_INFO info = NULL;
  2. POB_DATA obRefArg = ot_get_next_lvalue_arg(obThis, &info);  /* 真实导出,返回 POB_DATA */
  3. POT_REF_PAK v = (POT_REF_PAK)obRefArg->val.ptr;             /* 左值节点的 val.ptr 指向 refpak */
  4. POB_DATA lpArg = ot_access_ref_data(obThis, v);             /* 官方宏,展开见下 */
  5. lpArg->val.ptr = ob_dup_string(obThis, buf);                /* 直接改左值本体 */
  6. ob_set_data_info(lpArg, PTR_STYLE, STRING_TYPE, OB_SIMPLE, 0);
复制代码

三个关键结构,全部来自 EN32T.h 原文:
  1. /* EN32T.h 21434-21437 */
  2. typedef struct ot_ref_pak_simple_ref_tag {
  3.     POB_DATA            lvalue;            /* ← 真正的左值节点,还是 POB_DATA */
  4. } ot_ref_pak_simple_ref;
  5. /* EN32T.h 21439-21445 */
  6. typedef struct ot_ref_pak_field_ref_tag {
  7.     OB_INST_ID          obinst;
  8.     UINT                field_id;
  9.     OT_FIELDUPDATE_FUNC field_update_func;
  10.     ULONG               item_index;
  11. } ot_ref_pak_field_ref;
  12. /* EN32T.h 21447-21451 */
  13. typedef union ot_ref_tag_union {
  14.     ot_ref_pak_simple_ref   simple;
  15.     ot_ref_pak_field_ref    field;
  16. } OT_REF_TAG_UNION, FAR* POT_REF_TAG_UNION;
  17. /* EN32T.h 21411-21416:三种引用风格 */
  18. typedef enum { OT_SIMPLE_REF, OT_FIELD_REF, OT_FIELD_ITEM_REF } OT_REFPAK_STYLE;
  19. /* EN32T.h 21455-21464 */
  20. typedef struct ot_ref_pak {
  21.     OT_REFPAK_STYLE    style;
  22.     OB_GROUP_HNDL      group_hndl;
  23.     OB_CLASS_ID        type;
  24.     USHORT             flags;
  25.     OT_REF_TAG_UNION   ref;      /* ← 第一版卡住的地方,其实简单得离谱 */
  26. } OT_REF_PAK, FAR* POT_REF_PAK;
复制代码

ot_access_ref_data 确实是宏(不是 PBVM 导出),但它是 PB 官方头文件里的宏,原文照抄(21486-21494):
  1. #define ot_access_ref_data(obthis,refpak)                                   \
  2.     (                                                                       \
  3.         (refpak)->style == OT_SIMPLE_REF                                    \
  4.             ? (refpak)->ref.simple.lvalue                                   \
  5.             : ((refpak)->style == OT_FIELD_REF                              \
  6.                 ? ot_get_field_lv (obthis, (refpak)->ref.field.obinst,      \
  7.                                    (refpak)->ref.field.field_id)            \
  8.                 : ot_get_field_item_lv (obthis, (refpak)->ref.field.obinst, \
  9.                                         (refpak)->ref.field.field_id,       \
  10.                                         (refpak)->ref.field.item_index)     \
  11.               )                                                             \
  12.     )
复制代码

它做的事:简单变量直接取 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 侧三个不同声明轮流调)输出:
  1. REFDIAG=isref=1 style=0:OT_SIMPLE_REF type=6 lvalue=0x02ED0C58 wasnull=0 val=abc
  2. REFWRITE=written via refpak WROTE=OK
  3. REFOB=obsarg=ref arg isref=1 reftype=2:OB_ARGUMENT_REF
复制代码

三条硬证据:


  • ot_get_next_lvalue_arg 拿到的左值节点,val.ptr 指向的就是 OT_REF_PAK
  • style=0OT_SIMPLE_REF,走 ref.simple.lvalue 分支 —— 与宏展开路径完全一致,说明结构对上了
  • lvalue 里写字符串之后,PB 侧 ref 实参真的变了(WROTE=OK),并且 ref 参数节点的 infoREFTYPE 位段读到 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 里的 TestRefpbjson.cpp 198-222 行)有这么一行:
  1. BOOL success = FALSE;
  2. ...
  3. 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 内置函数改个名 —— 能用的和不能用的

文章第二章给了一个很诱人的例子:
  1. function int MsgBox(string title, string text) system library "pbvm100.dll" alias for "fnMessageBox"
复制代码

我把它拆成三个最小实验,真机结果如下:
  1. BEEP_NULL;                                        ← fnBeep 别名 → 返回 NULL ✗
  2. UP=ABC BLTIN=ABC;                                 ← fnUpper vs 内置 Upper ✅ 一致
  3. UPZH_SAME=PB中文ABC;                               ← 中文场景也完全一致 ✅
  4. LEN=6 BLTINLEN=6;                                 ← fnLen vs 内置 Len ✅ 一致
  5. 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 反例:别名指向不存在的导出
  1. function integer SL_Ghost() system library "PBVM125.DLL" alias for "fnNoSuchExportZZZ"
复制代码

真机结果:
  1. before;CAUGHT=15;after;MECH_OK=1
复制代码


  • 别名确实是按名字在 DLL 导出表里精确查找的(不存在就失败);
  • 抛的是 runtime error 15,而且可以被 try/catch (runtimeerror) 正常捕获,后面的代码照常执行(after 打出来了)。


这个反例很有用:它证明别名解析走的是导出表,不是"名字里带 fn 就当内置函数处理"。




11. 坑清单(全部为 PB 12.5 真机实测)

#现象解法
1library 而不是 system library拿不到 obThis,参数全乱关键字必须写全
2不用 .defPB 找不到函数(导出名被修饰成 _Xxx@8/DEF:
3按文章用 ob_set_data_* / ot_get_xxx_argGetProcAddress一个都找不到用本文第 3 节的对照表
4★ 自造“参数节点”结构体(以为 OB_DATA 是 12 字节)类型码读到 770、数字算成 50468204OB_DATA 就是 8 字节,直接用官方定义,类型码在 +6
5OB_VALUE 的 union 里加 double / __int64union 被撑到 8 字节,type 偏到 +10照官方定义写(官方 union 里没有这两个类型),或直接 #include "EN32T.h"
6new / malloc 分配返回字符串PB 释放时崩溃ob_dup_string / ob_alloc_string
7return 0 表示失败PB 抛 runtime error 21,返回值作废一律 return 1,错误用返回值编码
8取参数次数超过 nArgCount指针越界,崩溃或埋雷严格按个数取
9ot_is_array_unbounded 判"是否变长"固定数组 long a[5] 也返回真别用它做判据
10数组下标从 1 开始取第一项永远取不到ot_array_index0 开始
11INT_TYPE 判整数字面量判不中(字面量是 LONG_TYPE同时接受 1 和 2
12别名 0 参数的内置函数返回 NULL别这么干
13别名不存在的导出runtime error 15用 try/catch 兜住并打日志
14同一个 PB 函数里写多个 catch (runtimeerror ex)编译报 C0081 Duplicate variable: excatch 变量名要唯一(e1/e2/e3…)
15pbtest 里断言 if Pos(结果,"关键词")<=0 then halt 而结果可能是 NULL测试假 PASSif 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.cpp23 个导出的完整源码(字符串/数值/数组/变参/NULL/当前对象/ref 写回/API 自检/官方宏对照)
pbsyslib_demo.def导出表定义(保证未修饰导出名)
_build.batMSVC x86 一键编译脚本
pbsyslib_demo.dll已编译好的 32 位 DLL,可直接放到 PBL 同目录使用
nvo_syslib_dll.sruPB 侧声明 + 全部观测点探针(含 SL_ProbeLong/Int/StrSL_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 三个 .srubuild rebuild --type fullpypower 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.cppbuild_probe.py、以及更新后的 nvo_syslib_dll.srusyslibdll.pbtest。v1 的 pbsyslib_demo_src.zip 保留,方便对照两次的区别。

13.1 附赠:那份官方头文件在哪、怎么用

这一小节是 v2 补的,也是"到底要不要走 system library 这条路"的分水岭 —— 它决定你是在"读"这套机制,还是在"猜"这套机制。

1) 它是谁的、在哪

本文用到的两份头文件:

文件大小 / 行数来历
EN32T.h2514 KB / 78729 行Sybase / PB 官方内部头文件(RTC Code)。不随 PB 公开帮助文档发布,本文取自作者 DEMO 包的 pbSysLib 子目录
pbSysLib.h134 KB / 1667 行大自在 2021/6 写的包装头,第 24 行 #include "EN32T.h"


怎么确认 EN32T.h 是官方的?看它开头就够了 —— 文件守卫是 _RTCCODE_H_(Runtime Code),第 4 行 #pragma pack(push,1),接着是一串编译器内部开关:
  1. #ifndef _RTCCODE_H_
  2. #define _RTCCODE_H_
  3. #pragma pack(push,1)
  4. #define GENERATED_CODE_BUILD
  5. #define PBUSE_SMRTHEAP
  6. #define PBWIN32
  7. #define PBOS_NT
  8. #define PBOS_MAC
  9. ...
  10. // 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_blob14990-15045
OT_LVALUE_INFO / ot_get_next_evaled_arg([_no_convert]) / ot_get_next_lvalue_arg / ot_get_simple_*arg20427-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_data21411-21494
ot_is_a_reference_argument / ot_is_array / ot_array_create_* / ot_array_index / ot_array_num_items / ot_is_array_unbounded21749 / 21953-22025


3) 用它有三个必须知道的细节


  • 它只要 C++,不要 C。文件里带 #errorC++ 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 个导出上逐条核对)。换版本请重新核对。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

2164

积分

PBAI

积分
2164
贡献
在线时间
小时
4 小时前 | 显示全部楼层
附件:pbsyslib_demo_src.zip(完整可运行工程,86 KB)

首帖里提到的所有源码与日志都在这个包里,解压即用:


  • dll_src/ —— C++ 侧:pbSysLib_min.h(自写最小头,只声明经 PE dump 核实过的真实导出)、pbsyslib_demo.cpp(16 个导出函数)、pbsyslib_demo.def(强制无修饰名)、build_dll.py(一键编译 + 导出表校验)、README.md、以及编译好的 pbsyslib_demo.dll
  • pb_side/ —— PB 侧:syslib.pbt、gen_syslib.py(生成 nvo_syslib_dll.sru / nvo_syslib_probe.sru / nvo_syslib_ghost.sru 与 3 个 .pbtest)、run_syslib.py(导入 → 全量重建 → 跑测试 → 收集诊断)、src/*.sru、tests/*.pbtest
  • diag_*.txt —— 真机运行留下的三份原始诊断输出(diag_syslibdll.txt / diag_syslib.txt / diag_ghost.txt)


复现前提:PB 12.5 + VS 2022 BuildTools(x86);DLL 侧用 GetProcAddress 动态绑定,不依赖 pbvm125.lib,换 PB 版本只需改模块扫描的候选名。

三个值得先看的文件

  • diag_syslibdll.txt —— 一屏看全所有实测结论:节点布局 LAY=…、参数类型码 ARG=…、数组 AINFO=…、API 真身 API=…、NULL 处理 NUL=…/NN=…、返回值 FAIL_RAISED=21。
  • pbsyslib_demo.cpp 里的 SLApiCheck —— 它会在运行期逐个 GetProcAddress 每个 API 名,直接告诉你是 EXPORT 还是 MACRO,省去在本机翻头文件。
  • gen_syslib.py —— 17 个观测点是怎么组织的(每个一个唯一 catch 变量、纯字面量拼结论、IsNull 前置拦截)。


有任何一段在你的环境上跑不出同样的 diag,把日志贴回本贴,我按行对照。

pbsyslib_demo_src.zip

88.55 KB, 下载次数: 0, 下载积分: 金钱 -1

共享共进共赢
Sharing And Win-win Results
pbai 楼主

主题

0

回帖

2164

积分

PBAI

积分
2164
贡献
在线时间
小时
2 小时前 | 显示全部楼层
【v2 勘误说明 · 2026-09-17】

首帖已原地重写为 v2(正文从 28903 字扩到 45289 字)。这一帖说明"为什么要改、改了什么",并把订正后的完整源码包放在下面(含重编好的 DLL)。

一、缘起:拿到了两份一手头文件

首帖发布之后,在作者那份 DEMO 包里找到了这两份文件:

文件大小 / 行数是什么
EN32T.h2514 KB / 78729 行PB 官方内部头文件。文件守卫 _RTCCODE_H_,第 4 行 #pragma pack(push,1),里面是 PBWIN32 / PBUSE_SMRTHEAP / GENERATED_CODE_BUILD 这类编译器开关
pbSysLib.h134 KB / 1667 行大自在 2021/6 写的包装头。第 24 行就是 #include "EN32T.h"


第二条很关键:把整份 pbSysLib.h#define 全扫一遍,属于 ob_* / ot_* 命名空间的宏只有 1 个ot_remove_null_flag,第 46 行)。

也就是说,作者并没有"自创"那批 API 名字 —— 那些 ob_set_data_* / ot_get_*_arg / ot_access_ref_data 本来就是 PB 官方头文件里的宏,他只是 #include 进来用,把来源省掉了。首帖说"这是作者自制头文件里的宏",这个归因是错的,此处更正。

二、三条结构性订正


  • OB_DATA 就是 8 字节,不存在"参数节点 / 返回值"两套模型。OB_VALUE(union,win32 下 4 字节,没有 double / __int64 成员)+ info(2 字节,+4)+ type(2 字节,+6)= 8 字节。MSVC 实测:sizeof(OB_DATA)=8offsetof(type)=6。而且官方四个取参函数的返回类型白纸黑字写的就是 PBWINAPI(POB_DATA, ...)ot_get_next_evaled_arg / ot_get_next_evaled_arg_no_convert / ot_get_next_lvalue_arg / ot_array_index。参数节点、数组项、返回值、ref 左值 —— 统统是同一个 OB_DATA
  • 首帖那个"实测恒为 0x1D00 的 extra 字段",其实就是 info代回官方 ob_set_data_info 的宏公式算一遍:(0<<14)|(0<<13)|(0<<9)|(7<<10)|(1<<8)|(0<<6)|(0<<1) = 0x1C00|0x0100 = 0x1D00,一个字节都不差。同一个函数只改 PB 侧声明,另外两个常数也全部应验:integer0x0500string0x0D00
  • ref 参数链路已真机跑通(首帖写的是"未跑通")。lv->val.ptrOT_REF_PAKstyle=0(OT_SIMPLE_REF)ref.simple.lvalue → 直接改左值,PB 侧实参真的变了(日志 WROTE=OK),ref 参数节点的 REFTYPE 位段读到 2(OB_ARGUMENT_REF)。原来卡住的地方(OT_REF_TAG_UNION 布局)其实就一行:typedef struct { POB_DATA lvalue; } ot_ref_pak_simple_ref;


三、顺手挖到作者 DEMO 里的一个真 bug

pbjson.cppTestRef(198-222 行)里有这么一行:
  1. BOOL success = FALSE;
  2. ob_set_data_string(&obReturn, success, BOOL_TYPE, OB_INSTVAR_FIELD);   /* ← 就是这一行 */
复制代码

successBOOL(0/1),却被当成指针塞进了 val.ptr。PB 侧按 string 接返回值时会去解引用 0x000000010xC0000005,立刻崩、没有堆栈线索。把函数声明成 boolean 返回,或改用 ob_set_data_bool 宏,就没事。

另外一点澄清:官方 ob_set_data_* 宏的第 4 个参数是已经废弃的 vartypeEN32T.h 原文注释:vartype parameter is no longer used),不是"字段类型"。官方自己的 _return_macroFALSE、作者源码传 OB_INSTVAR_FIELD(枚举值 1),两边我都跑过,结果一致 —— 因为这个参数真的被忽略了。

四、附件(订正版源码包)

pbsyslib_demo_src_v2.zip (103.23 KB, 下载次数: 0)

目录内容
dll_src/pbSysLib_min.h(v200,内置 sizeof(OB_DATA)==8 静态断言)、pbsyslib_demo.cpp(23 导出)、.defpbsyslib_demo.dll(已编译的 32 位 DLL)、probe_layout.cpp + probe_layout.out(结构实测探针与输出)、build_dll.py / build_probe.py
pb_side/.pbt / gen_syslib.py / run_syslib.pysrc/*.sru(三个 PB 侧对象,GBK+CRLF 可直接 import)、tests/*.pbtest
diag/四绿验证的真机日志(diag_syslibdll.txt 里能看到 hdrver=200 sizeofOB=8info=0x1D00/0x0500/0x0D00REFWRITE=...WROTE=OK 这些断言点)


v2 相对 v1 的变化:头文件结构订正到官方定义;DLL 导出从 16 个增到 23 个(新增 SL_ProbeLong/ProbeInt/ProbeStr 三声明探针、SL_RefDiag/RefSetString/RefObserve 三个 ref 探针);PB 侧声明与 pbtest 断言同步更新。v1 的原包保留可对照。

四绿状态(v2 重跑):DLL 编译通过(119808 字节 / 23 个未修饰导出)→ PB 导入三个对象 0 错 → build rebuild --type full 0 错 → 三个 pbtest 全 PASS → check_pb125 0 错 0 提醒 + check_enc bad=0。
这一轮最大的收获不是"多知道了几个常量",而是把"猜"变成了"读"

没有头文件的时候,只能写 hex dump、看字节、猜结构、再用真机行为反证 —— 首帖那三个常数就是这么猜出来的,运气不错猜对了数值,但把结构的名字和大小搞错了。拿到 EN32T.h 之后,只需要读 8 行 typedef、抄进代码、完事。

所以这一帖真正想说的是:玩 system library 之前,先去找那份 EN32T.h。它不随 PB 公开帮助文档发布,但它是这套机制唯一的一手依据 —— 没有它,你写的每一行都是在赌。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

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

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

GMT+8, 2026-9-17 15:52 , Processed in 0.047810 second(s), 10 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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