计算化学公社

 找回密码 Forget password
 注册 Register
Views: 12656|回复 Reply: 3

[VMD] 简单聊聊如何用C语言给VMD的Tcl shell编写函数

[复制链接 Copy URL]

224

帖子

5

威望

4466

eV
积分
4790

Level 6 (一方通行)

发表于 Post on 2021-5-7 22:19:22 | 显示全部楼层 Show all |阅读模式 Reading model
本帖最后由 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语言的randsrand功能实现这一需求,虽然当对于随机数质量有更高要求时,我们也可以使用dSFMT等PRNG作为backend。以下是正文:

        2. 编译适当版本的Tcl库
        首先我们启动VMD,并输入如下命令:
  1. vmd > info patchlevel
复制代码
这条命令查询的是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语言,我们可以首先得到一个最简单的随机数生成函数:
  1. static inline double gen_rand(void)
  2. {
  3.         return (double)rand() / (double)RAND_MAX;
  4. }
复制代码
我们还需要一个用于设定随机数种子的函数,这个函数应当能够使用给定的数值,或随机的数值(例如时间)作为种子:
  1. static unsigned int seed;

  2. static void set_seed(int val)
  3. {
  4.         extern unsigned int seed;
  5.         static struct timeval start;

  6.         if (val < 0) {
  7.                 gettimeofday(&start, NULL);
  8.                 seed = start.tv_usec;
  9.         } else {
  10.                 seed = (unsigned int)val;
  11.         }
  12.         srand(seed);
  13. }
复制代码
接下来我们需要将这两个函数封装为Tcl解释器可以调用的格式。这一封装的具体方法在Tcl extensions开发手册中有非常详细的说明。这里我们仅仅简要介绍思路:
  1. static int set_seed_cmd(ClientData clientData, Tcl_Interp *interp, \
  2.         int objc, Tcl_Obj *CONST objv[])
  3. {
  4.         int seed = 0;

  5.         if (objc < 2) {
  6.                 return TCL_ERROR;
  7.         }
  8.         Tcl_GetIntFromObj(interp, objv[1], &seed);
  9.         if (seed < 0) {
  10.                 return TCL_ERROR;
  11.         } else {
  12.                 set_seed(seed);
  13.         }
  14.         return TCL_OK;
  15. }

  16. static int gen_rand_cmd(ClientData clientData, Tcl_Interp *interp, \
  17.         int objc, Tcl_Obj *CONST objv[])
  18. {
  19.         double ran1 = 0.0;
  20.         Tcl_Obj *tcl_result = NULL;

  21.         tcl_result = Tcl_GetObjResult(interp);
  22.         ran1 = gen_rand();
  23.         Tcl_SetDoubleObj(tcl_result, ran1);
  24.         return TCL_OK;        
  25. }
复制代码
在函数参数表中,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
  1. static int get_seed_cmd(ClientData clientData, Tcl_Interp *interp, \
  2.         int objc, Tcl_Obj *CONST objv[])
  3. {
  4.         extern unsigned int seed;
  5.         Tcl_Obj *tcl_result = NULL;

  6.         tcl_result = Tcl_GetObjResult(interp);
  7.         Tcl_SetIntObj(tcl_result, seed);
  8.         return TCL_OK;
  9. }

  10. static int gen_list_cmd(ClientData clientData, Tcl_Interp *interp, \
  11.         int objc, Tcl_Obj *CONST objv[])
  12. {
  13.         int i = 0;
  14.         int len = 0;
  15.         Tcl_Obj **ran = NULL;
  16.         Tcl_Obj *tcl_result = NULL;

  17.         if (objc < 2) {
  18.                 return TCL_ERROR;
  19.         }
  20.         Tcl_GetIntFromObj(interp, objv[1], &len);
  21.         if (len <= 0) {
  22.                 return TCL_ERROR;
  23.         }
  24.         ran = calloc(len, sizeof(Tcl_Obj*));
  25.         for (i = 0; i < len; i++) {
  26.                 ran[i] = Tcl_NewObj();
  27.                 Tcl_SetDoubleObj(ran[i], gen_rand());
  28.         }
  29.         tcl_result = Tcl_NewListObj(len, NULL);
  30.         Tcl_SetListObj(tcl_result, len, ran);
  31.         Tcl_SetObjResult(interp, tcl_result);
  32.         free(ran);
  33.         return TCL_OK;
  34. }
