playbook/docs/tsl/syntax_book/03_functions.md

33 KiB
Raw Blame History

03 函数

本章聚焦函数定义体、函数结构与相关语法。

目录

函数定义体和函数

概念

TSL 语言中,有两种可执行单元,它们分别是函数定义体和函数。

函数是一个由多条语句组合的可以在远程服务器上运行并允许返回结果的语句集合和最小运行单元TSL 语言是纯函数语言,所有的任务均需通过函数来实现。

函数的结构说明如下:

function 函数名称(参数1, 参数2, ...);
begin
    语句;
    ...
end;

函数说明的第一行为函数声明头。它指明一个函数的函数名和参数信息,一个函数允许带零到多个的任意数据类型的参数。一个完整的函数声明必须包含 beginend 标识符。

函数定义体是 TSL 是多个函数的集合,定义体的名称与函数集合中的主运行函数相同。

在一个函数定义体中不允许出现两个重名的函数声明。

函数中可以返回内容,返回的内容可以通过变参返回,返回值和 C 类似,使用 Return 返回。

函数返回参数类型可以任意类型。

TSL 仅允许 return 一个值,如果需要返回多个内容,建议用数组包装,例如:

return array(x, y);

参数类型与返回类型

函数参数与返回值可以在名称后使用 : 指定类型,类型注解可选。注解名称不参与编译检查,可任意命名,仅用于说明与阅读。

参数分隔规则:

  • 不带类型注解时,用逗号分隔参数,例如 function f1(a, b);
  • 带类型注解时,用分号分隔参数,例如 function f2(a: string; b: array of real);
  • 返回值类型在 ) 之后定义,例如 function f3(): void;
function SafeConvert(value: input_value; convert_func: handler): real;
begin
    return ##convert_func(value);
end;

function Open(path: string; flags: options = 0): result;
begin
    return array("path": path, "flags": flags);
end;

主函数与子函数

在 TSL 语言中,根据函数的可使用范围,将函数分成主函数和子函数两种。

主函数是指在一个函数定义体中与函数定义体同名的函数;子函数是指在一个函数定义体中声明的,与函数定义体不同名的函数;

主函数的可使用范围是全局的,它可以在任何的其它函数中被调用;但子函数的可使用范围是局部的,它只能被与其处在同一个函数定义体中的其它函数所调用。

如果一个函数仅需要为某个函数所调用,或者其公用性很低的时候,在编程时可以考虑把它作为子函数放在同一个函数定义体中声明。

目前在 TSL 语言中,已经提供了许多的系统函数,它们全部是主函数。你可以在编写自己的用户函数时直接调用。

例如:以下函数定义体内含有两个函数,其中 SubFunction 称为函数体 TestMultiFunc 的子函数,而 TestMultiFunc 为主函数,用于计算 1 到 100 的平方和。

function TestMultiFunc(); // 主函数
begin
    result := 0;
    for i := 1 to 100 do result := result + SubFunction(i);
    return result;
end;

function SubFunction(a); // 子函数
begin
    return a * a;
end;

形式参数与实际参数

形式参数是指在函数声明时同时指明的要传递到函数体内直接使用的变量名也就是参数名,简称形参;而实际参数是指在函数调用的同时被实际传递给调用函数的实际参数值,简称实参。

一个函数调用的执行顺序分为以下几步:

实参和形参结合——〉执行调用函数中语句——〉返回调用处继续执行

在函数中声明的形式参数表对函数体中直接引用的变量进行说明,详细指明这些参数的类别、数据类型要求和参数的个数。在函数被调用时必须为它的每个形参提供一个实参,按参数的位置顺序一一对应,每个实参必须满足对应形参的要求

值参数

如果在调用一个函数时,给定的实际参数是一个具体的数据,那这种参数是一种值参数。它类似于函数的局部变量,仅为函数的执行提供初值。

变量参数

