playbook/docs/tsl/syntax/21_external_calls_and_threa...

8.1 KiB

External Calls And Threads

文档类型:语法主线 是否可直接用于生成代码:仅部分 是否含已验证可执行示例:是 是否含已验证反例:是 遇到不确定时跳转到:06_functions_and_calls.md22_namespace_libpath_and_unit_runtime.md12_pitfalls.md

手册位置:第 21 篇,共 32 篇。上一篇:20_strings_and_text.md。下一篇:22_namespace_libpath_and_unit_runtime.md

这一篇吸收函数专题里和外部系统交互有关的部分:external、动态库调用、函数指针包装、线程调用。

这一篇解决什么问题

回答“普通函数怎么写已经清楚后,外部 DLL、函数指针、多线程这些系统交互能力应该去哪里查”。

必须记住的规则

  • 当前解释器接受 function Name(...): Type; stdcall|cdecl; external "dll" [name "symbol"]; 这类外部函数声明。
  • 当 TSL 函数名和 DLL 导出名一致时,name "symbol" 可以省略。
  • 在当前 Win64 解释器上,同一个 GetTickCount64 例子里,stdcallcdecl、省略调用约定都已跑通;手册示例仍默认把调用约定显式写出来。
  • procedure Name(...); ... external ...; 当前也已验证可用。
  • function(...): ...; external fp; 当前可以把原生函数指针重新包装成 TSL 可调用对象。
  • 当前已验证的 DLL 名写法包括字面量字符串和类常量字符串;不要把字符串拼接表达式直接当成稳定写法。
  • MakeInstance(ThisFunction(Func), "cdecl", 0) 当前可以把 TSL 函数包装成 C 调用约定函数指针。
  • MakeInstance(..., "cdecl", 1) 当前在 Win64 下已经拿到最小线程正例,可以直接交给 CreateThread
  • 当前页的线程示例是 Windows 专题,用到了 kernel32.dll

已验证语法

最小 external 声明

代码块身份:已验证可执行示例

program test;
function Tick64Alias(): int64; stdcall; external "kernel32.dll" name "GetTickCount64";
begin
    WriteLn(Tick64Alias() > 0);
end.

已验证运行结果:

  • 输出 1
  • 说明当前解释器接受 stdcall + external "dll" name "symbol" 这类外部函数声明骨架
  • 也说明 TSL 里的函数名可以和 DLL 导出名不同,再通过 name "ExportName" 绑定

当本地函数名和 DLL 导出名一致时,name 可以省略:

代码块身份:已验证可执行示例

program test;
function GetTickCount64(): int64; stdcall; external "kernel32.dll";
begin
    WriteLn(GetTickCount64() > 0);
end.

已验证运行结果:

  • 输出 1
  • 说明 name "symbol" 不是当前解释器下的强制写法

当前 Win64 基线下,省略调用约定与显式 cdecl 也通过了同一 API 的最小验证:

代码块身份:已验证可执行示例

program test;
function TickNoConv(): int64; external "kernel32.dll" name "GetTickCount64";
function TickCdecl(): int64; cdecl; external "kernel32.dll" name "GetTickCount64";
begin
    WriteLn(TickNoConv() > 0);
    WriteLn(TickCdecl() > 0);
end.

已验证运行结果:

  • 依次输出 11
  • 这只说明当前 Win64 解释器下,这两种写法都能跑通这个例子
  • 不要把这个结果直接泛化成“所有平台、所有架构下调用约定都等价”

procedure external

代码块身份:已验证可执行示例

program test;
function Tick64(): int64; stdcall; external "kernel32.dll" name "GetTickCount64";
procedure SleepMs(ms: integer); stdcall; external "kernel32.dll" name "Sleep";
begin
    t1 := Tick64();
    SleepMs(20);
    t2 := Tick64();
    WriteLn(t2 >= t1);
end.

已验证运行结果:

  • 输出 1
  • 说明无返回值的外部过程可以直接声明为 procedure

原生函数指针包装

代码块身份:已验证可执行示例

program test;
function LoadLibraryA(s: string): pointer; stdcall; external "kernel32.dll" name "LoadLibraryA";
function GetProcAddress(hModule: pointer; lpProcName: string): pointer; stdcall; external "kernel32.dll" name "GetProcAddress";
begin
    h := LoadLibraryA("kernel32.dll");
    fp := GetProcAddress(h, "GetTickCount64");
    f := function(): int64; stdcall; external fp;
    WriteLn(h <> nil);
    WriteLn(fp <> nil);
    WriteLn(##f() > 0);
end.

已验证运行结果:

  • 依次输出 111
  • 说明 function(...); ... external fp; 不只适用于 MakeInstance(...) 的结果,也适用于 GetProcAddress(...) 返回的原生函数指针

DLL 名的已验证边界

类常量字符串:

代码块身份:已验证可执行示例

program test;
type Demo = class
    const KRNL = "kernel32.dll";
public
    function Run();
    begin
        return TickConst() > 0;
    end;
    function TickConst(): int64; stdcall; external KRNL name "GetTickCount64";
end;
begin
    d := new Demo();
    WriteLn(d.Run());
end.

已验证运行结果:

  • 输出 1
  • 说明当前解释器接受“类常量字符串 -> external KRNL”这条最小路径

当前已验证的反向边界:

代码块身份:反例 / 不可照写

function TickFromExpr(): int64; stdcall; external "kernel32"$"."$"dll" name "GetTickCount64";

上面这种 DLL 名字符串拼接表达式在当前解释器里会报 dll filename const string not found after external。因此当前手册只把字面量字符串和已单独验证过的类常量字符串写成可靠规则。

MakeInstance

代码块身份:已验证可执行示例

program test;
function Add(a: integer; b: integer): integer;
begin
    return a + b;
end;
begin
    fp := MakeInstance(ThisFunction(Add), "cdecl", 0);
    f := function(a: integer; b: integer): integer; external fp;
    WriteLn(fp <> nil);
    WriteLn(##f(3, 4));
end.

已验证运行结果:

  • fp <> nil 输出 1
  • ##f(3, 4) 输出 7
  • 说明当前解释器里,MakeInstance(...) 生成的函数指针可以再通过 function(...); external fp; 包装回 TSL 侧调用

线程模式最小正例

代码块身份:已验证可执行示例

program test;
function CreateThread(attr: pointer; size: pointer; addr: pointer; p: pointer; flag: Integer; var thread_id: Integer): pointer; stdcall; external "kernel32.dll" name "CreateThread";
function WaitForSingleObject(h: pointer; timeout: Integer): Integer; stdcall; external "kernel32.dll" name "WaitForSingleObject";
function CloseHandle(h: pointer): Integer; stdcall; external "kernel32.dll" name "CloseHandle";
function Worker(p: pointer): integer;
begin
    SetGlobalCache("THREAD_TEST_KEY", 1);
    return 1;
end;
begin
    SetGlobalCache("THREAD_TEST_KEY", 0);
    fp := MakeInstance(ThisFunction(Worker), "cdecl", 1);
    h := CreateThread(nil, nil, fp, nil, 0, tid);
    WriteLn(fp <> nil);
    WriteLn(h <> nil);
    WriteLn(WaitForSingleObject(h, 5000) >= 0);
    GetGlobalCache("THREAD_TEST_KEY", v);
    WriteLn(v);
    CloseHandle(h);
end.

已验证运行结果:

  • 依次输出 1111
  • 说明 MakeInstance(..., "cdecl", 1) 当前可以生成可用于 CreateThread 的回调指针
  • 也说明线程体里的 SetGlobalCache(...) 当前能够跑通这个最小闭环

最小可编译示例

如果你只是要先记住最小的 DLL 引入骨架,用这个:

代码块身份:已验证可执行示例

program test;
function Tick64Alias(): int64; stdcall; external "kernel32.dll" name "GetTickCount64";
begin
    WriteLn(Tick64Alias() > 0);
end.

常见误写

  • external 的 DLL 名直接写成字符串拼接表达式。
  • 省略了外部函数的参数类型或返回类型。
  • MakeInstance(...) 生成的结果默认写成普通函数名直调,而不是先包装或用 ##.
  • 直接把 Windows 线程示例当成跨平台事实。

跳转指引