Tcl/C混合编程:将Tcl嵌入你自己的程序

  • Post author:
  • Post category:其他


原创】Tcl/C混合编程:将Tcl嵌入你自己的程序

Tcl/Tk同Python一样,是一种高级语言。所不同的是,Python追求高大全,几乎我们能想得到的所有计算机领域都有Python的库,甚至如CUDA调用,符号计算,3D显示等都可以。Tcl却恰恰相反,它追求的是小巧精致,尽管没有那么华丽的库,但这也是它的优点。整个Tcl/tk库可以简单而无缝的嵌入到任何人自己开发的程序而不使程序臃肿变大很多,这使得程序功能的提升具有无限的潜力。在很多EDA工具中都嵌入了Tcl环境。一些科学应用的程序如Hyperchem也嵌入了Tcl,做的最好的当属VMD。可以说,如果没有tcl,VMD的功能得打折90%!

这篇文章主要的内容,就是试图介绍一下如何将Tcl嵌入到自己编写的程序中。这里,默认读者已经熟悉Tcl/tk语言和简单的C语言编程。当然,我们不会将Tcl的全部API介绍一遍,这里只介绍一些最重要的函数。我们使用Tcl8.5.

如果需要tcl/C的更详细的文档,可以阅读Tcl.h中的注释。

1 准备

Tcl/tk是横跨Windows/Linux/Mac的语言,在各个环境下它的目录结构都类似。对于编程人员,最重要的是/lib和/include,它们分别是Tcl/tk的链接库和头文件文件夹。因此,在编译程序时,记得将这些文件夹加入到-L和-I选项中,并且开启-ltcl85 -ltk85开关。如果使用gcc,直接将这些选项加入到命令行即可,如果用Visual C++,记得修改Project的setting。

2 “Hello World!”

现在我们开始编写第一个Tcl/C混合程序。

// Filename: main.c
#include <tcl.h>
#include <stdio.h>
int Tcl_AppInit(Tcl_Interp *interp)
{
return (Tcl_Init(interp) == (TCL_ERROR))?TCL_ERROR:TCL_OK;
}
int main(int argc, char** argv)
{
printf(“— Tcl Third-Party Shell Start —\n”);
Tcl_Main(argc, argv, Tcl_AppInit);
printf(“— Tcl Third-Party Shell End —\n”);
return 0;
}

编译命令:gcc -o a.exe main.c -ltcl85 -IC:/Tcl/include -LC:/Tcl/lib
运行结果如图:

现在分析一下这个程序。一些顾名思义的地方我们就不费文字来介绍了。
1) 首先必须导入tcl的头文件<tcl.h>;
2) 在主程序main中,我们调用了一个Tcl函数:Tcl_Main(argc, argv, Tcl_AppInit)。这个函数的第三个参数是一个TCL初始化函数的指针,这个函数必须由自己定义。
3) 在Tcl_AppInit(Tcl_Interp *interp)中,interp是一个tcl解释器的指针,它必须由Tcl_Init来初始化。
我们的tcl初始化函数只干了这么多。
4) 我们发现,Tcl_Main之前的C语言与平时的编程没什么不同。而调用Tcl_Main之后,程序中开启了一个tcl的shell,在这个shell结束后,后面的命令并没有执行,而是直接退出了。
我们这个简单的程序算是一个完整的TCL/C程序了。

3 扩展新命令:字符串方法
这部分写在这里是出于易于理解的考虑,因为我本人不建议这种方法。读者可以跳过这里,直接阅读4.
扩展命令需要两个函数,一个创建命令,一个删除命令。需要注意,并不是创建的命令都需要删除,除非有特别原因。
Tcl_Command Tcl_CreateCommand(interp, cmdName, proc, clientData, deleteProc)
int Tcl_DeleteCommand(interp, cmdName)
这里,proc是实现名字为cmdName的新命令的函数,clientData在TK编程时很有用,在本文中先设为0;deleteProc是调用Tcl_DeleteCommand时自动先调用的函数指针,本文中也设为0。

新命令必须在Tcl_AppInit中定义。新命令的格式是:
int evod_proc(ClientData clientData, Tcl_Interp *interp, int argc, const char** argv)
对这些参数的理解,我们通过实例来看。假设我们在TCL中需要定义一个命令evod,它的格式:
evod int
若int是个偶数,则输出“even”,否则输出“odd”。可以这样实现:

