.NET 6 IDispatch 客户端实现崩溃
.NET 6 IDispatch client implementation crash
.NET 6 在调用自己的 IDispatch
对象时遇到问题,如果已编组。
重现:
if (Array.IndexOf(Environment.GetCommandLineArgs(), "/s")>=0) //Server
{
Thread t = new Thread(new ThreadStart(() =>
{
Hello h = new Hello();
new RunningObjectTable().Register("HelloTest", h);
Thread.Sleep(1000 * 3600);
}));
t.SetApartmentState(ApartmentState.MTA);
t.Start();
Thread.Sleep(1000 * 3600);
}
else //Client
{
object o = new RunningObjectTable().Get("HelloTest");
IHello h = o as IHello;
int f = h.Foo();
Console.WriteLine(h);
}
[ComImport]
[Guid("00020400-0000-0000-C000-000000000046")] //IID_IDispatch
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[TypeLibType(TypeLibTypeFlags.FDispatchable)]
public interface IHello
{
[MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
[DispId(1)]
public int Foo();
}
public class Hello: IHello
{
public int Foo()
{
Debug.WriteLine("Hello from server");
return 19;
}
}
运行带/s的程序,就是对象服务器。然后运行另一个副本成为客户端
客户端中的方法调用行因“找不到成员”而崩溃 - HRESULT 0x80020003,DISP_E_MEMBERNOTFOUND。通常这意味着一个伪造的 DISPID,但差异可能来自哪里?
该代码中的一个小错误是使用 IDispatch 的 IID 修饰了界面。使用自定义 IID,正如 COM 规定的那样,它甚至不会作为调度接口解组(as IHello
行 returns 空);在内部,有一个 QueryInterface
跨进程调用,具有所述自定义 IID,调度接口未在 HKCR\Interfaces 下注册,因此进程间 COM 机制不知道如何编组它。至少那是我的理论。
如果服务器是本地 (C++) 服务器,则类似的逻辑可以正常工作。如果针对 .NET Framework 4.72 重新编译相同的 C# 片段,它甚至不会那么远,o as IHello;
行 returns null.
RunningObjectTable
是 ROT 的帮手 class。为了完整起见,这里:
internal class RunningObjectTable
{
#region API
[DllImport("ole32.dll")]
private static extern int CreateItemMoniker([MarshalAs(UnmanagedType.LPWStr)] string
lpszDelim, [MarshalAs(UnmanagedType.LPWStr)] string lpszItem,
out IMoniker ppmk);
[DllImport("ole32.dll")]
private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
#endregion
private IRunningObjectTable m_rot;
public RunningObjectTable()
{
GetRunningObjectTable(0, out m_rot);
}
private IMoniker CreateItemMoniker(string s)
{
IMoniker mon;
CreateItemMoniker("", s, out mon);
return mon;
}
public int Register(string ItemName, object o)
{
return m_rot.Register(0, o, CreateItemMoniker(ItemName));
}
public void Unregister(int ROTCookie)
{
m_rot.Revoke(ROTCookie);
}
public object Get(string ItemName)
{
object o;
m_rot.GetObject(CreateItemMoniker(ItemName), out o);
return o;
}
}
可以访问 .NET Core 内部的人告诉我发生了什么事吗?
这是正在发生的事情。 .NET 对象支持两种调度接口——一个对应于 IHello
,另一个对应于 Hello
本身,后者被视为默认接口。我已经通过向 Hello
添加一个额外的方法 Bar()
并在本机 C++ 客户端中检查类型来确认:
IUnknownPtr pUnk;
ROTGet(L"HelloTest", &pUnk);
IDispatchPtr pDisp(pUnk);
LPOLESTR Name = (LPOLESTR)L"Bar";
DISPID dispid;
hr = pDisp->GetIDsOfNames(IID_NULL, &Name, 1, 0, &dispid);
我得到了 S_OK 和一个有效的 DISPID,如果是假的。然后我以同样的方式为 Foo
请求了一个 DISPID,并得到了另一个伪造的 DISPID。
我找到了两种解决方法。
第一,告诉框架它不应该为 Hello
隐式生成调度接口。用[ClassInterface(ClassInterfaceType.None)]
注释class,它不会。对 Foo
的 DISPID 的显式检查证实了这一点。客户端中未编组的调度接口对应于 IHello
,方法按预期工作。
另一种方法,给 IHello
一个身份(IID)并使其可编组。如果我在该界面上使用自定义 GUID,并将具有该 GUID 的键添加到 HKCR\Interfaces,提供 ProxyStubClsid32
等于 {00020420-0000-0000-C000-000000000046}
(即 PSDispatch,proxy/stub CLSID IDispatch
),该片段也有效。
另一种告诉 COM 一个 IID 对应于一个调度接口并且应该被编组的方法是通过实现 IStdMarshalInfo
。我没试过这个。
请注意:必须声明 Hello class public 才能正常工作。如果不是,由于某种原因 as IHello
returns null.
.NET 6 在调用自己的 IDispatch
对象时遇到问题,如果已编组。
重现:
if (Array.IndexOf(Environment.GetCommandLineArgs(), "/s")>=0) //Server
{
Thread t = new Thread(new ThreadStart(() =>
{
Hello h = new Hello();
new RunningObjectTable().Register("HelloTest", h);
Thread.Sleep(1000 * 3600);
}));
t.SetApartmentState(ApartmentState.MTA);
t.Start();
Thread.Sleep(1000 * 3600);
}
else //Client
{
object o = new RunningObjectTable().Get("HelloTest");
IHello h = o as IHello;
int f = h.Foo();
Console.WriteLine(h);
}
[ComImport]
[Guid("00020400-0000-0000-C000-000000000046")] //IID_IDispatch
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[TypeLibType(TypeLibTypeFlags.FDispatchable)]
public interface IHello
{
[MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
[DispId(1)]
public int Foo();
}
public class Hello: IHello
{
public int Foo()
{
Debug.WriteLine("Hello from server");
return 19;
}
}
运行带/s的程序,就是对象服务器。然后运行另一个副本成为客户端
客户端中的方法调用行因“找不到成员”而崩溃 - HRESULT 0x80020003,DISP_E_MEMBERNOTFOUND。通常这意味着一个伪造的 DISPID,但差异可能来自哪里?
该代码中的一个小错误是使用 IDispatch 的 IID 修饰了界面。使用自定义 IID,正如 COM 规定的那样,它甚至不会作为调度接口解组(as IHello
行 returns 空);在内部,有一个 QueryInterface
跨进程调用,具有所述自定义 IID,调度接口未在 HKCR\Interfaces 下注册,因此进程间 COM 机制不知道如何编组它。至少那是我的理论。
如果服务器是本地 (C++) 服务器,则类似的逻辑可以正常工作。如果针对 .NET Framework 4.72 重新编译相同的 C# 片段,它甚至不会那么远,o as IHello;
行 returns null.
RunningObjectTable
是 ROT 的帮手 class。为了完整起见,这里:
internal class RunningObjectTable
{
#region API
[DllImport("ole32.dll")]
private static extern int CreateItemMoniker([MarshalAs(UnmanagedType.LPWStr)] string
lpszDelim, [MarshalAs(UnmanagedType.LPWStr)] string lpszItem,
out IMoniker ppmk);
[DllImport("ole32.dll")]
private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
#endregion
private IRunningObjectTable m_rot;
public RunningObjectTable()
{
GetRunningObjectTable(0, out m_rot);
}
private IMoniker CreateItemMoniker(string s)
{
IMoniker mon;
CreateItemMoniker("", s, out mon);
return mon;
}
public int Register(string ItemName, object o)
{
return m_rot.Register(0, o, CreateItemMoniker(ItemName));
}
public void Unregister(int ROTCookie)
{
m_rot.Revoke(ROTCookie);
}
public object Get(string ItemName)
{
object o;
m_rot.GetObject(CreateItemMoniker(ItemName), out o);
return o;
}
}
可以访问 .NET Core 内部的人告诉我发生了什么事吗?
这是正在发生的事情。 .NET 对象支持两种调度接口——一个对应于 IHello
,另一个对应于 Hello
本身,后者被视为默认接口。我已经通过向 Hello
添加一个额外的方法 Bar()
并在本机 C++ 客户端中检查类型来确认:
IUnknownPtr pUnk;
ROTGet(L"HelloTest", &pUnk);
IDispatchPtr pDisp(pUnk);
LPOLESTR Name = (LPOLESTR)L"Bar";
DISPID dispid;
hr = pDisp->GetIDsOfNames(IID_NULL, &Name, 1, 0, &dispid);
我得到了 S_OK 和一个有效的 DISPID,如果是假的。然后我以同样的方式为 Foo
请求了一个 DISPID,并得到了另一个伪造的 DISPID。
我找到了两种解决方法。
第一,告诉框架它不应该为 Hello
隐式生成调度接口。用[ClassInterface(ClassInterfaceType.None)]
注释class,它不会。对 Foo
的 DISPID 的显式检查证实了这一点。客户端中未编组的调度接口对应于 IHello
,方法按预期工作。
另一种方法,给 IHello
一个身份(IID)并使其可编组。如果我在该界面上使用自定义 GUID,并将具有该 GUID 的键添加到 HKCR\Interfaces,提供 ProxyStubClsid32
等于 {00020420-0000-0000-C000-000000000046}
(即 PSDispatch,proxy/stub CLSID IDispatch
),该片段也有效。
另一种告诉 COM 一个 IID 对应于一个调度接口并且应该被编组的方法是通过实现 IStdMarshalInfo
。我没试过这个。
请注意:必须声明 Hello class public 才能正常工作。如果不是,由于某种原因 as IHello
returns null.