复制代码
最后我们需要向Tcl解释器注册这些函数,并为它们分配它们在Tcl shell中被调用时所使用的函数名。这些操作可以使用Tcl_CreateObjCommand函数完成。需要注意的时,当Tcl解释器加载名为libxxxx.so的动态库时,它会试图查找这一动态库中是否存在名为int Xxxx_Init(Tcl_Interp *interp)的函数。若存在这一函数,则Tcl解释器会首先调用一次该函数,并执行函数中所定义的操作。这里我们将会把编译得到的动态库命名为librandom.so,因此我们会得到如下的可以被Tcl解释器所调用的函数:
  1. int Random_Init(Tcl_Interp *interp)
  2. {
  3.         Tcl_Namespace *nsPtr = NULL;

  4.         set_seed(-1);
  5.         nsPtr = Tcl_CreateNamespace(interp, "Random", NULL, NULL);
  6.         if (nsPtr == NULL) {
  7.                 return TCL_ERROR;
  8.         }
  9.         Tcl_CreateObjCommand(interp, "Random::sran", set_seed_cmd, NULL, NULL);
  10.         Tcl_CreateObjCommand(interp, "Random::seed", get_seed_cmd, NULL, NULL);
  11.         Tcl_CreateObjCommand(interp, "Random::rand", gen_rand_cmd, NULL, NULL);
  12.         Tcl_CreateObjCommand(interp, "Random::list", gen_list_cmd, NULL, NULL);
  13.         return TCL_OK;
  14. }
复制代码
为不影响VMD Tcl shell中其他命令起见,我们这里通过Tcl_CreateNamespace函数将所有所定义的函数放置于名为Randomnamespace中。
        最后,我们使用gcc对我们编写的代码(librandom.c)进行编译:
  1. 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中输入:
  1. load ./librandom.so
复制代码
随后进行测试(如图):
Screenshot from 2021-05-07 22-12-43.png
可见我们编写的C库能够实现我们所期望的功能。

        5. 其他
        本文中所讲述的方法不仅仅适用于VMD,同样也适用于NAMD的TclForce模块。但应当注意的是,NAMD的内置Tcl解释器版本与VMD的内置Tcl解释器版本不一定相同,在使用前同样应当做好测试。此外,本文并未提及如何从Tcl解释器中获取列表形式的参数用于后续分析,这对于乐意思考的读者来说应当不成问题。
        最后附上本文中代码的文本文件:
librandom.c (2.3 KB, 下载次数 Times of downloads: 4)

评分 Rate

参与人数
Participants 10
威望 +1 eV +45 收起 理由
Reason
丁越 + 5 牛!
冷血 + 5 赞!
心向暖阳 + 5
zsu007 + 5 赞!
卡开发发 + 5
chenjinfeng850 + 5 谢谢分享
sobereva + 1
lyj714 + 5 好物!
snljty + 5 GJ!
ggdh + 5 高级啊

查看全部评分 View all ratings

我需要一些假日,但我不希望每天都是假日。因为我没有承担痛苦,因为那不是真正的自由。

224

帖子

5

威望

4466

eV
积分
4790

Level 6 (一方通行)

 楼主 Author| 发表于 Post on 2021-5-7 22:58:54 | 显示全部楼层 Show all
顺便吐槽一句,论坛的代码编辑器真是难用……
我需要一些假日,但我不希望每天都是假日。因为我没有承担痛苦,因为那不是真正的自由。

220

帖子

1

威望

1765

eV
积分
2005

Level 5 (御坂)

发表于 Post on 2021-5-8 01:09:36 | 显示全部楼层 Show all
本帖最后由 lyj714 于 2021-5-8 01:12 编辑

补个Win上编译的测试情况,咱就以Visual Studio 2019的cl编译器为例,配合官网稳定版本vmd1.9.3为例(注意这里是32位的程序哦!)
  • 如楼主写的那个程序为例,目前简单修改一下,去掉<sys/time.h>的依赖,,,添加上dll导出函数符号__declspec(dllexport),废话不多说,直接看修改好的附件
  • 编译并导出dll,咱就一步到位的命令,打开vs command prompt终端,注意一定要是32位的,必须与vmd保持一致
  1. cl -nologo -LD  -O2 -MD  librandom.c /IE:\tcl\include /link E:\tcl\lib\tcl85.lib
复制代码


这里的tcl库你可以通过pip安装,也可以源码编译(vs),注意一定要是tcl8.5才可以(与vmd中的保持一致,也必须是32位的)
  • 使用,通过vmd的tcl命令直接load就可以啦:
    1. load librandom.dll
    复制代码
    202105080102189310..png

当然用vmd1.9.4a的Win64版本也可以,比如这个版本http://bbs.keinsci.com/thread-19148-1-1.html,配合vs 64位编译器使用即可:

202105080106326417..png

librandom.c

2.99 KB, 下载次数 Times of downloads: 11

win && linux

评分 Rate

参与人数
Participants 2
eV +10 收起 理由
Reason
snljty + 5
ene + 5

查看全部评分 View all ratings

9

帖子

0

威望

400

eV
积分
409

Level 3 能力者

发表于 Post on 2022-3-15 14:03:05 | 显示全部楼层 Show all
牛的

本版积分规则 Credits rule

手机版 Mobile version|北京科音自然科学研究中心 Beijing Kein Research Center for Natural Sciences|京公网安备 11010502035419号|计算化学公社 — 北京科音旗下高水平计算化学交流论坛 ( 京ICP备14038949号-1 )|网站地图

GMT+8, 2023-2-2 23:22 , Processed in 0.214868 second(s), 25 queries .

快速回复 返回顶部 返回列表 Return to list