从 .NET 库模拟 COM 连接点






4.91/5 (23投票s)
一篇关于从 MFC/ATL COM 客户端访问 VB.NET 库的文章。
引言
本文演示了如何从 COM 客户端访问 .NET 库。它与其他 COM 互操作文章不同之处在于,它模拟了供使用 MFC 和 ATL 库实现的 COM 客户端使用的连接点。本文使用的 .NET 库大量使用了 System.Collections.Hashtable
类,因此我们还将演示一个可重用的非托管 C++ 包装器来处理此类。
背景
在 TrayGames,我们需要为提供给第三方开发者的多人在线游戏开发 SDK (TGSDK) 添加对非 .NET 游戏客户端的支持。尽管我们更喜欢 .NET 环境,因为它提供了丰富的功能和快速的开发,但我们不想排除尚未采用 .NET 的开发人员编写有趣游戏。由于这些是回合制游戏,我们的库利用了 .NET Framework 基于委托的事件系统,因此我们将详细介绍如何将其公开给 COM。在客户端,我们编写了一个 COM 版本的井字棋示例游戏(也有 C# 和 VB.NET 版本),以演示如何访问我们的 .NET 库,但此示例稍作修改即可应用于任何 .NET 库。
使用代码
.NET Framework 提供了一个基于委托的事件系统,用于将事件发送者(源)连接到事件接收者(接收器)。但是,当接收器是 COM 客户端时,源必须包含其他元素来模拟连接点。本文将演示这些修改,然后展示 COM 客户端如何通过调用 IConnectionPoint::Advise
方法(我们实际上使用 ATL 包装器方法)以通常的方式注册其事件接收器接口。这个 COM 客户端特别有趣之处还在于它用于访问 .NET Hashtable
的包装器,我们将对此以及 COM 互操作进行探讨。
我们的 .NET 库代码是专有的,因此我只会提供代码片段,展示对该(或任何).NET 库进行更改以使其可以通过 COM 访问。下载的演示在“COM Demo”文件夹中仅包含 VB.NET 库的二进制文件。但是,游戏服务器的源代码包含在下载的源代码中,位于“CPP Server”文件夹下。客户端提供示例的完整源代码,演示了如何访问 .NET 库。COM 客户端使用 MFC 和 ATL 库以 C++ 实现。还包含自定义 Hashtable 项目的源代码。这两个项目都可以在下载的源代码存档的“COM Client”文件夹下找到。
为了玩示例游戏,您需要在服务器“TGDaemonHarnes.exe”的 Daemon Harness 中加载游戏守护进程。在“Com Demo”文件夹中运行它,单击“Load Game Daemon”按钮,然后导航到“TicTacDoeDaemon.dll”。选择文件并打开它。Daemon Harness 应该报告游戏守护进程已在 Test Harness 模式下启动,并显示所需玩家数量。现在我们需要连接几个游戏到这个守护进程。由于这是两人游戏,我们将需要运行两个游戏客户端。运行一个“MFCTicTacToe.exe”游戏应用程序实例,然后再运行一个。运行游戏的第二个实例后,游戏守护进程应该会意识到它拥有足够的玩家,并继续开始游戏。
公开托管库
在此示例中,我们将使用一个名为 TGGameLib 的库。理解该库的功能对于理解如何将其公开给 COM 或如何模拟连接点并不重要。只需知道它是我们用于开发多人游戏的核心库之一。该库是用 VB.NET 编写的,如果您使用的是 C# 或其他 .NET 语言,修改将是相同的,但语法会略有不同。那么,让我们深入了解一下!
我们将使用 ClassInterfaceAttribute
类来公开我们的类。此类将为带属性的类自动生成一个类接口。类接口的名称与类本身的名称相同,但名称前面会加上下划线字符。公开时,类接口包含托管类的所有公共成员,包括从其基类继承的成员(如果有)。尽管类接口消除了为每个类显式定义接口的工作,但它们在生产应用程序中的使用并不推荐。双类接口允许客户端绑定到特定的接口布局,该布局可能会随着类的演进而更改。话虽如此,这是将 .NET 库公开给 COM 的**更快**的方式,而且由于 TGSDK 仍处于 beta 阶段,这对我们来说不是问题。您始终可以在以后(并让您的类实现该接口)在您已启动并运行并且知道它不会更改之后,显式定义您的接口。强名称也推荐使用,但我们在这里也没有这样做。
我们需要公开构成库的各种类、接口、enum
s 和事件。我们将从 TGGameClass
类开始,这只是一个普通的 VB.NET 类。这是它在公开给 COM **之前**的样子
Public Class TGGameClass
Implements IDisposable
Public Event GameEvent As TGGameEventHandler
Public Event AdminEvent As TGAdminEventHandler
Public Event DataEvent As EventHandler
Public Delegate Sub TGGameEventHandler(ByVal sender As Object, _
ByVal e As GameEventArgs)
Public Delegate Sub TGAdminEventHandler(ByVal sender As Object, _
ByVal e As AdminEventArgs)
Public Sub Start(ByVal gameQSize As Integer, _
ByVal retrieveMessagesManually As Boolean)
' method implmentation is here
End Sub
Public Sub RetrieveMessages()
' method implmentation is here
End Sub
Public Function IsConnected() As Boolean
' method implmentation is here
End Function
Public Function SendToServer(ByVal data As Hashtable) _
As Boolean
' method implmentation is here
End Function
End Class
为了公开此类,我们需要向文件添加互操作命名空间。我们还必须将 Guid
、ClassInterface
和 ComVisible
属性添加到类中。此外,因为我们要将事件接收器接口连接到此类,所以必须添加 ComSourceInterfaces
属性。
Guid
属性允许您添加全局唯一标识符 (GUID),这是其唯一名称,以便 COM 可以区分您的类。您必须将 GUID 指定为该属性的参数,您想公开的每个类型都必须有一个不同的 GUID。使用 Visual Studio .NET 中“工具”菜单下的“Create GUID”工具来创建 GUID。将给定的 GUID 作为 Guid
属性的参数粘贴。删除 GUID 中的大括号,您应该会得到类似以下内容
<Guid("F75C54A9-616F-4090-BC02-8B4795B6F499")>
ClassInterface
属性控制类型库导出器 (Tlbexp.exe) 是否自动为带属性的类生成类接口。您可以使用 Visual Studio .NET 中的“Register for COM Interop”项目选项(在“Properties->Configuration Properties->Build”下)、Tlbexp.exe 或 Regasm.exe 来生成类型库。Regasm.exe 基本上是 Tlbexp.exe 功能的超集。仅当您拥有完整源代码时,Visual Studio .NET 选项才可用。类接口可以是双接口或仅调度接口,如前所述,我们使用双接口来快速启动并运行。
<ClassInterface(ClassInterfaceType.AutoDual)>
ComVisible
属性控制单个托管类型或成员,或程序集中的所有类型对 COM 的可访问性。您可以将此属性应用于程序集、接口、类、结构、委托、枚举、字段、方法或属性。只有公共类型才能设为可见,此外,此属性**不**需要使公共托管程序集和类型可见,它们默认对 COM 是可见的。我们在库中使用它,因为我们的程序集将 ComVisible
设置为 False
。这是 FxCop 的一项建议,这是一个我们运行的代码以确保与 Microsoft 的最佳实践在一定程度上的兼容性。
<ComVisible(True)>
ComSourceInterfaces
属性定义将作为 COM 连接点公开的事件接口。我们必须传递源接口的 Type
,通过使用 GetType()
方法可以轻松获得它。请注意,类事件名称和传出接口方法名称必须相同。我们稍后会看 TGGameEvents
,但基本上我们是通过传递命名空间和事件接收器接口,将事件接收器接口(传出)TGGameEvents
连接到 TGGameClass
类。
<ComSourceInterfaces(GetType(TGGameEvents))>
将所有这些更改放在一起并应用于 TGGameClass
将使其可供 COM 客户端使用,这是它**应用**属性后的样子
Imports System.Runtime.InteropServices
<Guid("F75C54A9-616F-4090-BC02-8B4795B6F499"), _
ClassInterface(ClassInterfaceType.AutoDual), _
ComSourceInterfaces(GetType(TGGameEvents)), _
ComVisible(True)> _
Public Class TGGameClass
Implements IDisposable
Public Event GameEvent As TGGameEventHandler
Public Event AdminEvent As TGAdminEventHandler
Public Event DataEvent As EventHandler
Public Delegate Sub TGGameEventHandler(_
ByVal sender As Object, ByVal e As GameEventArgs)
Public Delegate Sub TGAdminEventHandler(_
ByVal sender As Object, ByVal e As AdminEventArgs)
Public Sub Start(ByVal gameQSize As Integer, _
ByVal retrieveMessagesManually As Boolean)
' . . .
End Sub
Public Sub RetrieveMessages()
' . . .
End Sub
Public Function IsConnected() As Boolean
' . . .
End Function
Public Function SendToServer(_
ByVal data As Hashtable) As Boolean
' . . .
End Function
End Class
下面我们定义了 TGGameEvents
事件接收器接口,供 COM 接收器实现(在本文的客户端部分讨论)。与其他从该库公开的类型不同,我们的事件接口必须显式公开为 IDispatch
接口;您可以使用 InterfaceType
属性来执行此操作。此属性允许您覆盖类型库导出器默认将托管接口公开给 COM 的方式(作为双接口)。InterfaceType
允许您指定 InterfaceIsDual
(默认)、InterfaceIsIDispatch
(仅用于后期绑定的 dispinterface)或 InterfaceIsUnknown
(仅用于早期绑定的 IUnknown
派生)。
在此接口中,我们通过分配任意 DispId
值来公开事件方法。我们已经看到了此事件接收器接口如何连接到 TGGameClass
,并且我们还定义了代表传出接口中方法定义的每个委托的事件。稍后我们将看到非托管客户端如何创建 TGGameClass
实例并实现事件接收器接口。
<Guid("7D809BD7-13D2-4b47-9B74-0236E9F01A99"), _
InterfaceType(ComInterfaceType.InterfaceIsIDispatch), _
ComVisible(True)> _
Public Interface TGGameEvents
<DispId(1)> _
Sub DataEvent(ByVal sender As Object, _
ByVal e As EventArgs)
<DispId(2)> _
Sub GameEvent(ByVal sender As Object, _
ByVal e As GameEventArgs)
<DispId(3)> _
Sub AdminEvent(ByVal sender As Object, _
ByVal e As AdminEventArgs)
End Interface
代码片段中引用的 GameEventArgs
和 AdminEventArgs
类没什么特别的。
<Guid("8B7134E9-C428-42d4-AF84-2E803A6F2099"), _
ClassInterface(ClassInterfaceType.AutoDual), _
ComVisible(True)> _
Public Class GameEventArgs
Inherits EventArgs
Private mValue As Hashtable
Public Sub New(ByVal value As Hashtable)
mValue = value
End Sub
Public ReadOnly Property Value() As Hashtable
Get
Return mValue
End Get
End Property
End Class
<Guid("B5085EE9-0C98-47ff-AE2A-4B0F9D960B99"), _
ClassInterface(ClassInterfaceType.AutoDual), _
ComVisible(True)> _
Public Class AdminEventArgs
Inherits EventArgs
Private mType As AdminEventType
Private mData As Hashtable
Public Sub New(ByVal type As AdminEventType, _
ByVal data As Hashtable)
mType = type
mData = data
End Sub
Public ReadOnly Property Type() As AdminEventType
Get
Return mType
End Get
End Property
Public ReadOnly Property Data() As Hashtable
Get
Return mData
End Get
End Property
End Class
最后,我们还有几个枚举想要公开给 COM。我们只需要为 enum
s 应用 Guid
和 ComVisible
属性。
<Guid("D5566638-49CD-4664-8DD2-BC49486A8799"), _
ComVisible(True)> _
Public Enum AdminEventType
Connected
Disconnected
Shutdown
Minimize
Restore
End Enum
<Guid("FAB7367B-EDA2-4447-B8C6-6869B2377699"), _
ComVisible(True)> _
Public Enum TGGameGetError
NoError
MissingSubtypeInGameMsg
MissingPlayerNumberInAdminConnectedMsg
MissingPlayerDataInAdminConnectedMsg
MissingDataInGameMsg
ExceptionDeserializingDataInGameMsg
End Enum
<Guid("97567966-C6ED-4249-BAA0-3BBDDCA3BB99"), _
ComVisible(True)> _
Public Enum TGGameSendError
NoError
NotConnected
ExceptionSerializingData
End Enum
我们在 TGDxGameLib 库中遇到的一个问题是。该库尝试公开一个接口 IGameClass
。不幸的是,当我们尝试注册项目输出以进行 COM 互操作时,我们收到了以下错误:“COM Interop registration failed. There are no registrable types in the built assembly”。要解决此问题,您可以创建一个实现您尝试公开的接口的类,或者只需向项目中添加一个虚拟类,这是我们选择的。
' Dummy class to get around an
' error exporting the Interface below
<Guid("D93A0FE1-1B0E-481d-90F6-C61D56E36499"), _
ClassInterface(ClassInterfaceType.AutoDual), _
ComVisible(True)> _
Public Class DxGameEventArgs
Inherits EventArgs
Public Sub New()
End Sub
End Class
<Guid("F5133BF0-77DB-4f43-96A2-347D978A1699"), _
InterfaceType(ComInterfaceType.InterfaceIsDual), _
ComVisible(True)> _
Public Interface IGameClass
' Declare an interface.
Sub Animate(ByVal time As Single)
Sub PreSkinPaint()
Sub Render(ByVal device As _
Microsoft.DirectX.Direct3D.Device)
Sub Init(ByVal gc As GraphicsClass)
Sub Dispose()
' etc. . .
' properties
ReadOnly Property GameName() As String
ReadOnly Property GameSize() As Drawing.Size
ReadOnly Property DisplayFPS() As Boolean
ReadOnly Property MaxActiveFPS() As Integer
ReadOnly Property MaxInactiveFPS() As Integer
End Interface
完成后,您可以通过 RegAsm.exe 注册程序集,将它们注册到 COM 并生成类型库。或者,如果您有完整源代码,可以使用 Visual Studio .NET 中的“Register for COM Interop”项目选项进行注册。然后,类型库可以在接收事件的客户端中被引用。
c:\Windows\Microsoft.NET\Framework\v1.1.4322\RegAsm.exe
TGGameLib.dll /codebase /tlb
创建非托管 COM 客户端
对于我们的 COM 客户端,我们决定制作一个我们井字棋示例游戏的版本。要创建使用 .NET 库并接收事件接口的 COM 客户端,我们使用 Visual Studio .NET 中的“New Project...”选项创建了一个新的“MFC application”,并将其命名为“MFCTicTacToe”。创建应用程序后,我们选择了“Add Class...”项目选项并添加了“ATL Support to MFC”。这为我们提供了继续进行所需的样板代码。我们从 MFC 应用程序开始,以便在我们的示例游戏中更容易地进行 GUI 操作,因为它是一个游戏,但这并非必需。文件 TGGameClass.h 包含 CTGGameClassEventWrapper
类。我们创建此类的目的是作为传出事件接口 TGGameEvents
的接收器。由于客户端接收器是本文的重点,我们将首先对其进行介绍。在我们的客户端中实现连接点接收器的基本步骤是
- 使用
#import
指令导入每个外部对象的类型库。 - 声明
IDispEventImpl
接口。 - 声明事件接收器映射。
- 建议并撤销连接点。
让我们更详细地看一下这些步骤。在文件顶部,我们必须添加必要的 import
语句。我们必须导入我们想要处理的每个外部对象的类型库。这将定义可以处理的事件,并提供声明事件接收器映射时使用的信息(稍后会详细介绍)。请注意,我们使用 rename
选项来避免一些名称冲突。
#import "c:\\WINDOWS\\Microsoft.NET\\Framework\\v1.1.4322\\mscorlib.tlb" rename("ReportEvent", "Report_Event") #import "..\\..\\Com Demo\\TGGameLib.tlb" rename("GetType", "Get_Type")
由于我们只想提供对我们感兴趣处理的事件的实现(而不是所有 IDispatch
的内容),因此我们选择使用 IDispEventImpl
模板类来为我们的 ATL 类提供连接点接收器的支持。该接收器将允许我们处理从 TGGameClass
类引发的事件。此模板类与我们 CTGGameClassEventWrapper
类中的事件接收器映射结合使用,以将事件路由到适当的处理程序方法。稍后会详细介绍这些方法,现在让我们看一下此模板类的构造函数调用。IDispEventImpl
构造函数的第三个参数是我们想要实现的事件 dispinterface
。此 dispinterface
必须在第四个参数指向的类型库中进行描述;这就是 IDispEventImpl
获取接口类型信息的地方。这就是为什么我们必须在 .NET 库上运行 RegAsm.exe。请注意,在 .NET 库中,我们为所有我们想公开给 COM 的类型分配了 GUID。在 COM 端,我们使用 C++ __uuidof
运算符来检索这些 GUID。
class ATL_NO_VTABLE CTGGameClassEventWrapper : public IDispEventImplDbg<0, CTGGameClassEventWrapper, &__uuidof(TGGameLib::TGGameEvents), &__uuidof(TGGameLib::__TGGameLib), 1, 0>
我们还声明了几个特定于此示例逻辑的数据成员。
HANDLE m_hDataEvent; TGGameLib::_TGGameClassPtr TheGameClass;
为了让 TGGameClass
的事件通知由适当的方法处理,我们必须将每个事件路由到一个处理程序。ATL 提供了宏 BEGIN_SINK_MAP
、END_SINK_MAP
和 SINK_ENTRY
,这使得映射变得简单。我们使用这些宏为我们想要作为事件处理的每个事件(在每个对象上)创建一个事件接收器映射。
BEGIN_SINK_MAP(CTGGameClassEventWrapper) SINK_ENTRY_EX(0, __uuidof(TGGameLib::TGGameEvents), 1 /* DISPID */, DataEvent) SINK_ENTRY_EX(0, __uuidof(TGGameLib::TGGameEvents), 2 /* DISPID */, GameEvent) SINK_ENTRY_EX(0, __uuidof(TGGameLib::TGGameEvents), 3 /* DISPID */, AdminEvent) END_SINK_MAP()
在 CTGGameClassEventWrapper
可见之前,CTGGameClassEventWrapper
支持的每个外部 dispinterface 都会被查询传出接口。建立连接并使用传出接口的引用来处理源对象(在此例中为 TGGameClass
)的事件。此过程称为“建议”。完成外部接口后,应该通知传出接口它们不再被我们的类使用。此过程称为“撤销”。我们在下面的构造函数和析构函数代码中调用 DispEventAdvise
来建立 TGGameClass
和 CTGGameClassEventWrapper
之间的连接,并调用 DispEventUnadvise
来断开连接。
CTGGameClassEventWrapper( TGGameLib::_TGGameClassPtr GameClass) { Advise(GameClass); } CTGGameClassEventWrapper() : TheGameClass(__uuidof(TGGameLib::TGGameClass)) { m_hDataEvent = CreateEvent(NULL, FALSE, FALSE, NULL); HRESULT _hr = Advise(TheGameClass); if (FAILED(_hr)) _com_issue_error(_hr); _hr = TheGameClass->Start(8000, VARIANT_TRUE); if (_hr != S_OK) { ATLASSERT(false && "The Game Class could not be started."); } } ~CTGGameClassEventWrapper() { if (TheGameClass != NULL) { DispEventUnadvise(TheGameClass); } } HRESULT Advise( TGGameLib::_TGGameClassPtr GameClass) { HRESULT hr = E_FAIL; TheGameClass = GameClass; // don't advise if it's already been done if (m_dwEventCookie == 0xFEFEFEFE) { hr = DispEventAdvise(TheGameClass); } return hr; }
完成事件接收器还剩下实际的事件处理程序。
STDMETHOD_(void, DataEvent)( VARIANT sender, mscorlib::_EventArgs* e) { SetEvent(m_hDataEvent); } STDMETHOD_(void, GameEvent)( VARIANT sender, TGGameLib::_GameEventArgs* e) = 0; STDMETHOD_(void, AdminEvent)( VARIANT sender, TGGameLib::_AdminEventArgs* e) = 0;
您可能会注意到其中两个事件处理程序是纯虚函数。这些纯虚函数在派生类 CMyTicTacToeGameClass
中实现。此类及其事件处理程序特定于此游戏示例的逻辑,因此我将不在此深入探讨。然而,.NET System.Collections.Hashtable
类的包装器是普遍感兴趣的,所以我们现在来看看它。在最初开发此示例时,我们不得不编写大量 COM 代码来从 .NET Hashtable
中提取信息。即使有 ATL 提供的许多免费 COM 代码,这也被证明很麻烦。首先,让我们看一下由我们的服务器代码准备的典型 Hashtable
,注意内部的 Hashtable
。
Dim PlayerData As New Hashtable ' Data for all players
For Each Player As GamePerson In Group.Players.Values
' Info on a particular player
Dim PlayerInfo As Hashtable = New Hashtable
PlayerInfo.Add("Nick", Player.Nick)
PlayerInfo.Add("Rank", Player.Rank)
PlayerInfo.Add("TimesPlayed", Player.TimesPlayed)
PlayerInfo.Add("HasBoughtGameLevel",
CType(Player.BoughtGameLevel, Integer))
PlayerData.Add(player.PlayerNumber, PlayerInfo)
Next
Msg.Add("PlayerData", PlayerData)
For Each player As GamePerson In Group.Players.Values
Msg("PlayerNum") = player.PlayerNumber
If IsTestHarnessMode Then
TestMsgToPlayer(Group.Id, player.Id, Msg)
Else
SendMsgToPlayer(Group.Id, player.id, Msg)
End If
Next
从 COM 客户端大量使用 .NET Hashtable
类可能很重复且难以维护,特别是当您像我们的库那样嵌套此类时。因此,我们编写了一个方便的可重用包装器来处理 Hashtable
类,以使其更清晰、更易于使用。这是我们 COM 代码使用 CustomHashtable
包装器访问此 Hashtable
的示例。
// Extract the Player Data from the EventArgs m_LocalPlayerIndex = pHash->Item["PlayerNum"]; ITGHashtablePtr pWrapPlayerData = ITGHashtablePtr(__uuidof(TGHashtable)); pWrapPlayerData->InnerHashtable = (mscorlib::_HashtablePtr)pHash->Item["PlayerData"]; // Here we get an actual IEnumerator mscorlib::IEnumeratorPtr playerDataEnum = pWrapPlayerData->GetEnumerator(); if (playerDataEnum == NULL) return; // And this is the representation // of IDictionaryEnumerator mscorlib::IDictionaryEnumeratorPtr playerDataEnumDict = playerDataEnum; while(playerDataEnum->MoveNext()) { // Get player info ITGHashtablePtr pWrapPlayerInfo = ITGHashtablePtr(__uuidof(TGHashtable)); pWrapPlayerInfo->InnerHashtable = (mscorlib::_HashtablePtr)playerDataEnumDict->value; // Do something . . . }
可重用 CustomHashtable
包装器的完整源代码可以在下载的源代码中找到。如果我们快速看一下 InnerHashtable
方法,我们可以看到它们通过设置适当的数据成员为我们节省了一些工作。
STDMETHODIMP CTGHashtable:: get_InnerHashtable(_Hashtable** pVal) { *pVal = m_pInnerHash; if(*pVal) { (*pVal)->AddRef(); } return S_OK; } STDMETHODIMP CTGHashtable:: put_InnerHashtable(_Hashtable* newVal) { m_pInnerHash = newVal; m_pCollection = m_pInnerHash; m_pDictionary = m_pInnerHash; return S_OK; }
这样我们就可以像这样包装 ICollection
和 IDictionary
调用。
// ICollection Methods public: STDMETHOD(CopyTo)(_Array * Array, long index) { return m_pCollection->CopyTo(Array,index); } STDMETHOD(get_SyncRoot)(VARIANT * pRetVal) { return m_pCollection->get_SyncRoot(pRetVal); } STDMETHOD(get_IsSynchronized)(VARIANT_BOOL * pRetVal) { return m_pCollection->get_IsSynchronized(pRetVal); } // IDictionary Methods public: STDMETHOD(get_Count)(long * pRetVal) { return m_pCollection->get_Count(pRetVal); } STDMETHOD(get_Item)(VARIANT key, VARIANT *pRetVal) { return m_pDictionary->get_Item(key,pRetVal); } STDMETHOD(putref_Item)(VARIANT key, VARIANT pRetVal) { return m_pDictionary->putref_Item(key,pRetVal); } STDMETHOD(get_Keys)(ICollection * * pRetVal) { return m_pDictionary->get_Keys(pRetVal); } STDMETHOD(get_Values)(ICollection * * pRetVal) { return m_pDictionary->get_Values(pRetVal); } STDMETHOD(Contains)(VARIANT key, VARIANT_BOOL * pRetVal) { return m_pDictionary->Contains(key,pRetVal); } STDMETHOD(Add)(VARIANT key, VARIANT value) { return m_pDictionary->Add(key,value); } STDMETHOD(Clear)() { return m_pDictionary->Clear(); } STDMETHOD(get_IsReadOnly)(VARIANT_BOOL * pRetVal) { return m_pDictionary->get_IsReadOnly(pRetVal); } STDMETHOD(get_IsFixedSize)(VARIANT_BOOL * pRetVal) { return m_pDictionary->get_IsFixedSize(pRetVal); } STDMETHOD(GetEnumerator)(IDictionaryEnumerator * * pRetVal) { return m_pDictionary->GetEnumerator(pRetVal); } STDMETHOD(Remove)(VARIANT key) { return m_pDictionary->Remove(key); }
由于 CustomHashtable
是一个被主“MFCTicTacToe”项目使用的 COM 库,它必须在您的计算机上注册。我们在项目中添加了一个“Post Build Step”来执行所需的注册命令。如果您只是运行演示,可以手动运行以下命令:
regsvr32 /s /c CustomHashtable.dll
我们第一次尝试编译此 COM 客户端时遇到的一个问题是,我们会收到“Class not registered”错误。这是因为在 .NET 1.0/1.1 中,mscorlib 不是全局程序集缓存 (GAC) 的一部分,根据我所读到的内容,它将在 .NET 2.0 中。现在解决此问题,您可以运行以下命令:
c:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\RegAsm.exe mscorlib.dll
关注点
如果您想了解一种实际方法来将您的托管库公开给 COM 世界,那么本文非常有趣。它尤其有趣,因为它展示了如何模拟 COM 连接点,并且有一个很棒的包装器用于访问 .NET Hashtable
类。如果您有兴趣查看完整的 TGSDK 以制作您自己的多人在线游戏,您可以在 TrayGames 网站上找到它。我想强调的是,使用 TGSDK 编写游戏时,托管代码绝对是首选。如果您想查看一篇关于如何使用 TGSDK 用托管代码编写游戏的文章,请查看我的 Navy Battle 文章。
致谢
特别感谢 Ryan Schneider 为启动此项目付出了大量时间。
修订历史
- 2005 年 8 月 22 日 - 初始修订。