工具






4.98/5 (48投票s)
2005年9月15日
76分钟阅读

239812

5483
TOOL(Tiny Object Oriented Language,微型面向对象语言)是一个易于嵌入、面向对象的、类似C++的语言解释器。本文的目的是从一个希望为其项目添加脚本解决方案的人的视角来介绍TOOL解释器和语言。
本次发布的重要新闻
TOOLForge,TOOL的IDE,已得到增强,允许可视化创建XML表单。这项增强功能将为有兴趣在TOOL中使用XMLForms的任何人节省手动创建表单定义文件的痛苦。对于更高级的XMLForm控件类型,还有更多工作要做,但我敢打赌现有功能集将满足大多数用户需求。然而,此程序只能以非源代码形式发布。此项目中商业库代码太多,别无选择。有关如何使用TOOLForge调试TOOL脚本的更多信息,请参见下文。
引言
TOOL(Tiny Object Oriented Language,微型面向对象语言)是一个易于嵌入、面向对象的、类似C++的语言解释器。该语言,甚至TOOL引擎的核心很大一部分,都基于BOB项目,该项目最初由David Betz开发,并在《Dr. Dobb's Journal》先前刊出的期号中有所介绍。
最初的语言解释器完全用K&R风格的C语言实现。TOOL解释器采用了原始BOB解释器的一种面向对象的实现方式,并包含了对原始项目的无数其他改进。通过将原始项目的功��封装到一组小的、可互操作的C++类中,实现了面向对象的重新设计。然后将这些类聚合到一个单一的容器类中,该类充当整个系统的外观。
重新设计原始解释器的一个主要目的是使代码符合“更现代的标准”。然而,将解释器封装到单一容器类中的关键优点是,任何应用程序都可以通过调用一个单一类来轻松创建“解释器上下文”。应用程序还可以通过同时拥有多个包装器类实例来运行多个“独立”的解释器会话。
本文的目的是从一个希望为其项目添加脚本解决方案的人的视角来介绍TOOL解释器和语言。为此,将讨论以下主题:
- 构成TOOL核心的C++类设计。
- 如何将TOOL集成到您的应用程序中。
- 如何为您的自定义需求扩展TOOL。
- 如何使用TOOL进行编程。
- 对TOOL类框架的考察,这是一个用TOOL语言编写的类集,它提供了一组经过测试且可立即使用的TOOL类,并且还说明了所有TOOL内置函数(TOOL API)的完整集合。
如今,人们对解释器的兴趣似乎达到了前所未有的高度,并且有许多解释器和技术可供选择。在这种情况下,为什么有人要考虑将TOOL引擎作为项目解决方案的一部分呢?以下是促使我将TOOL纳入我自己的产品中的一些考虑因素的简短列表。
- 无依赖项。当考虑包含一个不是操作系统安装的集成组件的脚本系统时,为了让您的产品运行,它将依赖于第三方产品的正确安装和配置。这种依赖性是一个潜在的“断点”,因此是我希望尽可能避免的。
- 易于嵌入。将第三方脚本引擎真正嵌入到您的解决方案中并非易事;特别是当您的目标之一是与脚本上下文在应用程序上下文之间交换数据时(实际上是采用解释器的方式,使其成为项目的一个组成部分,而不仅仅是“贴在旁边”)。通常,必须使用API或语法在两个上下文之间“编组”数据,而这并不总是容易理解或直观的。因此,为了解决这个问题,我使用TOOL的一个目标是使两个上下文之间的数据交换尽可能简单。为此,我在TOOL的设计中包含了一个应用程序上下文对象。此应用程序上下文提供了一种简单、松散耦合但有效的机制,用于在应用程序环境和脚本上下文之间交换数据。
- 易于扩展。在某些情况下,将脚本系统嵌入到项目中的部分原因是将应用程序的特定功能暴露给脚本上下文(实际上是使您的程序可编程)。对于当今大多数可用的解释器来说,扩展它们可能是一项艰巨的任务。在考察一些当前可用的更常见的解释器的源代码时,我发现有几十甚至几百个文件,其组织结构对普通观察者来说并不容易理解。在这种情况下,作为“外部人士”,很难理解在哪里以及编辑什么来扩展这些引擎的功能。如果您的项目是“典型的”,那么它很可能时间紧迫,您将无法花时间去理解这些项目的内部组织,直到能够修改它们的运行时。而且,即使您成功了,现在您可能拥有一份定制的“一次性”版本,它会带来自己维护的挑战,并可能需要不断验证您对选定引擎所有未来版本的扩展。与此直接相反的是,TOOL仍然是一个易于操作的引擎,并且非常容易扩展。本文将彻底讨论扩展TOOL的技巧。
- 无许可烦恼。即使所谓的“开源”解释器引擎,如果将其作为商业产品的一部分包含进来,也可能附带额外的许可费和其他费用。对于“低销量”产品,这类成本可能是 prohibitive 的。在我(您、我、和水冷却器)之间,似乎有些不对劲,有人认为他们有权为“应该免费”的东西收费,所以我倾向于出于原则避开这些情况。
构成TOOL的C++类的设计
如前所述,TOOL由少数几个核心类组成。此项目包含的大部分文件是对该基本引擎的扩展。由于没有足够的篇幅深入探讨所有这些类,我将不得不把扩展分析留给读者,因此我的目标将是努力让您理解TOOL引擎中的核心类。我希望提供足够的信息让您了解该项目;但要深入了解,最好的方式是与项目搏斗。
列表 1。驱动TOOL引擎的简单示例。这里我们展示了一个名为“oArgs
”的数据交换变量,它将用于在应用程序和脚本引擎之间共享数据。“oArgs
”然后被传递到一个VMFacade
对象的实例中,该对象是整个TOOL脚本引擎的容器。运行脚本所需的所有操作就是调用Facade对象上的编译和执行。脚本可以通过oArgs
容器将变量返回给应用程序。
int main(int argc, char* argv[]) { SCRIPT_ARGUMENTS oArgs; oArgs.insert( SCRIPT_ARGUMENTS::value_type( std::string( "TestVector" ) , new CAppContextVariant( "On" ) ) ); oArgs.insert( SCRIPT_ARGUMENTS::value_type( std::string( "TestStack" ) , new CAppContextVariant( "On" ) ) ); oArgs.insert( SCRIPT_ARGUMENTS::value_type( std::string( "TestQueue" ) , new CAppContextVariant( "On" ) ) ); oArgs.insert( SCRIPT_ARGUMENTS::value_type( std::string( "TestMap" ) , new CAppContextVariant( "On" ) ) ); oArgs.insert( SCRIPT_ARGUMENTS::value_type( std::string( "TestByteArray" ) , new CAppContextVariant( "On" ) ) ); oArgs.insert( SCRIPT_ARGUMENTS::value_type( std::string( "TestString" ) , new CAppContextVariant( "On" ) ) ); VMFacade* poScript = new VMFacade(); poScript->SetVar( &oArgs ); poScript->CompileFile( "C:\\Projects\\ScriptEngineTester\\CoreClasses.tool" ); poScript->Execute( "TestAll" ); return( 0 ); }
TOOL的用户将遇到的第一个类是VMFacade
类(在VMFacade.h中声明,在VMFacade.cpp中实现)。该类是聚合整个TOOL系统所有组件的单一包装器类;您将在您的项目中实例化它以获取脚本引擎的实例。从列表#1中的简短示例中,您可以看到这个类是多么容易使用。创建该类的实例后,只需调用CompileFile()
或CompileText()
来编译文件(或文本块,如果您愿意)。
如果您需要与脚本引擎共享程序中的变量,或者需要从脚本执行中检索结果,您可以通过将参数/变量放入SCRIPT_ARGUMENTS
类的实例来轻松实现。这个基于STL派生的映射被设置为存储可在应用程序和脚本环境之间交换的所有变量类型。应用程序上下文中的所有变量都存储在字符串键下,值类型是对象变体类型,可以是以下类型之一:字符串、长整型、布尔型、双精度浮点型、DWORD、日期时间、字节数组,以及NULL对象类型。CappContextVariant
类在文件:VMCoreVirtualMachine.h中声明。
脚本在运行时可以通过调用TOOL API函数GetAppEnvField()
按名称从容器中提取值。脚本还可以使用该上下文通过TOOL API函数SetAppEnvField()
将脚本执行中创建的变量返回给宿主应用程序。
为了促进宿主进程和脚本环境之间的通信,请声明一个SCRIPT_ARGUMENTS
类的实例,将其放入变量,然后通过调用VMFacade
对象上的SetVar()
将其与VMFacade
类共享。脚本完成后,宿主应用程序可以检查此映射以获取脚本引擎返回给应用程序的任何结果。
完成所有这些设置后,下一步是通过调用Facade上的Execute()
方法来调用脚本引擎。TOOL引擎的一个优点是,您可以通过在调用Execute()
时声明入口点来指定脚本的入口点。当您想基于主应用程序中的逻辑“选择运行哪个”时,如果一个脚本文件中包含多个脚本程序,此功能可能会非常方便。此功能的另一种用法是定义所有脚本的“标准接口”,然后简单地定义所有脚本,使其具有相同的入口点。
我在我的项目中都使用了这两种方法。第一种方法在您希望在一个脚本文件中拥有多个入口点(例如,为您的项目提供一种宏语言系统)时很有用。第二种方法在您想描述一组独立脚本文件,并且这些文件都将由宿主程序以与“脚本对象”相同的方式调用时很有用。在我的情况下,我成功地采用了Init/Run语义,其中所有脚本都必须包含Init()
和Run()
函数来执行特定于该脚本的工作。
深入VMFacade
类之后,遇到的下一个主要类是VMCore
类(在VMCore.h中声明,在VMCore.cpp中实现)。该类也是整个系统的外观类,但它执行更多详细操作,以将TOOL运行时缝合在一起。因此,该类聚合了TOOL引擎的所有其他主要组件。VMCore
类驱动TOOL系统的实际编译和执行操作。它还启动TOOL中的所有其他主要组件。作为构建TOOL引擎的一部分,该类向所有主要组件提供指向其他主要组件的指针引用。最后,该类还会捕获TOOL引擎抛出的所有异常。如果脚本中检测到语法错误,则在编译期间会抛出异常。如果TOOL引擎执行的完整性检查失败,脚本执行期间也会抛出异常。最后,脚本本身可以通过调用TOOL API函数Throw()
来引发异常。
TOOL主要组件之间深度“交叉连接”部分原因在于历史原因,即TOOL的原始设计和实现基于BOB引擎。即使主要对象是基于广泛的功能区域设计的,但在某些区域也无法完全分离(就像在“真实的”计算机系统中无法完全分离内存和CPU一样)。即便如此,我希望您会发现我将BOB项目重新包装到这组特定的类中是合理且连贯的。
更仔细地查看VMCore
中的内容,您会发现TOOL系统“底部”有一个VMCoreVirtualChip
(在VMCoreVirtualChip.h中声明)。该类是TOOL系统的“CPU”。在这个类中,您会找到诸如解释代码的向量、程序计数器、机器的堆栈和帧指针以及主堆存储区等内容。
在下一个级别,是VMCoreVirtualMachine
类(在VMCoreVirtualMachine.h中声明),它负责管理TOOL堆,并充当TOOL系统的主要内存管理器类。因此,该类还将与VMCoreVirtualChip
一起定义和管理脚本程序中包含的所有变量和字面量的简单字典。派生自VMCoreVirtualMachine
类的是VMCoreVirtualOpSys
类。这个类定义和实现了所有内置函数(基本上是TOOL引擎的全部API)。VMCoreVirtualOpSys
就是这样,它是TOOL引擎提供的“操作系统”,也是扩展引擎所在的位置。当您想在您的TOOL版本中添加一个新的内置函数时,您可能会在VMCoreVirtualOpSys
类中实现它。
继续我们从TOOL核心之旅,您会看到有一个VMCoreCompiler
类,它继承自VMCoreParserScanner
类。解析器-扫描器对象负责扫描和标记脚本文本,而编译器将驱动扫描器,并对扫描器-解析器输出进行操作,以组装脚本运行时执行的字节码数组。
在TOOL的最高级别,是VMCoreInterpreter
类,负责执行VMCoreCompiler
生成的编译代码向量。解释器充当一个大的切换扫描器。它扫描编译器生成的字节码,然后根据检测到的字节码执行一系列特定的操作。
TOOL引擎如何工作?
TOOL实现为编译器和解释器的混合体(一个“编释器”?)。当一个函数被定义时,它被编译成面向堆栈的字节码机器的指令。当函数被调用时,这些字节码指令被解释。与纯粹的解释器相比,这种方法的优点是语法分析只在编译时进行一次。这大大加快了函数执行速度,并为构建一个不包含编译器在内的运行时系统提供了可能性。事实上,在我的某些脚本(不太复杂,但也不太简单)中,每秒可以执行超过150次完整的系统执行。您的体验可能会有所不同,但TOOL虽然远不如编译代码快,但也不是吃素的。
执行TOOL编译器生成的字节码的虚拟机具有一组寄存器、一个堆栈和一个堆。这些结构包含在VMVirtualChip
类中。所有指令都从堆栈获取参数并返回结果到堆栈。字面量存储在代码对象本身中,并通过偏移量引用。分支指令测试堆栈顶部的��(不弹出堆栈),并相应地分支。函数参数通过堆栈传递,函数值通过堆栈顶返回。当讨论扩展TOOL引擎的技巧时,我们将更深入地研究这个概念。
在TOOL脚本类中,所有成员函数都是虚函数。这意味着当调用成员函数时,解释器必须确定调用哪个成员函数的实现。这是通过SEND
字节码实现的,它使用堆栈中的选择器(实际上只是一个包含成员函数名称的字符串),并与对象类关联的方法字典来确定使用哪个成员函数。如果查找失败,则检查基类的字典。此过程继续进行,沿着基类链查找,直到找到成员函数或没有基类。如果找到与选择器对应的成员函数,它将替换堆栈上的选择器,并将控制权转移到成员函数,就像对于普通函数一样。如果找不到成员函数,则报告错误并中止解释器。我为该机制添加的一个扩展是“强制转换调用”,语法为“*->
”。此调用允许脚本编写者将类“强制转换为”其基类之一(运算符左侧指定的名称),然后调用运算符右侧指定的选择器。在该运算符作用的脚本文件“coreclasses.tool”中可以找到该运算符的几个示例。此运算符主要有两个作用。一是它使得调用基类成为显式的,这在多个父类共享相同的函数名时特别有用,并且也为脚本代码提供了作者关于该类方法执行的具体意图的注释。
TOOL变量类型已扩展为支持以下基本数据类型:长整型、双精度浮点型、字节型、字符串型、标记型、字节数组型、向量型、类、日期时间型、队列型、映射型、堆栈型、等待对象型、NT内核句柄、DWORD、NT文件句柄、文件查找句柄、zib文件、SQLite数据库、ODBC连接和nil。在内部,解释器使用四种类型:类、编译后的字节码函数、内置函数头和变量。在可以存储值的地方,一个标签会指示当前存储的值类型。这是“v_type
”字段在VMVariant
类中,声明在“VMCoreGlobal.h”。
对象、向量和字节码对象都由值结构数组表示。对于字节码对象,向量中的第一个元素是指向函数字节码字符串的指针,其余元素是字节码指令引用的字面量。类对象是向量,其中第一个元素是指向类对象的指针,其余元素是非静态成员变量的值。内置函数只是指向实现内置函数的C函数的指针。变量是指向变量的全局符号和类字典条目的指针。每个类还有一个数据成员和成员函数的字典。
除了堆栈,TOOL还使用堆来存储对象、向量和字符串。TOOL的当前实现使用C堆和C函数malloc
和free
来管理堆空间,并使用紧凑型内存管理器。但是,我必须承认, this version of TOOL 的堆管理并非完全垃圾回收。这是由于引擎的演进特性以及一些新数据类型实际上是受VMVariant
对象管理的C++类。这就是为什么TOOL API调用“FreeObject
”会出现在TOOL API中。包含此函数是为了在脚本使用完后清理堆分配的C++类。我完全认识到这是当前实现中的一个弱点,也是我计划在新版本引擎中解决的问题。但目前,我已经习惯了“它的邪恶”,因此没有把解决当前实现中不足作为优先事项。
运算符重载
在TOOL解释器中,运算符可以根据运算符的LHS/RHS值具有多种不同的含义。考虑数组索引运算符“[]
”的情况。在这种情况下,该运算符可以有多种不同的解释。如果LHS是Map
对象,它应该执行一个动作,如果LHS是Vector
对象,它应该执行一个完全不同的操作。数学运算也需要类似的方法,例如“+
”或“-
”,如果运算是在不同类型的变量上执行的,那么其中一个必须被“提升”到另一个变量类型。鼓励有兴趣的读者研究VMInterpreter如何处理OP_ADD
和OP_VREF
字节码以获取有关如何扩展运算符重载的更多详细信息。如果您想为TOOL引擎添加更多变量类型并希望对不同的“标准运算符”有特定的行为,掌握这一点很重要。
函数重载
在TOOL API中,内置函数可以被“重载”,也就是说,它们可以被编写成接受不同的参数计数和类型。这是一个例子(还有许多其他例子),是VMCoreVirtualOpSys
类中的HandlerNewDateTime()
方法。此处理程序被编码为接受1个或6个参数。如果传递了一个参数,则检查其类型以验证它是否为DT_DATETIME
类型。如果传递了六个参数,则检查每个参数以验证它们是否为DT_INTEGER
类型。
此功能最极端的实现是在函数HandlerStringFormat()
中,它是一个椭圆函数的实现;也就是说,它可以接受任意数量和类型的参数,这正是您对sprintf
类型函数所期望的。
函数定义的这种灵活性是解释器将函数参数推送到堆栈的方式的自然结果。解释器没有任何关于任何内置函数所需的参数数量和类型的信息。它将简单地将找到的所有参数推送到堆栈上。此功能使任何内置函数都可以编写成接受来自解释器的所有类型的参数。
然而,这种灵活性也有“反面”,那就是所有内置函数都应该对所有参数和参数计数进行健全性检查,以验证它将要操作的对象是否符合函数预期。
所有准备就绪,即将起航
好了,这就是我所能提供的关于TOOL的全面介绍,而不会陷入所有细节。希望我已经为 TOOL 引擎的“地形”提供了一个足够的介绍,以便您在分析 TOOL 的内部工作原理时能够有坚实的基础。然而,请放心,即使您不知道所有内部零件是如何工作的,您也可以轻松地使用和扩展TOOL。我只是想带您四处看看这个项目,这样当您自己试用这个大家伙时,您就知道去哪里找零件了。
用TOOL编程
下面的示例演示了一个简单的示例程序;一个使用TOOL语言编写的计算阶乘的函数。注意:还鼓励读者查看此发行版中包含的示例脚本。
// factorial program // Factorial( iValue ) { return( ( iValue == 1 ) ? 1 : ( iValue * Factorial( iValue - 1 ) ) ); } Run( ;iLoop ) { iLoop = Long( 0 ); for ( iLoop = 1; iLoop < 10 iLoop++ ) { Echo( StringFormat( "Factorial for % is %", iLoop, Factorial( iLoop ) ) ); } }
您可以看到这个程序与它的C语言对应物非常相似。唯一明显的区别是,在传递给Factorial
函数的参数以及该函数本身的返回类型中,缺少iValue
参数的声明。这是因为在TOOL中不需要声明变量类型;尽管为了清晰和维护,声明和键入所有变量仍然被认为是“良好的风格”。其副作用是任何变量都可以接受任何类型的值。
第一个样本中其他值得注意的点是声明在Run
函数的正式参数列表中的变量。在这里,您可以看到变量iLoop
后面跟着一个分号。这种语法用于将iLoop
变量定义为“局部作用域”于Run
函数。默认情况下,TOOL脚本中的所有变量都采用全局作用域,除非使用这种专门的局部作用域语法进行声明。
重要提示:尽管TOOL是从脚本编写者的角度来看是一种弱类型语言,但TOOL解释器会进行类型检查,以验证传递给其例程的变量类型。如果变量不是正确的类型,解释器将停止脚本的执行。
再次,您应该主要注意到上面示例程序与C语言中的类似程序有多么相似。另外,请注意该程序使用StringFormat
函数创建格式化字符串,并使用Echo
函数显示结果。这些函数将在本文档稍后详细解释。TOOL中的StringFormat
函数按顺序将其每个参数打印到输出字符串中。它能够打印任何类型的参数,并自动进行适当格式化。
除了支持类C的表达式和控制结构外,TOOL还支持类C++的类。同样,由于TOOL是一种无类型语言,类定义的语法与C++略有不同,但足够相似,应该很容易从一种语言过渡到另一种语言。
下一个示例显示了一个简单的类定义,该定义定义了一个名为Foo
的类,其成员为m_A
和m_B
,一个静态成员m_Last
,以及一个静态成员函数GetLast
。与C++不同,在类定义中不必声明所有成员函数;只需声明静态成员函数即可。但是,必须在类定义中声明所有数据成员。
// class declaration // class Foo { m_A; m_B; static m_Last; static GetLast(); } Foo::Foo( AValue, BValue ) { m_A = AValue; m_B = BValue; m_Last = this; return( this ); } Foo::GetLast() { return( m_Last ); }
与C++一样,类的新的对象使用构造函数进行初始化,其名称与类本身相同。在此示例中,构造函数接受两个参数,它们是成员变量m_A
和m_B
的初始值。它还记住创建的最后一个Foo
类型对象实例作为静态成员变量m_Last
。最后,构造函数返回新对象。对于不熟悉C++的读者来说,变量this
指的是调用成员函数的对象。它是隐式传递给每个非静态成员函数的参数。在这种情况下,它是刚刚创建的新对象。
在TOOL中,所有类数据成员都是隐式保护的。访问或修改成员变量值的唯一方法是通过成员函数。如果您需要在成员函数外部访问成员变量,则必须提供访问成员函数的方法。
我们将在下面继续完善Foo
类,以展示如何设置成员变量的值。最后,我们将展示一个成员函数,它显示Foo
类类型的所有对象的m_A
和m_B
之间的数字,以及一个Run
函数,该函数创建一些对象并操作它们。new
运算符创建跟在其后面的类的��对象。类名后面的括号中的表达式是要传递给构造函数的参数。
// continuing to define the Foo class // Foo::GetAValue() { return( m_A ); } Foo::GetBValue() { return( m_B ); } Foo::SetAvalue( NewAValue ) { m_A = NewAValue; } Foo::SetBValue( NewBValue ) { m_B = NewBValue; } Foo::GetSpan() { return( m_B - m_A ); } Run( ;poFoo1, poFoo2 ) { poFoo1 = new Foo( 1 , 2 ); poFoo2 = new Foo( 11, 22 ); Echo( "Foo1 Span Is: " + poFoo1->GetSpan() ); Echo( "Foo2 Span Is: " + poFoo2->GetSpan() ); }
TOOL还支持一种类似于Java语言的继承模型,它允许一个类派生自另一个类。派生类将继承基类的行为,并可能添加自己的行为。TOOL只支持单继承;因此,每个类最多只能有一个基类。下一个代码示例定义了一个从前面定义的基类Foo
派生的类Bar
。
Bar
类将拥有从父类Foo
继承的成员变量m_A
和m_B
,以及额外的成员变量m_C
。Bar
的构造函数需要初始化这个新成员变量,并执行通常为Foo
类对象进行的初始化。下面的示例说明了如何做到这一点。
// class derivation in TOOL // class Bar : Foo { m_C; } Bar::Bar( AValue, BValue, CValue ) { Foo*->Foo( AValue, BValue ); m_C = CValue; return( this ); }
这个例子说明了TOOL和C++对象之间又一个区别。在C++中,不能调用构造函数来初始化已存在的对象。然而,在TOOL中是允许的,因此Foo
构造函数可用于对Foo
和Bar
类进行通用初始化。细心的读者可能会注意到在Bar
构造函数中使用的“*->
”操作。在TOOL中,这称为“强制转换调用运算符”。该运算符的作用是将“this
”强制转换为运算符左侧命名的基类类型,然后调用运算符右侧命名的基类中的函数。这种语法对于长类层次结构中的函数重载特别有用,因为可以在调用函数之前显式地将其强制转换为正确的父类类型。
简要提及编程风格
由于TOOL是一种弱类型语言,这意味着不需要指定变量的类型,我发现使用某种方法对脚本变量的数据类型进行分类(以便使代码更容易被他人理解)很有用。因此,在本文附带的所有脚本样本中,都使用了简化的匈牙利命名法。以下是所用表示法的样本。
Data Type Hungarian "Marker" Example
String s sMyString
Number i iValue
Object o oValue
我并不是想以任何特定的方式推广匈牙利命名法,因为我知道喜欢它的人和不喜欢它的人一样多,我只是觉得在努力理解大型项目时,它很有用。
关于建议的编程风格
TOOL是一种弱类型语言,这意味着不必指定变量的类型。因此,为了使代码更容易被他人理解,对变量中存储的数据进行分类的某种方法将大有裨益。因此,鼓励在所有TOOL程序脚本中使用简化的匈牙利命名法。以下是所用表示法的样本。
数据类型 |
建议标记 |
示例 |
字符串 |
s |
|
长整型 |
l 或 i |
|
双精度浮点型 |
dbl |
|
字节型 |
b |
|
堆栈 |
ostk |
|
向量 |
ovec |
|
映射 |
omap |
|
日期时间 |
dt |
|
队列 |
oqueue |
|
句柄 |
h |
|
Color |
clr |
|
DWORD |
dw |
|
数据库连接 |
oDB |
|
计算器 |
ocalc |
|
类实例 |
po |
|
创建和分配TOOL变量
TOOL中有几种类型的变量。它们都列在下面以及创建变量的TOOL调用。熟悉C++/C#/Java的读者可以认为这些API调用是变量的构造函数调用。
Vector(); // accepts no arguments String( 255 ); // reserve 255 characters for length String( "Hello" ); // create and assign value to string Queue(); // accepts no arguments Stack(); // accepts no arguments Map(); // accepts no arguments Tokenizer( sToTokenize, sDelimiter ); ByteArray(); // create byte array with default parameters ByteArray( Long( 25 ) ); // create byte array with 25 elements ByteArray( Long( 25 ), Long( 5 ) ); // create byte array with 25 // elements and grow factor of 5 // elements Long(); // create a long with value zero Long( 50 ); // assign value to a long variable Handle( 0 ); // create a handle type MUST have init value DateTime(); // create a datetime value equal to 'now' DateTime( dtOther ); // copy construct a date time variable DateTime( 2004, 01, 01, 12, 15, 0 ); // create date time with init // value of 01/01/2004 12:15:00 Color(); // create color with RGB 255,255,255 Color( 0, 0, 0, ); // create color with RGB 0,0,0 Color( clrOther ); // copy construct a color variable DWORD(); // create a DWORD with value of zero DWORD( dwOther ); // copy construct a DWORD variable DWORD( Long( 5 ) ); // construct a DWORD from a number value Database(); // create an ODBC database variable Calculator(); // create an calculator / equation handler Double(); // create a double with a value of 0.00 Double( Long( 6 ) ); // create a double from a long value Double( dblOther ); // copy construct a double variable Double( "123.45" ); // create a double from a string value Byte(); // create a byte initialized to zero/false Byte( bOther ); // copy construct a byte type variable Byte( Long( 10 ) ); // set byte variable from number // NOTE: limit of 0 - 255 for the number Page(); // report page object Table(); // table object in a report page Color(); // color object in a report page Database(); // odbc database connection object Calculator(); // function evaluation object NullValue(); // TOOL NULL Value MiniDatabase(); // SQL LITE Wrapper Object UnZipper(); // zlib Wrapper Object for unzip operations ZipMaker(); // zkib Wrapper Object for zip operations FileBuilder(); // string collection object for creating file output DelimitedFile(); // wrapper object for delimited file I/O ExcelExporter(); // Wrapper for an excel workbook export object
TOOL运行时类型检查
正如前面在另一个主题中解释过的,由于TOOL是一种相对弱类型的语言;同时,TOOL解释器会验证传递给TOOL API函数的所有参数是否都是正确的类型,TOOL为脚本编写者提供了一整套RTTI(运行时类型识别)函数,以帮助他们编写稳定可靠的脚本程序。
使用这些函数可以通过对所有变量进行的参数验证来创建更健壮的脚本。这些函数的另一个用途是编写基于正在测试的变量类型条件分支的脚本。本文档稍后将提供这两种用法的示例。RTTI函数的完整列表如下。
IsNull( oVarToTest ); // is the variable NULL? IsClass( oVarToTest ); // is the variable a TOOL script class? IsVector( oVarToTest ); // is the variable a vector type? IsNumber( oVarToTest ); // is the variable a long number? IsString( oVarToTest ); // is the variable a string? IsFile( oVarToTest ); // is the variable a file? IsKernelObject( oVarToTest ); // is the variable an NT kernel object? IsHandle( oVarToTest ); // is the variable an NT style handle? IsDateTime( oVarToTest ); // is the variable a TOOL date time type? IsDWord( oVarToTest ); // is the variable a DWORD? IsNTHandle( oVarToTest ); // is the variable an NT handle? IsFindHandle( oVarToTest ); // is the variable a "file find handle"? IsQueue( oVarToTest ); // is the variable a TOOL queue type? IsStack( oVarToTest ); // is the variable a TOOL stack type? IsHashTable( oVarToTest ); // is the variable a TOOL map type? IsConstant( oVarToTest ); // is the variable declared as const? IsConstCast( oVarToTest ); // is the variable cast as const? IsDouble( oVarToTest ); // is the variable a double? IsByte( oVarToTest ); // is the variable a byte? IsByteArray( oVarToTest ); // is the variable a byte array type? IsDatabase( oVarToTest ); // is the variable a database connection? IsReportPage( oVarToTest ); // is the variable a report page? IsPageRegion( oVarToTest ); // is the variable a report page region? IsPageTable( oVarToTest ); // is the variable a report page table? IsColor( oVarToTest ); // is the variable a report page color? IsMiniDatabase( oVarToTest ); // is the variable a sqlite database wrapper? IsUnZipper( oVarToTest ); // is the variable a zlib unzipper wrapper? IsZipMaker( oVarToTest ); // is the variable a zlib zipper wrapper? IsMiniRowSet( oVarToTest ); // is the variable a SQLite rowset wrapper? IsFileBuilder( oVarToTest ); // is the variable a file builder object? IsDelimitedFile( oVarToTest ); // is the variable a delimited file I/O wrapper? IsExcelExporter( oVarToTest ); // is the variable an Excel workbook // creator wrapper?
注意:请参阅文件ToolBox.Tool中的CRunTimeTypeInfo
类,该类将所有RTTI API整合到一个TOOL类中。所示的类样本是所有TOOL框架类的基类,因为所有TOOL类都使用变量类型检查来确保将正确类型的变量传递给TOOL框架的TOOL API函数。
TOOL日期时间操作
数据处理和报告经常需要处理日期或日期范围。事实上,日期时间处理可能是数据处理/报告数据类型中“第二常见”的,只有字符串数据的使用频率更高。因为DateTime是如此常见的需求,TOOL提供了一种内置的数据类型来处理这种类型的值;并且还提供了一组API函数,用于处理DateTime值最常见的操作类型。这些函数如下所示,并且该系列函数的类包装器可以在ToolBox.Tool文件中的CDateTime
类中找到。
dtNow = GetDate(); dtLater = DateAdd( sModPart, dtBase, iValue ); dtEarly = DateSub( sModPart, dtBase, iValue ); iPart = DatePart( sDatePart ); dtNew = DateSetHourPart( dtToChange, iHour ); dtNew = DateSetMinsPart( dtToChange, iMins ); dtNew = DateSetSecsPart( dtToChange, iSecs ); dtNew = DateSetHMS( dtToChange, iHour, iMins, iSecs ); sText = DateToString( dtDate ); dtNew = StringToDate( sText ); bLess = DateCompare( dtBase, dtOther ); iSpan = DateDiff( dtBase, dtOther );
TOOL文件和目录操作
目录相关函数
对于某些类型的数据处理应用程序,目录操作非常常见。为了支持这些类型的作业,TOOL包含了一些专门用于这些相关任务的函数。以下类说明了这些函数类型的用法。
此部分TOOL API要研究的第一个TOOL类位于ToolBox.Tool文件中,名为CCurrentDirectory
,它实现了对TOOL中与当前进程目录操作相关的函数��类包装器。当前进程目录通常由主要从单个目录操作并将其用作所有文件相关操作的“默认路径根”的脚本设置。
下一个与目录相关的TOOL类封装了实际操作磁盘目录结构的所有函数。存在用于在硬盘驱动器上移动、复制、比较、重命名和删除整个目录树的TOOL API函数。有关更多信息,请参阅ToolBox.Tool文件中的CDirectory
类。
目录和文件遍历相关函数
某些数据传输任务需要定期扫描文件和目录结构作为其处理过程的一部分。为了支持这些类型的作业,TOOL提供了一组用于遍历目录和文件的函数。这些函数类型的类包装器可以在ToolBox.Tool文件中的CFind
类中找到。
有关使用这些类的实际示例,请参阅TOOL网络驱动器操作主题中找到的简单网络文件备份程序。该示例将演示TOOL如何用于系统管理任务以及数据I/O、转换和报告任务。
TOOL也可用于严格数据传输和报告之外的各种任务,这增加了TOOL语言的附加价值,因为TOOL系统包含了足够多的设施,可以用于所有类型的操作。
文件相关函数
TOOL API还提供了与系统文件交互的文件相关函数。TOOL提供了一组用于检查文件属性的函数,另一组用于执行文本文件I/O和序列化TOOL变量、ini文件I/O和文件解析的函数。说明API这些部分的类可以在ToolBox.Tool文件中找到。
有关实现文件属性和文件管理API类包装器的类,请参阅ToolBox.Tool文件中的CFileInfo
。有关封装了所有文本文件I/O的TOOL API函数的TOOL类,请参阅ToolBox.Tool文件中的CTextFile
类。
最后,请参阅ToolBox.Tool文件中的CToolDataFile
类,它提供了TOOL变量值序列化的TOOL API的包装器。该类使用向量接口读写TOOL变量。TOOL序列化当前的工作方式是,脚本开发人员将他们想要保存到TOOL数据文件中的所有变量“加载到一个向量”中。向量元素将按顺序写入数据文件。要恢复值,脚本只需从文件中读回向量数据。
请注意,目前,数据集合(堆栈、队列、映射等)不能自动序列化。但是,如果需求允许这种功能,则可以轻松地将其添加到TOOL运行时引擎中。
TOOL文件I/O处理的最后一个专门领域是INI文件处理。由于INI文件是存储简单分层配置数据的方便易用的方法,它们仍然是存储应用程序参数的普遍方法。因此,作为一种便利,TOOL还提供了一组用于INI文件的函数。一个提供这些API函数包装器的类是ToolBox.Tool文件中的CIniFile
类。
TOOL网络驱动器操作
TOOL提供了一对与连接到网络驱动器相关的函数。一个函数用于连接到网络共享目录,另一个函数用于断开连接。名为CNetworkDrive
的示例类在ToolBox.Tool文件中显示了这些TOOL API函数的类包装器。
在CNetworkDrive
类中,您可以看到TOOL脚本连接到网络驱动器的直接方式。此外,该样本类还有一个有趣的项目,那就是Connect
方法;特别是,用于测试
sDriveLetter = "ERROR";
这显示了另一个用于字符串操作的重载运算符,其中TOOL字符串类型变量可以直接测试相等性,此外还有StringCompare
函数。作为如何使用ToolBox.Tool文件中包含的网络驱动器类(和其他类)的示例,以下程序显示了一个TOOL程序如何编写以执行网络数据文件备份操作。
Run() { CNetworkDrive poNetOut = new CNetworkDrive(); // since the '\' character is an escape character within strings // i.e., \t for tab // \r carriage-return // \n for line-feed // \" for embedded-quote // // we need to use \\ for each \ character that should be made // part of a string // // so in the string below: "\\\\MyServer\\BackupShare" // will resolve to: "\\MyServer\BackupShare" // if ( poNetOut->Connect( "\\\\MyServer\\BackupShare", "Admin", "Password" ) { sNetDrive = poNetOut->GetDriveLetter(); // this example will back up three other computers to // the single backup server // for ( int iLoop = 0; iLoop < 2; iLoop++ ) { // based on the machine being backed up, set up for // that copy operation // if ( iLoop == 0 ) { BackUpSingleServer( sNetDrive, "Workstatation1", "ImportantWork" ); } else if ( iLoop == 1 ) { BackUpSingleServer( sNetDrive, "Workstatation2", "FinanceData" ); } else if ( iLoop == 2 ) { BackUpSingleServer( sNetDrive, "Workstatation3", "CustomerData" ); } } } } // notice the parameter list in the function immediately below. // the first three parameters are inputs to this function and // as such are provided by the calling function. // // the next two parameters, which follow a ; character, are // locally scoped variables // BackUpSingleServer( sNetDrive, sServerName, sServerShare, ; sTargetCopyPath, sLastBackUpPath ) { CNetworkDrive poNetIn = new CNetworkDrive(); sTargetCopyPath = StringCopy( sNetDrive ); sLastBackUpPath = StringCopy( sNetDrive ); // set up the target paths for the backup file // storage // sTargetCopyPath += StringFormat( "\\%.Backup", sServerName ); sLastBackUpPath += StringFormat( "\\%.Prev.Backup", sServerName ); if ( poNetIn->Connect( StringFormat( "\\\\%\\%", sServerName, sServerShare ), "Admin", "Password" ) ) { sInDrive = poNetIn->GetDriveLetter(); // create a directory object to manage the data // copy // CDirectory poDir = new CDirectory( sInDrive ); poDir->CopyDirectory( sTargetCopyPath, sLastBackUpPath ); if ( poDir->CompareToOther( sTargetCopyPath ) ) { // file backup success } else { Echo( FormatString( "Failure during backup of \\\\%\\%.\r\n", sServerName, sServerShare ) ); } poNetIn->Disconnect(); } else { Echo( FormatString( "Failed to backup: \\\\%\\%.\r\n", sServerName, sServerShare ) ); } }
上面的TOOL程序说明了如何在程序中实例化和使用TOOL语言类对象。这个演示程序还说明了TOOL如何在组织中用于多种用途,而不仅仅是作为数据管理和报告系统,因为它展示了如何用TOOL编写简单的网络备份程序。
TOOL注册表操作
由于TOOL开发人员可能需要脚本与系统的注册表进行交互,TOOL也提供了与此主机系统方面交互的API。TOOL提供的支持足以用于字符串和数字的I/O。由于几乎所有TOOL数据类型都可以转换为这两种变量类型,因此TOOL API的这种约束是为了简洁和易用性(TOOL的一个反复出现的设计目标)。
如果需要额外的功能,那么扩展TOOL以支持与系统注册表操作相关的任何新需求将不是一项困难的任务。
与本文档的典型情况一样,此部分TOOL API的类包装器可以在ToolBox.Tool文件中找到,并且在名为CRegistry
的TOOL类中。
TOOL系统环境操作
在某些类型的系统安装中,操作系统环境变量会用于某些目的。对于这些类型的安装中的操作,TOOL提供了允许TOOL脚本读/写系统环境变量的函数。名为CEnvironment
的TOOL类在ToolBox.Tool文件中,说明了TOOL API的这部分。
TOOL进程控制函数
TOOL提供用于启动、检查和监视其他进程的函数。这允许TOOL与已存在的程序进行交互,和/或通过控制TOOL运行时引擎外部的进程来协调系统活动。TOOL可以通过以下调用简单地生成一个辅助进程:
ProcStartNoWait( sProcessCommandLine );
调用此API后,TOOL运行时会将进程命令行转发给操作系统执行,然后立即返回,以不等待新启动的进程。一个相关的函数是查询操作系统,以确定在托管TOOL运行时的操作系统中是否存在特定进程。下面显示了此调用。
IsProcessRunning( sProcessName );
这两个函数可以使TOOL被用作一个易于使用的监视程序(一个保持另一个进程运行的程序)。以下示例说明了此概念。
WatchDog( sProcessName, sProcessCmdLine ) { while ( 1 ) { if ( !IsProcessRunning( sProcessName ) ) { // if process is not running, then start it // ProcStartNoWait( sProcessCmdLine ); } // // since this is an infinite loop, yield control on // each pass through the loop // Sleep( 5000 ); } }
启动外部进程的另一个有趣的TOOL API是:
StartLoggedProcess( sProcssesCmdLine, sFullPathToLogFile );
此API将启动一个进程,然后将该进程的“stdout”中的所有数据捕获到一个传递给该函数的日志文件中。通过这种方式启动进程对于从TOOL系统运行简单的实用程序,然后将这些实用程序的输出捕获到文件中以便将来进行故障排除或分析非常有用。
下一个启动进程的TOOL API是允许TOOL脚本从操作系统获取刚刚启动的进程的进程句柄的API。然后,该句柄可用作“等待句柄”,以便TOOL可以等待进程完成然后再继续。
hHandle = StartProcessGetHandle( sProcessCmdLine ); .... ReleaseHandle( hHandle );
当对进程句柄的兴趣停止时,脚本应该调用ReleaseHandle
。TOOL提供的最后一个进程控制函数是允许TOOL脚本监视和分析辅助进程输出的函数。调用此API时,TOOL将启动并“连接到”进程的输出。这允许工具脚本读取和分析辅助进程的“标准输出”和/或“标准错误”流产生的所有输出。执行此操作的API函数如下。
ProcStartErrorCheck( sRunLine, oVecErrorList );
以下函数说明了如何使用此TOOL API函数。
StartProgramAndCheckResults( sCommandLine; oVecErrorList, bSuccess ) { oVecErrorList = Vector(); bSuccess = Byte( 0 ); // add a series of "error strings" to a list of phrases // the output from the slave process will be examined against the // error list. If the slave process produces any of these phrases // the results of the process execution will be reported as an // error // VectorAddItem( oVecErrorList , "ERROR" ); VectorAddItem( oVecErrorList , "UNAVAILABLE" ); VectorAddItem( oVecErrorList , "DOES NOT EXIST" ); VectorAddItem( oVecErrorList , "INCORRECT SYNTAX" ); VectorAddItem( oVecErrorList , "NO SUCH FILE OR DIRECTORY" ); VectorAddItem( oVecErrorList , "COLUMN DOES NOT ALLOW NULLS" ); VectorAddItem( oVecErrorList , "INVALID OBJECT NAME" ); VectorAddItem( oVecErrorList , "INVALID COLUMN NAME" ); VectorAddItem( oVecErrorList , "CHOOSE ANOTHER" ); VectorAddItem( oVecErrorList , " LEVEL" ); VectorAddItem( oVecErrorList , "FAILED" ); VectorAddItem( oVecErrorList , "FILE NOT FOUND" ); // run a slave process and verify its output. // NOTE: The TOOL script will pause at this point until the // slave process ends. Utility type programs that exit // after completion of a task(s) are the most appropriate // type of process to start with this TOOL API function. // bSuccess = ProcStartErrorCheck( sCommandLine, oVecErrorList ); if ( !bSuccess ) { // since Throw will exit this, make sure to free all // memory used here // VectorClear( oVecErrorList ); FreeObject( oVecErrorList ); Throw( StringFormat( "The program % returned an error.\r\n", sCommandLine ) ); } VectorClear( oVecErrorList ); FreeObject( oVecErrorList ); }
TOOL进程间同步原语
TOOL提供了一套完整的进程同步变量类型。这些变量类型在TOOL脚本程序将通过这些跨进程变量的信号与其他外部程序/进程协调操作的安装中很重要。这些变量也将在TOOL多线程脚本内部使用。
关于这些变量类型适当用途的解释超出了本文档的范围。有关此类信息,请读者参考任何涉及多线程编程技术的参考资料。
TOOL同步变量类型的每个类包装器都在ToolBox.Tool文件中。您可能想检查的第一个变量类型是信号量的类包装器,如您可能回忆的,它是一个带有“计数”的进程间变量。该变量可以被“获取”的进程/线程数量与“计数”支持的数量相同。有关该类,请参阅ToolBox.Tool中的CSemaphore
类。
TOOL支持的下一个进程间变量类型是互斥锁。这种类型的变量一次只能被一个进程和/或线程“获取”。这种类型的变量是一个“互斥”门,并在整个操作系统中充当全局“单线程控制”。该变量类型的TOOL类包装器称为CMutex
,可以在ToolBox.Tool文件中找到。
TOOL支持的最后一个进程间变量类型是事件。事件可用于在进程和/或线程之间“发送信号”,以协调它们之间的活动。正确理解和使用事件可以实现完全独立的进程之间的强大交互。ToolBox.Tool文件中的CEvent
类实现了事件类。
现在我们已经涵盖了TOOL支持的进程间变量的创建和简单管理,接下来的主题(自然是)是如何在“等待集”中使用它们,使用等待集可以使TOOL与其外部进程协调其操作。ToolBox.Tool中的CWaitSet
类展示了使用TOOL等待相关函数的一种可能方法。
处理TOOL集合对象
TOOL提供了几种用于管理数据集合的数据类型。TOOL安装中包含了演示如何使用每种集合类型的示例程序。希望读者在学习集合API函数本身的同时,能够更熟悉TOOL语言中的类声明。
确定任何集合的元素计数
TOOL提供了一个SizeOf
函数来确定任何集合类型变量中的元素数量。以下类型的变量可以传递给此函数。
- 字符串
- 向量
- 堆栈
- 队列
- 映射
- 字节数组
如下所示:
lElements = SizeOf( oCollectionTypeVar );
TOOL字符串操作
字符串可能是几乎所有数据处理任务中最常用的数据类型。由于这种情况,字符串操作可能是数据处理中最常见的操作类型。
因此,TOOL的字符串API是专门针对单个数据类型“最大的单一部分”。字符串相关API函数的完整列表如下。由于字符串相关API非常丰富,示例类可能是此发行版提供的样本中最大的。
iResult = StringCompare( sOne, sTwo ); // returns -1 if sOne < sTwo // 0 if sOne = sTwo // 1 if sOne > sTwo sResult = FillString( sBase, iStartAt, iSpanFor, sFillWith ); iIndex = IndexOf( sBase, sToFind ); sResult = TrimString( sToTrim, sType ); // sType = "l" trim left // = "r" trim right // = "b" trim both iLength = StringLength( sText ); sResult = ToLower( sInput ); sResult = ToUpper( sInput ); sResult = SubString( sInput, iStartAt, iLength ); sResult = StringGetAt( sInput, iIndex ); sResult = StringConcat( sInput, sToAppend ); sResult = StringReplace( sInput, sToReplace, sReplaceWith ); sResult = StringFormat( sFormatString, ... ); sResult = StringBoundedBy( sInput, sLowerToken, sUpperToken ); sResult = BoundedStringReplace( sIn, sLowToken, sUpToken, sNewText ); sResult = TrimQuotes( sInput ); sOutput = StringCopy( sInput ); sResult = PutQuote( sInput ); sResult = PadLeft( sInput, iPadSize ); sResult = PadRight( sInput, iPadSize ); bNumber = IsStringNumeric( sInput );
一个相当大的TOOL类,实现了TOOL字符串API函数的类包装器,是ToolBox.Tool文件中的CString
类。该类包含大量功能(因为通常对字符串类型数据执行的操作类型很多),因此它是本文档中迄今为止最大的。
该类还说明了TOOL提供的一些字符串到其他数据类型的转换函数。即便如此,它仍然是一个直接的类,因此应该不会给读者带来困难。
TOOL提供的另一个非常重要的字符串函数是“StringFormat
”函数。这个椭圆函数非常有用,它可以将任意数量的数据值组合成一个格式字符串,然后将它们插入到格式字符串中。请参阅下面的示例。
sResult = StringFormat( "Today's data is: %", GetDate() ); sResult = StringFormat( "The value mapped in the under the key % is %", iKey, MapFindByKey( oMap, iKey ) );
使用StringFormat
函数时,格式字符串在每个要插入到输出字符串中的“字符串值”的位置都包含一个“%”字符。
要“转义”字符“%”并输出实际的百分号,请使用“%%”,它将在该位置在结果字符串中输出一个百分号。
虽然传递给StringFormat
函数的参数数量没有实际限制,但结果字符串的长度限制为4KB。另一个非常重要的字符串运算符是“+
”号,它也可以用于字符串连接,如下所示。
sString = "The quick brown fox"; sString += "jumped over the lazy dog"; sString += "and the cow \"jumped\" over the moon";
在上面的示例中,您还可以看到“\”字符可以用来“转义”应该嵌入到结果字符串中的双引号。
在TOOL中令牌化字符串
要对字符串类型变量进行简单的令牌化,TOOL提供了一个内置的令牌化器。TOOL字符串令牌化器函数的类包装器可以在ToolBox.Tool文件中的CTokenizer
类中找到。
TOOL向量操作
TOOL提供了一组特定于操作Vector类型变量的函数。对于所有这些函数,每个函数的第一��数是函数应在其上操作的Vector对象。下面将介绍每个操作。
TOOL Vector对象可以包含“混合集合”,这意味着集合中存储的每个元素都可以是不同的类型。
VectorSetAtIndex( oVector, iIndex, vValue ); VectorGetAtIndex( oVector, iIndex ); VectorAddItem( oVector, vValue ); VectorDelAtIndex( oVector, iIndex ); VectorClear( oVector );
请参阅ToolBox.Tool文件中的CVector
类,这是一个TOOL类,它为所有与Vector相关的TOOL函数提供了类包装器。该示例再次说明了如何声明TOOL类,并提供了TOOL向量相关函数的相当完整的Vector对象包装器实现。
在检查CVector
类示例时,可以看到Vector类不仅提供了对所有TOOL向量相关函数的完整类包装器;而且它还扩展了在RTTI相关函数讨论中介绍的CRunTimeTypeInfo
类;并且在调用TOOL引擎之前使用对基类的调用来验证传递的参数。对于每个验证调用,您都可以看到强制转换调用运算符的使用。
注意:在调用API函数时,如果对传递给TOOL运行时引擎的变量类型有任何疑问,则认为验证所有参数是良好的做法。采用这种理念将有助于使您的TOOL脚本更加健壮和可靠,并防止运行时突然停止您的脚本,如果您传递了不正确的变量类型到TOOL API函数。
通过类设计技术,可以实现“类型检查”的TOOL集合,如ToolBox.Tool文件中对CVector
类的扩展CNumberSet
所示。该类展示了一个类型受限集合的示例,该集合只存储数字集合。
在CNumberSet
示例类中,可以看到SetAtIndex
和AppendItem
类函数已被重写,以便可以对传递给类函数的参数执行RTTI检查。您还可以再次看到强制转换调用运算符在几个函数调用中的使用,包括类构造函数。
此外,CNumberSet
类还说明了三个更专业的Vector操作的使用;特别是Max
、Min
和Avg
TOOL API函数,它们操作Vector变量中所有数字,并返回操作的相应值。
TOOL字节数组操作
TOOL数据集合变量类型之一是字节数组变量类型。这种类型类似于本文档另一部分解释的Vector类型,但有一个非常重要的区别:字节数组只存储字节数组。这种类在构建特定字节值排列时用作“缓冲区类”。
查看下面函数列表,您可以看到TOOL API提供的一些与字节数组相关的函数。您可以看到ByteArrayAppend
和ByteArrayInsertAtIndex
函数有几个不同的重载,并且可以处理几种类型的正式参数。由于TOOL引擎的面向堆栈的设计,这种函数重载是可能的,并且在提供重载实现有意义时使用此技术。
ByteArrayGetAtIndex( oByteArray, iIndex ); ByteArraySetAtIndex( oByteArray, iIndex, bValue ); ByteArrayAppend( oByteArray, bAByte ); ByteArrayAppend( oByteArray, sAString ); ByteArrayAppend( oByteArray, oAnotherByteArray ); ByteArrayInsertAtIndex( oByteArray, iIndex, bAByte ); ByteArrayInsertAtIndex( oByteArray, iIndex, sAString ); ByteArrayInsertAtIndex( oByteArray, iIndex, oAnotherByteArray ); ByteArrayDelAtIndex( oByteArray, iStartDeletAtIndex, iCountToRemove ); ByteArrayClear( oByteArray );
ToolBox.Tool文件中的CByteArray
类是TOOL提供的字节数组变量类型API函数的示例类包装器。在类示例中,您将再次看到强制转换调用运算符和RTTI函数的使用。在审查了字节数组类包装器之后,并通过研究ToolBox.Tool文件中的其他类,您应该开始对如何在TOOL语言中构建类有一个相当好的概念。
TOOL堆栈操作
TOOL为可以利用LIFO集合类型的脚本提供堆栈对象。与Vector类型一样,堆栈可以在单个集合中存储各种变量类型。堆栈是一种相当简单的集合类型,因此专门用于堆栈的API函数的数量比其他TOOL集合类型要少。请参阅以下列表。
StackPush( oStack, oToPush ); StackPop( oStack ); StackPeek( oStack ); StackClear( oStack );
再次参考ToolBox.Tool文件中的CStack
类,这是一个简单的TOOL堆栈类包装器,用于说明TOOL中所有堆栈相关函数的使用。如您所见,这个堆栈类相对简单,因为堆栈相关API函数受到堆栈变量类型直接性质的限制。
TOOL映射操作
目前,TOOL只提供一种关联容器;Map,它允许存储键值对的简单存储容器。TOOL Map数据类型限制了可用作集合键的变量类型;但与所有其他TOOL容器一样,容器中存储的值可以是任何TOOL值类型,并且可以在单个Map中存储多个变量类型。如果尝试使用非法的键类型与TOOL Map一起使用,TOOL运行时引擎将停止执行TOOL脚本。
像其他TOOL数据集合一样,Map容器的设计简单明了,因此Map相关函数的数量仅限于处理该数据类型所需的功能。下面列出了Map相关API调用的列表。
MapRemoveKey( oMap, aKey ); MapFindByKey( oMap, aKey ); MapHasKey( oMap, aKey ); MapClear( oMap );
有关TOOL Map类包装器的示例实现,请参阅ToolBox.Tool文件中的CMap
类。为了实现一个仅限于单一键或值类型的CMap
类,需要修改IsLegalKeyType
和/或IsLegalValueType
类方法,以便对允许存储在底层Map变量中的键和值提供更严格的测试集。
TOOL队列操作
TOOL提供的另一个经典数据集合是Queue数据类型。TOOL Queue是许多编程项目中常用的FIFO数据集合的实现。整个Queue相关API如下所示。
EnQueue( oQueue, oToPush ); DeQueue( oQueue ); QueueClear( oQueue );
TOOL类实现的Queue类是Toolbox.Tool文件中的CQueue
类。与Stack数据类型一样,TOOL Queue的实现是直接的,不需要大量函数来实现。从提供的样本中,您还可以看到使用TOOL队列是多么容易。
TOOL应用程序运行时环境
可以��TOOL运行时上下文传递参数,作为“环境变量”供该TOOL程序使用。TOOL脚本可以通过下面解释的调用访问这些变量。
可以使用命令行参数调用RunTool.exe程序,这些参数将转发到TOOL脚本的运行时上下文。有关如何实现此目的,请参阅以下示例。
对于在TOOL运行时环境中运行的其他TOOL程序,有两个附加的API调用可用于获取和设置程序上下文中的变量。要从应用程序服务器环境获取变量,请使用API“GetAppEnvField
”。“GetProperty
”和“GetAppEnvField
”之间的一个重要区别是,“GetProperty
”将始终返回字符串类型的变量,而“GetAppEnvField
”可以返回以下类型的变量:
- 字符串
- 长整型
- 字节(布尔值)
- 双精度浮点型
- DWORD
- 日期时间
- 字节数组
取决于通过“SetAppEnvField
”调用存储在应用程序上下文中的数据类型。(请注意,上述变量类型列表是唯一可以��“SetAppEnvField
”调用存储在应用程序上下文中的类型。如果将任何其他类型的变量传递给“SetAppEnvField
”函数,将导致运行时错误,并终止TOOL脚本的执行。)下面的示例显示了这些API函数可能如何使用。
// store the variables in the application context // SetAppEnvField( "THE_STRING", "Hello" ); SetAppEnvField( "THE_NUMBER", 1234567 ); SetAppEnvField( "FALSE_BOOL", Byte( 0 ) ); SetAppEnvField( "TRUE_BOOL", Byte( 1 ) ); SetAppEnvField( "DOUBLE_VAL", 1234.5678 ); SetAppEnvField( "NEW_YEARS", DateTime() ); // retrieve varialbes from the application context // xFetched = GetAppEnvField( "THE_STRING" ); xFetched = GetAppEnvField( "DOUBLE_VAL" );
请参阅TOOL类“CAppEnvironment
”在ToolBox.Tool文件中的应用程序环境接口实现。
TOOL数据库操作
由于TOOL的预期主要用途之一是用于数据收集、处理和存储数据库数据相关的任务,TOOL必须提供数据库I/O系统。本部分TOOL API的目标是提供足够的功能来“完成工作”,同时避免大量不必要的复杂性。
TOOL连接数据库的接口方法是所有主要DBMS供应商提供的ODBC驱动程序。之所以采用这种级别的接口,是因为它是所有数据库连接的“最低公分母”(实际上,甚至“高级接口”通常也构建在ODBC之上作为额外的代码层)。此外,它目前是一项普遍技术,因此允许TOOL“平等地连接到几乎任何东西”。
在内部,TOOL拥有先进的ODBC包装器技术,允许在不让TOOL程序员处理动态绑定到“任何可想象的查询”结果的“繁琐细节”的情况下,处理数据库结果。这些细节是TOOL作业的一部分。
TOOL提供的数据库函数设计用于简单直接的数据库I/O应用。TOOL脚本和连接的数据库之间的“命令接口”是通过传递给DBMS的SQL字符串,通过TOOL管理的数据库连接实现的。同样,主要的��计考虑是易用性和跨所有数据库类型/供应商的可移植性,而不是将TOOL与单个DBMS供应商过度绑定。下面的示例代码显示了TOOL提供的数据库API最简单的用法。
TestDB(;oDB,iTimeOut,iColumnCount,iLoop,oValue) { oDB = Database(); iTimeOut = Long( 200 ); iColumnCount = Long( 0 ); iLoop = Long( 0 ); oValue = Long( 0 ); if ( DBOpen( oDB, "DSN", "DB_USER", "DB_PASS", "USE_DB" ) ) { DBSetLogonTimeOut( oDB, iTimeOut ); DBSetQueryTimeOut( oDB, iTimeOut ); if( DBExecQuery( oDB, "select * from TABLE" ) ) { if ( !DBIsEmptySet( oDB ) ) { iColumnCount = DBGetColumnCount( oDB ); DBMoveToStart( oDB ); while ( !DBIsEOF( oDB ) ) { DBFetchRow( oDB ); for ( iLoop = 0; iLoop < iColumnCount; iLoop++ ) { oValue = DBGetFieldByIndex( oDB, iLoop ); // do something here with the data value // if ( IsByteArray( oValue ) ) { FreeObject( oValue ); } } DBMoveNext( oDB ); } } } } DBClose( oDB ); FreeObject( oDB ); }
与其展示TOOL数据库API的类包装器,不如在以下样本中包含一个示例数据复制程序,因为它说明了TOOL API这部分的更完整和“真实世界”的应用。
/////////////////////////////////////////////////////////// // // entry point for this TOOL program // Run() { SetupGlobals(); if ( ConnectToSource() && ConnectToTarget() ) { if ( PrepareSourceForDataMove() && PrepareTargetForDataMove() ) { MoveData(); PostSourceDataMove(); PostTargetDataMove(); } } CleanUp(); } /////////////////////////////////////////////////////////// // // set up globals for this TOOL program // recall that all variables default to global scope unless // they are explicitely declared as local variables in a // functions formal declaration // SetUpGlobals() { iTimeOut = Long( 200 ); oSourceDB = Database(); oTargetDB = Database(); sSourceDSN = String( "ODBC.DSN" ); oSourceUID = String( "USER" ); oSourcePWD = String( "PASS" ); oSourceDB = String( "DBNAME" ); sTargetDSN = String( "ODBC.DSN" ); oTargetUID = String( "USER" ); oTargetPWD = String( "PASS" ); oTargetDB = String( "DBNAME" ); sSourceQuery = String( "select * from T_SOME_TABLE" ); sTargetTable = String( "T_TARGET_TABLE" ); } /////////////////////////////////////////////////////////// // // tear down after the job is completed // CleanUp() { DBClose( oSourceDB ); FreeObject( oSourceDB ); DBClose( oTargetDB ); FreeObject( oTargetDB ); } /////////////////////////////////////////////////////////// // // use the global vars to connect to the source database // ConnectToSource() { if ( DBOpen( oSourceDB, sSourceDSN, sSourceUID, sSourcePWD, sSourceDB ) ) { DBSetLogonTimeOut( oSourceDB, iTimeOut ); DBSetQueryTimeOut( oSourceDB, iTimeOut ); return( Byte( 1 ) ); } return( Byte( 0 ) ); } /////////////////////////////////////////////////////////// // // use the global vars to connect to the target database // ConnectToTarget() { if ( DBOpen( oTargetDB, sTargetDSN, sTargetUID, sTargetPWD, sTargetDB ) ) { DBSetLogonTimeOut( oTargetDB, iTimeOut ); DBSetQueryTimeOut( oTargetDB, iTimeOut ); return( Byte( 1 ) ); } return( Byte( 0 ) ); } /////////////////////////////////////////////////////////// // // stub functions that could be used for more complex data // replications that require additional set up prior to a // data move and/or post-data-move operations // PrepareSourceForDataMove() { return( Byte( 1 ) ); } PrepareTargetForDataMove() { return( Byte( 1 ) ); } PostSourceDataMove() { } PostTargetDataMove() { } /////////////////////////////////////////////////////////// // // here is a simple sample of a data move operation where // data is selected from the source, then for each column // of data returned from the data query, an insert statement // is built up for the target table // // it is worth noting that any additional amount of processing // could be performed as part of preparing the data for the // target. In addition, it is possible that 'source data' could // arrive from multiple input locations // MoveData( ;oValue, iColumnCount, iLoop, sTargetInsert, bInTran ) { iLoop = Long( 0 ); iColumnCount = Long( 0 ); sTargetInsert = String( 255 ); // fetch some chunk of data from the data source // if ( DBExecQuery( oSourceDB, sSourceQuery ) ) { if ( !DBIsEmptySet( oSourceDB ) ) { // if the query returned any data // iColumnCount = DBGetColumnCount( oSourceDB ); // set the source result cursor to the start of // the result set // DBMoveToStart( oSourceDB ); // while there is still data that has not been // processed // while ( !DBIsEOF( oSourceDB ) ) { if ( !DBIsOpen( oSourceDB ) ) { // the source connection has failed? // Echo( "Source Connection Unexpectedly closed\r\n" ); return; } // pull the result set data into TOOL local // buffers DBFetchRow( oSourceDB ); // start to build up an insert string for the // target table that will be dynamically built // from the input data // sTargetInsert = "insert into "; sTargetInsert += sTargetTable; sTargetInsert += " values ( "; // this is one way to get data from query results // in this example, we walk over the data column // by column // for ( iLoop = 0; iLoop < iColumnCount; iLoop++ ) { oValue = DBGetFieldByIndex( oSourceDB, iLoop ); // now, based on the data type for the current // column append to the target-table insert string // if ( IsNull( oValue ) ) { sTargetInsert += "null "; } else if ( IsNumber( oValue ) ) { sTargetInsert += IntToString( oValue ); } else if ( IsString( oValue ) ) { // strings in insert statements must be wrapped // in quotes // sTargetInsert += "'"; sTargetInsert += oValue; sTargetInsert += "'"; } else if ( IsDateTime( oValue ) ) { // see above comment for string types // sTargetInsert += "'"; sTargetInsert += DateToString( "%x %X", oValue ); sTargetInsert += "'"; } else if ( IsDouble( oValue ) ) { sTargetInsert += DoubleToString( oValue, 10 ); } // if not the last column, then put a comma field // seperator in the insert string // if ( iLoop < iColumnCount - 1 ) { sTargetInsert += ", "; } } sTarget += " )"; bInTran = Byte( 0 ); if ( !DBIsOpen( oTargetDB ) ) { // the source connection has failed? // Echo( "Target Connection Unexpectedly closed\r\n" ); return; } // if target dbms can transact, then start a transaction // if ( DBCanTransact( oTargetDB ) ) { bInTran = Byte( 1 ); DBBeginTran( oTargetTB ); } // send the insert string to the target dbms // if ( DBExecQuery( oTargetDB, sTargetInsert ) ) { // if insert worked, and in a transaction, then // issue a commit // if ( bInTran ) { DBCommitTran( oTargetDB ); } } else { // query failed....if in transaction, then rollback // if ( bInTran ) { Echo( StringFormat( "\r\nWARNING! Query\r\n % \r\n FAILED", sTargetInsert ) ); DBRollbackTran( oTargetDB ); } } DBMoveNext( oSourceDB ); } } } }
上面的程序说明了一个用TOOL编写的数据复制程序的实际示例。您可以看到TOOL提供了一个足够完整的数据库接口,可以完成许多典型数据库I/O操作所需的99%以上的任务;同时仍然保持简单和直接。同样,TOOL设计的驱动因素是专注于专门的数据处理语言所需的工具,而不是陷入“特殊情况”,因为大多数项目并不能带来显著的价值,而且在几乎所有实际应用中只会使当前任务复杂化。这里有一些额外的TOOL数据库API函数没有在上面的示例中涵盖。它们是:
bIsAtBeginning = DBIsBOF( oDB ); // returns true if at start of records DBMovePrev( oDB ); // moves data cursor back one row DBMoveToEnd( oDB ); // moves data cursor to end of records DBMoveAheadBy( oDB, iMoveBy ); // moves data cursor ahead by n DBMoveBackBy( oDB, iMoveBy ); // moves data cursor back by n DBMoveFromStartBy( oDB, iIndex ); // move data cursor n away from start DBMoveFromEndBy( oDB, iIndex ); // move data cursor n away from end DBGetFieldByName( oDB, sFieldName ); // allows record column to be // moved into TOOL script variable // using the column name as a key // to look up the field by
TOOL内存管理函数
释放TOOL对象
有一些TOOL变量类型在脚本使用完毕后必须被处理掉。这些变量类型是:
- 令牌化器
- 堆栈
- 队列
- 映射
- 计算器
- 字节数组
- 向量
- 数据库
注意:TOOL的未来版本有望提供改进的内存管理器,这将消除释放TOOL变量的需要,但目前,只需调用APIFreeObject
即可轻松销毁变量,如下所示。
FreeObject( oSomeVar );
注意:如果调用FreeObject
时使用的变量类型不需要销毁,则不会有问题。
那很好,但我只想浅尝辄止
我们已经讨论了如何将TOOL集成到您的应用程序中。希望您会同意这项任务相对容易完成。下一步自然是发��如何根据您的特定目的扩展TOOL。我们在上面已经触及了其中的几个方面,但在这里我们将更深入地探讨这个话题。有无数种方法可以扩展TOOL,但根据我的经验,有些类型的扩展需要一些关于如何完全实现扩展的提示。我将在那里涵盖的领域是:
- 添加新的字节码
- 添加新的变量类型
- 添加新的内置函数
对于这些概述,我将总结需要扩展的代码库区域。我认为我没有空间做更多的事情,所以希望提供的信息足以让您开始。
向系统中添加新的字节码是最复杂的扩展之一。在您对您行为的所有含义都有了非常牢固的掌握之前,请不要尝试这样做。我做过一两次,这涉及到大量的挠头。但如果您必须这样做,请使用以下准则来添加新的字节码:在VMCoreGlobal.h的第597行附近声明字节码;在VMCoreInterpreter.cpp的第100行附近添加到otab表(当在跟踪/解码运行时转储字节码时使用此表);在解释器的主要循环中(大约在第349行)为新的字节码添加一个新的switch
/case
,并将任何特定的编码放在该case
块中以处理字节码;如果需要,在VMCoreScannerParser.cpp的第180行附近添加新代码来输出字节码;如果您的新字节码还将为系统添加一个新令牌,那么您需要��义该令牌在VMCoreGlobal.h的第109行附近;您还需要扩展VMCoreScannerParser.cpp在第180行附近;最后,将任何适当的代码添加到VMCoreCompiler
以构建适合新令牌的字节码数组值。
向TOOL系统添加新的变量类型是另一项涉及的操作,但远不如添加新的字节码复杂。需要记住的关键点是,您可能需要扩展某些解释器字节码处理程序的操作,以便它们了解您的新变量类型以及如何实现它们。这呼应了上面关于运算符重载的部分。考虑到这一点,添加新变量类型的步骤如下:在VMCoreGlobal.h的第202行附近添加一个新数据类型声明;如果您的新数据类型将实现为C++类,则在前向声明该类在VMCoreGlobal.h的第243行附近;修改(如适用)VMVariant
(用于所有TOOL变量的变体类)构造函数在VMCoreGlobal.h的第305行附近;修改(如适用)ResetValue()
方法在VMCoreGlobal.h的第319行附近;修改(如适用)VMVariant
联合在VMCoreGlobal.h的第369行附近;修改(如适用)VMVariant
复制构造函数在VMCoreVirtualOpSys.cpp的第76行附近;添加一个用于实例化/构造新变量类型的新内置函数,请参阅VMCoreVirtualOpSys.cpp的第207行附近以了解如何声明实例化处理程序;另请参阅VMCoreVirtualOpSys.cpp的第1841行附近的HandlerNewString()
方法以了解如何实现实例化处理程序;添加一个新的RTTI类型处理程序来处理您的新数据类型,请参阅VMCoreVirtualOpSys.cpp的第308行附近以了解如何声明您的新类型检查函数,请参阅VMCoreVirtualOpSys.cpp的第1234行附近的HandlerIsByteArray()
方法以了解如何实现您的新类型检查函数;并声明和添加特定于您的新变量类型的新内置函数,请参阅VMCoreVirtualOpSys.cpp的第238行附近以获取此步骤的一些示例。如果您的新变量受运算符重载的影响,那么您也可能需要根据您的新变量类型适当扩展VMCoreInterpreter
类字节码开关处理程序,用于逻辑、数组索引和数学处理。可能受影响的处理程序(如果有)是:OP_NOT
;OP_NEG
;OP_ADD
;OP_SUB
;OP_MUL
;OP_DIV
;OP_REM
;OP_BAND
;OP_BOR
;OP_XOR
;OP_BNOT
;OP_SHL
;OP_SHR
;OP_LT
、OP_LE
;OP_EQ
;OP_NE
;OP_NE
;OP_GE
;OP_GT
;OP_INC
;OP_DEC
;OP_VREF
;OP_VSET
。
接下来,我们将介绍如何向 TOOL 添加新的内置函数。您会发现,在所有讨论的方式中,这是扩展引擎最简单的方法。这样做时,您实际上是向解释器的函数指针映射添加了一个新的函数指针。每当解释器识别出内部函数的名称时,它就会为其构建调用堆栈(将所有参数按从左到右的顺序放在“调用堆栈”上),然后通过指向该函数的指针调用该函数。首先,您需要声明该函数,请参阅 VMCoreVirtualOpSys.h 第 100 行之后的任何位置,其中有此示例。您很快就会发现所有内置函数都具有相同的签名。接下来,您需要通过调用 ConfigureBuiltInFunction()
将处理程序添加到内部内置函数列表中。有关此内容的许多示例,请参阅 VMCoreVirtualOpSys.cpp 第 200 行开始的部分。配置函数的两个参数是:
- 函数在脚本文件中出现的“名称”;
- 指向您的处理程序实现的函数指针。
回想一下我们在上面的讨论中提到的,解释器在调用内置函数之前会“构建调用堆栈”。然后,处理程序必须(如果以安全为重进行实现)验证传递给处理程序的值的数量和类型。让我们在 VMCoreVirtualOpSys.cpp 的第 4311 行附近查看 HandlerStringCompare()
,其中有一些有关如何验证传入参数的示例。参数 iArgc
是解释器为该函数放入调用堆栈的参数数量。因此,如果内置函数对传递的参数数量有预期,则应首先验证这一点。您可以看到,通过调用 VerifyRunLineArguments()
可以轻松完成此操作。接下来,如果函数对传递的变量类型有预期,则应接下来验证这些类型。这也可以通过调用 VerifyElementType()
来轻松检查。此函数的第一个参数是要检查的参数的“堆栈偏移量”,第二个参数是该堆栈位置预期的变量类型。请记住,解释器会从“左到右”加载堆栈,这意味着“函数的第一个参数”在调用堆栈上具有最高的堆栈偏移量,而传递给内置函数的最后一个参数将具有零的堆栈偏移量。在编写自己的验证代码时,您需要牢记这一点。最后,如果您需要从函数中向解释器返回任何值,您需要通过调用 SetValue()
函数并传入结果来实现。解释器期望您的函数的任何返回值将在函数返回后留在堆栈上。这意味着您将始终将结果返回到堆栈位置:m_poVirtualChip->m_psStackPointer[iArgc]
。解释器还期望函数通过适当调整堆栈指针来“擦除调用帧”。这意味着任何内置函数的最后一行很可能是:m_poVirtualChip->m_psStackPointer += iArgc;
,其作用是“擦除调用堆栈”。
编写 TOOL 程序
我将引导读者查阅 CoreClasses.Tool 文件,以了解如何用 TOOL 语言编写程序。该文件包含许多 TOOL 风格的类,它们创建了 TOOL API 的类包装器。这组类以及该文件中的函数是我为 TOOL 准备的“测试平台”,因此它们在一个脚本中几乎测试了所有 TOOL API。因此,该脚本文件是 TOOL 编程的绝佳示例。此外,它还提供了一组用 TOOL 编写的类,并提供了一组经过测试且可随时使用的 TOOL 类,可用于您自己的脚本中。
您还会发现,TOOL 函数定义看起来很像它们的 C 对应项。唯一明显的区别是,在函数声明中缺少对传递给任何函数的参数类型以及函数的返回类型的声明。TOOL 中变量类型的这种“宽松性”是我将所有 RTTI 类型函数放入引擎的原因之一。这种未声明的变量类型的另一个后果是,任何变量都可以接受任何类型的值。在 TOOL 的当前实现中,我预见到这可能会导致内存泄漏,因此我尝试在 VMVariant
复制构造函数中解决此问题,即如果变量已包含任何堆分配,它将在采用新值之前删除它们。
重要提示:尽管从脚本编写者的角度来看,TOOL 是一种弱类型语言,但 TOOL 解释器会对其内置例程中传递的变量进行类型检查。如果变量不是正确的类型,解释器将中止脚本执行并抛出异常。
TOOL 和 C 程序之间的另一个关键区别是,TOOL 中的“局部变量”出现在函数的参数列表中。“局部变量”声明出现在函数声明中分号后的逗号分隔列表中。不受此方式限制范围的临时变量将被提升到全局命名空间。虽然 TOOL 函数可以很好地处理全局命名空间中的变量,但这可能会在脚本执行期间导致一些“奇怪且出乎意料”的行为(例如:“那个值是怎么来的?”)。虽然 TOOL 中不需要临时变量声明,但采用这种方法很有用。
与 C++ 一样,TOOL 类的所有新对象都使用构造函数进行初始化,该构造函数的名称与类本身相同。同样,请参阅 CoreClasses.Tool 文件,其中有几个关于如何在 TOOL 中声明类的示例。TOOL 类构造函数中的最后一个操作是通过“return( this )
”返回新对象。对于不熟悉 C++ 的人来说,变量 this
指的是正在调用其成员函数的对象。它是传递给每个非静态成员函数的隐式参数。在这种情况下,this
是新创建的对象。
在 TOOL 类中,所有数据成员都隐式地是 protected
的。访问或修改成员变量值的唯一方法是通过成员函数。如果您需要在成员函数外部访问成员变量,则必须提供访问该成员函数的权限。同样,请参阅 CoreClasses.Tool 文件中的几个示例。new
运算符创建类的新对象,该类的名称紧随其后。类名后面的括号中的表达式是要传递给构造函数的参数。
TOOL 还允许一个类派生自另一个类。派生类将继承基类的行为,并可能添加自己的行为。TOOL 只支持单重继承;因此,每个类最多只能有一个基类。核心类文件中的类都表达了语言的这一属性。您还可以从 TOOL 中看到派生类的构造函数如何直接调用其父类的构造函数。这是因为父类的整个接口对派生类都是可用的。
TOOL 脚本文件布局
TOOL 脚本布局的设计可以使运行时引擎对 TOOL 脚本程序进行简单的自省,以确定程序的性质。下面显示了一个空脚本程序。这种方法在 TOOL 运行时引擎和它运行的脚本之间提供了“对象接口方法”。
实际上,运行时会为任务操作的每个阶段“发送一条消息”给脚本。这种概念的程序化等价物是,运行时会在脚本程序中调用多达五个顶级函数;每次调用都代表任务完成的一个特定阶段。有关 TOOL 脚本的自省方法的示例,请参阅下面的示例程序。但是,您可以设计任何形式的类似方法,以最适合您的应用程序;由于 TOOL 引擎很容易由宿主环境驱动,因此您可以定义更具体的文件布局来满足您的特定需求。
/*********************************************************/ /* TOOL SOURCE FILE */ /*********************************************************/ /* $Revision: $ $Date: $ $Author: $ Description: This shows a simple example of a TOOL script program in order to illustrate the "object-interface" on TOOL scripts. */ /*********************************************************/ /////////////////////////////////////////////////////////// // // constants used in File Output // #const FILE_ACCESS_MODE_WRITE 1 #const FILE_SHARE_READ 1 #const FILE_OPEN_ALWAYS 3 #const FILE_POINTER_REF_END 2 /////////////////////////////////////////////////////////// // // This function is called by the TOOL Run-Time to "discover" // the interface to this script. The script can have up to // five entry points in order to define the task. However, // the script does not have to support all five entry points. // // For some tasks, it may make sense to only have a single // entry point (the script developer can select any one of // the five; or any number of them as appropriate) // // The TOOL Runtime will invoke each defined entry point // in order. The script can have any number of "private" // functions that can be called from any of the entry point // functions. // // This design was implemented in order to provide a maximum // level of flexibility in the definition of the script // program and recognizes that many jobs are completed in // several discrete stages/operations. // GetScriptInterface() { // Runtime Name of the // Entry Point Local Function // Name For That Task Stage // SetAppEnvField( "InitFunction", "Init" ); SetAppEnvField( "PreRunFunction", "PreJob" ); SetAppEnvField( "RunJobFunction", "RunJob" ); SetAppEnvField( "PostRunFunction", "PostJob" ); SetAppEnvField( "ExitFunction", "Exit" ); } /////////////////////////////////////////////////////////// // // simple logging function. Also provides an example of // a "script private" function. Meaning that the function // is not invoked by the TOOL Runtime, but rather from // other functions inside this program. // MakeLogRecord( sLogText ;dtNow, hFile, sOutput ) { dtNow = GetDate(); sOutput = StringFormat( "[ % ]--> % \r\n", DateToString( "%a, %b %d, %Y %X", dtNow ), sLogText ); Echo( sOutput ); if ( !PathExists( "c:\\tool.task.log" ) ) { CreateDirectory( "c:\\tool.task.log" ); } hFile = OpenCreateFile( "c:\\tool.task.log\\all.tasks.log", FILE_ACCESS_MODE_WRITE, FILE_SHARE_READ, FILE_OPEN_ALWAYS ); if ( 0 != hFile ) { SetFilePointer( hFile, 0, FILE_POINTER_REF_END ); WriteLineToFile( hFile, sOutput ); CloseFile( hFile ); } } /////////////////////////////////////////////////////////// // // Perform any global initializations here // Init() { MakeLogRecord( "Task -- Init() Called." ); } /////////////////////////////////////////////////////////// // // Perform any further preparatory work here // PreJob() { MakeLogRecord( "Task -- PreJob() Called." ); } /////////////////////////////////////////////////////////// // // Perform the primary work for the task here // RunJob() { MakeLogRecord( "Task -- RunJob() Called." ); } /////////////////////////////////////////////////////////// // // Perform any post processing work here // PostJob() { MakeLogRecord( "Task -- PostJob() Called." ); } /////////////////////////////////////////////////////////// // // Perform final clean up or other related work here // Exit() { MakeLogRecord( "Task -- Exit() Called." ); }
TOOL 脚本快速退出
如果 TOOL 脚本遇到无法(或不应该)继续执行的条件,那么 TOOL 脚本可以调用 Throw
API,这将导致 TOOL 程序立即停止。此 API 接受一个消息字符串,可以将其返回用于错误报告。下面显示了此调用的示例。
Throw( "Goodbye Cruel World" );
开始使用 TOOL
我在这里涵盖了大量内容,试图传达我如何将 David Betz 的 BOB 带入新的领域。我对 TOOL 的目标是扩展原始引擎,并使其“易于集成”。在本文中,我试图解释有关项目组织方式的足够信息。我还希望我提供了足够的信息,说明如何扩展 TOOL 引擎的功能以满足您自己的特定功能和/或项目要求。最后,我提供了一套相当完整的 TOOL 类,这些类演示了 TOOL API 的全部范围,并提供了一些“通用类”,这些类可以在您开发的任何 TOOL 脚本中使用。
我对 TOOL 的持续演变还有许多其他计划。有些问题确实需要解决,例如我之前提到的内存管理和垃圾回收问题。此外,我已采取了一些步骤来实现能够调试 TOOL 引擎的功能,并计划继续这一追求。我的待办事项列表还包括向引擎添加更多内置函数,添加多线程脚本功能,将其与我开发的元窗体语言集成等等。因此,我认为我还没有完成 TOOL 的成长。即便如此,我认为你们中的一些人会喜欢另一个解释器项目添加到你们的工具箱中。
虽然 TOOL 可能因为已经存在如此多的其他解释器引擎而有点迟到了,但我认为 TOOL 易于集成和扩展的特性使其比其他解释器具有优势。虽然我完全认识到它不像许多其他脚本引擎那样强大或完整,但我可以在我的程序中完全包含它,并且可以相对容易地按我想要的方式扩展它,这使得它成为我许多项目的首选。
另外,我真的很喜欢可编程程序的整个概念,并且拥有像 TOOL 引擎这样的工具在我的工具箱中,仅凭其闪光点就给了我一种特别的兴奋感。无论如何,我希望你们中的一些人也会发现 TOOL 值得学习,并且能够像我一样在完全由您控制的引擎中尽情探索。
但等等……还有更多…… TOOL 包含的奖励材料
TOOL 内置的其他值得注意且强大的功能之一是完整的运行时窗体引擎,我称之为 XMLForms。这会将“运行时解释的窗体引擎”直接放入解释器中,并允许创建功能齐全的 TOOL 应用程序,这些应用程序自带 GUI。
此功能本身值得深入研究,我现在将尝试从非常高的层面解释此功能。再次,由于这个项目如此庞大,在这里不可能描述项目的各个方面(我认为我几乎需要一本书来达到那个解释程度)。
XML-Forms 的全部代码库位于分发包中的 GUI.Tools/XMLForms 目录中。该目录中甚至还有一个小的示例应用程序,用于测试 XML 表单脚本。
与 TOOL 项目一样,有一个单一的外观类封装了整个 XML 表单类系统。该类的定义位于文件 XMLFormFacade.h 中,其实现位于 XMLFormFacade.cpp 中。
创建 XML 表单包含两个主要操作:解析 XML 表单定义,然后运行基于该定义的窗口。通过研究 XmlFormTest 项目,您可以了解这有多么简单。XmlFormTest 是一个小型实用程序,我曾无数次地用于测试我项目中自己的 XML 表单。具体来说,请参阅示例 XMLFormTestDlg.cpp 中的 OnButtonTestIt()
函数。
总的来说,XML Forms 的架构使用 XML 解析器从文件(或字符串缓冲区)读取表单定义。在读取 XML 表单数据时,会创建一系列屏幕控件元对象,并存储在 XMLFormFacade
对象中的集合中。当表单需要显示时,控件元对象的集合会被传递给一个控件工厂,该工厂会创建屏幕控件并将它们作为子控件放置在容器窗口中。XMLStyleDecoder
对象提供了额外的解析服务,该对象允许在 XMLForm 数据中显式定义控件和容器窗口的样式。
其他对象支持数据交换和验证,其规则也在表单的 XML 数据字符串中定义。数据验证任务包含在 DDV Wrapper 对象中(在 XMLFormDDVWrapper.cpp 中实现)。数据交换服务包含在 DDX Wrapper 对象中(在 XMLFomrDDXWrapper.cpp 中实现)。
标签控件管理是 XMLForms 项目中实现的一个非常特殊的功能。通常,标签顺序定义为 Z 顺序的函数,但这对于像这样的项目不太有效。因此,标签顺序可以在 XMLForm 数据文件中定义,并且该数据在 XMLFormTabSetManager
对象中进行处理。为了协助完成此任务,所有 XMLForm 控件都将参与焦点(OnGainFocus()
和 OnLoseFocus()
)通知到容器窗口。您可以在 XMLFomrControls.cpp 文件中大多数 XMLForm 控件的实现中看到这些特殊任务的示例。
渲染数据
作为一项附加功能,XMLForms 的背景可以由 XMLForms 项目中的另一项功能驱动,即该项目与 TOOL 解释器的集成方式。如果您在 samples 目录中看到 GiftCardPurchase.form 文件,您可以看到 TOOL 脚本如何嵌入到 XMLForm 数据文件中。请特别注意此示例文件中的 PAGE_BACKGROUND
部分。如果此节点存在于 XMLForm 文件中,那么 XMLForm 系统将运行该 XML 节点中包含的渲染脚本来执行 XMLForm 的背景绘制。将渲染脚本与 XMLForm 窗口混合使用可以为模拟“真实世界”业务表单外观的窗口带来一些非常有趣的可能。虽然我不得不承认,手动创建和调试这些屏幕有点费劲。
包含的示例
在本次初始交付中,我包含了以下示例:
- ToolDll 项目:此项目将 TOOL 解释器打包成一个 DLL。
- ToolDllTester 项目:此项目利用 TOOL DLL 并提供了一个如何与 TOOL DLL 集成的示例。
- ToolTester 项目:此项目提供了一个如何将 TOOL 直接集成到您的应用程序中的示例。
- ToolExtender 项目;这是一个 DLL Shell 项目,用于 TOOL Extender DLL,可以在运行时绑定和执行。
- XMLFormTest 项目:此项目提供了一个如何将 XMLForm(和 TOOL)直接集成到您的应用程序中的示例。
- 示例脚本:位于 Tools 文件夹中。
- 示例 XMLForms:位于 XMLForms 文件夹中。其中最有趣的屏幕是 PayByCreditCard.form 文件,它是迄今为止最复杂的 XMLForm,并展示了该库的许多高级功能,包括为表单定义的高级事件处理(请参阅文件中的
ACTIONS
块)。另一个有趣的 XMLForm 示例是 Tools 目录中的 GiftCardPurchase.form。此示例显示了自定义背景渲染的 XML 表单,以模仿“实际纸质表单”。 - 示例报表生成:位于 Sample.Report 文件夹中(这显示了如何格式化 TOOL 模板文件(一种特殊形式的 TOOL 脚本)),并说明了如何从 ODBC 数据源收集数据并将该数据加载到 SQLite 数据库中以进行中间存储和/或数据操作。
如何使用 RunTool 程序
RunTool 程序有什么用?
提供 RunTool 程序是为了让用户能够使用 TOOL 运行时运行 TOOL 脚本和交互式 TOOL 程序。RunTool 允许用 TOOL 语言为任何特定用途开发小型“批处理文件”。
RunTool 程序为用 TOOL 编写的“桌面应用程序”提供了一个独立的运行时环境。这些桌面应用程序的性质和范围不受任何限制;这意味着 TOOL 支持的任何内容都可以成为用 TOOL 编写的桌面实用程序的组成部分。
如何使用?
只需启动 RunTool 程序。
如果应将任何运行时参数传递给 TOOL 程序(用于任何初始化或控制目的),则使用以下格式在“参数”编辑框中输入这些参数。
PARAM=SomeParam=SomeValue PARAM=SomeOtherParam=SomeOtherValue
注意:参数字符串的长度目前限制为 255 个字符。
此时,只需单击“选择脚本”按钮,这将打开一个文件对话框。使用文件对话框选择您希望运行的 TOOL 脚本程序。从 TOOL 脚本生成的任何消息都将显示在 RunTool 程序中。可以通过单击“清除消息”按钮来清除此显示。
如何使用 ToolForge 程序
ToolForge 程序有什么用?
TOOLForge 程序是 TOOL 的 IDE。因此,它可以作为 TOOL 脚本的语法感知着色编辑器。它还可以作为 TOOL 脚本的交互式调试器。此版本的 TOOLForge 仍然有点粗糙,因为它仍在开发中;因此有时可能会崩溃。
如何使用?
要编辑 TOOL 脚本,只需启动 TOOLForge 程序并像使用其他编辑器一样使用它。以下是用于在 Forge 中调试 TOOL 脚本的步骤:
- 首先注意程序中的工具栏。工具栏的最右侧有三个控件,用于驱动调试会话。最右侧的复选框用于控制调试器的单步执行。您应该先勾选此项。
- 接下来,打开“调试”菜单并选择调试脚本命令。这将显示一个文件选择对话框,供您选择要调试的脚本文件。对于您第一次使用调试器,您可能需要使用包含在 samples 目录中的 FileBuilder.tool 文件。
- 选择文件后,调试器将通过代码窗口中的绿色条显示脚本的当前执行点。
- 要切换断点,请在代码窗口的某一行单击;打开编辑菜单并选择切换断点命令。断点在文本窗口中显示为深蓝色条,在代码窗口的装订区域显示为“停止标志”。
- 当勾选单步复选框时,单击工具栏上的“蓝色向下箭头代码页”将执行脚本中的单步。当未勾选此复选框时,脚本将以“动画模式”运行,这意味着编辑器窗口将跟随程序执行。要中断在动画模式下运行的脚本,只需勾选单步控制以发出程序中断。
- 程序底部的三个窗口分别是(从左到右):脚本输出窗口、调用堆栈窗口和变量显示窗口。请注意,并非所有 TOOL 变量都在变量窗口中完全展开。这是该项目仍在进行中的项目之一。但是,其中一些有效,您将对我的此功能方向有所了解。
- 调用堆栈和变量窗口在每个断点处更新。脚本输出窗口显示脚本运行时通过
Echo
输出的所有内容。 - 要运行脚本直到完成,请单击工具栏上的“红色向下箭头代码页”按钮。
已知问题
使用 TOOL DLL 实例化多个 XML 表单会导致应用程序消息泵出现问题。原因尚不清楚,并且当 XMLForms 直接在应用程序中使用时,此行为不会显现。
肯定还有其他一些小错误。虽然该项目已经得到了相当完整的测试,因为它已被包含在我的许多项目中,但仍然很有可能存在这个大小的项目中未发现的错误。
如果您发现任何您认为可能是错误的行为,请告诉我。如果该行为确实被证明是一个缺陷,我将努力纠正它。
寻求帮助和功能请求
TOOL 和 XML Forms 是一个相当庞大的项目核心。仍然有很大的改进和增强空间。待办事项列表中的项目包括:
- 添加其他 DSN 创建选项。目前仅定义了 SQL Server 和 Access 的 DSN 创建字符串。
- 增强 TOOL 以与(创建条目)“开始”菜单和用于创建快捷方式的 shell 功能进行交互。
- TOOL 运行时中的更好内存管理,包括解释器运行时中的更好垃圾回收。
- 在此版本中未涵盖的符合您特定要求的新功能。
简而言之,我认识到,尽管 TOOL 已经是一个功能丰富的解释器,但它仍有许多地方可以发展。这也是我将 TOOL 发布给广大社区的原因之一;我希望通过其他人尝试将 TOOL 用于自己的目的,该库本身能够扩展其广度。此外,我还认识到那里有一些非常聪明的人可能愿意帮助我找到并修复代码中肯定会存在的小问题。
关于源代码的说明
我是一位热衷的互联网爬虫,一直在寻找可以添加到 TOOL(以及其他项目)中的新颖有趣的 [代码]。该项目中有许多工作来自各地。但是,我通常不会直接采用代码,而是将所有“采用的代码”进行大量重格式化作为标准实践。这些重格式化工作包括以下任务:
- 类变量重命名:转换为匈牙利命名法,并与我自己的命名约定保持一致。
- 头文件/实现文件标题和页脚。
- 函数标题和页脚。
- 删除/将制表符替换为空格。
我为什么要这样做?原因如下:
- 代码审查:由于我经常需要扩展这些文件,或者在某些情况下修复它们,因此我发现用“放大镜”检查这些文件可以让我更熟悉文件的组织方式,这样如果我需要处理它们,我就知道它们是如何组合在一起的。
- 一致性:这可能是主观的和/或美学上的选择,但我认为任何项目的“内部实现”都应该努力做到“内部一致”。在我看来,这种方法使得试图熟悉该项目的人更容易理解整体。换句话说,我认为一个项目由许多个人的工作组成,每个文件都有自己的代码创建风格,这并没有什么帮助。对此规则有一些例外,例如,如果我包含像 SQLite 或 zlib 这样的主要开源库,我不会以如此细致的程度检查它们的所有文件,但我用来访问这些库的任何包装器对象都将使用我的首选样式。
因此,虽然“为”修改了文件的其他开发人员这样做没有什么好处,但至少您有了一个对这些更改的原理的解释。我试图保留原始作者的注释,在任何我集成到 TOOL 并经过上述编辑过程的文件中,但由于该项目已经进行了很长时间,有时我甚至不记得某些源代码是否基于我旅途中偶然发现的某个项目/示例。如果您认为该项目中的任何文件都是您项目的一个衍生品,并且您认为您应该为此项目的任何部分获得认可,那么请联系我并提出您的理由。
最后,仔细审查此代码将发现一些用“ifndef TOOL_PUBLIC_BUILD
”括起来的条件编译实例,其最终效果是“从本次发布中移除部分功能”。这些条件编译区域背后的原因是,某些功能依赖于我“私有构建”版本中使用的商业库,但我无权发布源代码。如果有人想为这些功能区域提出非商业库的替代方案,我很欢迎,因为它将增加该项目“私有构建”级别的实用性。
即使您不想使用 TOOL 或 XMLForms,您可能仍然想仔细看看这些代码,因为其中实现的一些类和技术可以在其他方面找到用途。
关于本文档的说明
TOOL 正在不断发展。由于这种演变,本文档可能不完全包含我添加到 API 的最新/最新功能。因此,如果本文档未涵盖 API 的某些领域,那么希望提供的示例能提供更多信息。
TOOL 的下一步是什么?
可能为 TOOL 添加无数万个新功能,我甚至无法想象。因此,我将这些方向性决策留给你们。此外,我已经在开发使用 TOOL 的其他产品,例如:报表生成器、安装程序、应用程序服务器等。如果这些领域中的任何一个引起了兴趣,我也可能以某种形式发布这些产品。
祝您好运
如果您读到了这篇文章的最后,感谢您忍受(有时是冗长的)提交。我希望您能找到这个项目的一些用途。如果您对 TOOL 的用途有任何想法,或者您有希望看到的特定请求,那么请联系我,也许我能提供一些帮助。
更新历史
- 2006/10/16:天哪,时间过得真快。难以置信,距离这个项目更新已经整整一年了。正如 2b|!2b==? 提示我赶紧行动所证明的。所以,我们终于在这里发布了一个更新。本次发布包括修复、更新和增强(太多记不清了,但主要更新如下):
- 在 **2B|!2b==?** 的 **大力协助** 下(他完成了大部分“移植”工作),TOOL 项目现在已更新为较新的 Microsoft 编译环境。
- 已更正解析错误(由 yiqianfeng 报告)。
- TOOL 引擎现在内置支持 SQLite 2.x 和 3.x 数据库格式。此外,还添加了几个用于 SQLite 3.x 管理的新 API 调用。由于这次集成,旧的“minidb”调用已被重命名(如果您的脚本因重命名而损坏,我提前道歉),因为它们不再有意义。所有“minidb”调用均已替换为“SqlLite2”调用。
- ODBC 类的“动态绑定”行为已修订/改进,以支持更广泛(更便携)的 SQL 数据类型。
- TOOLForge 应用程序已增强,可实现 XMLForms 的可视化创建和测试。这是通过使用 Johan Rosengren 在 此处提供的非常出色的屏幕设计器类的经过大量修改的版本来实现的。
- TOOL 计算器对象已增强,包含几个新的内置函数:
sin
、cos
、exp
、sqrt
、ln
、tan
、ctg
、asin
、acos
、atan
、sinh
、cosh
、tanh
、asinh
、acosh
、atanh
、log10
、log2
、abs
、rounddown
和roundup
。
- 2005/09/23:本次发布包括修复、更新和编辑,以解决迄今为止我所知道的所有问题。如下:
- 已更正阶乘示例错误(由 Abf23 报告)。通过包含一个示例脚本,还证明了该示例的正确性。
- 已更正构建错误(由 Abf23 报告),指示 stdafx.h 路径错误。已更正有问题的文件。
- (应 Manuele Turini 的要求)更新的交付包含了编译的示例 DLL 文件和可执行文件。
- hero3blade 报告的错误在此版本中并未完全解决,因为当我尝试更改代码以解决他的报告时,我无法再构建该版本。这里可能存在一些尚未确定的平台问题。