這篇文章是這系列文章的第三篇,說明了如何用另一種語言(如C、C 或Java)編寫的代碼插入Stata,這種技術被稱為編寫插件,或者為Stata編寫一個動態鏈接庫(DLL)。這篇文章介紹在C 中編寫插件,實現了在mymean11.ado中由mymean_work()執行的計算。
這篇文章類似于《在Stata中編寫估計命令:編寫插件》。不同之處在于,插件代碼是用C 而不是c語言,如果讀者熟悉那篇文章的內容,您會發現它在這里有很多的重復。
編寫一個hello-world C 插件
在進行任何計算之前,我都會演示如何編寫和編譯一個與Stata通信的C 插件。Code block 1包含myhellocpp.ado的代碼。調用C 插件hellocpp,它只在Stata中顯示Hello。
第6行執行插件,其句柄是hellocpp。第10行加載hellocpp中實現的插件。插件到句柄hellocpp。執行語句在加載語句開始之前就出現了。Stata ado-files被完整讀取,每個ado-program、Mata函數或插件句柄都被加載到主ado-program的行之前。第10行實際上是在第6行之前執行的。
在這種情況下,插件句柄的名稱hellocpp,必須與主ado編程的名稱不同,在本例中是myhellocpp,并且跟這個.ado文件中定義的任何其他ado編程不同。
Code block 2中的Hello.cpp代碼。
第2行包括Stata插件頭文件stplugin.h。第6行是Stata C 插件的入口函數的標準聲明。在stata_call(),argc包含了傳遞給插件的參數的數量,而字符串argv將包含參數本身。
第8行聲明并為C 字符數組msg分配空間。使用字符數組可能看起來很奇怪,因為C 程序通常使用C 字符串來操縱字符串。我們需要字符數組,因為在Stata程序接口(SPI)中接受字符串參數的函數使用C/C 字符數組。
例如,在第12行,SPI函數SF_display(),它使Stata從C 中顯示Hello,接受了C 字符數組msg。SF_display()不接受在第10行創建的C 字符串mystring。出于這個原因,我在第11行中復制了C mystring包含的字符數組msg。第14行返回0作為返回代碼。注意,我把字面上的0作為預期的ST_retcode。
現在,將討論如何從hello.cpp插件創建hellocpp.plugin插件。目錄中包含了myhellocpp.ado和hello.cpp,還有stplugin.cpp。stplugin.cpp是stplugin.c的副本。我必須把stplugin.c復制到stplugin.cpp中,因為有些C 編譯器不會把.c文件當作C 源文件來處理。stplugin.c定義的函數,在Stata中可以用它調用stata_call()函數。
不要更改stplugin.h或stplugin.c的內容。事實上,您甚至不需要管它們。在我的OS X Mac上安裝了命令行開發者工具,我使用g 從stplugin.cpp和hello.cpp創建hellocpp.plugin,通過輸入g -bundle -DSYSTEM=APPLEMAC stplugin.cpp hello.cpp -o hellocpp.plugin
上面的g 命令編譯兩個.cpp文件并將它們鏈接起來創建DLL hellocpp.plugin,可以被myhellocpp.ado調用。
在這篇文章的附錄中,我提供了在其他平臺創建hellocpp.plugin的說明。https://www.stata.com/plugins/為編寫和編譯C 插件提供完整的文檔。
已經創建了hellocpp.plugin,就可以在Stata中執行myhellocpp。
示例2: myhellocpp
為簡單起見,stplugin.h, stplugin.cpp, hello.cpp, myhellocpp.ado和hellocpp.plugin在同一個目錄中。對于較大的項目,我把.ado和.plugin文件放到Stata ADOPATH目錄中,并使用我的編譯器環境來管理我放置頭文件和C 源文件。對于本文中的示例,我將所有的.ado文件、頭文件、C 源文件和創建的.plugin文件放入單個目錄中。
在插件中訪問Stata數據
hellocpp.plugin使Stata顯示在插件內創建的內容。下一步是讓插件訪問Stata中的數據。為了說明這個過程,我將討論mylistcpp.ado,它使用一個插件來列出對指定變量的觀測。
讓我們先看一下ado-code。
在第6行,syntax創建三個本地宏。它將用戶指定的變量放入本地宏varlist中,將用戶指定的if條件放入本地宏if中,將用戶指定的in范圍放入到本地宏in中。我將max=3指定為syntax,變量的限制數量為3。這個限制是多余的,我不需要它作為Stata/Mata程序的例子,但是它簡化了示例C 插件。
在第7行中,marksample創建了一個包含樣本的變量,并將其名稱放在本地宏touse中。每個排除觀測值包含樣本的變量為0,每個包含觀測值的變量為1。marksample使用本地宏varlist中的變量,如果本地宏中的if條件,以及本地宏in的范圍來創建樣本包容變量(所有三個本地宏都是由syntax創建的)。如果本地宏varlist中的任何變量都包含缺失值,則排除一個觀測結果。如果它被本地宏if的條件排除,或者它被本地宏的范圍排除在外,則會將觀測排除在外。
第9行,進一步簡化了C 插件,通過顯示由插件列出的值的變量的名稱。
第10行,plugin調用mylistwcpp.plugin。因為“varlist”是指定的,SPI函數SF_vdata()將能夠訪問本地宏varlist中包含的變量。因為if “touse”是指定的,如果touse中的包容樣本變量為0,那么SPI函數SF_ifobs()將返回0; 如果包含樣本變量為1,則函數將返回1。因為“in”被指定,SPI函數SF_in1()和SF_in2()分別返回在用戶指定范圍的第一個和最后一個觀測值。
指定“In”不是識別用戶指定的樣本所必需的,因為if“touse已經指定了這個包含樣本的信息。然而,指定“in”可以極大地減少循環中對數據的觀測范圍,從而加快代碼的速度。
在包含mylistw.cpp, stplugin.cpp和stplugin.h的目錄中,我通過輸入下面的內容在Mac上創建了mylistwcpp.plugin,
g -bundle -DSYSTEM=APPLEMAC stplugin.cpp mylistw.cpp -o mylistwcpp.plugin
在這里,討論的是mylistw.cpp如何說明Stata中C 插件的結構,并解釋了代碼中使用的Stata插件接口(SPI)所定義的類型和功能。
如果Stata運行順利的話,mylistw.cpp會返回0。如果出錯,它會返回一個非零錯誤代碼。
每次調用mylistw.cpp中一個可能失敗的函數時,可以檢查它的返回代碼。如果該函數失敗,我會讓Stata顯示一個錯誤消息,并且將一個非零錯誤代碼返回到Stata中。這種邏輯提供了mylisw.cpp的總體結構。大多數代碼處理錯誤條件,或不要將更多的字符放入字符串緩沖區中。
C 插件使用SPI中定義的函數或寫入Stata對象。mylistw.cpp并未返回任何結果,因此它有一個簡單的結構:
? 使用SPI函數從Stata中指定的數據樣本中讀取。
? 使用標準的C 和SPI函數來列出對指定樣本的觀測值,并保持指定樣本中有多少觀測值的計數器。
? 使用標準的C 和SPI函數來顯示樣本中的第一個觀測值、樣本中最后的觀察值,以及在指定的樣本中有多少觀測值。
現在,我將討論mylistw.cpp的具體部分。
在第10-12行中,我使用SPI定義類型ST_int、ST_double和ST_retcode,用于SPI函數返回的變量,或者是Stata插件接口函數的參數。使用這些定義的類型是必要的,因為它們對原始C 類型的映射會隨著時間的變化而變化。
rc保存返回代碼,插件會返回到Stata中。在第17行,我將rc初始化為0。如果一個可能失敗的SPI函數執行請求,則返回一個0的返回代碼。如果SPI函數不能執行請求,那么它會返回一個非零返回碼。每當我調用一個可能失敗的SPI函數時,我都會把它保存在rc返回代碼中。如果rc不是0,我讓Stata顯示一條錯誤消息,并讓插件返回保存在rc中的非零值。
第19、21和23行使用SPI函數。SF_in1()將in范圍指定的第一個觀測值放在第一位。SF_in2()將in范圍指定的最后一個觀測值放到最后。如果in范圍沒有被指定為插件,那么first包含1,而last包含數據集中觀測值的數量。SF_nvars()把varlist中的變量數量指定到nVars中。第31–33行確保我們跳過了在mylistcpp.ado第10行中被if限制排除的觀測結果。為進一步說明細節,請參看示例2。
示例2: mylistcpp
在第31行,SF_ifobs(i)返回1時,指定到插件中的if限制是1,觀測值是i,否則是0.
在mylistcpp.ado的第10行,我們看到傳遞給插件的if限制是“touse”。正如上文所討論的,本地宏touse中樣本包含變量對于排除的觀測值是0,并且對于包含的觀測值是1。
mylistcpp.ado中第10行的in范圍被包含,因此,mylistw.cpp第28行的觀測值是循環的,在in范圍內可以從任何指定的地方開始或結束。在示例2中,mylistw.cpp第28行的循環只從2到10,而不是在自動數據集中的74個觀測值進行循環。
在示例2中,6個觀測值的樣本包含變量為1,而其他68個觀測值的變量為0。in 2/10的范圍不包括觀測值1和11-74的觀測值。在前10個觀測值中,2被排除在外,因為rep78缺失。排除一個觀測值,因為trunk是21。為了進行比較,在示例3中列出了2到10之間的9個觀測值。
回到mylistw.c的第39行, rc_st=SF_vdata(j,I,&value)將觀測值i在變量j中的值放入value中,并將SF_vdata()返回代碼放入rc中。如果一切順利,rc包含0,就不會輸入42-44行中的錯誤塊。如果SF_vdata()不能將數據保存到value中,那么就會輸入42—44行中的錯誤塊。誤差塊使Stata顯示錯誤消息,并導致mylistw.plugin退出rc包含的錯誤代碼。在誤差塊中,SF_error()使Stata顯示紅色的C 字符數組的內容。
SF_vdata()只能訪問數值變量之一,Stata的數據類型(byte、int、long、float或double)。(使用SF_vdata()的字符串數據。)不管變量的Stata數值類型是什么,SF_vdata()把結果作為ST_double來保存。在示例2中,mpg,trunk,rep78在Stata中都是int,但是每個都作為ST_double保存到value中。
在第47行,如果value是缺失值,那么SF_is_missing(value)返回1,否則它將返回0。如果其中一個變量中的任何一個觀測值包含缺失值,那么47-51行會導致mlistw.plugin退出,錯誤信息為416。這些行是多余的,因為樣本包含變量傳遞給mylistw.plugin排除包含缺失值的觀測。我包含這些行來說明如何安全地從插件內部排除缺失值,并重申C 代碼必須小心處理缺失值。Stata缺失的值是C 中有效的雙精度數。如果在計算中包含Stata缺失值,則會得到錯誤的結果。剩下的行構造C 字符數組,傳遞給Stata來顯示每個觀測結果,并顯示關于樣本的摘要信息。
C 插件中的均值估計
現在討論ado命令mymeancpp,使用mycalcscpp.plugin實現由mymean11.ado中的mymean_work()執行的計算,這個內容在《在Stata中編寫估計命令:編寫插件》討論過。
Code block 5中mymeancpp.ado的mymeancpp代碼,如下:
這個程序的大概結構跟mymean10.ado和mymean11一樣,這個內容在《在Stata中編寫估計命令:編寫插件》討論過。整體來看,mymeancpp.ado可以
? 解析用戶輸入
? 創建文件名稱和對象來保存結果
? 調用一個工作程序進行計算
? 將工作程序返回的結果存儲在e()中,并且
? 顯示結果
mymeancpp.ado和mymean11.ado的主要區別這個工作程序是一個C 插件,而不是一個Mata函數。
第6行和第7行與mylistcpp.ado中的代碼相同。關于這些行如何創建本地宏varlist的描述,本地宏touse中樣本包含變量,其中本地宏包含范圍內指定的任何用戶,具體可以參考 Getting access to the Stata data in your plugin中的mylistcpp.ado。
第8行將臨時名稱放入本地的宏b、V和N中。我們使用這些名稱來計算由C 插件計算的結果,并且我們知道,我們不會覆蓋用戶保存在全局Stata內存中的任何結果(回想Stata矩陣和標量是Stata中的全局對象);請參閱Programming an estimation command in Stata: A first ado-command中的Using temporary names for global objects討論的就是這方面的主題)。此外,Stata會在mymeancpp終止時,刪除由tempname創建的臨時名稱中的對象。
第10-12行創建Stata矩陣來保存結果。我們使用由tempname創建的臨時名稱用于這些矩陣。
mymeancpp中第14行類似于mylistcpp.ado第10行的對應部分。在這種情況下,plugin調用mycalcspp.plugin來做這個工作。varlist的詳細信息,if “touse”和“in”上文已經討論過了。 新的情況是,我們傳遞參數'b'V'‘N’,將臨時名稱傳遞給mycalcs.plugin。
mycalcscpp.plugin可以:
? 執行計算
? 將估計的方法放入Stata矩陣中,它的名稱在本地宏b中
? 將VCE放入Stata矩陣中它的名稱在本地宏V中,并且
? 將樣本內的觀測數量放入Stata標量中,其名稱位于本地宏N中
第16-18行將變量名稱放在估計方法向量的列條紋上,并在VCE矩陣的行和列條紋上。第19-21行將結果保存在e()中,第22行顯示結果。
現在,我將討論創建mycalcs.plugin的代碼。在討論細節之前,讓我們創建插件并運行一個示例。
在包含mycalcs.cpp, mymatrix.cpp, mycalcsv.cpp, mymatrix.h, mycalcsv.h, stplugin.cpp和stplugin.h的目錄中,我創建了mycalcscpp.plugin在我的Mac上,通過輸入
g -bundle -DSYSTEM=APPLEMAC mycalcs.cpp mymatrix.cpp mycalcsv.cpp stplugin.cpp -o mycalcscpp.plugin
創建好mycalcscpp.plugin之后,我運行示例3。
現在,我將討論用于創建mycalcscpp.plugin的C 代碼的一些問題。我從code block 6中的mycalcs.cpp開始,包含入口函數代碼stata_call()。
總之,mycalcs.cpp中的代碼可以執行以下任務:
將Stata對象的名稱作為參數傳遞給C 字符數組,這些數組可以傳遞給SPI函數。
創建了MyMatrix類的bmat和vmat實例,并保存結果。
使用工作函數MyAve()和MyV()來計算保存在bmat、vmat和nobs中的結果。
使用MyMatrix類的成員函數CopyCtoStataMatrix(),SPI函數SF_scal_save()從bmat,vmat和nobs中復制結果到Stata對象中,它的名稱在步驟1中已經被解析。
mycalcs.cpp很容易讀懂,因為我把所有的細節都放到了MyMatrix類和工作函數中。MyMatrix類是在mymatrix.cpp中定義的,而工作函數是在mycalcsv.cpp中定義的。
像mylistw.cpp, mycalcs.cpp使用返回碼rc來處理錯誤條件。如果函數進行得很好,每個函數都返回0,并且如果每個函數執行請求的作業,則每個函數返回非零錯誤代碼。如果返回的代碼不是0,mycalcs.cpp立即返回到Stata_call()中,這反過來又將非零代碼返回到Stata中。與錯誤條件相關聯的錯誤消息由工作函數顯示。
在point(2)中,我注意到bmat和vmat是MyMatrix類的實例。樣本平均值和VCE最好保存在矩陣中。為了使事物簡單且自包含,我定義了一個使用矩陣行保存和僅需要的成員函數的裸矩陣類MyMatrix。除了成員函數copyCtoStataMatrix()之外,MyMatrix的代碼是標準C ,可以在code block 7中看到。
在第8行,我使用了一個預處理器宏來簡化引用矩陣元素的代碼。我在第79行上定義了宏。
第57-77行包括了CopyCtoStataMatrix()代碼。第68行使用了一個我還沒有討論過的SPI函數。SF_mat_store(char *sname, ST_int i, ST_int j, ST_double val)將值val保存在Stata矩陣中的行i和列j中。矩陣的名稱包含在字符數組sname中。在一個基于索引的行中給出行i和列j。
第67行使用成員函數GetValue(i,j),在MyMatrix類的實例中返回矩陣的(i,j)元素。GetValue()在mymatrix.h中定義,這是包含MyMatrix類聲明的頭文件。Code block 8包含mymatrix.h中的代碼。
這段代碼只使用標準C 和我已經討論過的編碼技術。在point(3)中,我注意到mycalcs.cpp使用工作函數MyAve()和MyV()來計算結果。這些函數是在code block 9中的mycalcsv.cpp定義的。
工作函數MyAve()是C 在Mata中MyAve()的實現,具體參看《在Stata中編寫估計命令:編寫插件》。它將樣本平均值放入MyMatrix類的bmat實例中,并將樣本中觀測的數量放入nobs中。這個函數的大部分代碼都是標準C ,或者它使用了我已經討論過的SPI函數。
第35行使用MyMatrix的IncrementByValue()成員函數。當計算樣本均值并將其保存在一個向量b的jth元素中時,需要將b[j] value保存到b[j]中。換句話說,b中jth元素增量的數量由vaule來表示。bmat中的bmat.IncrementByValue(0,j, value)增量元素j由value表示。
第39行使用MyMatrix的成員函數DivideByScalar()。bmat.DivideByScalar(z)將bmat的每個元素替換為能被z除的元素。
MyV()是Mata函數MyV()的C 實現,這個內容在《在Stata中編寫估計命令:編寫插件》有講到。它將VCE放入MyMatrix類的vmat實例中。這個函數的大部分代碼要么是標準C ,要么是我們已經討論過的技術。第71、78和87行使用MyMatrix成員函數Storevalue()和GetValue(),這兩個函數由mymatrix.h定義。
vmat.StoreValue(i, j, z)將z值保存在MyMatrix的vmat實例(i, j)元素中。vmat.GetValue(i, j)返回保存在MyMatrix的vmat實例(i, j)元素中的值。
附錄
本文展示了如何使用命令行開發者工具在OS 10 Mac上編譯和鏈接一個插件。在這里給出了gcc編譯器在Windows 10和RedHat Linux上的命令。
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-g 編譯器。
hellocpp.plugin
在包含stplugin.h, stplugin.cpp和hello.cpp的目錄中,通過輸入以下內容創建hellocpp.plugin,
x86_64-w64-mingw32-g -shared -static stplugin.cpp hello.cpp -o hellocpp.plugin
mylistwcpp.plugin
在包含stplugin.h,stplugin.cpp和mylistw.cpp的目錄中,通過輸入下面內容創建mylistwcpp.plugin
x86_64-w64-mingw32-g -shared -static stplugin.cpp mylistw.cpp -o mylistwcpp.plugin
mycalcscpp.plugin
在包含stplugin.cpp,stplugin.h,mycalcs.cpp,mycalcsv.h和mycalcsv.cpp的目錄中,通過輸入以下內容創建mycalcscpp.plugin,
x86_64-w64-mingw32-g -shared -static stplugin.cpp mycalcsv.cpp mycalcs.cpp -o mycalcscpp.plugin
RedHat Linux
這個子部分提供了gcc命令來編譯和鏈接RedHat Linux上的插件。
hellocpp.plugin
在包含stplugin.h, stplugin.cpp和hello.cpp的目錄中,通過輸入以下內容創建hellocpp.plugin
g -shared -fPIC -DSYSTEM=OPUNIX stplugin.cpp hello.cpp -o hellocpp.plugin
在包含stplugin.h,stplugin.cpp和mylistw.cpp的目錄中,通過輸入以下內容創建mylistwcpp.plugin,
g -shared -fPIC -DSYSTEM=OPUNIX stplugin.cpp mylistw.cpp -o mylistwcpp.plugin
mycalcscpp.plugin
在包含stplugin.cpp,stplugin.h,mymatrix.cpp,mycalcsv.cpp,mycalcs.cpp,mymatrix.h和mycalcsv.h的目錄中,通過輸入以下內容創建mycalcscpp.plugin,
g -shared -fPIC -DSYSTEM=OPUNIX stplugin.cpp mymatrix.cpp mycalcsv.cpp mycalcs.cpp -o mycalcscpp.plugin