#include <tcl.h>
#include <string.h>
#include <stdio.h>
int evod_proc(ClientData clientData, Tcl_Interp *interp, int argc, const char** argv)
{
int a;
char res[8];
if(argc != 2)
{
Tcl_SetResult(interp, “wrong # args: should be \”evod ?int?\””, TCL_STATIC);
return TCL_ERROR;
}
else
{
if(Tcl_GetInt(interp, argv[1], &a) != TCL_OK)
{
Tcl_SetResult(interp, “Usage: input should be an integer.”, TCL_STATIC);
return TCL_ERROR;
}
strcpy(res, (a%2)?”odd”:”even”);
Tcl_SetResult(interp, res, TCL_VOLATILE);
return TCL_OK;
}
}
int Tcl_AppInit(Tcl_Interp *interp)
{
if(Tcl_Init(interp) != TCL_OK) return TCL_ERROR;
Tcl_CreateCommand(interp, “evod”, evod_proc, (ClientData)0, 0);
return TCL_OK;
}
int main(int argc, char** argv)
{
printf(“— Tcl Third-Party Shell Start —\n”);
Tcl_Main(argc, argv, Tcl_AppInit);
printf(“— Tcl Third-Party Shell End–\n”);
return 0;
}

好啦,evod已经诞生了。

1) 我们在Tcl_AppInit添加了evod这个命令:

Tcl_CreateCommand(interp, “evod”, evod_proc, (ClientData)0, 0);

然后在evod_proc中实现,我们发现,对argc argv的使用方法和一般C语言的使用方法一样,就不再赘述。如果命令错误,则返回TCL_ERROR,如果顺利则返回TCL_OK。

2) 对于输出的字符串,我们使用Tcl_SetResult(interp, buffer, TCL_XX);其中buffer是要显示的字符串的缓冲区。第三个参数是结果的“释放方式”。如果是静态字符串,通常可以用TCL_STATIC,如果是某种堆栈变量,就用TCL_VOLATILE。如果这个字符串是通过Tcl_Alloc动态分配的,就设为TCL_DYNAMIC。

3) Tcl_GetInt是一个读取整数的函数。

4 扩展新命令:Tcl_Obj方法

这种单纯使用字符串的方法比较简单,但是原始。TCL8.5似乎更倾向使用Tcl_Obj的方法。请看下面的代码,它可以实现与上述代码完全相同的功能:

#include <tcl.h>
#include <string.h>
#include <stdio.h>
int evod_objproc(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[])
{
int a;
char res[8];
if(objc != 2)
{
Tcl_WrongNumArgs(interp, 1, objv, “?int?”);
return TCL_ERROR;
}
else
{
if(Tcl_GetIntFromObj(interp, objv[1], &a) != TCL_OK)
{
Tcl_SetStringObj(Tcl_GetObjResult(interp),  “Usage: input should be an integer.”, -1);
return TCL_ERROR;
}
strcpy(res, (a%2)?”odd”:”even”);
Tcl_SetStringObj(Tcl_GetObjResult(interp), res, -1);
//Tcl_SetIntObj(Tcl_GetObjResult(interp), 10);
return TCL_OK;
}
}
int Tcl_AppInit(Tcl_Interp *interp)
{
if(Tcl_Init(interp) != TCL_OK) return TCL_ERROR;
//Tcl_CreateCommand(interp, “evod”, evod_proc, (ClientData)0, 0);
Tcl_CreateObjCommand(interp, “evod”, evod_objproc, (ClientData)0, 0);
return TCL_OK;
}
int main(int argc, char** argv)
{
printf(“— Tcl Third-Party Shell Start —\n”);
Tcl_Main(argc, argv, Tcl_AppInit);
printf(“— Tcl Third-Party Shell End —\n”);
return 0;

}

我们对比一下,

1) 命令的创建由Tcl_CreateCommand改为Tcl_CreateObjCommand;

2) 命令的格式:

int cmdName(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[])

这里,CONST是TCL定义的一个宏,可以理解为一个跨平台的const。

