马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-10-12 08:53 编辑
PowerBuilder 外部函数调用实战:Declare / LIBRARY 声明、DLL 调用与它的真实边界(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(本文不涉及 PBIDEA 组件)
2. 支持数据库:本文不涉及数据库
3. 操作系统与环境要求:Windows 7+;PB 程序需与所调 DLL 同为 32 位(PB 10/12.5 均为 32 位程序);被调的 kernel32.dll 由系统提供,无需额外安装;把自写的第三方 DLL 放进 EXE 同目录或 PATH 即可
4. 难度系数:★★★★☆
5. 其它阅读说明:本文所有读数来自 PB10 与 PB12.5 双轨实机运行。正文里凡是「实测失败」的结论,都是在本机把代码原样跑出来看��结果,不是文档转述。外部函数的错误几乎全部在运行期才暴露,这正是本文要讲的事。
一、先说结论:外部函数能做什么、不能做什么
PowerBuilder 本身没有 DllCall,也没有 Python 那种 ctypes。它调 DLL 只有一条路:在对象里声明一个外部函数,然后当普通函数调用。
先把边界划清楚,这决定了后面所有写法:
| 能力 | 实机结果 | 说明 | | 声明无参 / 全数值参的函数 | ✅ 全部跑通 | 最稳的一类,GetTickCount、GetCurrentProcessId、IsDebuggerPresent、Beep 全通 | | 用 ALIAS 改名 / 加 ;ansi | ✅ 全部跑通 | PB 侧名字与 DLL 侧名字可以完全不同 | | 用 SUBROUTINE 声明外部子程序 | ❌ 运行期报错 | 本机实测 SleepX 调不动,见第 8 节 | | 用 ref string 当出参缓冲区 | ❌ 运行期报错 | 最常见的误用,见第 6 节 | | 用 ref blob 当出参缓冲区 | ❌ 进程崩溃 | 0xC0000005,见第 6 节 | | 把返回 long 的函数声明成 string | ❌ 运行期报错 | 不是静默乱码,见第 7 节 | | 编译期发现函数名 / DLL 名写错 | ❌ 发现不了 | import 0 错、rebuild 0 错,等到运行时才炸 |
★最后一行是全文最该记住的一条:PowerBuilder 完全不校验外部函数的签名。本文实测了三条「一定失败」的声明,全部编译通过:
- declare function long NoSuchFnXYZ(long a) LIBRARY "kernel32.dll"
- declare function long GetTickCount() LIBRARY "nosuchdll_zzz.dll"
- declare function string GetTickCountStr() LIBRARY "kernel32.dll" // 签名是错的
复制代码
三条的 import 与 rebuild 都是 0 错误。函数名不存在、DLL 文件不存在、返回类型声明错了——编译器一概不管。这跟内置函数「拼错了立刻报 C0051 Unknown function name」形成鲜明对比。
二、外部函数写在哪里:.sru 里的正确位置
这一节不是抄文档,是本轮踩坑踩出来的。
PBIDE 里你在脚本视图的 Declare 下拉区写 declare function ...。但导出的 .sru 文本里不是这么组织的。拿 PBIDEA 组件源码里的 uo_led 对象可以作证,它声明了两个 user32 函数,导出文本长这样:
- type prototypes
- FUNCTION ulong SetTimer(ulong hWnd, ulong nIDEvent, ulong uElapse, ulong lpTimerFunc) LIBRARY "user32.dll"
- FUNCTION ulong KillTimer(ulong hwnd, ulong nIDEvent) LIBRARY "user32.dll"
- end prototypes
复制代码
也就是说:外部函数写在 type prototypes 段里,不带 declare 关键字。
本轮把三个位置都试了一遍,判据是 import 的报错:
| 写法 | 结果 | | 顶层(global xxx 之后、type prototypes 之前)加 declare function ... | ❌ C0114: Error scanning object source entry | | forward prototypes 段内写 declare function ... | ❌ C0031: Syntax error | | 旧式 declare function long X FOR Library "..." | ❌ C0114 | | type prototypes 段内写 FUNCTION ... LIBRARY "..." | ✅ 导入成功、双轨编译 0 错 |
★还有一个连带坑:type prototypes 段的声明行必须带 LIBRARY,一行都不能漏。本轮有一条声明忘了写 LIBRARY,import 立刻报:
- (0005): Error C0183: Must specify either LIBRARY or RPCFUNC.
复制代码
三、声明语法全貌
PB 官方帮助给的语法是这个形状:
- { access } FUNCTION returndatatype name ( { { REF } datatype1 arg1 , ..., { REF } datatypen argn } )
- LIBRARY "libname" ALIAS FOR "extname {;ansi}"
复制代码
拆开看,每个部分都对应一个实际用途:
| 组成部分 | 写法 | 实机验证结论 | | FUNCTION / SUBROUTINE | 无返回值用 SUBROUTINE | ⚠️ SUBR 形态本机运行期报错,第 8 节详述 | | 返回类型 | long / integer / string | ⚠️ 必须与真实签名一致,第 7 节详述 | | 函数名 | 不能带 @ 等特殊字符 | 带特殊字符须用 ALIAS 绕开 | | 参数 | { REF } datatype name | ⚠️ 出参缓冲区的 ref 极难写对,第 6 节详述 | | LIBRARY "libname" | 写库名或全路径 | 漏写立即报 C0183 | | ALIAS FOR "extname" | DLL 里的真实名 | ✅ 实测可用,PB 侧可任意改名 | | ;ansi | 只能跟在 ALIAS 里 | ✅ 实测可用 |
关于 ;ansi,官方原文有一句容易看漏的强调:即使你用的是默认名字(不写 ALIAS),只要这个函数收/传 ANSI 字符串,也必须用 ALIAS 把它限定住。所以正确的形式永远是 ALIAS FOR "函数名;ansi" 这种把两件事合在一句里的写法。本轮实测这条路径正常(见第五节)。
四、最稳的一类:纯数值参数的函数
先给一段完整可运行的代码。演示步骤:新建一个 nvo_ext_basic 非可视用户对象(自定义 NVO,注意不是 autoinstantiate),在 Script 视图的 Declare 下拉区粘贴外部函数声明,在 forward prototypes 区声明方法,然后逐个实现下方代码。
- // ===== 外部函数声明:写在 Script 视图的 Declare 下拉区 =====
- // 无参数、返回 32 位整数
- declare function long GetTickCount() LIBRARY "kernel32.dll"
- // 无参数、返回 16 位整数
- declare function integer IsDebuggerPresent() LIBRARY "kernel32.dll"
- // 两个数值参数,参数顺序与 C 原型一致
- declare function long Beep(long f, long d) LIBRARY "kernel32.dll"
复制代码- // ===== nvo_ext_basic 的 of_tick 方法 =====
- // 用途:验证 GetTickCount 确实在走系统计时器,而不是返回一个常量
- long l1
- long l2
- // 取第一个时间戳
- l1 = gettickcount()
- // 停顿(PB 的 sleep 单位是「秒」,详见第 9 节;这里 40 表示 40 秒)
- sleep(40)
- // 取第二个时间戳
- l2 = gettickcount()
- // 两个值之差就是经过的毫秒数
- return 'tick1=' + string(l1) + ' tick2=' + string(l2) + ' diff=' + string(l2 - l1)
复制代码- // ===== nvo_ext_basic 的 of_beep 方法 =====
- // 用途:验证带数值参数的函数能正常传参
- // 示例输入:频率传 0、时长传 0(不发声也不阻塞),只测返回值
- long lr
- lr = beep(0, 0)
- // 返回非 0 表示调用成功
- return 'beep=' + string(lr)
复制代码
在窗口上放一个按钮 cb_test,在它的 clicked 事件里贴入下面这段并运行:
- // ===== w_demo 的 cb_test.clicked 事件 =====
- // 创建被测对象(非 autoinstantiate,必须自己 create)
- nvo_ext_basic lx
- lx = create nvo_ext_basic
- // 跑两个方法,把结果拼成一段可读文本
- string ls_r
- ls_r = lx.of_tick() + '~n' + lx.of_beep()
- // 释放对象
- destroy lx
- // 结尾用 MessageBox 输出结果
- MessageBox('外部函数调用结果', ls_r)
复制代码
本机实测读数:
| 用例 | 实测输出 | | GetTickCount 差值 | diff=40000 —— 恰好等于 sleep(40) 的 40 秒,说明它确实在计数 | | IsDebuggerPresent | dbg=0 —— 正常运行环境下返回 0 | | Beep(0, 0) | beep=1 —— 返回非 0,调用成功 | | GetCurrentProcessId(同进程连调两次) | pid1=14244 pid2=14244 same=true —— 同进程内一致 |
这一类没有任何坑,可以放心用。
五、ALIAS 与 ;ansi:让 PB 侧和 DLL 侧的名字解耦
ALIAS 的价值有两个:PB 侧起个更顺手的名字,以及指定 ;ansi。
- // ===== nvo_ext_alias 的外部函数声明 =====
- // PB 侧叫 PbTickA,DLL 里的真名是 GetTickCount,并限定为 ANSI 版
- declare function long PbTickA() alias for "GetTickCount;ansi" LIBRARY "kernel32.dll"
- // 不带 ;ansi 的写法,同样能跑
- declare function long PbTickB() alias for "GetTickCount" LIBRARY "kernel32.dll"
- // 彻底改个名:PB 叫 PbProc,DLL 里其实叫 GetCurrentProcessId
- declare function long PbProc() alias for "GetCurrentProcessId" LIBRARY "kernel32.dll"
复制代码- // ===== nvo_ext_alias 的 of_renamed 方法 =====
- // 用途:证明改名调用与直接调用拿到的是同一个值
- // 第一次调用(PB 名字 PbProc,DLL 名字 GetCurrentProcessId)
- long l1
- l1 = pbproc()
- // 第二次调用,用来验证两次结果一致
- long l2
- l2 = pbproc()
- // 拼成可读文本返回
- return 'p1=' + string(l1) + ' p2=' + string(l2) + ' same=' + string(l1 = l2)
复制代码
本机实测读数:
| 用例 | 实测输出 | 结论 | | ALIAS ... ;ansi 路径 | ansi_diff=40000 | 带 ;ansi 能正常调用 | | 不带 ;ansi 的 ALIAS 路径 | noansi_diff=40015 | 同样能调,两条路径等价 | | 完全改名(PbProc → GetCurrentProcessId) | same=true | 改名不影响调用 |
★注意最后一行同进程内的对照:ALIAS 改名后调用的是同一个 Win32 函数,返回值与直调完全一致。
六、最容易踩的坑:出参缓冲区怎么传
Windows API 里有一大批「往缓冲区里写」的函数,最典型的形态是:
UINT GetSystemDirectoryW(LPWSTR lpBuffer, UINT nBufferLength);
第一个参数是出缓冲区(ref),第二个是缓冲区容量。你会理所当然地在 PB 里这么写:
- // ===== 错误写法,本机实测运行期失败 =====
- // 声明时把第一个参数写成 ref string
- declare function long GetSystemDirectory(ref string a, long n) LIBRARY "kernel32.dll"
- // 调用:看起来天经地义
- string ls_buf
- ls_buf = ''
- long ll_n
- ll_n = getsystemdirectory(ls_buf, 260)
复制代码
本机实测:这样写运行期直接报错,PB 抛出:
- PB runtime error: Error calling external function GetSystemDirectory at line 6
- in function of_sysdir of object nvo_ext_arg.
复制代码
本轮一共试了四条路,全军覆没:
| 缓冲区写法 | 实机结果 | | ref string,先给 '' | ❌ 运行期 Error calling external function | | ref blob,空 blob | ❌ 进程崩溃,退出码 3221225477(0xC0000005 访问违例) | | blob 按值传(声明不带 ref) | ❌ 进程崩溃(用例在写诊断文件前就退出) | | 预分配 blob 后按 ref 传 | ⚠️ 未实测——该用例的源码在本机 import 期就静默失败(无输出、对象进不了 PBL),无法进入运行环节 |
★这里有一处需要说明:本轮没有找到在本机可用的缓冲区写法。这不代表 PB 调不通 Win32——论坛与官方样例(u_external_function_win32)里这类调用是有的。本文把已跑通编译的几条候选路径逐一实测并把失败现象固定下来,是因为这个「写错了不报编译错、只在运行时炸,且其中两条直接崩进程」的坑非常隐蔽;至于你手上某个具体 API 该怎么声明,本机环境没能给出可复现的正解,故不在此处给出结论。
顺带一个连带发现:ref blob 与按值 blob 崩进程时诊断文件一个字都没写出来——进程在 PB 还没来得及执行 FileWrite 之前就死了。如果你在带 TRY-CATCH 的代码里调它,别指望能兜住。
- // ===== nvo_ext_byval 的 of_t 方法:把失败现象固定下来 =====
- // 该 API 的第一个参数声明成 ref string —— 本机实测运行期报错
- string ls_buf
- ls_buf = ''
- // 默认值:万一没进 catch 分支,至少能看出异常没被捕获
- string ls_r
- ls_r = 'not_tried'
- long ll_n
- // 用 TRY-CATCH 兜住运行期错误,避免整个进程挂掉
- try
- ll_n = getsystemdirectory(ls_buf, 260)
- ls_r = 'ok n=' + string(ll_n)
- catch(runtimeerror e)
- ls_r = 'RTERR'
- end try
- // 返回实际结果
- return ls_r
复制代码
本机实测读数:RTERR —— 确认被 TRY-CATCH 捕获,异常类型是 runtimeerror。
★顺带记一个写 .sru 源码时的语法点:catch 必须带异常变量名,正确写法是 catch(runtimeerror e)。写成 catch(runtimeerror) 会在 import 阶段报 4 条 C0031: Syntax error。
七、返回类型写错:编译器不管,运行期报错
把一个真实返回 DWORD 的函数声明成 string,很多人以为会拿到一串乱码。实测不是。
- // ===== nvo_ext_str 的外部函数声明:签名是错的 =====
- // GetTickCount 真实返回 DWORD,这里声明成 string
- declare function string GetTickCountStr() LIBRARY "kernel32.dll"
复制代码- // ===== nvo_ext_str 的 of_retstr 方法 =====
- // 目的:看「声明成 string」会得到乱码还是报错
- string ls_r
- ls_r = 'not_tried'
- try
- string ls
- ls = ''
- // 调用这条声明错误的外部函数
- ls = gettickcountstr()
- ls_r = 'len=' + string(len(ls)) + ' val=「' + ls + '」'
- catch(runtimeerror e)
- ls_r = 'RTERR'
- end try
- return ls_r
复制代码
本机实测读数:RTERR。既没有乱码,也没有空串,是直接抛运行期异常。 这一点比「给一段乱码」要好——至少你知道出事了。
八、SUBR 形态:本机调不通
SUBROUTINE 是 PB 语法里声明「无返回值外部函数」的关键字。理论上 Win32 的 Sleep(DWORD) 正该用它。
- // ===== nvo_ext_subr 的外部函数声明 =====
- // SUBROUTINE 表示无返回值
- declare subroutine SleepX(long ms) LIBRARY "kernel32.dll"
复制代码- // ===== nvo_ext_subr 的 of_t 方法 =====
- // 目的:量一下这个外部子程序到底睡没睡
- long l1
- long l2
- // 睡之前记时间
- l1 = gettickcount()
- // 调用外部子程序,参数 120 毫秒
- sleepx(120)
- // 睡之后再记时间
- l2 = gettickcount()
- // 差值就是实际睡眠毫秒数
- return 'subr_slept_ms=' + string(l2 - l1)
复制代码
本机实测结果:运行期报错
- PB runtime error: Error calling external function SleepX at line 6
- in function of_t of object t_n_subr.
复制代码
★本轮只验证了这一条 SUBR 声明,不能据此断定「PB 调不通所有 SUBR」。可以确定的是:在本机环境下,以 SUBROUTINE 形态声明的 Win32 函数在运行期被 PB 拒绝调用。
九、integer 截断:一个不会报错、只默默丢数的坑
这一条是本文最阴的坑,因为它不报错、不崩溃,返回值就是个错的数。
Win32 的 DWORD 是 32 位无符号,PB 的 integer 是 16 位有符号。声明成 integer 接,值会被截断:
- // ===== 同一个函数用两种返回类型各声明一次 =====
- // 正确:GetTickCount 返回 32 位,PB 侧要用 long 接
- declare function long GetTickCount() LIBRARY "kernel32.dll"
- // 错误:用 integer(16 位)去接同一个 32 位返回值
- declare function integer GetTickCountI() alias for "GetTickCount" LIBRARY "kernel32.dll"
复制代码- // ===== of_t 方法:同一次调用,两种接法 =====
- // 正确接法
- long lr
- lr = gettickcount()
- // 错误接法:同一个函数,用 integer 接
- integer li
- li = gettickcounti()
- // 两个值都返回,拼成一段文本
- return 'long=' + string(lr) + ' int=' + string(li) + ' eq=' + string(lr = li)
复制代码
本机实测读数:
- long=169230625 int=16673 eq=false
复制代码
169230625 是开机以来的毫秒数,被截成 16673。没有运行期错误,没有任何警告,程序照常运行,只是数是错的。
★补一条对照:不是所有 API 都会踩这个坑。用 GetCurrentProcessId 做同样实验,本机 PID = 12748,小于 32767,long 与 integer 拿到的是同一个值(eq=true)。所以这个坑是「看运气」的——低位运行时不炸,机器跑久了就悄悄出错。这也是它危险的地方。
十、一个与本文主题相关的意外收获:PB 的 sleep 单位是「秒」
写验证代码时需要让时间过去一点,本轮顺手量了 PB 自带的 sleep:
| 调用 | 实测差值 | | sleep(1) | 1000 毫秒 | | sleep(0) | 0 毫秒 | | sleep(40) | 40000 毫秒 |
★PB 的 sleep(n) 参数单位是秒,不是毫秒,尽管函数名后面没有后缀。想睡 100 毫秒,sleep(1) 会让你多等 900 毫秒。这一点与「想调 Win32 的 Sleep」是两回事——那才是真正的毫秒级接口。
十一、实战示例:一个封装成业务方法的外部函数工具类
把上面的结论落到一个能直接用的对象上。演示步骤:新建 nvo_ext_demo 非可视用户对象,声明区贴下列外部函数,然后实现四个方法。
- // ===== nvo_ext_demo 的外部函数声明 =====
- // 开机以来的毫秒数(PB 侧要用 long 接,不能用 integer)
- declare function long GetTickCount() LIBRARY "kernel32.dll"
- // 求一个 blob 的字节长度:宽字符版(PB 字符串是 UTF-16LE,用它才对)
- declare function long lstrlenW(blob b) LIBRARY "kernel32.dll"
- // 窄字符版,用来做对照
- declare function long lstrlenA(blob b) LIBRARY "kernel32.dll"
复制代码- // ===== nvo_ext_demo 的 of_uptime_ms 方法 =====
- // 用途:算出程序已经跑了多少毫秒
- // 示例输入:程序启动那一刻记下的时间戳(真实项目里在 Open 事件里存)
- long ll_start
- ll_start = 100000
- // 现在的开机毫秒数
- long ll_now
- ll_now = gettickcount()
- // 两者相减
- return 'uptime_ms=' + string(ll_now - ll_start)
复制代码- // ===== nvo_ext_demo 的 of_lstrlen 方法 =====
- // 用途:演示「PB 字符串是 UTF-16LE,宽窄两版长度函数结果不同」
- // 一个纯 ASCII 字符串
- string ls
- ls = 'abc'
- // 用窄版 lstrlenA 量它的 blob 长度
- long la
- la = lstrlena(blob(ls))
- // 用宽版 lstrlenW 量同一个 blob
- long lw
- lw = lstrlenw(blob(ls))
- // PB 自己的 blob 长度作为第三方对照
- long lp
- lp = len(blob(ls))
- // 三个值都返回
- return 'A=' + string(la) + ' W=' + string(lw) + ' PB_len=' + string(lp)
复制代码
在窗口 w_demo 的按钮 cb_run.clicked 里:
- // ===== w_demo 的 cb_run.clicked 事件 =====
- // 示例输入:无(下面两个方法的输入值都已在各自方法里定义并注释)
- // 创建对象
- nvo_ext_demo lx
- lx = create nvo_ext_demo
- // 跑两个方法
- string ls_a
- ls_a = lx.of_uptime_ms()
- string ls_b
- ls_b = lx.of_lstrlen()
- // 释放
- destroy lx
- // 结尾 MessageBox 输出
- MessageBox('结果', ls_a + '~n' + ls_b)
复制代码
本机实测读数:
| 方法 | 实测输出 | | of_uptime_ms | uptime_ms=167329359 | | of_lstrlen(ASCII 'abc') | A=1 W=3 PB_len=6 | | of_lstrlen(把输入换成中文「中文」2 个字符) | cnchars=2 cnbloblen=5(这个数不唯一,见下) |
★A=1 W=3 PB_len=6 这一行值得停下来看一眼。同一个字符串 'abc':
- PB 内部存成 UTF-16LE,blob() 出来是 6 字节;
- lstrlenW 认宽字符,返回 3(正确字符数);
- lstrlenA 认 ANSI 字节,读到第一个高位 0 就停,返回 1。
也就是说,对 PB 字符串取 blob 长度,lstrlenW 才是对的,lstrlenA 会给出完全错误的值,而且不会报错。 中文这一行要拆开看:lstrlenW 给 3、lstrlenA 给 5,而「中文」只有 2 个字符 —— 两个数都不是 2。原因是 blob(中文) 只有 4 个字节,而这 4 个字节(2D 4E 87 65)里一个 0 都没有;lstrlenA / lstrlenW 的规则都是「扫到第一个 0 才停」,于是它们越过缓冲区继续往后读,读到的是相邻内存里的内容。实测:把同一段逻辑搬进两个写法不同的对象,lstrlenA 一个给 5、一个给 4,各自连跑 5 次都稳定 —— 稳定地读越界的那几个字节。所以对非 ASCII 字符串,这两个函数给的不是「长度」,而是「从这个地址起、到下一个 0 为止有多远」;'abc' 之所以看着对,是因为 UTF-16LE 下每个 ASCII 字符后面都跟着一个 0 字节,正好替我们当了终止符。这与《字符串编码全解》那篇讲的「PB 内部是 Unicode 而非 GBK」是同一个底层事实的两个侧面。
十二、坑清单
- ★编译器不校验外部函数。函数名不存在、DLL 名不存在、返回类型写错,import 与 rebuild 全部 0 错,只有运行时才炸。
- ★.sru 里外部函数写在 type prototypes 段,不带 declare 关键字。写在顶层报 C0114,写在 forward prototypes 报 C0031。
- ★type prototypes 段的外部函数声明行必须每行都带 LIBRARY,漏一行报 C0183: Must specify either LIBRARY or RPCFUNC。
- ★外部函数是对象局部的。写在 A 对象的 type prototypes 里,B 对象和测试脚本里直接调会报 C0051: Unknown function name——要通过该对象自己的方法转出去。
- ★返回 DWORD 的函数必须用 long 接,用 integer 接会静默截断。实测 169230625 → 16673,eq=false,无任何报错。
- ★这个截断坑是看运气��。本机 PID = 12748 时 long 与 integer 结果相同(eq=true),低位运行时完全不暴露。
- ★出参缓冲区(ref string / ref blob)在本机四种写法全部失败:运行期报错或直接 0xC0000005 崩进程。
- ★ref blob 崩进程时 TRY-CATCH 兜不住——它不是抛 PB 异常,是访问违例。
- ★把返回 long 的函数声明成 string 会抛运行期异常,不是给乱码也不是给空串。
- SUBROUTINE 形态的外部函数在本机运行期被拒绝调用(本轮只验证了 1 条,不代表全部 SUBR 都不行)。
- PB 字符串是 UTF-16LE,对 blob(字符串) 量长度要用 lstrlenW;lstrlenA 会返回错误值且不报错('abc':W=3 正确、A=1 错误)。
- PB 的 sleep(n) 单位是秒,sleep(1) 实测 1000 毫秒。想睡毫秒级得另想办法。
- ;ansi 只能跟在 ALIAS 里写,形式是 alias for "函数名;ansi";不带 ALIAS 直接写 ;ansi 不成立。
- catch 必须带异常变量名:catch(runtimeerror e),漏变量名在 import 阶段报 C0031。
- repeat() 不存在(C0051)。要造 N 字节的 blob 只能用循环 insertblob 逐字节追加。
- blob = space(300) 报 C0008 类型不匹配——space() 返回 string 不是 blob。
- FileWrite 的第 2 参只接受 string/blob,传 long / integer / boolean 报 C0052: Bad argument list。
- ALIAS 改名不影响调用结果,本机实测改名后与直调拿到完全相同的值。
十三、PB12.5 差异
本轮所有 8 个测试对象、22 个运行用例在 PB10 与 PB12.5 双轨各跑一遍。结论:
外部函数声明、调用、报错行为在 PB10 与 PB 12.5 之间完全一致,本轮未发现任何版本差异。
逐条对应:
| 维度 | PB10 | PB12.5 | | type prototypes 段写法 | import 0 错 | import 0 错 | | rebuild --type full | 0 错 | 0 错 | | pbl list 对象齐全 | 8/8 | 8/8 | | 22 个运行用例 | 全部与 125 轨同结论 | 16 PASS / 6 失败(失败项两轨完全相同) | | ALIAS / ;ansi 改名 | 正常 | 正常 | | integer 截断 | 存在 | 存在 | | SUBR 运行期被拒 | 存在 | 存在 | | lstrlenA 对 PB 字符串返回错值 | 存在 | 存在 |
十四、实机验证情况
测试环境
| 项 | 值 | | PB10 轨 | import 0 错;rebuild --type full 0 错;PBVM 22 用例 16 PASS,判定与 125 轨逐条一致 | | PB12.5 轨 | import 0 错;rebuild --type full 0 错;PBVM 16/22 PASS | | 测试对象 | 8 个 NVO(5 个正向 + 3 个负向),pbl list 双轨均 8/8 齐全 | | 运行用例 | 22 个,每个用例独立成一次运行 | | 正文代码逐字验证 | 另建 nvo_art_check 把正文第 4/11 节示例原样搬进去跑,5 个方法(of_tick / of_beep / of_uptime_ms / of_lstrlen / 按钮拼接段)双轨 5/5 PASS,读数与正文引用值一致(A=1 W=3 PB_len=6、diff=40000/40016、beep=1) | | 双轨诊断比对 | 22 份诊断文件 SAME=17 / DIFF=5,5 处差异全部是墙钟与 PID 环境值(diff=40000 vs 40016、两轨各自 pid1=pid2),零行为差异 | | 被调 DLL | kernel32.dll(系统自带) |
6 个未通过的用例及原因(如实标注)
| 用例 | 结果 | 原因 | | of_sysdir(ref string 缓冲区) | 运行期 Error calling external function | 第 6 节记录的失败现象,本轮未找到可用写法 | | of_blobbuf / of_blob_lstrlen | 同上 / 进程崩溃 | 同上 | | of_host / of_summary(demo 对象) | 运行期 Error calling external function | 依赖上面的缓冲区写法,一并失败 | | of_retstr(返回类型写错) | 运行期报错,TRY-CATCH 捕获成功返回 RTERR | 这正是预期行为,作为负向用例验证成功 |
未实测项(明确划出边界)
- ★出参缓冲区在 PB10 / PB12.5 上的可用写法,本轮未找到可复现的正解。第六节里凡涉及 ref string / ref blob 的结论,都是「本机实测失败」的读数,不是「PB 一定不支持」的理论判断;「预分配 blob 后按 ref 传」这一条则因源码 import 期静默失败而未进入运行环节,按未实测处理。你若在别的环境上调通了,正确的写法可能与本机不同。
- ★SUBROUTINE 形态只验证了 1 条(SleepX),不能推广到所有 SUBR 声明。
- ★未测试任何自编 C/C++ DLL。本文所有读数都来自 kernel32.dll(系统 DLL),自编 DLL 的调用约定(stdcall/cdecl)、导出名修饰(_GetTickCount@0)等问题不在本文验证范围内。
- ★未测试 64 位环境。PB10 与 PB12.5 均为 32 位程序,只能调 32 位 DLL。
- ★多线程下的外部函数调用安全性未测(Windows 32 位 API 一般线程安全,但本轮未实测)。
静态检查
- 编码检查:0 错(全部 GBK + CRLF + 无 BOM,首行 $PBExportHeader$ 齐全)。
- 帮助库核对:0 错误、3 条提醒。3 条提醒全部是 nvo_ext_alias 里经 ALIAS 改名的外部函数(pbproc / pbticka / pbtickb)——它们是我们自己起的 PB 侧名字,本来就不会在帮助库里命中,属本主题的预期现象,非缺陷。
- 本文所有代码均在 PB10 与 PB12.5 双轨编译通过;运行级结论均来自实机读数。
十五、写在最后
外部函数这件事,PB 的设计哲学是「把声明和验证都甩给程序员」。它给你一条从 PowerScript 直通 Win32 的捷径,但代价是:
- 编译器不查函数名、不查 DLL、不查签名;
- 出错的时候全在运行期,其中一种还是直接崩进程;
- 最危险的那个坑(integer 截断)甚至连运行期错误都没有。
所以我的建议是三条:
第一,声明一律用 long 接 32 位返回值。 这一条成本最低、收益最大,能挡掉最隐蔽的一类错误。
第二,把外部函数调用包在自己的 NVO 里,只对外暴露 of_ 方法。 一是解决「外部函数是对象局部」的可见性问题,二是把风险关在一个对象里,出错时好定位。
第三,运行时错误一律用 catch(runtimeerror e) 包起来,但别指望它能兜住 ref blob 那条——那个是进程崩溃,不是异常。
如果你手上的项目里已经有能跑通的缓冲区写法,非常欢迎在回帖里贴出来。本文把这四条候选路径的失败读数都固定下来了,缺的正是那个「能成的写法」,而我本机没能找到。 |