|
本帖最后由 ene 于 2021-5-7 23:30 编辑
1. 前言
VMD提供了强大的动力学模拟结果分析功能,我们常常会利用它内置的Tcl shell编写各种Tcl语言分析脚本,以实现不同的目的。然而当有时我们需要较高的执行速度,或一些使用纯Tcl语言不方便实现的功能时,VMD内置的Tcl解释器的表现就不那样尽如人意了。因此这里我简单介绍一下怎么使用C语言给VMD编写它的Tcl解释器能够识别的函数(所谓Tcl C extensions)。本文的实现参考了这篇文章,如果需要更复杂的功能,可以参考Tcl extensions开发手册以及Tcl C API List,编写所需的代码。本文的结果全部基于Linux平台,Windows和Mac用户需要自行尝试。
在本文中,我们将尝试编写一个random函数,用来产生指定数量的随机数,并将结果作为一个list返回。VMD的Tcl解释器已经内置了一个rand函数,但这个PRNG的功能较为简陋,最主要的一点是用户没法指定随机数种子,当我们需要固定的随机数序列(用于debug等等),Tcl内置的rand函数就不太合用了。因此我们编写的函数应该能够自行指定随机数种子,并能够返回随机数种子的数值。我们将使用C语言的rand与srand功能实现这一需求,虽然当对于随机数质量有更高要求时,我们也可以使用dSFMT等PRNG作为backend。以下是正文:
2. 编译适当版本的Tcl库
首先我们启动VMD,并输入如下命令:这条命令查询的是VMD内置的Tcl解释器的版本,我们使用的Tcl库版本应当与这个版本严格一致。当查询到结果后,我们可以在这里下载对应版本的Tcl解释器源码,编译安装方法见这里。在编译时我们可以激活 --enable-shared=no选项,用来生成静态的Tcl库。这样我们后续编译得到的插件动态库能够静态的链接到Tcl库,避免配置环境变量带来的麻烦。后文中我们假设编译好的Tcl解释器安装到了/path/to/tcl,也就是在/path/to/tcl下存在bin/,include/,lib/,man/等文件夹。
3. 编写Tcl C extensions
使用C语言,我们可以首先得到一个最简单的随机数生成函数:
- static inline double gen_rand(void)
- {
- return (double)rand() / (double)RAND_MAX;
- }
复制代码 我们还需要一个用于设定随机数种子的函数,这个函数应当能够使用给定的数值,或随机的数值(例如时间)作为种子:- static unsigned int seed;
- static void set_seed(int val)
- {
- extern unsigned int seed;
- static struct timeval start;
- if (val < 0) {
- gettimeofday(&start, NULL);
- seed = start.tv_usec;
- } else {
- seed = (unsigned int)val;
- }
- srand(seed);
- }
复制代码 接下来我们需要将这两个函数封装为Tcl解释器可以调用的格式。这一封装的具体方法在Tcl extensions开发手册中有非常详细的说明。这里我们仅仅简要介绍思路:- static int set_seed_cmd(ClientData clientData, Tcl_Interp *interp, \
- int objc, Tcl_Obj *CONST objv[])
- {
- int seed = 0;
- if (objc < 2) {
- return TCL_ERROR;
- }
- Tcl_GetIntFromObj(interp, objv[1], &seed);
- if (seed < 0) {
- return TCL_ERROR;
- } else {
- set_seed(seed);
- }
- return TCL_OK;
- }
- static int gen_rand_cmd(ClientData clientData, Tcl_Interp *interp, \
- int objc, Tcl_Obj *CONST objv[])
- {
- double ran1 = 0.0;
- Tcl_Obj *tcl_result = NULL;
- tcl_result = Tcl_GetObjResult(interp);
- ran1 = gen_rand();
- Tcl_SetDoubleObj(tcl_result, ran1);
- return TCL_OK;
- }
复制代码 在函数参数表中,ClientData clientData是由Tcl解释器分配的用于保存全局数据的结构体,在这里对我们用处不大。 Tcl_Interp *interp是一个指向Tcl解释器结构体的指针,其中保存了对我们可见的数据,包括将要返回给解释器的计算结果等等。int objc是我们的函数被调用时,接收到的参数数量,Tcl_Obj *CONST objv[]是我们的函数在被调用时实际接收到的参数。在set_seed_cmd中我们使用Tcl_GetIntFromObj函数,将函数接受到的第一个参数转化为整数,并将其用作PRNG种子的数值。在gen_rand_cmd中我们首先使用Tcl_GetObjResult获取interp中,保存计算结果的结构体的地址,随后我们使用Tcl_SetDoubleObj将一个随机数填入这个结构体中。基于类似的思路,我们还可以得到用于返回当前随机数种子数值的get_seed_cmd函数与用于得到包含指定数量随机数的列表的gen_list_cmd函数。其中gen_list_cmd函数略有不同,我们首先使用Tcl_NewListObj函数得到一个填充了给定数量随机数的list,随后再将这个list作为结果传递给interp。
- static int get_seed_cmd(ClientData clientData, Tcl_Interp *interp, \
- int objc, Tcl_Obj *CONST objv[])
- {
- extern unsigned int seed;
- Tcl_Obj *tcl_result = NULL;
- tcl_result = Tcl_GetObjResult(interp);
- Tcl_SetIntObj(tcl_result, seed);
- return TCL_OK;
- }
- static int gen_list_cmd(ClientData clientData, Tcl_Interp *interp, \
- int objc, Tcl_Obj *CONST objv[])
- {
- int i = 0;
- int len = 0;
- Tcl_Obj **ran = NULL;
- Tcl_Obj *tcl_result = NULL;
- if (objc < 2) {
- return TCL_ERROR;
- }
- Tcl_GetIntFromObj(interp, objv[1], &len);
- if (len <= 0) {
- return TCL_ERROR;
- }
- ran = calloc(len, sizeof(Tcl_Obj*));
- for (i = 0; i < len; i++) {
- ran[i] = Tcl_NewObj();
- Tcl_SetDoubleObj(ran[i], gen_rand());
- }
- tcl_result = Tcl_NewListObj(len, NULL);
- Tcl_SetListObj(tcl_result, len, ran);
- Tcl_SetObjResult(interp, tcl_result);
- free(ran);
- return TCL_OK;
- }
复制代码 最后我们需要向Tcl解释器注册这些函数,并为它们分配它们在Tcl shell中被调用时所使用的函数名。这些操作可以使用Tcl_CreateObjCommand函数完成。需要注意的时,当Tcl解释器加载名为libxxxx.so的动态库时,它会试图查找这一动态库中是否存在名为int Xxxx_Init(Tcl_Interp *interp)的函数。若存在这一函数,则Tcl解释器会首先调用一次该函数,并执行函数中所定义的操作。这里我们将会把编译得到的动态库命名为librandom.so,因此我们会得到如下的可以被Tcl解释器所调用的函数:
- int Random_Init(Tcl_Interp *interp)
- {
- Tcl_Namespace *nsPtr = NULL;
- set_seed(-1);
- nsPtr = Tcl_CreateNamespace(interp, "Random", NULL, NULL);
- if (nsPtr == NULL) {
- return TCL_ERROR;
- }
- Tcl_CreateObjCommand(interp, "Random::sran", set_seed_cmd, NULL, NULL);
- Tcl_CreateObjCommand(interp, "Random::seed", get_seed_cmd, NULL, NULL);
- Tcl_CreateObjCommand(interp, "Random::rand", gen_rand_cmd, NULL, NULL);
- Tcl_CreateObjCommand(interp, "Random::list", gen_list_cmd, NULL, NULL);
- return TCL_OK;
- }
复制代码 为不影响VMD Tcl shell中其他命令起见,我们这里通过Tcl_CreateNamespace函数将所有所定义的函数放置于名为Random的namespace中。
最后,我们使用gcc对我们编写的代码(librandom.c)进行编译:
- gcc -shared -fPIC -o librandom.so -I/path/to/tcl/include librandom.c -L/path/to/tcl/lib/ -ltcl8.5 -Wall
复制代码 若一切正常,当前目录下即可产生librandom.so文件。
4. 测试
在编译目录下启动VMD,并在TkConsole中输入:
随后进行测试(如图):
可见我们编写的C库能够实现我们所期望的功能。
5. 其他
本文中所讲述的方法不仅仅适用于VMD,同样也适用于NAMD的TclForce模块。但应当注意的是,NAMD的内置Tcl解释器版本与VMD的内置Tcl解释器版本不一定相同,在使用前同样应当做好测试。此外,本文并未提及如何从Tcl解释器中获取列表形式的参数用于后续分析,这对于乐意思考的读者来说应当不成问题。
最后附上本文中代码的文本文件:
librandom.c
(2.3 KB, 下载次数 Times of downloads: 7)
|
评分 Rate
-
查看全部评分 View all ratings
|