程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> CLR完全介紹 - COM Interop簡介

CLR完全介紹 - COM Interop簡介

編輯:關於.NET

COM 是一種很出色的技術。正是由於公共語言運行庫 (CLR) 能夠使 Microsoft® .NET 應用程序 和非托管 COM 組件之間進行無縫交互,才使得 CLR 成為極其強大的平台。但是我在網絡上進行搜索時, 幾乎找不到能夠說明 COM Interop 的最基本概念的有用示例。本專欄的目的是講解這些基本概念,並提 供切實有用的示例,幫助這一技術領域的用戶快速入門。

我將以一個簡單的活動模板庫 (ATL) COM 服務器開始介紹,使用一個非托管 COM 客戶端對該服務器 嘗試不同的訪問方法,然後使用托管客戶端進行相同的操作。我將逐一介紹各個 DLL,說明從非托管到托 管的轉換,並且還將說明如何使用 P/Invoke 在非托管 DLL 中訪問導出的方法。本文中最難的部分是理 解復雜結構的封送,在本專欄中將不對這一點進行詳盡的介紹。僅介紹這一知識點就需要開辟一個完整的 專欄或寫一本書。我將為您展示非托管代碼如何使用接口來回調托管代碼。(您也可以使用委托達到這一 目的,但本專欄不介紹這種方法。)

最後,我將介紹使用公共符號對您的 COM Interop 項目進行 調試。這部分將對 WinDbg.exe、非托管調試和使用 SOS 進行的托管調試進行最基本的介紹。我將演示在 托管代碼中調用非托管代碼或在非托管代碼中調用托管代碼時堆棧的情況。

一個簡單的 ATL COM 服務器

我們首先來編寫一個簡單的 COM 服務器。客戶端將承載運行中的服務器,並在服務器中執 行方法。

為了使開發變得簡單,我將使用 ATL COM。創建名為 COMInteropSample 的目錄,然後 創建一個新的 Visual C++® ATL 項目並為它命名(假設為 MSDNCOMServer)。取消選擇“創建 新目錄”選項,其他項采用默認值。

現在向解決方案中添加一個新的類/接口。如果您還沒 有看到它,請打開解決方案資源管理器。右鍵單擊 MSDNCOMServer,然後選擇“添加 ”|“類”。選擇“ATL”和“ATL 簡單對象”,然後單擊“ 添加”。輸入類的簡稱(假設為 MyCOMServer),然後其他項采用默認值。單擊“完成 ”以添加該類。

向為您創建好的接口添加一個新方法。轉到“類”視圖,右鍵單 擊 IMyCOMServer 接口,然後選擇“添加”|“添加方法”。為該方法命名(假設 為 MyCOMServerMethod),然後單擊“完成”。

現在,我們要在類中實現該方法。選 擇 MyCOMServer.cpp 並查看源代碼。您將在 cpp 文件的接口中看到向導添加的方法。在 TODO 部分中, 添加以下代碼:

wprintf_s(L"Inside MyCOMServerMethod");

編譯代碼以 生成服務器 DLL。

現在,我們已完成了 ATL COM 服務器。它將在類 CMyCOMServer 中實現 IMyCOMServer 接口,並向默認輸出流中寫入一條消息。接口的 GUID 在 IDL 文件中定義為接口定義的一 個屬性,如圖 1 所示。

Figure 1 IMyCOMServer 定義

[
  object,
  uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
  nonextensible,
  helpstring("IMyCOMServer Interface"),
  pointer_default(unique)
]
interface IMyCOMServer : IUnknown {
  [helpstring("method MyCOMServerMethod")]
  HRESULT(MyCOMServerMethod)( void);
};
[
  uuid(3BA5DF7B-F8EF-4EDE-A7B5-5E7D13764ACC),
  version(1.0),
  helpstring("MSDNCOMServer 1.0 Type Library")
]
library MSDNCOMServerLib
{
  importlib("stdole2.tlb");
  [
    uuid(2D6D2821-A7FB-4F99-A61A-2286A47CD4D1),
    helpstring("MyCOMServer Class")
  ]
  coclass MyCOMServer
  {
    [default] interface IMyCOMServer;
  };
};

