這篇文章演示了如何用其他語言(如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