如果在调用一个函数时,给定的实际参数是一个变量,那么在函数执行过程中如果对此变量进行重新赋值,则此变量的值和数据类型就会被彻底改变,包括调用此函数之后的执行语句,对此变量取值得到的将是重新赋值后的内容。

形参与实参的转换

在 TSL 语言中,与绝大多数语言不同的是,对形参与实参的支持是通过调用者而不是函数申明来实现的。

例如调用上边的 Pascal 的代码写成 TSL 可以这样写:

real_a := 100;
real_b := 200;
WriteLn(Abcd(in real_a, out real_b));
WriteLn("real_a=", real_a, " real_b=", real_b);

function Abcd(a, b);
begin
    a := a * 2;
    b := b * 3;
    return a + b;
end;

这样运行的结果和上边的一摸一样TSL 用 in 或者 const 表示送入的为值(形参),而用 out 或者 var 表示送入的为引用(实参)。

既然上边说到了 TSL 语言默认都是实参,为什么还多此一举加入了 out,var 前缀呢?

在 TSL 语言中支持通过{$VarByRef-}关闭允许参数值修改的编译选项,同时也可通过 IN、OUT 指示来修改默认的参数方式。

如:

{$VarByRef-} // 取消允许参数修改的设定
a1 := 0;
b1 := 0;
c1 := 0;
Abcd(a1, b1, c1); // 此处 a1,b1,c1 仍为 0形参
Abcd(in a1, out b1, out c1); // 此处 a1 为 0b1,c1 为 1
{$VarByRef+} // 启用允许参数修改的设定
Abcd(a1, b1, c1); // 此处 a1,b1,c1 都会被改为 1
Abcd(in a1, in b1, c1); // 此处 a1,b1 为 0c1 为 1

function Abcd(a, b, c);
begin
    a := 1;
    b := 2;
    c := 3;
end;
VarByRef 编译选项

编译选项。

插入{$VarByRef-}可以关闭允许参数值修改的编译选项,系统默认是允许修改的,一旦关闭了编译选项,函数将无法修改传入的参数。

如果要重新打开该选项,用{$VarByRef}或者{$VarByRef+}即可。如果我们要临时允许修改,可以使用 var 前缀。

编译选项是影响的编译,而非运行。所以在一个函数中打开或者关闭了开关,调用到另外一个函数的时候,该开关是不会生效的。所以每一个函数里都需要使用开关。

编译选项影响的编译是编译选项后的程序的编译代码,如果一个在函数体内里包括局部函数,编译选项也会影响到下边的局部函数。

事实上TSL 语言可以利用编译选项修改默认参数传递为形参的。

例如:

{$VarByRef-} // 关闭允许参数值修改的编译选项
real_a := 100;
real_b := 200;
WriteLn(Abcd(real_a, real_b));
WriteLn("real_a=", real_a, " real_b=", real_b);

function Abcd(a, b);
begin
    a := a * 2;
    b := b * 3;
    return a + b;
end;

结果:

800

real_a=100 real_b=200

也就是说默认变为形参后,在函数 abcd 里修改 a,b 参数都不再会影响 RealA,RealB 的值了。

{$VarByRef+}可以重新打开开关。

注意:编译选项对后边的所有源代码的编译都有效,并不仅仅局限在当前的函数内(而是整个文件或者函数体内有效)。

为了改变参数传递的方式,用户可以在参数前加 in/const,var/out 前缀。

Explicit 编译选项

编译选项。插入{$Explicit}或者{$Explicit+},可以打开变量申明检查的编译选项,系统默认是不检查的,一旦打开了编译选项,局部变量在使用前必须用 Var 来申明。使用完后可以用{$Explicit-}关闭这个检查选项。

编译选项是影响的编译,而非运行。所以在一个函数中打开或者关闭了开关,调用到另外一个函数的时候,该开关是不会生效的。所以每一个函数里都需要使用开关。

