运行对象表:.NET 中的提供者,MFC 中的使用者
两个示例类:一个在 C# 中向 ROT 注册自己,另一个在 MFC/C++ 中使用该对象
引言
使用 ROT(运行对象表)是建立两个 Windows 应用程序之间进程间通信的绝佳方式。从纯逻辑角度来看,一个应用程序将类的实例指针注册到 ROT 中,另一个应用程序获取指向同一类实例的指针,从而可以通过该指针使用该类的同一实例。注册的类必须是 COM 类,否则它可以用任何语言编写。将从 ROT 中检索指针的应用程序可以用任何可以调用 COM 的语言编写,因为 ROT 提供指向 COM 接口的指针。我需要一个 C# 应用程序将类注册到 ROT,一个 MFC(原生 C++)应用程序检索它并使用它。我创建了一个小的示例应用程序来测试该系统,并想在此展示。
使用代码
C# 端
首先要展示的是在创建时将自己注册到 ROT 的 .NET 类。由于这个类必须是 COM 类,所以它必须有一个接口。我的类名为 COMROTVictim
,所以接口命名为 ICOMROTVictim
。
[ComVisible(true),
GuidAttribute("14C09983-FA4B-44e2-9910-6461728F7883"),
InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICOMROTVictim
{
[DispId(1)]
void MFCStart();
[DispId(2)]
void MFCStop();
[DispId(3)]
void MFCCallsCSharp(string s);
}
我还需要回调(在 C# 应用程序中的某些处理结果在 MFC 应用程序中调用的函数),所以我声明了一个事件接口,如下所示:
[Guid("E00FA736-8C24-467a-BEA0-F0AC8E528207"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch),
ComVisible(true)]
public interface ICOMROTVictimEvents
{
[DispId(1)]
void CEClose(string s);
[DispId(2)]
void CECSharpCallsMfc(string s);
}
我需要一些 Ole32
调用,我创建了一个类来处理这些函数。
class Ole32
{
[ DllImport ( "Ole32.Dll" ) ]
public static extern int CreateBindCtx ( int reserved,
out UCOMIBindCtx bindCtx );
[DllImport("oleaut32.dll")]
public static extern int RegisterActiveObject
([MarshalAs(UnmanagedType.IUnknown)] object punk,
ref Guid rclsid, uint dwFlags, out int pdwRegister);
}
最后是类本身。由于我非常喜欢封装,我的类会在构造函数中注册自己到 ROT,并在 Dispose()
方法中移除。我认为这样代码更清晰。这将是我 C# 应用程序与 MFC 应用程序之间的接口,它解决通信问题是它自己的责任。
[ComVisible(true),
GuidAttribute("5E9C1704-28BC-499c-A54F-A19F082D08B6"),
ComSourceInterfaces(typeof(ICOMROTVictimEvents)),
ClassInterface(ClassInterfaceType.None)]
public class COMROTVictim : ICOMROTVictim, IDisposable
{
public delegate void ComEvent(string p);
public event ComEvent CECSharpCallsMfc;
public event ComEvent CEClose;
int m_dwRegister;
bool m_bMFCConnected;
public COMROTVictim()
{
Guid guid = Marshal.GenerateGuidForType(typeof(COMROTVictim));
Ole32.RegisterActiveObject(this, ref guid, 0, out m_dwRegister);
MessageBox.Show("Registering: " + guid.ToString());
}
public void Dispose()
{
if (m_dwRegister != 0)
{
if (m_bMFCConnected )
CSharpClose(0);
UCOMIBindCtx bc;
Ole32.CreateBindCtx ( 0, out bc );
UCOMIRunningObjectTable rot;
bc.GetRunningObjectTable ( out rot );
rot.Revoke(m_dwRegister);
m_dwRegister = 0;
}
}
~COMROTVictim()
{
Dispose();
}
public void MFCStart()
{
MessageBox.Show("MFCApp connected");
m_bMFCConnected = true;
}
public void MFCStop()
{
MessageBox.Show("MFCApp disconnected");
m_bMFCConnected = false;
}
public void MFCCallsCSharp(string s)
{
MessageBox.Show(string.Format("MFCCallsCSharp param: {0}", s));
}
public void CSharpCallsMfc(string s)
{
if (CECSharpCallsMfc != null)
CECSharpCallsMfc(s);
}
public void CSharpClose(int i)
{
if (CEClose != null)
CEClose("");
m_bMFCConnected = false;
}
}
这是将与我的 MFC 应用程序通信的 C# 类的代码。以 MFC 开头的函数将从我的 MFC 应用程序调用。CSharp 应用程序调用 CSharpCallsMfc()
来将字符串发送到 MFC 应用程序,并调用 CSharpClose()
来通知 MFC 应用程序它正在关闭不再有效的指针。
MFC 端
我的 MFC 应用程序还有一个用于管理此通信的类,名为 CCOMROTVictimWrapper
。由于我需要 COM 事件接口(用于双向通信),这个类派生自 IDispEventImpl
的适当特化。下面的类声明可以看到。
class CCOMROTVictimWrapper : public IDispEventImpl<0, CCOMROTVictimWrapper,
&DIID_ICOMROTVictimEvents,&LIBID_ROTProviderDotNetDlg, 1, 0>
{
public:
CCOMROTVictimWrapper(void);
virtual ~CCOMROTVictimWrapper(void);
BEGIN_SINK_MAP(CCOMROTVictimWrapper)
SINK_ENTRY_EX(0, DIID_ICOMROTVictimEvents, 1, OnClose)
SINK_ENTRY_EX(0, DIID_ICOMROTVictimEvents, 2, OnCSharpCallsMfc)
END_SINK_MAP()
// incoming calls from the C# app
STDMETHOD_(void, OnClose)(BSTR param);
STDMETHOD_(void, OnCSharpCallsMfc)(BSTR param);
// outgoing calls to the C# app
//connect and call start()
bool Start();
// an example function call
bool MfcCallsCSharp(CString s);
//call stop and disconnect
bool Stop();
protected:
ICOMROTVictim* m_pIF;
};
Start()
将连接到 C# 应用程序,Stop()
将断开连接。函数 MfcCallsCSharp()
将字符串发送到 CSharp 应用程序,后者将显示一个带有该字符串的消息框。最相关函数的实现如下所示。
//connect and call start()
bool CCOMROTVictimWrapper::Start()
{
try
{
USES_CONVERSION;
if (m_pIF != NULL)
return false;
IRunningObjectTable* pRunningObjectTable = NULL;
IMoniker* pMoniker = NULL;
HRESULT hr = GetRunningObjectTable(0, &pRunningObjectTable);
LPOLESTR strClsid;
StringFromCLSID(CLSID_COMROTVictim, &strClsid);
CreateItemMoniker(T2W("!"), strClsid, &pMoniker);
IUnknown* punk = NULL;
IBindCtx* pctx;
hr = CreateBindCtx(0, &pctx);
if (pRunningObjectTable->GetObject( pMoniker, &punk) != S_OK)
{
pctx->Release();
throw 1; //object not running
}
hr = punk->QueryInterface(__uuidof(ICOMROTVictim), (((void**)&m_pIF)));
hr = DispEventAdvise(m_pIF);
pctx->Release();
punk->Release();
m_pIF->MFCStart();
}
catch(...)
{
return false;
}
return true;
}
//How to disconnect
bool CCOMROTVictimWrapper::Stop()
{
try
{
if (!m_pIF)
return false;
m_pIF->MFCStop();
DispEventUnadvise(m_pIF);
m_pIF->Release();
m_pIF = NULL;
}
catch(...)
{
return false;
}
return true;
}
// an example function-call
bool CCOMROTVictimWrapper::MfcCallsCSharp(CString s)
{
try
{
m_pIF->MFCCallsCSharp(s.AllocSysString());
}
catch(...)
{
return false;
}
return true;
}
//called by the C# app
void CCOMROTVictimWrapper::OnClose(BSTR)
{
AfxMessageBox("CSharpApp closed");
DispEventUnadvise(m_pIF);
m_pIF->Release();
m_pIF = NULL;
}
//called by the C# app
void CCOMROTVictimWrapper::OnCSharpCallsMfc(BSTR str)
{
AfxMessageBox("OnCSharpCallsMfc param: " + CString(str));
}
使用 VB6 作为使用者
在 fredobedo007 的帮助下(参见本文下方的 FAQ),我能够从 VB6 客户端使用 ROT 中的同一个 C# 对象(如果您阅读了本文的早期版本,我已经修改了 C# 代码,使其能够与 VB6 客户端协作,并且还修改了 C++ 代码以使其能够与新版本的 C# 代码协作)。下面显示了使用 C# 对象的 VB6 类。Private WithEvents m_rv As ROTProviderDotNetDlg.COMROTVictim
Private Sub Class_Initialize()
Set m_rv = GetObject(, "ROTProviderDotNetDlg.COMROTVictim")
m_rv.MFCStart
End Sub
Public Sub SendMsg(ByVal s As String)
m_rv.MFCCallsCSharp ("")
End Sub
Public Sub Disconnect(ByVal s As String)
m_rv.MFCStop
End Sub
Private Sub m_rv_CEClose(ByVal s As String)
MsgBox "a"
End Sub
Private Sub m_rv_CECSharpCallsMfc(ByVal s As String)
MsgBox "b"
End Sub
当然,您需要为 C# 项目生成的 tlb 文件添加引用。结论
如果您理解了以上所有内容,您可以复制粘贴代码并修改它以满足您的需求(更改 GUID 号码!!!)。但在编程之前,请阅读以下内容:- C# exe 必须使用 regasm.exe 注册为 COM 应用程序。
- 事件接口编程可能非常烦人。对于几种错误,
DispEventAdvise
返回S_OK
,但事件根本不被调用。此类错误包括版本冲突(IDispEventImpl
特化的版本必须与 C# 程序集的 Major 和 Minor 版本匹配)。此外,MFC 代码中用于实现事件接口的不匹配函数签名会导致代码编译通过,但函数不被调用。这只是一个警告,该工具很棒,但您必须小心使用它。 - Visual Studio 6 有一个名为 ROT Viewer 的工具。这可能是一个有用的应用程序,但任何显示 ROT 内容的其他应用程序也可以用于检查注册是否正常。
- 同样,您的类已注册到 ROT 并在 ROT 查看器中显示的事实并不意味着一切正常。如果您遇到问题,请检查不仅调用方,还要检查注册指针的代码。
- 在 .NET 2.0 中有一个用于 COM 类的新命名空间:
System.Runtime.InteropServices.ComTypes
。如果您使用 .NET 2.0,您应该使用此命名空间中的类,但基本理论应该是相同的(我没有机会尝试)。 - VB6 无法处理 COM 事件接口中包含任何下划线字符的函数名。如果您打算有一个 VB6 客户端,您应该避免使用它。
- 在 VB6 中,当您指定
WithEvents
时,事件函数应该由调试器自动添加(当您在代码区域顶部的左侧组合框中选择变量时)。对我来说,只添加了第一个函数。第二个函数是我手动添加的。
下载内容
- 源代码链接下载了上面解释的 4 个文件(cs、cpp、h、vb)。
- 演示链接下载了我创建的 C# 和 Visual C++ 项目。
使用方法:解压,构建,注册 .NET 应用程序(regasm.exe ROTProviderDotNetDlg.exe /codebase /tlb),运行两个 exe 文件,按按钮,并留意消息框。