diff --git a/skills/tsl-syntax-reference/data/lexicon.json b/skills/tsl-syntax-reference/data/lexicon.json index 7e540e85..f0c53bdf 100644 --- a/skills/tsl-syntax-reference/data/lexicon.json +++ b/skills/tsl-syntax-reference/data/lexicon.json @@ -24,7 +24,14 @@ "方括号": ["operator", "下标"], "按引用传": ["varByRef", "var"], "引用传递": ["varByRef", "var"], - "计时": ["mtic", "mtoc"] + "计时": ["mtic", "mtoc"], + "外部调用": ["external", "动态库", "函数指针"], + "动态库常驻": ["KeepResident", "external"], + "外部出参": ["external", "var", "ABI"], + "函数指针释放": ["DeleteInstance", "MakeInstance"], + "Linux 动态加载": ["dlopen", "dlsym", ".so"], + "远程调用客户端": ["RDo", "RDo2", "客户端远程调用"], + "本地弹窗": ["RDo2", "InputQuery", "客户端远程调用"] }, "page_intent_aliases": { "01_quickstart.md": ["最简单能跑", "最简单的脚本", "天软脚本", "tinysoft"], @@ -77,7 +84,17 @@ "注释怎么写" ], "16_types_and_conversions.md": ["字符串转整数", "类型转换"], - "17_external_calls_and_threads.md": ["调用 dll", "dll", "动态库", "开线程"], + "17_external_calls_and_threads.md": [ + "调用 dll", + "dll", + "动态库", + "外部调用", + "KeepResident", + "外部出参", + "函数指针释放", + "Linux 动态加载", + "开线程" + ], "18_namespace_libpath_and_unit_runtime.md": ["找不到 tsf", "搜索路径"], "19_object_runtime_and_introspection.md": [ "查看对象属于哪个类", @@ -93,6 +110,13 @@ "运算符重载", "算符重载", "for in" + ], + "24_client_remote_calls.md": [ + "客户端远程调用", + "远程调用客户端", + "rdo", + "rdo2", + "本地弹窗" ] } } diff --git a/skills/tsl-syntax-reference/references/15_lexical_structure_and_compile_options.md b/skills/tsl-syntax-reference/references/15_lexical_structure_and_compile_options.md index 6037ff66..8eabad11 100644 --- a/skills/tsl-syntax-reference/references/15_lexical_structure_and_compile_options.md +++ b/skills/tsl-syntax-reference/references/15_lexical_structure_and_compile_options.md @@ -43,7 +43,7 @@ TSL 关键字大小写无关;本表统一按文档推荐写法展示。生成 | LIKE 精度 | `likeEps`、`likeEpsRate` | 只在需要调整 `like` 数值近似判断阈值时使用。 | | 类与对象 | `type`、`class`、`new`、`findClass`、`findFunction`、`fackClass`、`property`、`self`、`virtual`、`override`、`overload`、`Inherited`、`protected`、`public`、`private`、`published`、`static` | 类声明、对象创建和成员规则见 [08_objects_and_classes.md](08_objects_and_classes.md);`Inherited;` / `Inherited MethodName(...)` 是祖先类调用写法,同见该页。 | | 外部调用约定 | `external`、`cdecl`、`pascal`、`stdcall`、`safecall`、`fastcall`、`register` | 外部调用细节见 [17_external_calls_and_threads.md](17_external_calls_and_threads.md)。 | -| 客户端远程调用与权限 | `rdo`、`rdo2`、`sudo`、`setUid` | 平台/客户端远程调用和权限语义不作为普通本地语法模板。 | +| 客户端远程调用与权限 | `rdo`、`rdo2`、`sudo`、`setUid` | `rdo` / `rdo2` 见 [24_client_remote_calls.md](24_client_remote_calls.md);它们不能建模成普通函数。 | | TS-SQL 查询 | `select`、`vselect`、`sselect`、`mselect`、`distinct`、`selectOpt`、`dRange`、`as`、`from`、`marketTable`、`infoTable`、`tradeTable`、`sqlTable`、`hugeSqlTable`、`keepNull`、`dateKey`、`of`、`order`、`by`、`where`、`desc`、`asc`、`group`、`having` | 查询语法见 [13_ts_sql.md](13_ts_sql.md)。 | | TS-SQL 聚合与上下文 | `checksumOf`、`countOf`、`sumOf`、`maxOf`、`stdevOf`、`varOf`、`totalVarOf`、`normOf`、`medianOf`、`aveDevOf`、`geoMeanOf`、`skewOf`、`kurtosisOf`、`skew2Of`、`kurtosis2Of`、`largeOf`、`percentileOf`、`quartileOf`、`trimMeanOf`、`avgOf`、`minOf`、`aggOf`、`stdevpOf`、`varpOf`、`modeOf`、`devSqOf`、`harMeanOf`、`checksum_aggOf`、`smallOf`、`percentRankOf`、`rankOf`、`frequencyOf`、`productOf`、`refOf`、`refsOf`、`aggValue`、`thisGroup`、`thisRow`、`thisRowIndex`、`thisOrder` | 这些名称只在 TS-SQL 语境中生成。 | | TS-SQL 写入 | `insert`、`insertFields`、`values`、`update`、`set`、`delete`、`deleteOpt`、`fetchFirst`、`fetchNext` | 写回/变更型查询只按 TS-SQL 专题页生成。 | diff --git a/skills/tsl-syntax-reference/references/17_external_calls_and_threads.md b/skills/tsl-syntax-reference/references/17_external_calls_and_threads.md index 32ec4542..3dabf314 100644 --- a/skills/tsl-syntax-reference/references/17_external_calls_and_threads.md +++ b/skills/tsl-syntax-reference/references/17_external_calls_and_threads.md @@ -1,12 +1,12 @@ # TSL 外部调用与线程 -这一篇吸收函数专题里和外部系统交互有关的部分:`external`、动态库调用、原生函数指针包装、C 回调和线程调用。 +本页说明动态库声明、原生函数指针、C 回调和线程调用。`MakeInstance`、`DeleteInstance` 是 builtin API;`external`、`name`、`KeepResident`、`cdecl`、`stdcall` 和外部参数声明是语法。 ## 本篇职责 -回答“外部 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 使用 DLL,Linux 使用对应 `.so`。 +- 文档列出的基础外部类型包括 `Integer`、`String`、`Double`、`Single`、`Boolean`、`Pointer`、`PChar`、`Int64`、`Short` 和 `Byte`,未列出的类型按 `String` 处理。结构体等复合类型仍须按原生 ABI 组装内存;不能直接导入 C++ 引出类。 + +## 外部 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 写回 + + + + + +当外部函数需要通过指针写回调用方变量时,在参数名前使用 `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 动态加载与函数指针生命周期 + + + + + +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 -类常量字符串: - 代码块身份:可直接照写示例 ```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` - + 代码块身份:可直接照写示例 ```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`。 ## 默认生成模板 -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(...)`。 +- 使用与目标平台不匹配的库名或调用约定。 diff --git a/skills/tsl-syntax-reference/references/24_client_remote_calls.md b/skills/tsl-syntax-reference/references/24_client_remote_calls.md new file mode 100644 index 00000000..df6586ca --- /dev/null +++ b/skills/tsl-syntax-reference/references/24_client_remote_calls.md @@ -0,0 +1,67 @@ +# TSL 客户端远程调用 + +这一篇只处理 `RDo` / `RDo2` 调用客户端本地函数的语法外形、等待行为、系统参数后缀和超时后缀。 + +## 本篇职责 + + + +回答“怎样从平台调用客户端本地函数、`RDo` 与 `RDo2` 有什么区别、`With` 与 `TimeOut` 写在哪里,以及为什么不能写成 `RDo(...)` / `RDo2(...)`”。 + +## 核心规则 + + + + + +- `RDo` 和 `RDo2` 是客户端远程调用关键字,不是普通函数。 +- 两者都写在被调用的本地函数之前;圆括号属于本地函数调用,不属于 `RDo` / `RDo2`。 +- `RDo` 只提交任务,不等待本地函数执行完成,也不取得函数返回值。 +- `RDo2` 等待本地函数执行完成,并把该函数的结果作为整个表达式的值。 +- 本地函数需要系统参数时,在调用后追加 `With SysParams`。 +- `RDo2` 默认超时为 300 秒;需要自定义时,在末尾追加 `TimeOut Seconds`。文档定义的后缀顺序是先 `With`、后 `TimeOut`。 +- `RDo2` 在客户机执行时需要用户权限许可;依赖客户端资源的函数不能在无 GUI 的命令行解释器中验证其交互结果。 +- 被调用函数的精确签名、参数写回、返回值及运行时 scope 必须使用 `tsl-api-reference` skill 单独查询;本页只拥有 `RDo` / `RDo2` 的语法形态。 + +## `RDo` / `RDo2` 语法与示例 + + + + + +原文定义使用下面的元语法;方括号表示可选部分,不是需要写进 TSL 源码的字符: + +代码块身份:配置片段 / 概念骨架 + +```text +RDo LocalFunctionName(P1; P2; …) [With SysParams]; +RDo2 LocalFunctionName(P1; P2; …) [With SysParams] [TimeOut Seconds]: Any; +``` + +`RDo2` 调用客户端 `InputQuery` 的原文范例可整理为: + +代码块身份:可直接照写示例 +代码块说明:需要平台客户端交互环境、用户权限许可,并要求目标环境提供 `InputQuery`;无 GUI 的命令行解释器不能验证弹窗结果。 + +```tsl +value := ""; +if rdo2 InputQuery("Input", "Hint", value) then + return value; +else + return "Canceled"; +``` + +结果说明: + +- 用户确认时,`InputQuery` 返回真,输入内容写回 `value`,脚本返回该字符串。 +- 用户取消时,`InputQuery` 返回假,脚本返回 `Canceled`。 +- 这段代码证明的是前缀形态 `rdo2 InputQuery(...)`;`InputQuery` 的 API 事实仍需按目标 scope 单独查询。 + +## 禁止项 + + + +- 把关键字建模成 `RDo()` 或 `RDo2(...)` 普通函数。 +- 把被调用本地函数的参数误写成 `RDo2` 自身的参数表。 +- 声称 `RDo` 会返回客户端函数结果。 +- 忽略 `RDo2` 的客户端权限与交互环境要求。 diff --git a/test/test_tsl_syntax_reference.py b/test/test_tsl_syntax_reference.py index 205ea5a7..7f4d7455 100644 --- a/test/test_tsl_syntax_reference.py +++ b/test/test_tsl_syntax_reference.py @@ -316,6 +316,18 @@ class TslSyntaxReferenceTests(unittest.TestCase): self.assertEqual("syntax-05-008", write_result.matches[0].section.id) self.assertEqual("syntax-02-006", diagnose_result.matches[0].section.id) + def test_external_call_queries_retrieve_platform_and_abi_boundaries(self): + cases = { + "动态库常驻": "syntax-17-012", + "外部出参": "syntax-17-013", + "Linux 动态加载": "syntax-17-014", + "函数指针释放": "syntax-17-008", + } + for query, expected_id in cases.items(): + with self.subTest(query=query): + result = lookup.query_sections(query, "write", limit=1) + self.assertEqual(expected_id, result.matches[0].section.id) + def test_index_origin_variants_rank_value_rules_first(self): for query in ("下标从几开始", "下标是从几开始", "请问下标是从几开始"): with self.subTest(query=query):