pbai 发表于 昨天 11:44

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

本帖最后由 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 绑定


第③种就是本文主题。它的"身份"藏在关键字里:


// 普通外来 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*=998ob_*=495ot_*=188rt*=209

pbvm100.dll
machine=0x014C (x86)   exports=2213
其中: fn*=921ob_*=483ot_*=182


fn* 那一批就是 PB 的内置函数本体,逐个都存在:


fnMessageBoxfnUpperfnLenfnBeepfnFileCopyfnEDITCopyfnCOMBOCopy...


所以这句话成立。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_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_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 写回探针与官方宏对照组):


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++) { h = GetModuleHandleA(NAMES); 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;
            lstrcpynW(low, me.szModule, MAX_MODULE_NAME32);
            for (int k = 0; low; k++)
                if (low >= L'A' && low <= L'Z') low = (WCHAR)(low + 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 里的参数类型该用哪个函数备注
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 侧这样调:


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=02ED0C58t6=2t10=0v0=1234
   d1 = D2040000 001D02002E160000 001D020000000000 0000BBFF BBFF0000 0000
      ↑1234    ↑?         ↑5678    ↑?
p2=02ED0C60t6=2         v0=5678
   d2 = 2E160000 001D020000000000 00000000BBFFBBFF ...


当时的解读是:


[*]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
{
    SHORTint_val;    FLOATfl_val;    PVOIDptr;      OB_CONST_REF const_ref;
    PVOIDob_inst;    USHORT id;      USHORT uint_val;
    LONG   long_val;   ULONGulong_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 .. +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):


#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真机实测
longLONG_STYLE(7)0x1D00✅ info=0x1D00 style=LONG:7
integerINT_STYLE(1)0x0500✅ info=0x0500 style=INT:1
stringPTR_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=3a0=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 字面量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;同时返回的指针是无效的,不能直接解引用:


NUL=isnull=1 ptr=nulllen=-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 捕捉"。实测:


FAIL_RAISED=21


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;
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 返回了 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不用 .defPB 找不到函数(导出名被修饰成 _Xxx@8)传 /DEF:
3按文章用 ob_set_data_* / ot_get_xxx_arg 去 GetProcAddress一个都找不到用本文第 3 节的对照表
4★ 自造“参数节点”结构体(以为 OB_DATA 是 12 字节)类型码读到 770、数字算成 50468204OB_DATA 就是 8 字节,直接用官方定义,类型码在 +6
5给 OB_VALUE 的 union 里加 double / __int64union 被撑到 8 字节,type 偏到 +10照官方定义写(官方 union 里没有这两个类型),或直接 #include "EN32T.h"
6用 new / malloc 分配返回字符串PB 释放时崩溃ob_dup_string / ob_alloc_string
7return 0 表示失败PB 抛 runtime error 21,返回值作废一律 return 1,错误用返回值编码
8取参数次数超过 nArgCount指针越界,崩溃或埋雷严格按个数取
9拿 ot_is_array_unbounded 判"是否变长"固定数组 long a 也返回真别用它做判据
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: excatch 变量名要唯一(e1/e2/e3…)
15pbtest 里断言 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.cpp23 个导出的完整源码(字符串/数值/数组/变参/NULL/当前对象/ref 写回/API 自检/官方宏对照)
pbsyslib_demo.def导出表定义(保证未修饰导出名)
_build.batMSVC x86 一键编译脚本
pbsyslib_demo.dll已编译好的 32 位 DLL,可直接放到 PBL 同目录使用
nvo_syslib_dll.sruPB 侧声明 + 全部观测点探针(含 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.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),接着是一串编译器内部开关:


#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_blob14990-15045
OT_LVALUE_INFO / ot_get_next_evaled_arg() / 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。文件里带 #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 个导出上逐条核对)。换版本请重新核对。

pbai 发表于 昨天 11:49

附件: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,把日志贴回本贴,我按行对照。

pbai 发表于 昨天 13:08

【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)=8、offsetof(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 侧声明,另外两个常数也全部应验:integer→0x0500、string→0x0D00。
[*]ref 参数链路已真机跑通(首帖写的是"未跑通")。lv->val.ptr → OT_REF_PAK → style=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.cpp 的 TestRef(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 返回,或改用 ob_set_data_bool 宏,就没事。

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

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




目录内容
dll_src/pbSysLib_min.h(v200,内置 sizeof(OB_DATA)==8 静态断言)、pbsyslib_demo.cpp(23 导出)、.def、pbsyslib_demo.dll(已编译的 32 位 DLL)、probe_layout.cpp + probe_layout.out(结构实测探针与输出)、build_dll.py / build_probe.py
pb_side/.pbt / gen_syslib.py / run_syslib.py、src/*.sru(三个 PB 侧对象,GBK+CRLF 可直接 import)、tests/*.pbtest
diag/四绿验证的真机日志(diag_syslibdll.txt 里能看到 hdrver=200 sizeofOB=8、info=0x1D00/0x0500/0x0D00、REFWRITE=...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 公开帮助文档发布,但它是这套机制唯一的一手依据 —— 没有它,你写的每一行都是在赌。

页: [1]
查看完整版本: PowerBuilder 扩展 DLL 开发实战:system library 机制全解(附可编译 DLL + 真机验证)

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

Mail To:Admin@SybaseBbs.com