標籤:style class blog http ext color
在機器碼中通過COM 使用 F#
雖然大多數情況下,我們可能希望從 F# 代碼調用機器碼,但是,在某些情況下,也可能想從機器碼中調用 F# 庫函數。例如,假設我們有一個大型的程式是用 C++ 寫的,有可能希望使用者介面保持用 C++,而把一些邏輯,比如執行複雜數學計算的部分遷移到 F#,以方便維護。在這種情況下,我們就要從機器碼中調用 F# 了。簡單的方法是借用.NET 提供的工具,為我們的 F# 程式集建立 COM 封裝;然後,使用COM 運行時從 C++ 中調用 F# 函數。
要通過 COM 公開函數,需要用特殊方法進行開發。首先,必須定義介面,為函數指定契約,介面的成員必須使用具名引數(參見本章前面的“C# 中調用 F# 庫”一節),介面本身使用 System.Runtime.InteropServices.Guid特性來標記;然後,必須提供一個類來實現這個介面,這要用System.Runtime.InteropServices.Guid 和System.Runtime.InteropServices.ClassInterface 特性進行標記,還應該總是把 ClassInterfaceType.None枚舉成員傳遞給 ClassInterface 特性的建構函式,說明沒有介面應該自動產生。
我們來看一下樣本是如何做的。假設我們想公開兩個函數Add 和 Sub 給非託管的用戶端,需要在命名空間Strangelights 下建立介面IMath,然後,建立類 Math 實現這個介面,還需要保證類和介面用適當的特性進行標記。最後的代碼可能像這樣:
namespaceStrangelights
open System
open System.Runtime.InteropServices
// define an interface(since all COM classes must
// have a seperateinterface)
// mark it with afreshly generated Guid
[<Guid("6180B9DF-2BA7-4a9f-8B67-AD43D4EE0563")>]
type IMath =
abstract Add : x: int* y: int -> int
abstract Sub : x: int* y: int -> int
// implement theinterface, the class must:
// - have an emptyconstuctor
// - be marked with itsown guid
// - be marked with theClassInterface attribute
[<Guid("B040B134-734B-4a57-8B46-9090B41F0D62");
ClassInterface(ClassInterfaceType.None)>]
typeMath() =
interface IMath with
memberthis.Add(x, y) = x + y
memberthis.Sub(x, y) = x - y
函數 Add 和 Sub很簡單,因此,直接在 Math 類的主體中實現,自然沒有問題;但是,如果需要把它們拆分到類之外的其他助理函數,也不會有問題,可以用自己覺得合適的任何方法實作類別成員,都行,只需要提供介面和類,這樣,COM 運行時就能夠在代碼中有一個進入點。
下面是這個過程中公開的最複雜的部分,註冊程式集,使 COM 運行時能找到它。這是通過使用工具RegAsm.exe 實現的。假設我們把前面的範例程式碼編譯成的 .NET .dll 通過OM 需要保證類的s 叫 ComLibrary.dll,那麼,需要調用 RegAsm.exe兩次,使用下面的命令:
regasm comlibrary.dll /tlb:comlibrary.tlb
regasm comlibrary.dll
第一次建立類型庫檔案 .tlb,它是能夠用於開發的 C++ 項目中的;第二次是註冊程式集,使 COM 運行時能夠找到它;這兩個步驟還需要在分發程式集的機器上運行。
C++ 調用 Add 函數是這樣的,開發環境以及如何設定 C++ 編譯器都會對代碼的編譯產生影響。在這裡,我們建立的Visual Studio 項目,選擇了控制台應用程式模板,並啟用了 ATL。注意,下面是有關原始碼的描述:
#import 命令告訴編譯器需要匯入我們自己的類型庫,可能需要使用檔案(.tlb)的完整路徑;編譯器還將自動產生一個標頭檔,在這裡是comlibrary.tlh,檔案的位置在debug 或 release 目錄下。它的作用是讓我們知道類型庫中可用的函數和標識符;
然後,需要初始化 COM 運行時,這是通過調用CoInitialize 函數實現的;
接著,需要聲明一個指標,指向我們建立的介面 IMath,這是通過代碼comlibrary::IMathPtr pDotNetCOMPtr; 完成的。注意,命名空間是來自庫的名字,而不是 .NET 的命名空間;
下一步,需要建立 Math 類的執行個體,這是通過調用CreateInstance 方法完成的,把 Math 類的GUID 傳遞給它。幸運的是,為此目的,有一個常量定義;
如果這些都成功了,就能名調用 Add 函數了。注意,函數的返回返回dd 目的,有一個常量這 r pDotNetCOMPtr; 實際上是 HRESULT,這個值告訴我們調用是否已經成功,而實際的實際結果是通過一個輸出參數傳出來的。
// !!! C++ Source !!!
#include "stdafx.h"
// import the meta data about out.NET/COM library
#import "..\ComLibrary\ComLibrary.tlb"named_guids raw_interfaces_only
// the applications main entry point
int _tmain(int argc, _TCHAR* argv[])
{
// initialize the COM runtime
CoInitialize(NULL);
// a pointer to our COM class
comlibrary::IMathPtr pDotNetCOMPtr;
// create a new instance of the Math class
HRESULT hRes =pDotNetCOMPtr.CreateInstance(comlibrary::CLSID_Math);
// check it was created okay
if (hRes == S_OK)
{
// define a local to hold the result
long res = 0L;
// call the Add function
hRes =pDotNetCOMPtr->Add(1, 2, &res);
// check Add was called okay
if (hRes == S_OK)
{
// print the result
printf("The result was: %ld", res);
}
// release the pointer to the math COM class
pDotNetCOMPtr.Release();
}
// uninitialise the COM runtime
CoUninitialize();
}
樣本的運行結果如下:
The result was: 3
當我們運行最後的程式時,必須保證ComLibrary.dll 與程式在同樣的目錄中,否則,COM 運行時會找不到檔案。如果打算讓這個庫被多個用戶端使用,那麼,我強烈建議對程式集簽名,並放在全域組件快取(Global Assembly Cache,GAC)中,這樣,所有的用戶端都能找到它,就不必要在第一個目錄下都複製一份。