如何在C++和C#之间传递对象引用

5
有没有办法通过本机互操作将C#对象引用(类类型,而不是结构体)传递到和从C ++中传出?
这是我的用例:
我正在尝试使用C#编写游戏引擎,但希望使用本机(C ++)物理库。物理库维护所有物理体的内部状态,并允许用户将少量数据(指针)与每个物体相关联。在物理模拟时刻之后,该库提供了移动的所有物理体的列表。我想遍历此列表并将所有更改转发给相应的C#对象。
问题是:在C++中将C#对象与相应的物理体关联起来的最佳方法是什么?
我可以想到几种方法:
1. 给每个物理体一个ID,将ID与C ++中的物理体相关联,并维护ID到C#对象的相应映射。即使有优化的映射机制,这对我来说似乎也是不必要的间接方式。
2. 利用编组C#委托(可以隐式引用C#对象实例)为C ++函数指针。这可能是最好的方法;我不知道C#如何实现委托以及这会带来什么类型的开销。
3. 查找一些其他方式来引用C#对象并将该引用与每个本机物理体相关联。
是否有像选项3这样的机制我不知道?由于我想支持没有C++ / CLI的平台,因此C ++ / CLI不是一个选项。

如果 C++/CLI 不是一个选项,那么 P/Invoke 就是下一个要考虑的事情。通过 P/Invoke 引用 C#对象并不是真正可行的,委托也可能会很困难甚至不可能,但是或许你能从中得到一些东西。 - citykid
是的,P/Invoke 就是我所说的“本地互操作”的意思;我不知道它有一个名字。使用 P/Invoke 将 C# 委托传递到 C++ 中作为函数指针是完全可行的,只是我不知道它会有什么样的开销。 https://msdn.microsoft.com/zh-cn/library/367eeye0.aspx - jbatez
发现另一个回答描述了委托的一些(被认为的)性能特征。 https://dev59.com/T3I95IYBdhLWcg3w8iz1#2082975 也许使用它们是实现这个的最佳方式。在C++中遍历已更改的数据列表时,只需为每个调用相应的委托,将新数据传递到C#环境中。 - jbatez
Pinvoke是朝着错误的方向发展,虽然从C#调用本地代码很有帮助,但反过来就不行了。以跨平台的方式让本地C++了解外部类型系统是不可能的。微软通过COM和C++/CX做得很好,但*nix用户不会使用它,因为他们认为这不是自己发明的。 - Hans Passant
C++代码实际上不需要了解C#类型系统的任何信息;在这种情况下,它可以返回一个不透明指针给C#来完成工作。只需要一些东西来表示“嘿,你知道我说的那个东西吗?”我想这只是因为垃圾回收和重定位而不可用。 - jbatez
2个回答

6
我建议使用专门为此类情况设计的工具,即System.Runtime.InteropServices.GCHandle
用法:
输入:
1. 使用GCHandleType.NormalGCHandleType.Weak为您的CLR对象分配GCHandle。 2. 通过GCHandle.ToIntPtrGCHandle转换为IntPtr。 3. 将该IntPtr作为您提到的userdata指针传递到C++侧。
输出:
1. 从C++一侧获取IntPtr。 2. 通过GCHandle.FromIntPtrIntPtr转换回GCHandle。 3. 使用GCHandleTarget属性访问原始CLR对象。
当CLR对象不再被C++侧引用(因为C++对象已被销毁或将userdata指针重置为例如IntPtr.Zero)时,请调用GCHandle.Free释放GCHandle
分配哪种GCHandle类型取决于您是否希望GCHandle保持对象的存活状态(GCHandleType.Normal)还是不想保持对象的存活状态(GCHandleType.Weak)。

在委托的情况下,我可以从C++中调用委托并在C#端使用this来获取对象引用。但这不是重点;在这种情况下拥有对象引用的目的是为了能够调用其方法。如果我可以直接调用该方法,那么就可以完成工作。 - jbatez
我不知道 GCHandle 的相关内容。我会去了解一下。谢谢! - jbatez
GCHandle似乎正是我所寻找的。如果您去掉答案中的委托部分,我将接受这个答案。 - jbatez
1
@JoBates 完成了。我不确定删除那部分是否会改善我的答案,但既然我是个贪心的混蛋... :) - Paul Groke
很抱歉只接受有条件的建议。我已经有一种使用委托的方法,我知道它是可行的;真正的问题是“还有更直接的替代方案吗?”,而GCHandle就能完美地回答这个问题。 - jbatez

2
  1. 映射一个唯一标识符可以让你隔离托管代码和非托管代码。如果非托管代码不对托管对象做任何事情,那么最好,因为你无法控制对象的生命周期。
  2. 委托作为函数指针是可能的。你只需要定义你的委托类型并创建一个实例。然后,你可以使用Marshal.GetFunctionPointerForDelegate来获取一个地址传递给非托管侧。需要注意的是,你必须确保回调正在使用__stdcall
  3. 直接访问类成员和函数可能是可能的,但没有C++/CLI包装器会很危险。
  4. 在C#端分配一块内存并将其传递给C++。在托管端的类中使用属性以适当的偏移量访问这个内存。
  5. 使对象COM可见

另外,记住,在.NET中,AccessViolationException通常是无法捕获的。


#4很有趣。我曾考虑为每个对象分配一块内存,但很快就拒绝了这个想法,因为没有办法为每个对象都有一个“固定”的语句。不过,如果它们都共享一个单一的内存块... - jbatez
通过分配一个块,我想到的是类似于Marshal.AllocCoTaskMemMarshal.AllocHGlobal的东西。 - theB
哇,我不知道这些方法存在。谢谢!我可以想到我的代码中还有几个地方可以用到它们。但是现在我想一想,即使它们不可用,我也可以很容易地自己实现AllocHGlobal和FreeHGlobal。 - jbatez
如果你非常有雄心壮志,你也可以使用任何WinAPI 内存管理函数,并使用p/Invoke。 - theB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接