编译选项影响的编译是编译选项后的程序的编译代码,如果一个在函数体内里包括局部函数,编译选项也会影响到下边的局部函数。

例如:

b := 2;
{$Explicit}
var a;
a := 1;
b := 3;

结果:由于 b 未声明,b := 3 处会报错。

函数返回以及退出

return

在函数执行的任何时候,可以使用 RETURN 语句来返回,其后可跟有返回的结果值。

EXIT

在函数执行的任何时候,可以使用 EXIT 语句来终止语句的执行,并退回到调用此函数的上层。

不定个数参数...

定义FunctionName([a, [b, [c ...]]] ...);

其中a,b,c,表示已定义的参数,...表示不定个数的参数,不限个数,在调用时进行定义。

即支持定义如FuncA(a,b,...)、FuncA(...)等。

支持将未定义参数的函数作为不定参数个数的函数。即定义 FuncA()等同于定义 FuncA(...)

调用时,根据输入的参数个数,按参数位置进行优先识别已定义的参数,多出来的则按顺序依次识别为不定个数参数。

不定个数参数组的传递:

在不定个数参数定义的函数中进行调用时,不定个数参数组可通过...在函数调用之间进行传递。

该功能在新一代 TSL 语言中支持。

在不定个数参数的函数实现中:

1、可通过 params 获得调用时所有输入参数的值,若通过 params[i]进行访问时,下标 i 从 1 开始,当输入参数少于定义参数个数时,未输入的参数值为 nil不定个数参数在调用时确定。

2、可通过 ParamCount 返回定义的参数个数,逻辑同 params。

3、可通过 RealParamCount 获得实际输入参数的个数,当输入参数少于定义参数个数时,返回的是实际输入参数的个数。

如,封装不定参数个数函数 FuncB(a,b,c,...),如下:

function FuncB(a, b, c, ...);
begin
    echo "所有参数值:", tostn(Params);
    echo "定义的参数个数:", ParamCount;
    echo "实际的参数个数:", RealParamCount;
    return;
end;

执行调用少于定义个数参数时:

FuncB(1);

打印结果如下ParamCount>RealParamCount,Params 获取到的未被输入的参数值为 nil

所有参数值:

array(1, nil, nil)

定义的参数个数3

实际的参数个数1

执行调用多于定义个数参数时:

FuncB(1, 2, 3, 4, 5);

打印结果如下ParamCount=RealParamCount,Params 获取到所有参数值

所有参数值:

array(1,2,3,4,5)

定义的参数个数5

实际的参数个数5

范例

范例 01实现 DoSum(a, b, c, ...) 多个数据求和的功能

// 封装函数
function DoSum(a, ...); // 指定不定参数
begin
    s := 0;
    for i, v in params do s += v;
    return s;
end;
// 调用
return DoSum(1, 2, 3, 4);

返回10

范例 02通过不定个数参数给子函数传参

// 封装函数
function AAA(a, b, c, d); // 展示各参数的值
begin
    return (b + c + d) / a;
end;
function BBB(a, b, ...);
begin
    c := a + b;
    return AAA(c, ...);
end;
// 调用
return BBB(1, 2, 3, 4, 5);

返回4

解析:函数中参数的传递按顺序解读,上面示例中即为(3+4+5)/(1+2)最终结果为 4。

范例 03不定个数参数在执行不定函数中的应用call 方式)

// 封装函数,实现统一接口调用不同参数的函数
function DoFunc(fc, ...); // fc 为函数名或函数指针
begin
    return call(fc, ...);
end;
// 调用
return DoFunc("StockName", "SZ000002"); // 返回:"万科A"
return DoFunc("FormatFloatExt", 101.4567, 3); // 返回:"101.457"
// 或用下面这种调用方式,效果一样:
return DoFunc(thisfunction(StockName), "SZ000002");

范例 04支持在 ## 方式中调用

注:与范例 03 的区别在于,##调用必须是函数指针,而非函数名字符串,可通过 thisfunction(functioname)方式获取。

