这篇文章演示了如何用其他语言(如C,C 或Java)编写的代码插入到Stata中。这种技术被称为Stata编写插件或编写动态链接库(DLL)。本文中,在C语言中编写一个插件,它实现了mymean11.ado中mymean_work()执行的计算,在文章在Stata中编写估≡计命令编写插件中讨论过。
编写一个hello-world C插件
在进行任何计算之前,先说明如何编写和编译与Stata通信的C插件。Code block 1包含myhello.ado的代码,该←代码调用C插件hello,它只显示Stata中的“Hello from C”。
第6行执行句柄hello的插件。第10行将hello.plugin中实现的插件加载到句柄hello中。执行语句在加载语句开始出现之前就是奇数。完整地读取Stata ado文件,并且在执行主要ado程序行之前加载每个ado程序,Mata函数或插件句柄。所以第10行实际上是【在第6行之前@ 执行的。
插件的句柄名称,本例中的hello,必№须与主要ado程序的名称,本例中的myhello以及此.ado文件中定义的任何其他ado程序不同。
Code block 2中的hello.c的代码。
第2行包括Stata插件页眉文件stplugin.h。第6行是Stata C插件入口函数的标准声明。您应该复制它。在stata_call()中,argc将包含传递给插件的参数数量,字符串向量argv将包含参数本身。
第8行声明并为C字符串msg分配空间。第10行将“Hello from C”中的新行添加到msg中。第11行有Stata显示msg包含的内¤容。12行将0作为返▽回码。请注意,我将文字0转换为预期类型ST_retcode。
现在讨论如何从hello.c创建插件hello.plugin。在包含myhello.ado和hello.c的目录中,我也有stplugin.c。stplugin.c定义了一个函数,使stata_call()函数对Stata可用。
不要更改stplugin.h或stplugin.c的内容。事实上,您甚至不需要看它们。
在安装了命令行开发工具的OS X Mac上,使用gcc通过输入stplugin.c和hello.c来创建hello.plugin,
gcc -bundle -DSYSTEM=APPLEMAC stplugin.c hello.c -o hello.plugin
上面的gcc命令编译两个.c文件并链接它们以创建myhello.ado可以调用的DLL hello.plugin。
在本文的附录中,我提供了在其他平台上创建hello.plugin的说明。
创建了hello.plugin后,就可以在Stata中执行myhello。
示例1:myhello
为简单起见,我将stplugin.h,stplugin.c,hello.c,myhello.ado和hello.plugin放在同一目录中。对于较大的项目,我会将.ado和.plugin文件放在Stata的ADOPATH目录中,并使用我的编译器环境来管理我放置标题和C源文件的位置。 对于这篇文章中的示例,我将所有.ado文件,头文件,C源文件和创建的.plugin文件放入一个目录中。
访问插件中的Stata数据
hello.plugin使Stata显示在插件中创建的内容。下一步是让插♂件访问Stata中的数据。 为了说明这个过程,我讨论了mylistc.ado,它使用插件列出指定变量的观察结果。
我◥们先来看一下ado-code。
第6行中,syntax创建了三个本地宏。它将用户指定的变量放入本地宏varlist中。它将用户指定的任何if条件放入本地宏if。将用户指定的任何条件放入本地宏in中。为syntax指定了max = 3以将变量数量限制为3。我不需要它作为Stata / Mata程序示例,但它简化了示例C插件。
第7行中,marksample创建了一个样本包含变量■,并将其名称放在Ψ本地宏touse中。样本包含变量◥对于每一个被排除的观察和每个被包含的观察都是0。marksample使用本地宏varlist中的变量,本∮地宏中的if的条件,以及本地宏in中的范围来创建样本包含变量。 (所有三个本地宏由syntax创建。)如果本地宏varlist中的任何变量包含缺失值,如果被本地宏if的条件排除,或者被本地宏in的范围排除⊙,则排除观察。样本包含变量是未被排除的观测值之一。
第9行中,通过显示插件列出的值ㄨ的变量名称进一步【简化了C插件。
第10行,plugin调用mylistw.plugin。因为指定了'varlist',所以Stata插件接口(SPI)函数SF_vdata()能访问本地宏varlist中包含的变量。因为如果指定了`touse',如果`touse'中的样本包含变量为0,则SPI函数SF_ifobs()将返回0,如果样本包含变量为1,则函数将】返回1。由于指定了“in”,因此SPI函数SF_in1()和SF_in2()分别返回范围内任何用户指定的第一个和最后一】个观察值。
指定“in”不是识别用◥户指定样本所必需的,因为如果`touse'已经指定了此样本包含信息←。 但是,指定“in”可以显著减少数据循环中的观察范围,从而加快代码速度。
在包含stplugin.h,stplugin.c和mylistw.c的目录中,通过键入以下代码在Mac上创建了mylistw.plugin
gcc -bundle -DSYSTEM=APPLEMAC stplugin.c mylistw.c -o mylistw.plugin
如果你正在阅读这篇文章,你可以阅读标准C。我讨论了mylistw.c如何说明Stata C插件的】结构,并解释了代码中使用的SPI定义的类型和函数。
如果⊙一切顺利,mylistw.c会向Stata返回0,如果出错,会返◥回一个非0错误的代码▆。 每当在mylistw.c中调用一个可能失败的函数时,我都会检查它的返回码。如果该函数失败,我会让Stata显示错误消息,并向Stata返回非0错误代码。 该逻辑为mylisw.c提供了整体结构。大多数代码处理错误条件或者注意不要在字符串缓冲区中放入超出其容量的字符。
C插件使用SPI中定义的函数读取或写入Stata对象。mylistw.c不会返回任何结︼果,因此它具有简单的结构。
. 使用SPI函数从Stata中指定的数据样本↓中读取数据。
. 使用标准C和SPI函数列出指定样本的观察值,并保留指定样本中观察数量的计数器。
. 使用』标准的C和SPI函数来显示样本中的第一个观察结果,这是样本中的最后一个观察结果,以及指定样本中有多少观察结果。
现在,我将讨论mylistw.c的具体部分。
在9-12行中,使用SPI定义的类型ST_int,ST_double和ST_retcode来处理SPI函数返回的变量或SPI函数的参数。 使用这些〗定义的类型是必不可少的,因为它们与原始C类型的映射会随着时间而变化。
rc保存插件将返回到Stata的返回代码。在16行,我将rc初始化为0。如果SPI函数可能ω 会失败,那么它会返回0的返回码。如果SPI函数无法执行请求,则返回的是非0返回码。每次调用可能失败的SPI函数时,我都会将它返回的代码存储在rc中。如果rc不是0,我会让Stata显示错误消息并使插件返回存储在rc中的非0值。
第18,20和22行使用SPI功能。SF_in1()将in范围指定的第一个观察值放入first。SF_in2()将in范围内指定的最后一个观察值放入last。如果没有为plugin指定in范围,则first包含1,last将包含数据集◣中的观察数。SF_nvars()将varlist中指定◆的变量数放入nVars。
第30-32行确⌒ 保我们跳过mylistc.ado第10行中为插件指定的if限制所排除的观察结果。为了说明一些细▽节,请参考示例2。
示例2:mylistc
在第30行中,对于观察值i来说,当为plugin指定的if限制为1,SF_ifobs(i)返回1,否则返回0。在mylist.ado的第10行中,我们看到传入plugin的if限制是“touse”。如上所述,本地宏touse的样本╳包含变量对于排除的观察值是0,对于包括的观察值※是1。
在mylistc.ado第10行的范围■内,使mylistw.c第27行中循环的观察结果只能从范围内任何指定的开始到结束。在示例2中,mylistw.c的第27行上的循≡环从2到10,而不是︾在自动数据集上对所有74个观察结果进行循环。
在示例2中,样本包含变量对于6个观察值为1,对于其他68个观察值为0。在in 2/10范围内不包括观察结果1和11-74的观察结果。 在前10个观察中,2个被排除,因为缺少rep78。 排除一个观察因为trunk是21。
为了比较,在示例3中列出了2和10之间的所有9个观察结果。
案例3:list
返回mylistw.c的第38行,rc = SF_vdata(j,i,&value)将观察值i放在变量j上,它将SF_vdata()返回的代码放入rc。 如果一切顺利,rc包含0,并且不输入第41-43行中▲的错误块。 如果SF_vdata()无法将数据存储到value中,则会输入 第41-43行中的错误』块,这会使Stata显示错误消息并导致mylistw.plugin以rc包含的错误代码退出。在错误块中,SF_error()使Stata以红色显∏示C字符串∑ 的内容。
SF_vdata()只能访问♂一个数字Stata数据类型(byte,int,long,float或double)的变量。(对于字符串数据使用SF_sdata()。)无论变量是哪个Stata数字类型,SF_vdata()都将▂结果存储为ST_double。 在示例2中,mpg,trunk,rep78在Stata中都是int类型,但每个都作为ST_double存储到value中。
第46行,如果value是缺失值,则SF_is_missing(value)返回1,否则返回0。如果其中一个变量中的任何◤观察值包含缺失值,则第46-50行导致mlistw.plugin以错误416退出。这些行是多余的,因为传递到mylistw.plugin的样本包含变量排除了包含缺失值的观察。 我包含这些行是来说明如何安全地从插→件中排除缺失值并重申C代码必须小心处理缺失值。 Stata缺失值在C代码中的是有效的双精度数。如果在计算中包含Stata缺失值,则会得到错误的▲结果。
剩余的△行构造C字符串行,传递给Stata以显示每个观察结果,最后显示有关样本的摘要信息。
C语言插件中的均值估计
我现在讨々论ado-命令mymeanc,它使用mycalcs.plugin实现mymean_work()执行的计算,在Programming an estimation command in Stata: Preparing to write a plugin中有讨论mymean11.ado。
mymeanc的代码位于mymeanc.ado中,在下面代码块5中。
该程序的∑一般结构与mymean10.ado和mymean11相同,在Programming an estimation command in Stata: Preparing to write a plugin中讨论过。 从@全局角度来看,mymeanc.ado:
. 解析用户输入;
. 创建一些名称和对象来保存结果;
. 调用工作程序来进ξ 行计算;
. 将工作程序返回的结果存储在e()中;
. 显示结果.
mymeanc.ado和mymean11.ado之间的←主要区别在于工作程序是C插件而不是Mata函数。
第6行和第7行与mylistc.ado中的相同。有关这些行如何创建本地宏varlist的说明,本地宏touse中包含的样本包含变量以及包含任何用户指定范围的本地宏,请参阅Getting access to the Stata data in your plugin中mylistc.ado的讨论。
第8行将临时名称放入本地宏b,V和N中。我们将这些名称用于C插件计算的々结果,并知道不会覆盖用户存储在全局Stata内存中的任何结果。(回想一下,Stata矩阵和标量是Stata中【的全局对象;Using temporary names for global objects in Programming an estimation command in Stata: A first ado-command文中有讨论本@ 话题。)另外,此外,当mymeanc终止时,Stata将删除tempname创建的临时名称中的对象。
第10-12行创建Stata矩阵来保存结果。我们使用tempname为这些矩阵创建临时名称。
mymeanc.ado中的第14行类似于mylistc.ado中第10行的对应部分。在这种情况下,插件调用mycalcs.plugin来完成工作。 varlist的细节,`if' `touse' 和 `in'在上面讨论过。 最新的是我们将参数`b'`V'`N'将临时名称传递给mycalcs.plugin。
mycalcs.plugin
. 做计算
. 估计均值放入Stata矩阵中,该矩阵的名称在○本地宏b中
. 估计量的♀估计方差(VCE)放入名称在本地宏V中的Stata矩阵中
. 样本中的观察数量放入名称在本地宏N中的Stata标量中
16-18行将︼变量名称放在估计均值向量的列条带上以↓及VCE矩阵的行和列条带上。 第19-21行将结果存储在e()中。 在第22行显示结果。
现在讨论创建mycalcs.plugin的代码。 在讨论细节之前,我们创建插件并运行一个例子。
在包含mycalcs.c,mycalcsw.h,mycalcsw.c,stplugin.c和stplugin.h的目录中,通过输入□以下代码在Mac上创建mycalcs.plugin
gcc -bundle -DSYSTEM=APPLEMAC stplugin.c mycalcsw.c mycalcs.c -o mycalcs.plugin
创建mycalcs.plugin后,运行示例3。
示例3:mymeanc
现在讨论用于∞创建mycalcs.plugin的C代码的一些√方面。 从代码块6中的mycalcs.c开始,包含入口函数stata_call()的代码。
总之,mycalcs.c中的代码执行以下任务。
它将作为参数传入的Stata对象名称放入可以传递给工作函数的C字符串中。
它使用※工作函数InitCmat()为C数组bmat和vmat分配空间,以∮保存矩阵结果。
它使用工作函数MyAve()和MyV()来计算存储在bmat,vmat和nObs中的结果。
它使用工作函数CopyCtoStataMatrix()和SPI函数SF_scal_save()从bmat,vmat和nObs中将结果复制到◆在步骤1中解析◥其名称的Stata对象中。
它释放分配的C数组并返回返回代码。
mycalcs.c很容易阅读,因为我将所有细节都放入了工作函数中。这些函数是在mycalcsw.c中定义,在下面我们将讨论它们。
与mylistw.c一样,mycalcs.c使用返回代码rc来处理错误情况。如果一切顺利,则每个工作函╱数返回0,如■果无法执行所请求的作业,则返回非0错误代码。 如▃果返回代码不为0,mycalcs.c会进入一个代码块来处理错误。每个错误块使Stata显示错误消息,它释放任何已分配的C数组,最后,导致stata_call()返回非0代码。
现在在Code block 7中讨论mycalcsw.c中的工作函数。
如何在C数组中实现矩阵的两个方面值得讨论。首先,将矩阵存储为具有行主要存储的向量,正如第7-10行的注释中所提到的那样。 其次,使用第14-18行定义的预处理器宏来使代码更易于阅读。 请注意,在第166-169行上未定义这些宏。
除了使用SF_error()使Stata显示错误消息,如果malloc()不能分配内存,工作函数InitCmat()使用标准C来实现矩阵分配和初始化函数。
工作函数MyAve()是在Mata中实现的MyAve()的C实现,详见: Programming an estimation command in Stata: Preparing to write a plugin。当我讨论mylistw.c时,如上所述,MyAve()处理Stata数据』和缺失值。在第71行调用的工作函数①DivideByScalar(),通过存储在n中的样本观察数划分bmat中的每个♂元素。(强制转换可确保执行浮点而不是整数除法。)
工作函数MyV()是在Mata中实现的MyV()的C实现,参考Programming an estimation command in Stata: Preparing to write a plugin。MyV()使用到目前为止讨论的大多数编码技术和函数。 此函数比其他函数更长,但其中的所有内容都是标准C或我已经讨论过的内容。
工作函数CopyCtoStataMatrix()将结果从C数组复制到Stata矩阵。使用SF_mat_store(smat,(i 1),(j 1),C(i,j))将元素从C数组的第i行和第j列复制到Stata矩阵中的相应元素。Stata矩阵元素被指定为(i 1)和(j 1),因为在代码中的C矩阵使用基于零开始的索引,而SF_mat_store()使用基于一◎个索引的Stata矩阵元素。
工作函数Divide By Scalar()用标▓量划分C数组中的每个元素。
为了完整起见,我现在讨论mycalcsw.h。 代码块8中给出的mycalcsw.h包含mycalcsw.c中定义的工作函数的函数原型。
完成和撤消
我展示了如何实现一个C插件,该插件执行mymean10.ado和mymean11.ado中Mata工作函数执行的计∮算,如程序29中所述。
附录
在文中,我展示了如何使用命令行开发工具编译和链接一个OS 10 MAC的插件。 在这里,我在Windows 10和RedHat Linux上为gcc编译器提∑供命令。
Windows 10
本小节提◣供了在64位Windows 10系统上〗编译和链接Cygwin环境中插件的命令。与其他平台不同,我们不能只使用gcc。在Cygwin中,gcc编译应用程序以在Cygwin POSIX / Unix环境中运行。我们希望使用Cygwin编译一个链接到本机Windows应用程序并在其中运行的库。 Cygwin拥有适用于Windows(MinGW)的极简的GNU编译器,可以满ζ 足我们的需求。 相应编译器的名称取决于平々台。在64位x86-Intel机器上,我使用了x86_64-w64-mingw32-gcc编译器。
hello.plugin
在包含stplugin.h,stplugin.c和hello.c的目录中,通过键入以下命令来创〓建hello.plugin:
x86_64-w64-mingw32-gcc -shared -mno-clwb stplugin.c hello.c -o hello.plugin
mylistw.plugin
在包含stplugin.h,stplugin.c和mylistw.c的目录中,通过键入以下命令来创建mylistw.plugin:
x86_64-w64-mingw32-gcc -shared -mno-clwb stplugin.c mylistw.c -o mylistw.plugin
mycalcs.plugin
在包含stplugin.c,stplugin.h,mycalcs.c,mycalcsw.h和mycalcsw.c的目录中,通过键入以下命令来创建mycalcs.plugin:
x86_64-w64-mingw32-gcc -shared -mno-clwb stplugin.c mycalcsw.c mycalcs.c -o mycalcs.plugin
RedHat Linux
本小节提供了在RedHat Linux上编译【和链接插件的gcc命令。
hello.plugin
在包含stplugin.h,stplugin.c和hello.c的目录中,通过键入以下命令来创建hello.plugin:
gcc -shared -fPIC -DSYSTEM=OPUNIX stplugin.c hello.c -o hello.plugin
mylistw.plugin
在包含stplugin.h,stplugin.c和mylistw.c的目录中,通过键入以下命令来创建mylistw.plugin:
gcc -shared -fPIC -DSYSTEM=OPUNIX stplugin.c mylistw.c -o mylistw.plugin1
mycalcs.plugin
在包含stplugin.c,stplugin.h,mycalcs.c,mycalcsw.h和mycalcsw.c的目录中,通过键入以下命令来创建mycalcs.plugin:
gcc -shared -fPIC -DSYSTEM=OPUNIX stplugin.c mycalcsw.c mycalcs.c -o mycalcs.plugin