按語:
在參考CVI參考書使用CVI生成動態庫后,在另一工程中調用DLL ,編譯通不過,后參考此文,豁然開朗。
http://blog.sina.com.cn/s/blog_6373e9e60101bpsm.html
4.1 靜態庫與動態庫
4.1.1 簡介
通過前幾章的學習,大家已經掌握了利用CVI開發涉及到UI、硬件、軟件組件的程序。但在現實生活中,前幾章示例程序那樣的幾十行幾百行的代碼的項目幾乎不存在,大家將來遇到更多的是幾萬行乃至幾千萬行代碼的軟件開發項目。這種中型、大型的軟件項目一般需要多人進行合作開發,此時就有必要將項目分為一個個小的功能模塊,以方便其他程序員在應用程序中調用。在Windows中,常常采用目標文件(*.obj)、靜態庫(*.lib)或動態鏈接庫文件(*.dll)進行功能模塊的分割。
目標文件和靜態庫文件比較類似,函數和數據被編譯為機器碼之后存入一個二進制文件中,在使用目標文件或者靜態庫文件時,鏈接器(Linker)從目標文件或靜態庫中找到這些代表函數和數據的二進制代碼並把它們復制到exe應用程序文件中,和其他模塊組合起來並生成最終可被執行的exe文件。因此,目標文件和靜態庫文件只是起到一個“二進制數據源”的作用,當最終版的exe發布時,由於exe文件中已經包含了目標文件或靜態庫中的數據,因此無需隨exe一起發布。目標文件與靜態庫文件不同的是,目標文件一般是C文件或其他編程語言文件編譯得到的二進制代碼,不會用來發布;而靜態庫文件一般發布以供其他程序員或者其他開發環境使用。
動態鏈接庫(DLL,Dynamic Link Library)文件是從Microsoft推出第一個版本的Windows操作系統以來就有的一種可供多個程序調用的功能模塊。跟目標文件、靜態鏈接庫一樣,大部分的DLL中存放的也是可以直接被執行的機器碼(之所以用“大部分”而不是“全部”的原因將會在“4.4 CVI調用.Net平台下的DLL”中提到)。跟靜態庫文件不同的是,應用程序運行時才鏈接到DLL中的功能模塊中,也就是說,應用程序發布時其鏈接的DLL文件需要同時發布。在程序設計時,一般更傾向於使用DLL而不是靜態庫,有以下原因:
·DLL支持任何其他Windows下的編程語言,避免了編譯器的兼容問題
·只要DLL中導出函數的接口不變,修改動態鏈接庫的功能模塊時就不必修改與之相互依存的其他模塊的代碼
·在同一個Windows操作系統下,不同的應用程序可以共享使用相同的DLL,可以減少應用程序可執行文件的大小,節省空間
·以DLL發布的程序可以給用戶提供一個方便的二次開發平台而又不必擔心自身源代碼的泄漏
4.1.2 使用靜態庫與動態庫
由於靜態庫文件實際上就是編譯過的代碼塊,在一個工程中添加了一個靜態庫,就如同添加了“加密過的”普通的編程語言文件一樣。當需要調用靜態庫文件時,只需要將靜態庫文件(*.lib)和相應的頭文件加入到工程中,在其他文件中引用庫的頭文件之后,調用靜態庫中的函數或變量即可。
在使用動態庫文件的時候,往往會用到另外兩個文件文件:一個引入庫(*.lib)文件和一個頭(*.h)文件。雖然引入庫文件的后綴名也是.lib,但是動態鏈接庫的引入庫文件和靜態庫文件有着本質上的區別。一般而言,靜態庫文件中包含了所有變量和函數以及函數執行的機器碼,而DLL的引入庫文件只包含了該DLL導出的函數以及變量的名稱,而真正的可執行的機器碼在DLL文件中。
一般情況下,編譯生成DLL的時候導入庫(.lib)文件會一同被創建。但若導入庫文件丟失,在CVI中可以通過點擊菜單Options-Generate DLL Import Library…來生成一個導入庫文件。
4.1.3 靜態庫生成和使用的例子
為了方便大家充分的理解靜態庫的概念與功能,我們不妨舉個例子。動態庫的例子將會在“4.2 CVI生成DLL”和“4.3 CVI調用DLL”中給出,此小節不再給出具體的實例。
我們新建一個名為“StaticLib”的工程,工程下新建Main.c、StaticLib.c以及StaticLib.h三個文件。
其中,在StaticLib.c文件中添加了如下代碼:
在StaticLib.h頭文件中添加如下代碼:
在Main.c中添加如下代碼:
整個工程建立完畢之后,CVI下的工程目錄如圖 4‑1所示。
從以上代碼我們可以看出,在StaticLib.c文件中,我們寫了一個AddTest函數,輸入兩個int型變量,返回兩個變量的和。在StaticLib.h文件中,我們提供了AddTest函數的聲明。在Main.c文件中,我們調用了AddTest函數。以上工程運行的結果如圖 4‑2所示。
若此時我們右擊StaticLib工程中的Main.c文件,選擇Exclude File from Build,如圖 4‑3所示,然后點擊CVI菜單-Build-Target Type-Static Library,將工程的輸出由exe文件改為靜態庫文件,則編譯之后,CVI將會編譯未被排除在編譯列表的StaticLib.c文件,並在工程目錄下會生成一個名為“StaticLib.lib”的靜態庫文件。
生成靜態庫文件之后,將StaticLib.c文件排除在編譯列表之外,將剛生成的StaticLib.lib文件添加進工程中,並恢復Main.c文件到編譯列表中,恢復工程編譯生成的目標類型為可執行文件(菜單Build-Target Type-Executable)。此時工程目錄如圖 4‑4所示。
重新編譯工程,運行后將會得到跟剛才完全一樣的結果。如圖 4‑5所示。
4.2 CVI生成DLL
4.2.1 CVI下生成DLL文件
在上一節中,我們通過一個實例了解了在CVI下生成靜態庫文件的過程。生成DLL的步驟跟生成靜態庫的步驟基本相同,除了配置生成文件類型的時候選擇“Dynamic Link Library”而不是“Static Library”之外。
設置生成文件類型為DLL后,在工程中只保留StaticLib.c文件與StaticLib.h文件在編譯列表中,將其他文件排除到編譯列表之外,繼續Build上一節的工程。此時CVI彈出如所示的對話框,提示用戶沒有任何函數被導出。
圖 4‑6 CVI提示沒有任何函數被導出
若此時點擊OK,DLL文件仍然可以被生成,但是DLL文件里不包含任何函數的任何信息。此時若使用CVI安裝目錄sdk\bin下的DEPENDS.EXE程序查看生成的DLL中包含的函數,則可以知道,在生成的DLL文件中確實不包含任何的函數的信息。如圖 4‑7所示。
圖 4‑7 使用Depends程序查看DLL中發現不包含任何函數信息
出現以上問題的原因在於,我們沒有定義導出的函數庫列表。點擊CVI菜單-Build-Target Settings…之后,彈出如圖 4‑8所示的窗口,在窗口中可以對生成的DLL進行設置。
在上面的對話框中,點擊Exports中的Change…按鈕,將Export what設置為Include file symbols,並點擊下面的StaticLib.h使得前面打勾,點擊OK即可。
圖 4‑9 設置工程的導出內容
圖 4‑8對話框各種設置說明如下:
(1) DLL file 設置DLL文件的類型(Debug/Release)以及位置、文件名。Debug類型的DLL文件可以方便的進行調試,而Release類型的DLL文件中去除了調試信息,文件體積較小,運行速度相對較快,適合發布。
(2)Import library base name 設置DLL導入庫文件名。
(3)Where to copy DLL 設置是否將生成的DLL文件拷貝到System目錄下或驅動目錄下
(4)Run-time support 是否提供CVI運行引擎的支持
(5)Embed project .UIRs 是否將*.uir文件編譯到DLL文件中
(6)Generate map file 是否生成map文件
(7)Version Info設置DLL文件的版本信息
(8)Import Library Choices… 設置生成DLL導入庫的方式,是只與當前編譯器兼容還是與CVI支持的四種編譯器兼容。
(9)Type Library 在CVI中很多庫文件擁有自己獨特的數據類型,如VISA中整數類型為ViInt32,在這個選項中可以定義是否將這些類型庫和幫助文檔連接到DLL文件。
(10)LoadExternalModule Options 是否將外部的庫文件編譯到DLL中
(11)Exports 設置輸出的方式,是將頭文件定義的內容全部導出,還是只導出具有導出標志的內容。
設置導出列表之后,Build工程,則CVI發出如圖 4‑10所示的提示,告訴用戶DLL已經成功生成。
其中.dll(Dynamic Link Library)文件是動態鏈接庫文件,.cdb(CVI Debug)是供調試用的參數文件,.lib(Library)文件是DLL文件的導入庫文件。需要注意的是,此時的.lib導入庫文件跟之前生成的.lib靜態庫文件內容與大小均不一樣。這再次提醒我們,靜態庫文件與DLL的導入庫文件雖然都是*.lib,但是內容不同,不能混淆。
若需將此次編譯出來的DLL發布以供其他編譯程序或者其他程序員使用,只需要將StaticLib_dbg.dll、StaticLib_dbg.lib以及StaticLib.h這三個文件打包發布即可。
4.2.2 在CVI下調用CVI生成的DLL
發布DLL文件時,一般會連同導入庫(*.lib)文件、頭(*.h)文件一起發布。有了導入庫(*.lib)文件的幫助,在CVI下調用動態鏈接庫DLL的方法跟調用靜態鏈接庫(*.lib)的方法大同小異。
調用靜態庫時,我們需要將靜態庫(*.lib)文件加入工程,並將必要的頭文件加入工程之后,在工程的其他文件中即可調用靜態庫文件中的函數或者參數。
而在調用動態鏈接庫時,我們需要把動態鏈接庫的導入庫(*.lib)文件加入工程,並將必要的頭文件加入工程之后,在工程的其他文件中即可調用動態鏈接庫中的函數或者參數。
需要額外注意的是,因為動態鏈接庫(*.dll)文件中的二進制機器碼並不會被連接器(Linker)復制到exe文件中,所以在運行、發布時,需要確保DLL文件已經在系統的system目錄下或者工程當前目錄下存在。
在下面的例子中,我們將使用上一節生成並發布的StaticLib_dbg.dll、StaticLib_dbg.lib以及StaticLib.h。
我們在工程中添加Main.c源文件、StaticLib.h頭文件以及StaticLib_dbg.lib導入庫文件,將StaticLib_dbg.dll文件復制到工程所在目錄下。此時工程目錄如圖 4‑11所示。
其中Main.c文件與上一節中的內容完全一致。如下所示:
將工程的生成文件類型設置為Executable,編譯、運行程序,程序運行結果如圖 4‑12所示。
4.2.3 在VC下調用CVI生成的DLL
在VC中調用DLL跟在CVI中調用DLL的整體思路相同。將DLL的導入庫文件以及庫文件的頭文件加入工程中,即可在其他編程語言文件中調用該DLL中的函數以及參數。
下面我們將以一個實例來調用上一節發布的StaticLib_dbg.dll、StaticLib_dbg.lib以及StaticLib.h文件。
建立VC工程
打開VC6.0,在VC6.0中建立一個名為TestDll的控制台工程。如圖 4‑13所示,將StaticLib.h文件、StaticLib_dbg.lib導入庫文件以及StaticLib_dbg.dll動態鏈接庫文件復制到工程所在目錄下,並將頭文件及導入庫文件加入到工程中。
編寫C語言文件
新建TestDll.cpp文件,並在cpp文件中輸入以下代碼,並將其加入到工程。
編譯、調試
點擊組建按鈕,我們注意到,編譯器此時並沒有成功的找到AddTest函數,而是報以下錯誤:
在上一節我們使用CVI調用該DLL時,完全正常,但是為什么此時會出現問題呢?
問題的原因
我們注意到,上述錯誤是一個連接器錯誤。而且我們考慮到,從CVI遷移到VC,無非是換了一個編譯器。而編譯器理論上應該都支持ANSI C標准,理論上這種改變不會導致出現此類問題。兩個集成開發環境最大的不同在於其環境變量不同。
問題就出在這兒。由於每個C語言(*.c)或C++語言(*.cpp)文件都是被編譯器編譯為目標文件之后再交由連接器(Linker)進行連接的。每個編譯器在生成目標文件的過程中,編程語言文件中的函數名會被重命名,而對於C++編譯器來說,環境變量不同,重命名的方法也就不同。
作為一種面向對象的語言,C++支持函數重載,而過程式語言C則不支持。函數被C++編譯后在符號庫中的名字與C語言的不同。例如,假設某個函數的原型為:
void foo( int x, int y );
該函數被C編譯器編譯后在符號庫中的名字為_foo,而C++編譯器則會產生像_foo_int_int之類的名字(不同的編譯器在不同的環境中可能生成的名字還略有區別)。
在上述例子中,C++編譯器對TestDll.cpp進行編譯時,AddTest函數在目標文件中被重命名為_Add_Test_int_int,而CVI中C編譯器對StaticLib.c進行編譯時AddTest函數在導入庫文件中被重命名為_Add_Test。兩個函數在目標文件中的名字不相同,連接器自然也就無法成功的將兩個文件組建成一個exe文件。
解決C++編譯器兼容問題
C++語言的創建初衷是“a better C”,因此C++之父在設計C++之時就考慮到C++里面的重載會引起C庫與C++庫的不兼容。為了在C++中盡可能的支持C,extern “C”{}就是其中的一個策略。extern “C”{}常用在頭文件中,使用方法如下:
為了讓編譯器得知StaticLib.h文件中所有的函數及參數都是由C編譯器編譯產生的,我們只需要把頭文件的所有內容加入到extern “C”內即可。
但是將extern “C”{}加入頭文件以使得C與C++編譯器兼容存在另外一個問題。extern “C”{}本身並不是C語言的關鍵字,extern “C”加在C語言中會引起編譯錯誤。為了避免C語言編譯錯誤,我們只需要在extern “C”的聲明前后分別加入#if defined (__cplusplus)以及#endif即可。
修改之后的StaticLib.h文件如下所示:
再次編譯、運行
再次編譯,運行,則原先的連接錯誤不再出現,運行結果如圖 4‑14所示。
4.3 CVI調用DLL
上節我們已經初步接觸到了在CVI中調用DLL的方法。在本節中,我們將系統的介紹CVI調用其他編譯器生成的DLL的方法。
調用DLL的方法分為‘顯示調用’和‘隱式調用’兩種方法。
隱式調用方法需要被調用DLL的頭文件,采用創建DLL導入庫.lib文件的方法調用。該方式使用Options選項卡中的Generate DLL Import Library向導創建DLL導入庫.lib(若提供DLL同時也提供了.lib則不需要此步),將導入庫.lib添加至工程項目后,就可以通過函數名方便靈活的使用被調用DLL中的函數,需要注意的是,頭文件中需要對所調用的函數進行申明。
顯式調用方法不需要被調用DLL的頭文件以及導入庫文件,使用windows.h提供的Loadlibrary、GetProcAddress、Freelibrary函數,直接根據指針訪問DLL中的函數。該方式適用於沒有DLL的頭文件以及導入庫文件,但知道被調用函數的原型的場合。
4.3.1 顯式調用方法的例子
下面我們創建一個名為LoadLibrary的工程,並只將上節發布的StaticLib_dbg.dll文件復制到工程目錄下,使用顯示調用的方法使用StaticLib_dbg.dll文件中的AddTest函數。
我們在LoadLibrary工程中創建一個LoadLibrary.c的C語言源文件,創建完畢之后,LoadLibrary工程目錄如所示。
圖 4‑15 LoadLibrary工程目錄
向LoadLibrary.c文件中添加如下內容:
在上述文件中,我們先通過加載DLL文件獲取DLL文件的句柄,然后從DLL文件中查得AddTest函數的指針地址,然后賦值給一個函數指針,隨即執行函數指針所指向的函數,即可顯示最終執行的結果。
上述工程不需要使用頭文件也不需要使用導入庫文件。運行結果如圖 4‑16所示。
對大家來說,上述代碼讓人不解的地方可能在於函數指針的使用。其實函數指針是內核編程等常用到的一種指針,通過函數指針的使用大大增加了靈活度,使程序變得更加清晰和簡潔。如果大家的C語言功底足夠強,應該可以發現,上述C語言代碼中,main函數內的代碼可以精簡為以下四行代碼:
對函數指針感興趣的同學可以深入研究可以這樣精簡的原因,此處盡起一個拋磚引玉的作用,具體內容不再深究。
4.3.2 VC生成DLL文件
在VC下編寫一個簡單的DLL程序也並不復雜。下面我們將以一個實例來說明在VC下編寫一個簡單的DLL程序的方法。
組建VC下的DLL工程
打開VC,點擊菜單-文件-新建,在工程頁面中選擇Win32 Dynamic-Link Library,選擇工程路徑與工程名稱之后,點擊確定,創建一個空白的DLL工程。
圖 4‑17 VC下新建一DLL工程
新建一個C++源文件(*.cpp)與頭文件(*.h),添加到工程中。其中cpp文件的源代碼如下:
頭文件的源代碼如下:
點擊VC組建按鈕,則VC發出如下提示,告訴大家組建成功,DLL文件已經生成。
我們使用“4.2.1 CVI下生成DLL文件”中提到的DEPENDS工具查看生成的DLL文件內部是否已經包含了我們所需要的SubTest函數。結果如圖 4‑18所示。
我們並沒有在生成的DLL文件中發現任何函數。同時,我們也沒有在DLL的目錄下發現有導入庫(*.lib)文件生成!
問題的原因
跟在CVI下生成DLL時遇到的問題相似,我們並沒有定義要導出的函數。在VC中,我們需要使用__declspec關鍵詞來定義需要導出的函數。
__declspec 是MFC提供的修飾符號。在要輸出的函數、類、數據的聲明前加上__declspec(dllexport)的修飾符,表示該函數、類、數據需要被輸出到DLL文件中。
因此,我們若把頭文件中SubTest的聲明中添加__declspec(dllexport),即告訴編譯器我們需要將SubTest函數導出到DLL文件中。
添加__declspec(dllexport)關鍵詞之后,我們重新編譯,組建,則在Debug目錄下生成了我們所需要的導入庫文件、DLL文件。用DEPENDS工具查看生成的DLL,可以發現SubTest函數已經在DLL文件中了。如圖 4‑19所示。
4.3.3 CVI下調用VC生成的DLL
在CVI下調用VC生成的DLL,跟調用CVI生成的DLL並無區別。
我們新建一CVI工程,將上一小節生成的DLL文件、導入庫(*.lib)文件以及頭文件復制到CVI工程目錄下,將導入庫(*.lib)文件添加到工程中,在CVI工程中添加一C語言源文件,代碼如下:
編譯、連接、運行之后,測試結果如圖 4‑20所示。
通過上面的示例程序我們可以發現,使用CVI調用VC生成的DLL的步驟跟調用CVI生成的DLL步驟幾乎完全一樣。
需要額外注意的是,倘若VC生成的DLL運行時也依賴與其他的非系統system下的DLL(運行依賴的DLL可以使用DEPENDS工具查看),那么也需要將對應的DLL文件復制到CVI工程所在文件夾下或者復制到系統的system目錄下。
4.4 CVI調用.Net平台下的DLL
常用的DLL包括托管DLL和非托管DLL兩種。托管DLL為中間代碼,完全依賴於.NET平台運行,而非托管DLL是機器代碼,不依賴於.NET平台運行。C#、VB.NET和F#編程采用純.NET語言開發,生成的DLL屬於托管DLL。而平時接觸到的其它語言編寫的DLL,包括C/C++、CVI、LabVIEW編寫的DLL都屬於非托管DLL。托管的應用程序和DLL可以直接調用非托管的DLL,而非托管的應用程序和DLL必須通過.NET Runtime才能調用托管的DLL。
前兩節介紹的調用的DLL與生成的DLL都是非托管的DLL,是機器代碼,不依賴於.Net平台運行。但是若在CVI下調用.Net平台下的DLL,那么則需要借助CVI的.Net工具。
CVI提供了幫助用戶調用.Net程序集的.Net庫。用戶可以通過菜單-Tools-Create .Net Controller…向導生成可在CVI下使用的.Net庫。通過使用CVI以及.Net庫,我們可以完成以下工作:
·注冊.Net控件並且加載控件
·創建.Net對象並且調用.Net對象
·管理系統資源
·創建.Net數組,並從.Net數據中獲取元素
·獲取.Net的錯誤與異常信息
·提供.Net組件的基本信息
·同COM組件交互操作
但是CVI對.Net的支持也有一些限制。CVI不支持.Net事件和委托。並且CVI也不是一個.Net控件容器。因此,在CVI中不能使用.Net的用戶界面對象。
CVI調用C#等.NET語言編寫的DLL需要使用工具選項卡中的‘Creat .NET controller’。通過它生成一個調用.NET匯編代碼的包裝器(wrapper),該包裝器包含對應的儀器驅動、源文件和頭文件。包裝器生成具體步驟如下:
(1)選擇Tools選項卡中的‘Creat .NET controller’
(2)一般第三方開發的DLL都不在Global Assembly Cache中,因此彈出對話框中勾選‘Specify Assembly by Path’,選擇需要調用的 DLL
(3)在‘Target Instrument’中指定一個儀器驅動.fp文件,點擊OK,CVI程序便生成一個可以調用所選DLL的儀器驅動
(4)將生成的.fp文件添加至工程項目
包裝器自動生成的儀器驅動中會封裝好一些調用函數,函數的命名方式為 [命名空間]_[類名稱] ,命名空間和類名稱都是在編寫.NET程序時定義好的,命名空間也就是DLL的名稱。如圖 4‑21,ClassLibrary1為命名空間,Class1為類名稱,一個DLL中包括一個命名空間,一個命名空間下可以包含一個或多個類,一個類下又可以包含多個函數。圖 4‑21中的ClassLibrary1_Class1__add就是一個DLL中的加法函數。
一般如 Initialize_[命名空間] 和 Close_[命名空間] 兩個函數分別調用 CDotNetLoadAssembly 和CDotNetDiscardAssemblyHandle 兩個函數,[命名空間]_[類名稱]__Create 調用CDotNetCreateGenericInstance 函數, [命名空間]_[類名稱]_[函數名稱] 調用CDotNetInvokeGenericStaticMember 函數等。
特別注意的是,由於被調用的DLL沒有在全局程序集緩存(GAC,Global Assembly Cache)中,調用前需要使用CDotNetRegisterAssemblyPath函數先注冊.NET的DLL。如果沒有注冊將出現如圖4所提示的調用失敗的錯誤。全局程序集緩存所在的文件夾為C:\WINDOWS\assembly。
圖 4‑22 .NET的DLL加載失敗錯誤提示
另外,C#編程中無需關心垃圾內存的回收(Garbage Collector),而回到C環境中被調用函數涉及的變量最后需要通過手動調用釋放內存的函數來釋放變量的內存空間。
編程的主要流程如下:
(1)聲明“[DLL名稱]_[類名稱]”類型的句柄;
(2)調用CDotNetRegisterAssemblyPath("[DLL名稱] , Version=x.x.x.x, Culture=xx , PublicKeyToken=xx" , ''Full Path of DLL")注冊.NET的DLL,DLL的路徑分隔符用"\",如D:\\CVI\\Projects\\C#net DLL call;
(3)調用“Initialize_[DLL名稱]”函數初始化.NET controller;
(4)根據句柄,調用“[DLL名稱]_[類名稱]__Create”創建被調用DLL的實例;
(5)調用“[DLL名稱]_[類名稱]_[函數名稱]”等具體函數,編寫相應代碼;
(6)調用CDotNetDiscardHandle釋放.NET DLL實例句柄;
(7)調用CDotNetFreeMemory釋放變量內存;
(8)調用“Close_[DLL名稱]”卸載.NET DLL;
需要額外注意數據類型轉換的問題,.NET語言中的Enum、Rectangular Array、String、System.Decimal和System.Boolean可以自動完成轉換,而COM Run-Time Callable Wrapper (RCW) types、Jagged arrays和Boxed data types需要手動調用庫函數轉換。
在接下來的示例中,我們將在C#中寫一個基於.Net的DLL,並在系統的全局程序集緩存(GAC)中注冊,並在CVI中調用此DLL。
新建C#類庫工程
打開Visual Studio 2008,新建一個C#類庫(Class Library)工程。如圖 4‑23所示。
為工程添加密鑰文件
點擊開始菜單-程序- Microsoft Visual Studio 2008 SDK- Tools- System Definition Model Command Prompt,敲入sn.exe –k c:\1.snk,為C#的工程生成一個密鑰文件。生成的密鑰文件將會保存在指定的c:\1.snk下。
打開C#工程的AssemblyInfo.cs文件,在其中插入以下代碼:
在工程的Class1.cs文件中,輸入以下代碼:
點擊Visual Studio 2008菜單Build-Build Solution,編譯C#工程。在工程目錄的bin\Debug目錄下會生成ClassLibrary1.dll文件。
把DLL文件注冊到系統全局程序集緩存中
點擊開始菜單-程序-管理工具-Microsoft .NET Framework 2.0 配置,如所示,在程序集緩存中,將生成的DLL文件加入全局程序集緩存(GAC)中。
圖 4‑24 Microsoft .NET Framework 2.0 配置
添加.Net控件
新建CVI工程,在CVI中點擊菜單Tools-Create .Net Controller…,添加生成的ClassLibrary1.dll文件,並添加生成的.fp文件的路徑。點擊確定之后,在CVI左下角的函數庫窗口的Instruments文件夾下多出了ClassLibrary1庫,在當前工程下多出了剛才指定的.fp文件。
編寫代碼,編譯運行
新建一C文件,在C文件中添加如下代碼,並添加到當前工程中:
CVI程序目的是調用基於C#.Net的DLL的函數實現計算1+2的功能。編譯運行后,程序運行結果如圖 4‑25所示。
4.5 示例:CVI獲取計算機CPU、硬盤、網卡ID
在實際的軟件項目中,當一個收費軟件發布時,常常需要用戶購買“注冊碼”,輸入正確的注冊碼之后才可以正常使用。然而,若該注冊碼一旦被人公開,整個收費軟件的注冊碼形同虛設。為了避免注冊碼共享后即可被所有人使用的問題,我們必須針對每一台計算機生成獨一無二的注冊號。
對於一台計算機而言,軟件環境經常會被改變,所以靠檢測軟件使用環境來識別一台計算機顯然不夠嚴密。一般情況下,最常用的方法是獲取計算機的CPU序列號、網卡號或者硬盤序列號后來計算得知該計算機的“注冊碼”。
然而,在Intel的CPU中,獲取計算機序列號是靠一條叫做cpuid的匯編指令來完成的。而在CVI下,筆者尚未發現嵌入匯編語言的方法。倘若一個軟件整體框架是采用CVI寫的,那么我們可以通過將使用VC來獲取CPU序列號的代碼封裝成一個DLL,以提供給CVI使用。
軟件運行時,首先調用這個DLL獲取CPU序列號與網卡號,經過某種算法計算得到“注冊碼”之后與用戶輸入的注冊碼比較,若相同則軟件繼續運行,若不相同,則用戶軟件提示相應的注冊信息並退出。
在本小節例子中,我們將只演示獲取CPU序列號以及網卡物理地址的內容。獲取硬盤序列號的方法略復雜,步驟與獲取網卡物理地址類似,此處不再給出具體代碼,具體代碼可以從例程中獲得。
獲取CPU序列號(Intel)
在Intel的處理器中,獲取CPU序列號需要用到匯編指令CPUID。由於入口參數存放在EAX寄存器中,執行前,往EAX寄存器賦值,再執行CPUID指令,即可從EAX、EBX、ECX以及EDX中獲取CPUID的返回值。具體EAX輸入以及四個寄存器輸出的對應關系參見表 4‑1。
從上面的表格可以看出,往EAX中賦值0x00,則運行CPUID后可以得到字符GenuineIntel。往EAX中賦值0x01,運行CPUID后從EAX與EBX中獲得處理器簽名以及一些特性值。往EAX中賦值0x03,運行CPUID后從ECX以及EDX后即可得到CPU的序列號。
獲取網卡號
獲取網卡號通過Windows IP輔助API庫(IPHlpApi.h)來完成,通過GetAdaptersInfo函數可以獲取網卡的信息,並且將PIP_ADAPTER_INFO結構體中的Address等成員變量進行處理之后顯示出來即可。
運行GetAdaptersInfo函數需要庫文件Iphlpapi.lib的支持。Iphlpapi.lib在VC6.0的SDK(可能需要單獨安裝)的lib文件夾下,若添加
語句之后還不能找到Iphlpapi.lib文件可能是因為沒有將Microsoft SDK加入VC的連接目錄中導致的。此時需要點擊VC菜單-工具-選項-目錄-Library files,將Microsoft SDK目錄加入其中即可。
VC最終代碼
了解如何使用VC獲取計算機的網卡號與CPU序列號之后,我們就可以着手實現具體程序了。
首先新建一個空的DLL工程,具體步驟參見“4.3.2 VC生成DLL文件”,並在工程中創建、添加一個cpp文件、一個頭文件。CPP文件代碼如下:
頭文件代碼如下:
在上述代碼中,我們通過__asm指令完成了C++代碼與匯編代碼的嵌套。我們利用Windows IP輔助庫的GetAdaptersInfo函數實現了網卡物理地址的獲取。通過接口函數GetSerialNum,我們可以在CVI中方便的調用該DLL,實現獲取CPU的序列號以及網卡的物理地址的功能。
CVI調用DLL
在CVI中,我們創建一個名為GetPhyNum的工程,將VC創建生成的DLL文件、lib文件以及頭文件復制到工程目錄下。在工程中新建一名為GetPhyNum.c的C語言源文件並將SerialNum.lib導入庫文件添加進工程中。添加完畢后,工程目錄如所示。
圖 4‑26 CVI獲取CPU、網卡序列號的工程目錄
GetPhyNum.c源代碼如下所示:
在上述C語言文件中,我們通過定義一個字符串並將字符串指針傳給GetSerialNum函數。GetSerialNum函數運行時會完成將指針指向的字符串賦值的功能。最后我們調用printf函數顯示獲取得到的含有CPU的序列號以及網卡物理地址的字符串。
調試、運行
當CVI運行時,屏幕上會顯示出CPU序列號以及網卡物理地址。程序運行結果如所示。
圖 4‑27 CVI獲取CPU序列號、網卡物理地址運行結果
運行計算機硬件檢測程序Everest查看計算機的CPU序列號與網卡物理地址,可以發現以上程序檢測得到的CPU序列號與網卡物理地址均准確無誤。
圖 4‑28 用Everest軟件檢測得到的CPU序列號與網卡物理地址
4.6 探索與實驗
1.6.1 實驗
使用CVI實時獲取USB攝像頭中的圖像,顯示在界面中,並使用算法檢測一張白紙上的黑色方塊。檢測到的方塊用紅色方框實時標注出來。
1.6.2 探索
探索利用Matlab生成DLL文件並在CVI中調用Matlab生成的DLL的方法。