// 封装函数 DoFunc2通过函数指针方式调用函数
function DoFunc2(fc, ...); // fc 为函数指针
begin
    return ##fc(...);
end;
// 调用
return DoFunc2(thisfunction(StockName), "SZ000002"); // 返回:"万科A"

范例 05不定个数参数在类中方法的应用示例

// 封装类 TestC
type TestC = class
public
    function FuncA(a, b, c);
    begin
        return a + b + c;
    end;
    function FuncB(a, b, c);
    begin
        return a$b$c;
    end;
    function Dispatch(kind, ...);
    begin
        if kind = "Int" then r := FuncA(...);
        else r := FuncB(...);
        return r;
    end;
end;
// 调用
obj := new TestC();
echo obj.Dispatch("Int", 2, 20, 200);
echo obj.Dispatch("Str", 2, 20, 200);

打印结果为:

222

220200

拓展:不定个数参数…在 invoke 中的使用示例,在上面示例的基础上,再封装模型设用该类的任意方法,如实现如下:

function DoTestCFuc(fc_name, ...);
begin
    obj := new TestC();
    return invoke(obj, fc_name, 0, ...);
    // return InvokeInArray(obj, fc_name, 0, ...);
end;
// 调用
return DoTestCFuc("Dispatch", "Int", 2, 20, 200);

返回结果为222

函数的调用

与 Pascal 语法不同的是,无论函数是否有参数,调用均需要使用()。

例如:

t1 := FuncA(100, 200, "abc"); // 多参数函数的调用
t2 := Today(); // 无参数函数的调用

默认的函数调用都是实参,也就是说函数内部可以修改入口参数,对于用户会造成一定的困扰,有两种方式可以解决这个问题。

方式一:代码中采用{$VarByRef-}编译选项可以使得默认采用形参的模式。

例如:

{$VarByRef - }//关闭允许参数值修改的编译选项
v := abcd(a, b, c); // 此时,a,b,c都为形参

具体用法可参考VarByRef 编译选项

方式二:用户还可以用 In out 前缀指示送入的是形参还是实参。

例如调用 abcd(in a,out b,c)这样就是 a 为形参, b 为实参, c 为默认模式(编译选项指定的)

一些注意事项

1 函数调用,必须写全用调用函数的名称,并输入相应的实际参数值。即使某些函数没有参数,也不可以省略后面的括号。

2 在编写函数时,尽量不要使用与系统函数同名的变量名和函数名。

3 在编写函数时,建议所有的地方均有返回值;如果不需要返回值,则直接用 Return 或者 Exit 命令返回。

4 在函数调用的时候,参数是按址传递的,就是说如果参数在函数的内部执行中被修改,那么在函数执行完毕后,改变了值的参数会继续生效。可以利用这一点可以实现在调用一个函数的时候返回多个值。

5 函数调用的优先级别:若存在同名函数,则函数调用次序为

1 首先是系统函数

2 其次是函数文件的子函数

3 然后是用户函数

4 最后是公用函数

参数缺省值

功能:天软函数支持缺省参数的定义,即通过定义设置参数默认值,当缺省该参数时不再是 Nil方便调用时使用。

用法:function FuncName(参数名 = 缺省值, ...)

说明:函数的缺省值可以是简单类型,也可以是复杂类型,还可以是函数调用等复杂计算表达式。

此外,支持函数缺省值定义中使用可以访问的 const 定义的常量,如类成员函数中使用类常量,或者单元函数中使用单元常量等。

例如:

function Abcd(a, b = "bbb", e, c, d);
begin
    echo "a:", a, " b:", b, " e:", e, " c:", c, " d:", d, "\r\n";
end;

示例调用:Abcd(); 打印结果为a: b:bbb e: c: d:

在客户端中定义函数时,在参数的缺省值中输入指定值,即可定义一个缺省参数,如下:

示例调用:return TestABC(); 结果为:

缺省参数支持表达式

缺省参数值不仅支持数字、字符串等常用的基础类型,还支持基于这些基础类型构建的各类表达式。

如:

return Foo(1);

function Foo(a, b = 1 + 2 + 3 + 4);
begin
    return a + b;
end;

返回结果11

需要注意的是,若表达式中包含变量,该变量的值始终为 0。

return Foo(1);

function Foo(a, b = a + 1);
begin
    return b;
end;

返回结果1

20250827 后的 NG 客户端及使用新一代 TSL 的服务器支持该功能。

在客户端中设置:

调用:

命名参数调用

功能:调用函数时,通过给指定参数名进行传值,无需区分先后次序。

多用于数学方法的调用在定义时可搭配缺省参数的使用、OFFICE 的 COM 调用以及 PYTHON 的一些机器学习方法调用中。

TSL 内置已实现对 COM 命名参数调用,并支持 PYTHON 对象以及函数的命名参数调用。

用法funcName(参数名:参数值,…)

说明:天软新一代语言中支持。

例如:

function FuncA(a, b, c);
begin
    echo "funcA--", "a:", a, " b:", b, " c:", c;
end;

示例调用:FuncA(1, c: 3); 打印funcA--a:1 b: c:3

注意:

1、在传统传参方式与命名参数传参方式混合使用时

a、命名参数方式出现之后后面所有参数都需用命名参数的方式传入否则会引发语法报错命名参数传入参数后就不再考虑参数位置关系根据参数名对指定参数进行传值。

b、前面有通过非命名参数方式传过参的参数后面不可再通过命名参数对该参数再次传参否则会引发运行时错误找不到该变量只能对未赋值过的参数进行传参。即不能出现重复传参数的情况。

2、函数体是 TSL 语言实现的才可用命名参数的方式传入,二进制函数(看不到函数体的)不支持,需要用户重新封装。

例如:系统函数 inttostr(value),由于属于二进制函数,所以不支持 inttostr(value:200)方式调用,可以进行如下封装进行转变:

// 封装 MM_IntToStr(V) 调用 IntToStr 来替代它
function MM_IntToStr(v);
begin
    return IntToStr(v);
end;

示例调用:return MM_IntToStr(v: 200); 返回:'200'

3、除了函数的直接调用上支持命名参数的调用外通过函数名或指针去调用函数或类的成员方法的模型也支持传参时以命名参数的方式进行例如call、invokeinarray 等。

例如:有函数 TestFunc

function TestFunc(a, b, c);
begin
    return array(a, b, c);
end;

示例调用:return call("TestFunc", a: 1, c: 2, b: 3); 返回结果array(3,2,1)

4、在函数存在多态的功能时比如类成员方法中存在多个同名函数重载的方法时此时不建议使用命名参数的调用可能引发不可控的调用问题。

匿名函数

除了匿名函数不允许带函数名以外,定义一个匿名函数和普通函数看起来没啥差异,匿名函数的本质就是函数指针,可以用 Call、CallInArray 或者##来直接调用。

一个匿名函数的定义以及使用案例:

a := function(x, y)
begin
    return x + y;
end;
Apply(a);

function Apply(fun);
begin
    echo Call(fun, 1, 2);
    echo ##fun(5, 6);
end;

注:匿名函数也可以直接在函数参数中直接定义。

例如:

Apply(function(x, y)
begin
    return x + y;
end);

##匿名函数以及函数指针的调用

A(1,2)代表 A 的类型是一个匿名函数或者函数指针类型,调用这个匿名函数或者函数指针代表的函数,送入 12 两个参数。

A(1,2)则表示 A 是一个名字为 A 的函数。

B := FindFunction("A"); // FindFunction 可以查找函数并返回该函数的指针。

B(1,2)的运行过程等同于 A(1,2)

::指定调用全局函/单元函数

功能:支持 ::前缀在局部函数和全局函数/单元函数发生冲突的时候调用到全局函数/单元函数