創建一個 COM 客戶端

我們將嘗試使用本機(非托管)客戶端訪問 COM 服務器。為了 簡單起見,我們仍然使用 Visual Studio® 及其向導。創建一個空的、新 Visual C++ 項目。從解決 方案資源管理器中選擇“源文件”,然後添加一個新的 C++ 文件。為它命名(假設為 COMClient.cpp),然後將圖 2 中的代碼粘貼到該文件中。

Figure 2 COMClient.cpp

#include "MSDNCOMServer.h"
#include "wchar.h"
#include "MSDNCOMServer_i.c"
void main()
{
  // Initialize COM
  IMyCOMServer *pUnk = NULL;
  CoInitialize(NULL);
  // Create the com object
  HRESULT hr = CoCreateInstance(CLSID_MyCOMServer,
    NULL, CLSCTX_ALL, IID_IMyCOMServer, (void **)&pUnk);
  if(S_OK != hr)
  {
    wprintf(L"Error creating COM object ... %d", hr);
    return;
  }
  // Call a method on the COM object
  pUnk->MyCOMServerMethod();
  // Free resources and uninitialize COM
  pUnk->Release();
  CoUninitialize();
  return;
}

從服務器添加頭文件(MSDNCOMServer.h 和 MSDNCOMServer_i.c),然後編譯並運行客戶端。您會看 到控制台顯示“Inside MyCOMServerMethod”。此消息來自 COM 服務器。

成功了!您已經創建了一個 COM 服務器並將其編譯成為一個 DLL,創建了一個 COM 客戶端並從客戶 端調用了 COM 服務器的方法。

使用 Tlbimp 轉換 COM DLL

現在有了 COM DLL,讓我們來看看如何從一個托管客戶端訪問它。打開 Visual Studio 命令提示,然 後轉到創建 COM DLL 的目錄。現在運行以下命令:

tlbimp MSDNCOMServer.dll

Tlbimp.exe 是 .NET Framework SDK 中附帶的類型庫導入程序。此命令輸出一個名為 MSDNCOMServerLIB.dll 的托管 DLL,該 DLL 作為非托管 COM DLL 的托管包裝。

現在我們正在運行非托管客戶端,讓我們仔細看看從 Tlbimp.exe 生成的原始非托管 COM DLL 和托管 DLL。圖 3 顯示了來自原始 COM DLL 的 Oleview 轉儲。

Figure 3 來自 MSDNCOMServer.dll 的 Oleview 轉儲

// Generated .IDL file (by the OLE/COM Object Viewer)
// typelib filename: MSDNCOMServer.dll
[
  uuid(3BA5DF7B-F8EF-4EDE-A7B5-5E7D13764ACC),
  version(1.0),
  helpstring("MSDNCOMServer 1.0 Type Library"),
  custom(DE77BA64-517C-11D1-A2DA-0000F8773CE9, 100663662),
  custom(DE77BA63-517C-11D1-A2DA-0000F8773CE9, 1158351043),

  custom(DE77BA65-517C-11D1-A2DA-0000F8773CE9, Created by MIDL version
      6.00.0366 at Fri Sep 15 13:10:42 2006)
]
library MSDNCOMServerLib
{
  // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
  importlib("stdole2.tlb");
  // Forward declare all types defined in this typelib
  interface IMyCOMServer;
  [
   uuid(2D6D2821-A7FB-4F99-A61A-2286A47CD4D1),
   helpstring("MyCOMServer Class")
  ]
  coclass MyCOMServer {
    [default] interface IMyCOMServer;
  };
  [
   odl,
   uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
   helpstring("IMyCOMServer Interface"),
   nonextensible
  ]
  interface IMyCOMServer : IUnknown {
    [helpstring("method MyCOMServerMethod")]
    HRESULT_stdcall MyCOMServerMethod();
  };
};

如果您使用的是 .NET 反編譯器(如 Lutz Roeder 的 .NET Reflector),並且查看的是 Tlbimp 的 托管庫輸出,則您將看到類似於圖 4 的內容。

Figure 4 托管的 MSDNCOMServerLib