3) 设置结果时,Tcl_SetResult变为Tcl_SetStringObj,Tcl_SetIntObj或Tcl_SetDoubleObj等。其中第一个参数是interp->result的指针,我们用Tcl_GetObjResult来得到它。至于Tcl_SetStringObj,它不仅可以接受字符串,还可以是任意的二进制串;第三个参数是表明串中止的符号。对于通常的字符串,如果以’\0’结尾,则为-1。

4) Tcl_WrongNumArgs是一个简单的函数,直接格式化成标准的“参数个数不对”的字符串。

5) Tcl_Obj方法的好处是可以很容易的在各种格式间转换。事实上,对于任意一个类型XX,都存在

Tcl_SetXXObj(resultPtr, value)

Tcl_GetXXFromObj(interp, objPtr, valuePtr)

Tcl_NewXXObj(resultPtr, value)

如果自己定义了一些特殊结构,则可以定义相应的这类函数来直接操作Tcl_Obj。已经内置的XX有String,Int,Double等。

5 扩展新命令:使用列表

也许有人会对Tcl_Obj的使用感到厌恶。但是我强烈建议在编写Tcl/C程序是使用这种方法。它使得编程变得更加统一。看一个例子。我们知道,tcl中有一个非常好用的数据结构list,我们可以通过Tcl_Obj的方法很容易的在底层访问list。如果用字符串的方法,这将非常麻烦。

现在改进evod,使它的参数变为一个列表,返回也是一个列表:偶数为0,奇数为1,非整数为-1.代码见下:

#include <tcl.h>

#include <stdio.h>

int evod_objproc(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[])

{

int num;

int a, i;

Tcl_Obj **list;

Tcl_Obj *result = Tcl_NewListObj(0, NULL);

if(objc != 2)

{

Tcl_WrongNumArgs(interp, 1, objv, “?list?”);

return TCL_ERROR;

}

else

{

if(Tcl_ListObjGetElements(interp, objv[1], &num, &list) != TCL_OK)

{

Tcl_SetStringObj(Tcl_GetObjResult(interp),  “Internal Error!”, -1);

return TCL_ERROR;

}

for(i = 0; i < num; i++)

{

if(Tcl_GetIntFromObj(interp, list[i], &a) != TCL_OK)

Tcl_ListObjAppendElement(interp, result, Tcl_NewIntObj(-1));

else

Tcl_ListObjAppendElement(interp, result, Tcl_NewIntObj(a%2));

}

Tcl_SetObjResult(interp, result);

return TCL_OK;

}

}

int Tcl_AppInit(Tcl_Interp *interp)

{

if(Tcl_Init(interp) != TCL_OK) return TCL_ERROR;

//Tcl_CreateCommand(interp, “evod”, evod_proc, (ClientData)0, 0);

Tcl_CreateObjCommand(interp, “evod”, evod_objproc, (ClientData)0, 0);

return TCL_OK;

}

int main(int argc, char** argv)

{

printf(“— Tcl Third-Party Shell Start —\n”);

Tcl_Main(argc, argv, Tcl_AppInit);

printf(“— Tcl Third-Party Shell End —\n”);

return 0;

}

1) 我们用Tcl_ListObjGetElements得到列表。可见第四个参数是一个Tcl_Obj指针数组(三重指针),他就是列表,第三个参数返回了列表长度.

2) Tcl_ListObjAppendElement向列表添加Tcl_Obj.注意我们用了Tcl_NewListObj和Tcl_NewIntObj轻易的产生了Tcl_Obj。

3) Tcl_SetObjResult可将任意Tcl_Obj导入结果中。

可见使用Tcl_Obj具有很大的优越性!

6 离开Tcl_Main

我们以上的程序似乎只是个TCL的shell。如果把主程序改成如下的样子,就可以更加的灵活:

int main(int argc, char** argv)

{

printf(“— Tcl Third-Party Shell Start —\n”);

//Tcl_Main(argc, argv, Tcl_AppInit);

Tcl_Interp *interp;

interp = Tcl_CreateInterp();

Tcl_CreateObjCommand(interp, “evod”, evod_objproc, (ClientData)0, 0);

Tcl_Eval(interp, “puts [evod {56 37 love -5.8}]”);

printf(“— Tcl Third-Party Shell End —\n”);

return 0;

}

好了,经过这些讲解,原则上你可以将tcl嵌入你自己编的任何程序了,只要你再多熟悉一些API。