说明:当子函数、单元方法、全局函数同名时,优先调用子函数,加上::后,调用全局函数

即,不加::时,调用优先级:子函数>全局函数>单元方法

加上::时,调用优先级:全局函数>单元方法,此时调子函数会报错

另,在对象中对系统函数的重写时可通过::指定调用系统函数,此方式仅新一代 TSL 语言支持。

全局函数即表示作用域于全局的函数,一般如以函数名命名的用户函数(常说主函数),公用函数,系统函数等,都属于全局函数;

而局部函数一般如子函数,仅能在同文件中的函数中访问到。

范例 01子函数与全局函数(公用函数为例)同名调公用函数

// 主函数
function NoName111();
begin
    // 调用子函数与全局函数
    local_value := RoundTo5(3.66); // 返回 888子函数
    global_value := ::RoundTo5(3.66); // 返回 3.5(全局函数)
    return array(local_value, global_value);
end;

// 主函数 NoName111 下存在子函数 RoundTo5
function RoundTo5(v);
begin
    return 888;
end;

范例 02对象重载二进制函数中的应用

// 创建对象 TestObj并重载 DateToStr() 二进制函数
type TestObj = class
    ftd;
    function Create(t);
    begin
        ftd := t;
    end;
    function operator DateToStr();
    begin
        // 函数前面加 :: 可指定调用全局函数,这里即系统函数,避免自己调自己
        return ::DateToStr(ftd);
    end;
end;

// 使用效果
obj := new TestObj(20250808T);
return DateToStr(obj);

外部交互扩展调用接口

External 接口实现 TSL 调用外部函数或函数指针的功能。

MakeInstance 接口实现将 TSL 函数生成 C 语言的函数指针,供外部语言调用。

DLL 外部函数引入

TSL 语言直接支持对动态库的调用,当然,在金融分析.NET 平台上由于安全的原因需要设置后才可以运行,但是我们可以用 RDO2 等模式来调用本地的动态库函数。

TSL 调用动态库的函数申明基本与 PASCAL 的申明方式相同。

例如我们要调用一个 Windows 的函数 GetTickCount我们可以申明:

function GetTickCount():Integer; stdcall; external"kernel32.dll"name
"GetTickCount"KeepResident;

Stdcall是调用方式对于操作系统库基本是 STDCALL而 C++开发的动态库则是 CDECL

External后接包含动态库名称的常量也可以包含完整路径应注意\的转义)

Name后接动态库中的输出函数名这个也是必要的因为 TSL 语言的函数名是大小写不分的。

KeepResident关键字是用来描述这个函数调用完是否释放动态库句柄有 KeepResident 则会常驻内存。TSL 特有)

参数申明与返回值均需要用:来指定类型信息

TSL 支持的基础类型有:

Integer,String, Double,Single,Boolean,Pointer,PChar, Int64short,byte

除上述支持的以外的类型在 TSL 语言种均以 String 类型来处理

如果有变参需要返回值,则在申明中在参数名之前使用 Var 来标记

对于 C++等高级语言,有结构体等类型,对于到 TSL 语言,则均使用 String 类型来替代,入是输入参数,则组装一个结构体的内存块放在字符串中,如果要返回,则先申明一个足够大小的字符串(例如用 SetLength

对于拥有特殊数据类型的函数而言,一般除了直接申明引入以外,一般还会在上层包装一个 TSL 的函数解决参数组装传递等工作,方便调用。

目前 TSL 还无法直接调用 C++的引出类。

External 接口

TSL 和 C 外部交互的扩展调用接口

支持 Win64/Win32Linux x64arm64loongarchriscv

支持使用常量/常量计算结果指定动态库名称

External 功能总览

TSL 调用外部动态库的函数

通过外部动态库名与函数名生成天软函数。

TSL 调用外部函数指针

将外部动态库中的函数指针生成天软函数指针。

TSL 调用外部动态库的函数

语法Function newFuncName(p1:DataType;p2:Datetype;…):returnType;external

FuncDLL name FuncName;

其中,多个参数用;隔开,注意每个参数的参数类型要与原函数对应

returnType是函数返回结果数据类型

FuncDLL字符串如”funcs.dll函数所在的动态库名。若平台支持 External 常量,则该字段除了可以是字符串名,还可以是一个常量,在跨平台上有帮助,可以在不同平台上定义不同的常量。

FuncName是动态库中的函数原名注意大小写.

newFuncName是自定义的在 TSL 语言中被调用的函数名。

举例:

TSL 调用“tslkrnl.dll”动态库中的函数 TS_strtofloat 实现字符串转浮点数的功能。

Echo TSstrtofloat("1.23");
// 声明
function TSstrtofloat(s:string):double;external"tslkrnl.dll"name"TS_strtofloat";
调用获得的外部函数指针

假定外部程序给出的是一个函数指针TSL 也可以对它动态进行调用。是本次新增的功能。

使用说明:

TSLfunPointer := function(p1:DataType;p2:Datetype;…):return type;external
funcPointer;

其中funcPointer 为外部函数指针TSLfunPointer 为返回结果,即为天软函数指针。

范例:

tslkrnl := LoadlibraryA("tslkrnl.dll"); // 加载动态库
// 获得到函数指针
Strtofloatptr := GetProcAddress(tslkrnl, "TS_strtofloat");
// 申明得到一个TSL函数指针
Strtofloatfun := function(s:string):double;external strtofloatptr;
// TSL调用函数指针还可以用call之类的调用
Echo ##strtofloatfun("2.34"), "\r\n";
// 加载操作系统提供的动态库,获得到模块指针
function LoadlibraryA(s:string):Pointer;stdcall;external"kernel32.dll"name"LoadLibraryA";
// 从模块指针中查找出函数的指针即C的函数指针
function GetProcAddress(hModule:Pointer;lpProcName:string):Pointer;stdcall;external"kernel32.dll"name"GetProcAddress";
32 位 windows 下的调用方式申明

一般的调用方式的申明约定有以下这几种:

Register

Pascal

Cdecl

Stdcall

Safecall

Fastcall

Windows 的 API 使用 stdcall 模式。

默认情况下,我们采用 cdecl 申明模式:

function TS_strtofloat(s:string):double;external"tslkrnl.dll"name
"TS_strtofloat";

等同于:

function TS_strtofloat(s:string):double;cdecl;external"tslkrnl.dll"name
"TS_strtofloat";

除了 Win32 以外,每种 CPU 架构下只有一套标准调用模式,不需考虑是 stdcall 还是 cdecl。目前只有 Win32 还需要考虑调用方式的申明。

申明函数时使用常量/常量计算结果指定动态库名称

在不同平台Win、Linux同一动态库文件名并不相同。

此时,可使用常量/常量计算结果指定动态库名称,与条件编译相结合,根据不同的平台调用同一动态库中函数。

范例

范例一:使用常量指定动态库名称

obj := new TestClass();
obj.Test();
type TestClass = class
{$IFDEF LINUX}
const krnl = "libTSLkrnl.so";
{$else}
const krnl = "tslkrnl.dll";
{$ENDIF}
function Test();
begin
    t := TSStrToFloat("1.23");
    echo datatype(t), "\r\n";
    echo t, "\r\n";
end;
function TSStrToFloat(s: string): double; external krnl name "TS_strtofloat";
end;

Linux 结果:

Windows 结果:

范例二:使用常量计算结果指定动态库名称

obj := new TestClass();
obj.Test();
type TestClass = class
const krnl = "tslkrnl";
const ext = "dll";
function Test();
begin
    t := TSStrToFloat("1.23");
    echo datatype(t), "\r\n";
    echo t, "\r\n";
end;
function TSStrToFloat(s: string): double; external krnl$"."$ext name "TS_strtofloat";
end;

结果:

MakeInstance

范例 c 代码

####include <Windows.h>
####include <string>
using std::string;
extern"C"{
#include"TSSVRAPI.h"
}
using func = double( * )(int);//c++11
int main()
{
TObject * r = TSL_NewObject();
TObject * v = TSL_NewObject();
TObject * err = TSL_NewObject();
TSL_State * L = TS_GetGlobalL();
//重新定义TSL函数接口并生成该函数的函数指针
string s = "f1 := MakeInstance(findfunction('TS_inttodate'));   return f1;//对天软公用函数进行重新封装,声明接口输入输出的类型   function TS_inttodate(a:integer) :Double;    begin      return inttodate(a);    end; ";
int ret = TSL_ScriptGo(L, s.c_str(), v);
if (ret == TSL_OK)
{
func inttodate;
inttodate := (func)TSL_AsInt64(v);
double re = inttodate(20220101);
printf("%lf\n", re);
}
else {
printf("TSL error");
}
}

执行结果:

多线程调用

如果需要将 TSL 函数输出为外部程序调用的函数指针,如 C 语言函数的指针,就可以通过 MakeInstance 来实现。该功能是方便外部库调用 TSL 函数功能,比如 windows 有窗口消息循环就需要指针,可以把 TSL 的函数进行 MakeInstance 后交给 windows 的 API如此就可以直接在 windows 的 API 中调到 TSL 本身的消息处理函数。

以前的调用模式中,只支持在单线程中运行,即便是在多线中的调用,也是单线程方式在运行。升级后,新增加多线程单模与多线程多模两种多线程的调用。

以前的多线程调用,运行的优点是可以共用运行环境,可访问到全部信息等。

新增的多线程运行的优点是每个线程都在独立环境中进行,其中单模运行是指在调用完之后会立即清除掉该线程的运行环境,而多模方式则是会保留线程运行环境,可方便线程回调。

线程模式说明

为支持多线程单模与多模方式, MakeInstance 新增加 ThreadMode 参数来指定调用模式。

多线程调用案例

创建多线程程序例子:利用 windows API 的 CreateThread 函数创建多线程调用,通过控制台结束指定线程。

代码如下:

a := array();
// 将 Abcd 函数创建一个 C 调用的函数指针 b
b := makeinstance(thisfunction(Abcd), "cdecl", 1);
for i := 0 to 3 do
begin
    // 利用函数指针 b 调用操作系统函数创建线程
    a[i][0] := CreateThread(nil, 10240000, b, nil, 0, id);
    a[i][1] := id;
    WriteLn("tid:", id, " created");
end;
while true do
begin
    WriteLn("input exit to quit or input tid to exit thread");
    // 从控制台读字符串
    s := ReadLn();
    id := Trim(s);
    if id = "exit" then break;
    // 用全局缓存设置退出标识
    if length(select [1] from a where [1] = StrToIntDef(id, 0) end) > 0 then
        SetGlobalCache(id, 1);
end;
WriteLn("wait for exit....");
SetGlobalCache("exit", 1);
for i := 0 to length(a) - 1 do
begin
    SysWaitForSingleObject(a[i][0], -1);
    SysCloseHandle(a[i][0]);
end;
return 1;
// 定义 Windows API 的 CreateThread 函数声明
function CreateThread(attr: pointer; size: pointer; addr: pointer; p: pointer; flag: Integer; var thread_id: Integer): pointer;
    external "kernel32.dll" name "CreateThread";
function Abcd(p: pointer): integer;
begin
    tid := SysThreadId();
    // 检查全局缓存的退出标识来结束线程
    while not (GetGlobalCache("exit", v), v) and not (GetGlobalCache(IntToStr(tid), v1)) do
    begin
        sleep(random(3000));
    end;
    WriteLn("\r\n tid:", tid, " done");
    return 1;
end;