namespace MSDNCOMServerLib
{
  [ComImport, Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE"),
   InterfaceType(1), TypeLibType(0x80)]
  public interface IMyCOMServer
  {
    [MethodImpl(MethodImplOptions.InternalCall,
     MethodCodeType=MethodCodeType.Runtime)]
    void MyCOMServerMethod();
  }
  [ComImport, CoClass(typeof(MyCOMServerClass)),
   Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE")]
  public interface MyCOMServer : IMyCOMServer
  {
  }
  [ComImport, TypeLibType(2),
   Guid("2D6D2821-A7FB-4F99-A61A-2286A47CD4D1"),
   ClassInterface(0)]
  public class MyCOMServerClass : IMyCOMServer, MyCOMServer
  {
    // Methods
    [MethodImpl(MethodImplOptions.InternalCall,
     MethodCodeType=MethodCodeType.Runtime)]
    public virtual extern void MyCOMServerMethod();
  }
}

看來發生了很多事情。首先,我們注意到庫 MSDNCOMServerLib 轉換為命名空間 MSDNCOMServerLib。 其次,接口 IMyCOMServer 通過屬性化的 GUID 保留下來,CoClass MyCOMServer 被轉換為:一個由 IMyCOMServer 派生而來的公共接口 MyCOMServer;一個用於實現接口 IMyCOMServer 和 MyCOMServer 的 類 MyCOMServerClass。

編寫一個托管客戶端

至此,您已經看到了編寫 COM 服務器和非托管 COM 客戶端的過程,以及在使用 Tlbimp 將非托管 DLL 轉換成托管 DLL 時,所發生的具體轉換。我們將在托管代碼中使用 Tlbimp 導出的庫,以編寫一個 托管 COM 客戶端:

using System;
using MSDNCOMServerLib;
class Test
{
  static void Main()
  {
    MyCOMServerClass comServer = new MyCOMServerClass();
    comServer.MyCOMServerMethod();
  }
}

托管代碼十分簡單和簡潔。我已將這段代碼保存在名為 ManagedClient.cs 的文件中,並使用以下命 令對其進行了編譯:

csc ManagedClient.cs /reference:MSDNCOMServerLib.dll

如果您運行可執行文件 ManagedClient.exe,您會得到我們在非托管客戶端中所得到的結果。

封送一個結構和一個字符串

現在,讓我們為服務器添加一些參數:一個字符串和一個結構。這就需要對服務器自身和客戶端均進 行修改。為了完成這項工作,您需要先在服務器上定義方法。

首先,編輯 IDL 文件以包含如圖 5 所示的代碼。我添加了一個結構的定義,並且修改了 MyCOMServerMethod 的聲明,以便引入兩個參數:一個字符串和一個結構。

Figure 5 修改 MyCOMServerMethod

struct MyTestStruct
{
  int nIndex;
  [string] WCHAR* sName;
  int nValue;
};
[
  object,
  uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
  nonextensible,
  helpstring("IMyCOMServer Interface"),
  pointer_default(unique)
]
interface IMyCOMServer : IUnknown{
  [helpstring("method MyCOMServerMethod")]
  HRESULT(MyCOMServerMethod)(
    BSTR *sTestString, struct MyTestStruct *pMyTestStruct);
};

接著,編輯服務器上的頭文件並按照 IDL 中的定義添加以下方法原型。

class ATL_NO_VTABLE CMyCOMServer :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CMyCOMServer, &CLSID_MyCOMServer>,
  public IMyCOMServer
{
public:
  STDMETHOD(MyCOMServerMethod)(
    BSTR *sTestString, struct MyTestStruct *pMyTestStruct);
};

修改方法的實現以反映原型的變化,還要添加更多的代碼以操作傳遞給它的字符串。最終代碼如圖 6 所示。這樣就完成了對服務器的更改,現在重新編譯它。

Figure 6 修改 CMyCOMServer

// MyCOMServer.cpp : Implementation of CMyCOMServer
#include "stdafx.h"
#include "MyCOMServer.h"
// CMyCOMServer
STDMETHODIMP CMyCOMServer::MyCOMServerMethod(
    BSTR *sString, struct MyTestStruct *pStruct)
{
    if((NULL == sString) || (NULL == pStruct) || (NULL ==
        pStruct->sName))
    {
        wprintf_s(L"Invalid input\r\n");
        return E_FAIL;
    }
    // Print the input to the COM Method
    wprintf_s(

        L"Inside MyCOMServerMethod: Param-1 = %s Param-2 = %s\r\n",
        *sString, pStruct->sName);
    // Modifying the input string
    SysFreeString(*sString);
    *sString = SysAllocString(L"Changed String from COM Server");
    if(NULL == *sString)
    {
        wprintf_s(L"Allocation failed ...\r\n");
        return E_FAIL;
    }
    // Modifying the string in the structure
    CoTaskMemFree(pStruct->sName);
    pStruct->sName = reinterpret_cast<WCHAR *>
        (CoTaskMemAlloc(sizeof
             (L"Changed Structure from COM Server")+10));
    if(NULL == pStruct->sName)
    {
        wprintf_s(L"Allocation failed ...\r\n");
        return E_FAIL;
    }
    size_t size = 0;
    size = wcslen(L"Changed Structure from COM Server");
    wcscpy_s(pStruct->sName, size+1,
        L"Changed Structure from COM Server");
    //Print the changed strings in COM Server
    wprintf_s(
        L"Exiting MyCOMServerMethod: Param-1 = %s Param-2 = %s\r\n",
        *sString, pStruct->sName);
    return S_OK;
}

為了訪問托管客戶端上的新 COM 方法,我將 DLL 復制到了我的托管客戶端所在的目錄中。我重新運 行以下命令,以獲得包含新方法的新的 MSDNCOMServerLib.dll。

tlbimp MSDNCOMServer.dll

您可以在 Ildasm 中查看新的方法簽名和實現。Ildasm 是 .NET Framework SDK 附帶的另一個工具。 在查看它時,您會注意到封送拆收器已將如圖 7 所示的代碼添加到了調用中。

Figure 7 COM 服務器封送代碼

namespace MSDNCOMServerLib
{
  [ComImport, Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE"),
   InterfaceType(1), TypeLibType(0x80)]
  public interface IMyCOMServer
  {
    [MethodImpl(MethodImplOptions.InternalCall,
     MethodCodeType=MethodCodeType.Runtime)]
    void MyCOMServerMethod(
      [MarshalAs(UnmanagedType.BStr)] ref string sTestString,
      ref MyTestStruct pMyTestStruct);
  }
  [ComImport, CoClass(typeof(MyCOMServerClass)),
   Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE")]
  public interface MyCOMServer : IMyCOMServer { }
  [ComImport, TypeLibType(2),
   Guid("2D6D2821-A7FB-4F99-A61A-2286A47CD4D1"),
   ClassInterface(0)]
  public class MyCOMServerClass : IMyCOMServer, MyCOMServer
  {
    // Methods
    [MethodImpl(MethodImplOptions.InternalCall,
     MethodCodeType=MethodCodeType.Runtime)]
    public virtual extern void MyCOMServerMethod(
      [MarshalAs(UnmanagedType.BStr)] ref string sTestString,
      ref MyTestStruct pMyTestStruct);
  }
  [StructLayout(LayoutKind.Sequential, Pack=4)]
  public struct MyTestStruct
  {
    public int nIndex;
    [MarshalAs(UnmanagedType.LPWStr)]
    public string sName;
    public int nValue;
  }
}

輸入的 ref 字符串被封送為 In,OUT BSTR 和結構保持不變,但是結構內的字符串被封送為 LPWStr 。

既然服務器和由 Tlbimp 工具轉換的托管 DLL 均已就緒,我就可以繼續修改托管客戶端以便傳遞一個 字符串和一個結構,然後檢查返回的值,看它是否有變化。圖 8 顯示的是修改後的客戶端代碼。

Figure 8 傳遞字符串和結構

using System;
using MSDNCOMServerLib;
using System.Runtime.InteropServices;
class Test
{
  [STAThread]
  static void Main()
  {
    // Calling COM methods from managed Client
    String param1 = "Original String from Managed Client";
    MyTestStruct param2 = new MyTestStruct();
    param2.sName = "Original Structure from Managed Client";
    
    MyCOMServerClass comServer = new MyCOMServerClass();
    comServer.MyCOMServerMethod(ref param1, ref param2);
    Console.WriteLine("Param1: {0} Param2: {1}\r\n\r\n",
      param1, param2.sName);
  }
}

托管客戶端仍然十分簡潔和簡單。它創建了字符串和從非托管 DLL 中導入的結構的一個實例。現在, 按照與此前相同的方法編譯以上代碼,然後運行可執行文件。您將看到以下輸出:

Inside MsyCOMServerMethod: Param-1 = Original String from Managed Client
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from COM Server

托管代碼中的 Main 部分的調用訪問了非托管服務器並顯示了所傳遞的輸入。服務器修改了字符串並 在服務器上進行顯示。為了驗證修改後的字符串是否傳遞回客戶端,我們還要在客戶端顯示字符串。

為了在同一個 COM DLL 中創建一個非托管函數(非 COM),我必須首先在非托管 DLL 中定義方法並 實現它。選擇 MSDNCOMServer.cpp,然後將以下代碼粘貼到該文件的最底部。

STDAPI MyNonCOMMethod()
{
  wprintf_s(L"Inside MyNonCOMMethod\r\n");
  return true;
}

您需要從 DLL 中導出該方法才能訪問它。要執行此操作,請在 COMServer.def 文件中向此函數添加 一個條目:

; COMServer.def : Declares the module parameters.
LIBRARY   "COMServer.DLL"
EXPORTS
  DllCanUnloadNow    PRIVATE
  DllGetClassObject   PRIVATE
  DllRegisterServer   PRIVATE
  DllUnregisterServer  PRIVATE
  MyNonCOMMethod    PRIVATE

編譯服務器並生成內含 COM 方法和導出的函數的新 DLL。

將該 DLL 復制到托管客戶端所在的目錄中,然後對它運行 Tlbimp。使用 Ildasm,您可以看到導出的 函數出現在托管 DLL 中。

在托管代碼中使用 P/Invoke

接下來,我們將修改托管客戶端,以便使用 P/Invoke 調用非托管函數。要執行此操作,您必須在托 管代碼中定義方法,然後進行調用。托管方法聲明映射了非托管聲明,並告知 CLR 可以在何處找到真正 的實現部分。圖 9 顯示了對代碼所做的更改。

Figure 9 使用 P/Invoke

class Test
{
  [DllImport("MSDNCOMServer.dll")]
  static extern bool MyNonCOMMethod();
  [STAThread]
  static void Main()
  {
    // Calling COM methods from managed Client
    ...
    // Calling unmanaged function using pInvoke
    MyNonCOMMethod();
  }
}

我們已經為我們的原始托管客戶端添加了一條 DllImport 語句和一個 MyNonCOMMethod 調用。當您編 譯並運行客戶端時,您會看到如下輸出:

Inside MyCOMServerMethod: Param-1 = Original String from Managed Client
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from COM Server
Inside MyNonCOMMethod

對 MyNonCOMMethod 的調用確實被執行並且顯示了正確的語句。

使用接口調用托管代碼

最後,讓我們使用反向的 P/Invoke 在非托管函數中進行托管調用。這並不是一個常用的方法,但是 非常有用。您可以向非托管代碼傳遞一個托管接口或一個委托,然後非托管代碼便可以通過接口調用委托 或方法。讓我們先來處理托管接口,以便能夠通過非托管代碼調用它。要執行此操作,請在 ManagedClient.cs 中聲明接口,如圖 10 所示。

Figure 10 聲明托管接口

using System;
using MSDNCOMServerLib;
using System.Runtime.InteropServices;
[Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
public interface IReversePInvoke
{
  void ManagedMethod(ref int val);
}
class ManagedClass : IReversePInvoke
{
  public void ManagedMethod(ref int nVal)
  {
    Console.WriteLine("ManagedMethod: " + nVal);
    nVal = 20;
    Console.WriteLine("ManagedMethod Value changed to: " + nVal);
  }
}
[DllImport("MSDNCOMServer.dll")]
static extern bool MyNonCOMMethod(IReversePInvoke ptr);
[STAThread]
static void Main()
{
  ...
  ManagedClass man = new ManagedClass();
  MyNonCOMMethod(man);
}

在此代碼中,我向非托管 DLL 中添加了一個 IReversePInvoke 的聲明、一個實現接口的類和一個修 改後的 DLLImport 方法,以便獲得一個接口指針。我還修改了調用方法的位置,以便將 ManagedClass 實例傳遞給 MyNonCOMMethod。

您現在可以編譯該客戶端,但是它不能成功運行,因為還沒有修改服務器,以使其將接口作為參數。 所以,我們對托管可執行文件運行 Tlbexp(.NET Framework SDK 附帶的另一個工具,可從托管程序集中 生成一個類型庫)以得到 .tlb 文件。在 Oleview 中打開它,復制接口的原型,並將其粘貼到服務器 IDL 文件中,如下所示:

[
  odl,
  uuid(7DAC8207-D3AE-4C75-9B67-92801A497D44)
]
interface IReversePInvoke : IUnknown {
  HRESULT _stdcall ManagedMethod([in, out] long* val);
};

現在服務器上便有了接口。您必須更改方法的簽名,並添加對接口上的托管方法的調用方法如下:

STDAPI MyNonCOMMethod(IReversePInvoke *ptr)
{
int param = 10;
wprintf_s(L"Inside MyNonCOMMethod\r\n");
HRESULT hr = ptr->ManagedMethod(&param);
wprintf_s(L"Value in MyNonCOMMethod = %d\r\n", param);
wprintf_s(L"Exiting MyNonCOMMethod\r\n");
return true;
}

現在我們有了獲得托管接口並調用托管接口上的方法的非托管函數。將它編譯為一個 DLL,對其運行 Tlbimp 以得到一個更新的托管互操作 DLL,然後對 Tlbimp 生成的托管 DLL 再次編譯托管代碼。以下是 得到的輸出:

Inside MyCOMServerMethod: Param-1 = Original String from Managed Client
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from
COM Server
Inside MyNonCOMMethod
ManagedMethod: 10
ManagedMethod Value changed to: 20
Value in MyNonCOMMethod = 20
Exiting MyNonCOMMethod

調用確實回到了托管代碼並顯示了我們所期望的“10”。托管方法將該值修改為 20,然後在非托管函 數中顯示它。

調試托管代碼和非托管代碼

非常希望您的所有代碼都能正常工作。但是,顯然我們很少能夠編寫出絕對沒有 Bug 的代碼。從某種 意義上講,您需要知道如何調試使用 interop 的代碼。

為了成功完成調試,請確保您的計算機加載了完整的符號。符號可以使調試器能夠更好地理解和解釋 代碼中的操作。Microsoft 提供了一個公共服務器,調試器可以連接到該服務器來訪問這些用於 Microsoft 提供的 DLL 的符號。若要配置 windbg 調試器以使用此公共符號服務器,請首先運行 windbg :

windbg managedclient.exe

然後使用以下命令語法來將其指向符號服務器:

.SymFix
.sympath+ http://msdl.microsoft.com/download/symbols
.Reload

接著,需要告知 windbg 在加載 mscorwks、COMServerLib 和 COMServer DLLs 時設置斷點。使用 sxe ld 命令可以完成此操作:

sxe ld mscorwks
sxe ld COMServerLib
sxe ld COMServer

若要在調試器下使應用程序開始運行,請鍵入 g(表示“go”),然後按回車鍵。應用程序在運行過 程中,會在加載 COMServer DLL 時中斷並進入調試器,此時可以在 MyCOMServerMethod 處設置另一個斷 點(然後可以使用 bl 命令驗證是否確實設置了斷點):

bp COMServer!CMyCOMServer::MyCOMServerMethod (wchar_t **,
    struct MyTestStruct *)

現在已經加載了 mscorwks,我們希望 windbg 加載 SOS 調試器擴展,以便更加強大的調試器與托管 應用程序交互:

.loadby sos mscorwks

為了在通過反向 P/Invoke 從 COM 服務器調用的方法 ManagedMethod 上設置斷點,我們使用 SOS 的 !bpmd 命令:

!bpmd ManagedClient ManagedClass.ManagedMethod

使用此斷點工具時,我們仍然可以使用 g 命令繼續。在某一時刻,我們會遇到 MyCOMServerMethod 上的非托管斷點,然後我們可以使用 windbg kL 命令查看當前堆棧跟蹤信息,如圖 11 所示(該圖還顯 示了使用其他 windbg 命令檢查變量值的情形)。

Figure 11 獲取堆棧跟蹤信息並查看內存

0:000> kL
ChildEBP RetAddr
0012f328 79f21268 MSDNCOMServer!CMyCOMServer::MyCOMServerMethod
0012f3fc 0090a28e mscorwks!CLRToCOMWorker+0x196
0012f438 00f800e5 CLRStub[StubLinkStub]@90a28e
01271c08 79e88f63 ManagedClient!Test.Main()+0x75
0012f490 79e88ee4 mscorwks!CallDescrWorker+0x33
0012f510 79e88e31 mscorwks!CallDescrWorkerWithHandler+0xa3
0012f650 79e88d19 mscorwks!MethodDesc::CallDescr+0x19c
0012f668 79e88cf6 mscorwks!MethodDesc::CallTargetWorker+0x20
0012f67c 79f084b0 mscorwks!MethodDescCallSite::Call_RetArgSlot+0x18
0012f7e0 79f082a9 mscorwks!ClassLoader::RunMain+0x220
0012fa48 79f0817e mscorwks!Assembly::ExecuteMainMethod+0xa6
0012ff18 79f07dc7 mscorwks!SystemDomain::ExecuteMainMethod+0x398
0012ff68 79f05f61 mscorwks!ExecuteEXE+0x59
0012ffb0 79011b5f mscorwks!_CorExeMain+0x11b
0012ffc0 7c816fd7 mscoree!_CorExeMain+0x2c
0012fff0 00000000 KERNEL32!BaseProcessStart+0x23
0:000> dv
           this = 0x00fc2fb8
        sString = 0x0012f364
        pStruct = 0x00195ef8
           size = 0x12f330
0:000> dt sString
Local var @ 0x12f334 Type wchar_t**
0x0012f364
 -> 0x001a61e4  "Original String from Managed Client"
0:000> dt -r pStruct
Local var @ 0x12f338 Type MyTestStruct*
0x00195ef8
   +0x000 nIndex   : 0
   +0x004 sName    : 0x001a4f38  "Original Structure from Managed Client"
   +0x008 nValue   : 0

如果我們使用 g 命令繼續,應用程序會在調用 ManagedMethod 時中斷,此時,可以使用 SOS 命令 !clrstack 來查看托管堆棧:

0:000> !clrstack
OS Thread Id: 0x87c (0)
ESP       EIP
0012ef80 00f80168 ManagedClass.ManagedMethod(Int32 ByRef)
0012f148 79f1ef33 [GCFrame: 0012f148]
0012f2a0 79f1ef33 [ComMethodFrame: 0012f2a0]
0012f454 79f1ef33 [NDirectMethodFrameSlim: 0012f454] Test.MyNonCOMMethod(IReversePInvoke)
0012f464 00f80119 Test.Main()
0012f69c 79e88f63 [GCFrame: 0012f69c]

總結

有關其他信息,可以先訪問 PInvoke.net 進行了解,這是一個社區型的資源站點。Adam Nathan 的 COM interop 著作,《NET and COM:The Complete Interoperability Guide》即將面世(他的博客為“ Adam Nathan's Blog”)。有關調試(包括 WinDbg 下載和參考)的信息,請訪問此處,您還可以在此處 找到有關 SOS 調試擴展 (SOS.dll) 的參考信息。

將您想詢問的問題和提出的意見發送至[email protected].

Thottam R. Sriram具有加拿大蒙特利爾的肯考迪亞大學的計算機科學碩士學位。目前他是 CLR 團隊 中研究 COM Interop 的項目經理。在加入 CLR 團隊之前,Thottam 在 Windows 團隊工作。感謝 COM Interop 團隊成員 Mason Bendixen、Claudio Caldato、Alessandro Catorcini、Ben Gillis 和 Snesha Arumugam 為本專欄提供幫助。

本文配套源碼:http://www.bianceng.net/dotnet/201212/740.htm

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved