📝 docs(tsl-syntax-reference): document external and remote calls

This commit is contained in:
csh
2026-08-10 18:22:35 +08:00
parent a23f688b01
commit 2938300acb
5 changed files with 233 additions and 69 deletions
@@ -1,12 +1,12 @@
# TSL 外部调用与线程
这一篇吸收函数专题里和外部系统交互有关的部分:`external`动态库调用、原生函数指针包装、C 回调和线程调用。
本页说明动态库声明、原生函数指针、C 回调和线程调用。`MakeInstance``DeleteInstance` 是 builtin API`external``name``KeepResident``cdecl``stdcall` 和外部参数声明是语法。
## 本篇职责
<!-- section-id: syntax-17-001 -->
回答外部 DLL 声明、原生函数指针包装、C 回调和线程调用有哪些文档明确写法”。本页只覆盖系统交互能力,普通 TSL 函数的定义与调用不在本页收口
回答外部动态库、函数指针、C 回调和线程调用的写法。`LoadLibraryA``GetProcAddress``dlopen``dlsym` 是宿主 API`TSL_ScriptGo``TSSVRAPI.h` 名称是 C/C++ SDK API,均不进入 TSL builtin 索引
## 核心规则
@@ -14,14 +14,112 @@
- 外部函数声明的文档明确形态是 `function Name(...): Type; stdcall|cdecl; external "dll" [name "symbol"];`
- 当 TSL 函数名和 DLL 导出名一致时,`name "symbol"` 可以省略。
- Windows 示例默认显式写调用约定;不要把省略调用约定当成跨平台默认规则
- Win32 下调用约定必须与宿主函数一致;Linux 与 Win64 下 `stdcall``cdecl` 使用相同调用模式
- 无返回值的外部接口可声明为 `procedure Name(...); ... external ...;`
- 外部函数需要写回调用方变量时,在参数名前写 `var`;TSL 参数类型必须与原生 ABI 的参数宽度和含义一致。
- `KeepResident` 写在外部库声明末尾,用于保留动态库句柄。
- `function(...): ...; external fp;` 可以把原生函数指针重新包装成 TSL 可调用对象。
- DLL 名写法包括字面量字符串类常量字符串;不要把字符串拼接表达式直接当成稳定写法
- `makeInstance(thisFunction(Func), "cdecl", 0)` 可以把 TSL 函数包装成 C 调用约定函数指针。
- Windows 线程示例用 `makeInstance(..., "cdecl", 1)` 生成可交给 `CreateThread` 的回调指针。
- 外部库名和调用约定要按目标平台选择;不要把 Windows 的 `kernel32.dll` 示例直接复制到 Linux,或把 Linux 的 `.so` 示例直接复制到 Windows
- 本页线程示例是 Windows 专题,用到了 `kernel32.dll`
- 库名可使用字面量字符串类常量字符串,不能使用字符串拼接表达式。
- `MakeInstance(thisFunction(Func), "cdecl", 0)` `MakeInstance(findFunction("Func"), "cdecl", 0)` 可以把有完整参数及返回类型声明的 TSL 函数包装成 C 调用约定函数指针。
- `DeleteInstance(ptr)` 用于删除 `MakeInstance` 生成的指针;包装出来的 TSL 函数变量应先解除引用,再删除原始指针。
- Windows 线程示例用 `MakeInstance(..., "cdecl", 1)` 生成可交给 `CreateThread` 的回调指针
- 外部库名按目标平台选择:Windows 使用 DLLLinux 使用对应 `.so`
- 文档列出的基础外部类型包括 `Integer``String``Double``Single``Boolean``Pointer``PChar``Int64``Short``Byte`,未列出的类型按 `String` 处理。结构体等复合类型仍须按原生 ABI 组装内存;不能直接导入 C++ 引出类。
## 外部 ABI 与动态库常驻
<!-- section-id: syntax-17-012 -->
<!-- tags: KeepResident 动态库常驻 external ABI 类型映射 -->
`external` 是 TSL 与宿主 ABI 的边界,不是 builtin 定义。参数类型、宽度、对齐和调用约定必须与原生函数一致;结构体等复合参数须自行封装内存布局。
`KeepResident` 放在 `external` 库声明末尾,用于要求动态库句柄在调用后保持常驻。例如:
代码块身份:可直接照写示例
代码块说明:Linux 示例,依赖 `libc.so.6`
```tsl
writeLn(GetPidResident() > 0);
function GetPidResident(): integer; cdecl; external "libc.so.6" name "getpid" KeepResident;
```
代码块身份:输出片段
```text
1
```
## 外部出参与 ABI 写回
<!-- section-id: syntax-17-013 -->
<!-- tags: 外部出参 external var 出参 写回调用方 glibc libc time ABI -->
当外部函数需要通过指针写回调用方变量时,在参数名前使用 `var`。这条规则只说明 TSL 声明形态;具体类型、宽度、对齐和调用约定仍必须与原生函数一致。
下面用 libc `time` 演示 `var` 外部出参:
代码块身份:可直接照写示例
代码块说明:Linux x86_64 示例,依赖 `libc.so.6``time_t` 按该 ABI 映射为 `int64`
```tsl
stamp := 0;
result := UnixTime(stamp);
writeLn(result > 0);
writeLn(stamp = result);
function UnixTime(var stamp: int64): int64; cdecl; external "libc.so.6" name "time";
```
代码块身份:输出片段
```text
1
1
```
`UnixTime` 是脚本别名,`time` 是 libc 导出符号,二者都不是 TSL builtin。
## Linux 动态加载与函数指针生命周期
<!-- section-id: syntax-17-014 -->
<!-- tags: Linux 动态加载 glibc dlsym dlopen external 原生指针 -->
Linux 可用 `dlopen` / `dlsym` 取得原生函数指针,再用 `external fp` 包装。Windows 对应使用 `LoadLibraryA` / `GetProcAddress`。这些名称都是宿主 API,不是 TSL builtin。
代码块身份:可直接照写示例
代码块说明:Linux 示例,依赖 `libdl.so.2``libc.so.6``1` 为该环境的 `RTLD_LAZY`
```tsl
module_handle := dlopen("libc.so.6", 1);
func_ptr := dlsym(module_handle, "getpid");
wrapped_func := function(): integer; cdecl; external func_ptr;
writeLn(module_handle <> nil);
writeLn(func_ptr <> nil);
writeLn(##wrapped_func() > 0);
wrapped_func := nil;
writeLn(dlclose(module_handle) = 0);
function dlopen(filename: string; flags: integer): pointer; cdecl; external "libdl.so.2" name "dlopen";
function dlsym(handle: pointer; symbol: string): pointer; cdecl; external "libdl.so.2" name "dlsym";
function dlclose(handle: pointer): integer; cdecl; external "libdl.so.2" name "dlclose";
```
代码块身份:输出片段
```text
1
1
1
1
```
释放顺序是先将包装变量设为 `nil`,再调用 `dlclose`
`MakeInstance` 生成的指针由 `DeleteInstance` 释放;若已包装为 TSL 函数变量,先将包装变量设为 `nil`。示例见 `syntax-17-008`API 签名见 `tsl-api-reference`
## 可直接照写示例
@@ -41,11 +139,7 @@ writeLn(Tick64Alias() > 0);
function Tick64Alias(): int64; stdcall; external "kernel32.dll" name "GetTickCount64";
```
结果说明:
- 输出 `1`
- 说明 `stdcall` + `external "dll" name "symbol"` 是本页明确的外部函数声明骨架
- 也说明 TSL 里的函数名可以和 DLL 导出名不同,再通过 `name "ExportName"` 绑定
结果说明:输出 `1``name` 用于绑定不同名的导出符号。
代码块身份:输出片段
@@ -53,10 +147,10 @@ function Tick64Alias(): int64; stdcall; external "kernel32.dll" name "GetTickCou
1
```
Linux / POSIX 环境的同类最小骨架
Linux 的同类声明
代码块身份:可直接照写示例
代码块说明:仅类 Unix 环境可执行(依赖 `libc.so.6`);Windows 下不可照抄,本块只演示 `.so` 库名写法
代码块说明:依赖 `libc.so.6`
```tsl
writeLn(getpid() > 0);
@@ -70,8 +164,6 @@ function getpid(): integer; cdecl; external "libc.so.6";
1
```
这段说明 Linux / POSIX 目标下可以用 `.so` 库名声明外部函数;生成代码时仍要先判断用户的目标平台。
当本地函数名和 DLL 导出名一致时,`name` 可以省略:
代码块身份:可直接照写示例
@@ -82,12 +174,9 @@ writeLn(GetTickCount64() > 0);
function GetTickCount64(): int64; stdcall; external "kernel32.dll";
```
结果说明:
结果说明:输出 `1`
- 输出 `1`
- 说明当本地函数名和导出名一致时,`name "symbol"` 不是强制写法
Windows 的同一 API 示例里,省略调用约定与显式 `cdecl` 也列入文档边界:
也可以省略调用约定或显式写 `cdecl`
代码块身份:可直接照写示例
@@ -102,8 +191,7 @@ function TickCdecl(): int64; cdecl; external "kernel32.dll" name "GetTickCount64
结果说明:
- 依次输出 `1``1`
- 这只说明 Windows 的同一 API 示例里,这两种写法属于文档边界
- 不要把这个结果直接泛化成“所有平台、所有架构下调用约定都等价”
- Win32 下仍须按原函数的真实调用约定声明
### `procedure external`
@@ -123,10 +211,7 @@ function Tick64(): int64; stdcall; external "kernel32.dll" name "GetTickCount64"
procedure SleepMs(ms: integer); stdcall; external "kernel32.dll" name "Sleep";
```
结果说明:
- 输出 `1`
- 说明无返回值的外部过程可以直接声明为 `procedure`
结果说明:输出 `1`;无返回值接口可声明为 `procedure`
### 原生函数指针包装
@@ -148,10 +233,7 @@ function LoadLibraryA(lib_name: string): pointer; stdcall; external "kernel32.dl
function GetProcAddress(module_handle: pointer; proc_name: string): pointer; stdcall; external "kernel32.dll" name "GetProcAddress";
```
结果说明:
- 依次输出 `1``1``1`
- 说明 `function(...); ... external fp;` 不只适用于 `makeInstance(...)` 的结果,也适用于 `GetProcAddress(...)` 返回的原生函数指针
结果说明:依次输出 `1``1``1``GetProcAddress(...)` 返回的指针可用 `external fp` 包装。
### DLL 名的文档边界
@@ -159,8 +241,6 @@ function GetProcAddress(module_handle: pointer; proc_name: string): pointer; std
<!-- tags: dll 名写在哪, 库名怎么给, 常量放路径 -->
类常量字符串:
代码块身份:可直接照写示例
```tsl
@@ -178,12 +258,7 @@ public
end;
```
结果说明:
- 输出 `1`
- 说明类常量字符串(如 `kKernelDll`)可以用于 `external kKernelDll` 这种 DLL 名位置
不作为可写事实边界:
结果说明:输出 `1`;类常量字符串可以用在 DLL 名位置。
代码块身份:反例 / 不可照写
@@ -191,21 +266,23 @@ end;
function TickFromExpr(): int64; stdcall; external "kernel32"$"."$"dll" name "GetTickCount64";
```
上面这种 DLL 名字符串拼接表达式不作为可写事实,会报 `dll filename const string not found after external`本页只把字面量字符串和类常量字符串写成可靠规则。
字符串拼接会报 `dll filename const string not found after external`
### `makeInstance`
### `MakeInstance`
<!-- section-id: syntax-17-008 -->
<!-- tags: 回调函数, 把 TSL 函数给 C 用, 生成函数实例 -->
<!-- tags: 回调函数, 把 TSL 函数给 C 用, 生成函数实例, 函数指针释放, DeleteInstance, MakeInstance 生命周期 -->
代码块身份:可直接照写示例
```tsl
func_ptr := makeInstance(thisFunction(Add), "cdecl", 0);
func_ptr := MakeInstance(thisFunction(Add), "cdecl", 0);
wrapped_func := function(a: integer; b: integer): integer; external func_ptr;
writeLn(func_ptr <> nil);
writeLn(##wrapped_func(3, 4));
wrapped_func := nil;
writeLn(DeleteInstance(func_ptr));
function Add(a: integer; b: integer): integer;
begin
@@ -213,11 +290,7 @@ begin
end;
```
结果说明:
- `func_ptr <> nil` 输出 `1`
- `##wrapped_func(3, 4)` 输出 `7`
- 说明 `makeInstance(...)` 生成的函数指针可以再通过 `function(...); external fp;` 包装回 TSL 侧调用
结果说明:依次输出 `1``7``1`
### 线程模式最小正例
@@ -229,7 +302,7 @@ end;
```tsl
setGlobalCache("THREAD_TEST_KEY", 0);
worker_ptr := makeInstance(thisFunction(Worker), "cdecl", 1);
worker_ptr := MakeInstance(thisFunction(Worker), "cdecl", 1);
thread_handle := CreateThread(nil, nil, worker_ptr, nil, 0, thread_id);
writeLn(worker_ptr <> nil);
writeLn(thread_handle <> nil);
@@ -248,25 +321,13 @@ begin
end;
```
结果说明:
- 依次输出 `1``1``1``1`
- 说明 `makeInstance(..., "cdecl", 1)` 可以生成可用于 `CreateThread` 的回调指针
- 也说明线程体里的 `setGlobalCache(...)` 可用于这个最小闭环
结果说明:依次输出 `1``1``1``1`
## 默认生成模板
<!-- section-id: syntax-17-010 -->
DLL 引入的最小默认骨架如下:
代码块身份:可直接照写示例
```tsl
writeLn(Tick64Alias() > 0);
function Tick64Alias(): int64; stdcall; external "kernel32.dll" name "GetTickCount64";
```
默认使用 `syntax-17-004` 的显式调用约定、`external``name` 骨架。
## 禁止项
@@ -274,5 +335,5 @@ function Tick64Alias(): int64; stdcall; external "kernel32.dll" name "GetTickCou
-`external` 的 DLL 名直接写成字符串拼接表达式。
- 省略了外部函数的参数类型或返回类型。
-`makeInstance(...)` 生成的结果默认写成普通函数名直调,而不是先包装或用 `##f(...)`
- 直接把 Windows 线程示例当成跨平台事实
-`MakeInstance(...)` 生成的结果默认写成普通函数名直调,而不是先包装或用 `##f(...)`
- 使用与目标平台不匹配的库名或调用约定