使用托管 C++ 时,如何从另一个 AppDomain 返回托管对象?
我正在使用用 C++ 编写的非托管库。该库有一个托管 C++ (CLI) 包装器,我正在使用托管代码中的库。非托管库(包括 CLI 包装器)是由第三方编写的,但我可以访问源代码。
不幸的是,托管包装器不能很好地与 AppDomains 配合使用。非托管库创建线程并将从这些线程调用托管代码。当托管代码在非默认 AppDomain 中运行时,这会导致问题。我需要跨 AppDomain 调用才能使用标准工具进行单元测试。
为了解决这个问题,我在托管包装器中引入了委托,并使用 Marshal.GetFunctionPointerForDelegate() 来获取允许跨 AppDomain 调用成功的函数指针。
这通常工作得很好,但现在我遇到了一个新问题。我有以下事件顺序。
Unmanaged thread -> Unmanaged code 1 -> Managed wrapper 1 -> AppDomain transition (via delegate) -> Managed wrapper 2 -> Unmanaged code 2 ->
我省略了一些关于该库如何允许您重写托管包装器 2 上面的托管代码中的某些功能的详细信息,这是首先进行非托管到托管转换的全部要点。
最终,非托管代码 2 必须将非托管对象返回给非托管代码 1。
如果没有委托和 AppDomain,托管包装器 2 将包装非托管对象并将其返回到托管包装器 1,然后托管包装器 1 将状态传输到 使用的非托管对象非托管代码 1。
不幸的是,我很难在 AppDomain 转换中返回托管对象。
我认为我必须使跨 AppDomain 边界传递的托管对象可序列化。然而,这并不容易做到。相反,我创建了一个简单的类,在其中可以存储要传输的对象的类型以及表示对象状态的字符串。 Type
和 String
都很容易编组,幸运的是,我总是可以使用默认构造函数创建对象的实例,然后从字符串初始化它:
// Message is the base class of large hierarchy of managed classes.
[Serializable]
// Sorry for the "oldsyntax", but that is what the C++ library uses.
__gc class SerializedMessage {
public:
SerializedMessage(Message* message)
: _type(message->GetType()), _string(message->ToString()) { }
Message* Create() {
Message* message = static_cast<Message*>(Activator::CreateInstance(_type));
message->InitializeFromString(_string);
return message;
}
private:
Type* _type;
String* _string;
};
在托管包装器中2 我返回一个 SerializedMessage
,然后在托管包装器 1 中,我通过调用 SerializedMessage::Create
取回原始消息的副本>。或者至少这是我希望实现的目标。
不幸的是,AppDomain 转换失败,并出现 InvalidCastException
,消息为 Unable to cast object of type 'SerializedMessage' to type 'SerializedMessage'。
我不确定发生了什么,但是该错误消息可能表明正在从错误的 AppDomain 访问 SerializedMessage
对象。但是,使用 Marshal.GetFunctionPointerForDelegate
的全部目的是能够跨 AppDomain 进行调用。
我还尝试从 MarshalByRefObject
派生 SerializedMessage
,但随后我收到 InvalidCastException
消息,Unable to cast object of type 'System. MarshalByRefObject' 来键入“SerializedMessage”。
当我通过 Marshal.GetFunctionPointerForDelegate() 返回的指针调用时,我需要做什么才能从另一个 AppDomain 传回托管对象
?
对已接受答案的评论
关于“程序集如何加载到第二个AppDomain中”的部分是正确的。
C++ 代码在由单元测试运行程序控制的默认 AppDomain 中执行。托管程序集被加载到由该单元测试运行程序创建的第二个 AppDomain 中。我使用 Marshal.GetFunctionPointerForDelegate
启用从第一个到第二个 AppDomain 的托管 C++ 调用。
最初,我遇到了一些 FileNotFoundException
试图加载我的托管程序集,为了解决这个问题,我将托管程序集复制到单元测试运行程序的 AppBase 中。我仍然有点困惑为什么 .NET 坚持加载正在执行的程序集,但决定稍后解决该问题,只是将丢失的文件复制为拼凑文件。
不幸的是,这会从两个不同 AppDomain 中同一程序集的两个不同副本加载相同类型,我认为这是 InvalidCastException 的根本原因。
我的结论是,我无法使用“标准”单元测试运行程序来测试托管 C++ 库,因为该库的回调来自我无法控制的错误 AppDomain。
I'm using an unmanaged library written in C++. The library has a managed C++ (CLI) wrapper, and I'm using the library from managed code. The unmanaged library (including the CLI wrapper) is written by a third party but I have access to the source code.
Unfortunately the managed wrapper does not play well with AppDomains. The unmanaged library creates threads and will call into managed code from these threads. This leads to problems when the managed code is running in a non-default AppDomain. And I need cross-AppDomain calls to be able to unit test using standard tools.
To solve that I have introduced delegates in the managed wrapper and used Marshal.GetFunctionPointerForDelegate()
to get a function pointer that allows cross-AppDomain calls to succeed.
This generally work well but now I have a new problem. I have the following sequence of events.
Unmanaged thread -> Unmanaged code 1 -> Managed wrapper 1 -> AppDomain transition (via delegate) -> Managed wrapper 2 -> Unmanaged code 2 ->
I have left out some details on how the library allows you to override some functionality in managed code above managed wrapper 2 which is the whole point of doing the unmanaged to managed transition in the first place.
Eventually unmanaged code 2 will have to return an unmanaged object to unmanaged code 1.
Without the delegate and AppDomain stuff managed wrapper 2 will wrap the unmanaged object and return it to managed wrapper 1 that will then transfer the state to the unmanaged object used by unmanaged code 1.
Unfortunately I'm having a hard time returning a managed object across the AppDomain transition.
I figured that I had to make the managed object passed across the AppDomain boundary serializable. However, that is not easily done. Instead I've created a simple class where I can store the type of the object I want to transfer and a string representing the state of the object. Both Type
and String
are easily marshalled and luckily I can always create an instance of the object using the default constructor and then initialize it from a string:
// Message is the base class of large hierarchy of managed classes.
[Serializable]
// Sorry for the "oldsyntax", but that is what the C++ library uses.
__gc class SerializedMessage {
public:
SerializedMessage(Message* message)
: _type(message->GetType()), _string(message->ToString()) { }
Message* Create() {
Message* message = static_cast<Message*>(Activator::CreateInstance(_type));
message->InitializeFromString(_string);
return message;
}
private:
Type* _type;
String* _string;
};
In managed wrapper 2 I return a SerializedMessage
and in managed wrapper 1 I then get back a copy of the original message by calling SerializedMessage::Create
. Or at least that was what I was hoping to achieve.
Unfortunately the AppDomain transition fails with a InvalidCastException
having the message Unable to cast object of type 'SerializedMessage' to type 'SerializedMessage'.
I'm not sure what is going on but the error message could indicate that the SerializedMessage
object is being accessed from the wrong AppDomain. However, the whole point of using Marshal.GetFunctionPointerForDelegate
is to be able to call across AppDomains.
I also tried to derive SerializedMessage
from MarshalByRefObject
but then I get and InvalidCastException
having the message Unable to cast object of type 'System.MarshalByRefObject' to type 'SerializedMessage'.
What do I need to do to be able to pass a managed object back from another AppDomain when I call through a pointer returned by Marshal.GetFunctionPointerForDelegate()
?
Comments on the accepted answer
The part about "how the assembly got loaded into the second AppDomain" is correct.
The C++ code executes in the default AppDomain which is controlled by the unit test runner. The managed assembly is loaded into a second AppDomain created by this unit test runner. I'm using Marshal.GetFunctionPointerForDelegate
to enable managed C++ calls from the first to the second AppDomain.
Initially I got some FileNotFoundException
's trying to load my managed assembly and to work around this I copied my managed assembly to the AppBase of the unit test runner. I'm still a bit confused why .NET insist on loading the very assembly that is executing, but decided to work on that problem later and simply copied the missing file as a kludge.
Unfortunately this loads the same type from two different copies of the same assembly in two different AppDomains and I assume this is the root cause of the InvalidCastException
's.
My conclusion is that I'm unable to use a "standard" unit test runner to test the managed C++ library because the callbacks from this library is from the wrong AppDomain which I have no control over.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我将重点关注这个问题作为您问题的核心。真正的消息应该是“类型为 Foo.SerializedMessage 到类型为 Bar.SerializedMessage”。换句话说,这里涉及两种类型,来自不同的程序集。 .NET 类型的标识不仅仅是类名,它还包括完全限定的程序集名称。这是程序集显示名称、程序集版本和区域性。 DLL Hell 对策。
检查程序集的构建方式,并验证 SerializedMessage 在任何程序集中仅出现一次。它也可能是由程序集加载到第二个 AppDomain 的方式引起的,例如使用 LoadFile() 就会导致它。它在没有加载上下文的情况下加载程序集,并且从此类程序集加载的任何类型甚至与正常加载的完全相同的程序集的完全相同的类型都不兼容。
而且你最好远离 GetFunctionPointerForDelegate(),非托管指针不遵守 AppDomain 边界。尽管我不知道你为什么使用它。
I'd focus on this problem for the core of your issue. The real message should be "of type Foo.SerializedMessage to type Bar.SerializedMessage". In other words, there are two types involved here, from different assemblies. A .NET type's identity is not just the class name, it also includes the fully qualified assembly name. Which is the assembly display name and assembly version and culture. A DLL Hell counter-measure.
Check how your assemblies are built and verify that SerializedMessage only appears once in any of your assemblies. It can also be caused by the way the assembly got loaded into the second AppDomain, using LoadFile() will cause it for example. It loads assemblies without a loading context and any types loaded from such an assembly are not even compatible with the exact same type from the exact same assembly that was loaded normally.
And you'd better stay away from GetFunctionPointerForDelegate(), unmanaged pointers don't observe AppDomain boundaries. Albeit that I have no clue why you're using it.