2

我正在尝试使用非托管 C dll 将图像数据加载到 C# 应用程序中。该库有一个相当简单的接口,您可以在其中传入一个包含三个回调的结构,一个接收图像的大小,一个接收每一行像素,最后一个在加载完成时调用。像这样(C# 托管定义):

[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct st_ImageProtocol
{
   public st_ImageProtocol_done Done;    
   public st_ImageProtocol_setSize SetSize;    
   public st_ImageProtocol_sendLine SendLine;
}

以 st_ImageProtocol 开头的类型是委托:

public delegate int st_ImageProtocol_sendLine(System.IntPtr localData, int rowNumber, System.IntPtr pixelData);

对于我正在使用的测试文件,SetSize 应该被调用一次,然后 SendLine 将被调用 200 次(图像中的每一行像素一次),最后触发了 Done 回调。实际发生的是 SendLine 被调用 19 次,然后抛出 AccessViolationException 声称库试图访问受保护的内存。

我可以访问 C 库的代码(尽管我无法更改功能),并且在它调用 SendLine 方法的循环期间它不会分配或释放任何新内存,所以我的假设是委托本身是问题,我需要在传递它之前将其固定(我目前在委托本身内没有代码,除了一个计数器来查看它被调用的频率,所以我怀疑我在托管方面破坏了任何东西)。问题是我不知道该怎么做;我一直用来在非托管空间中声明结构的方法不适用于委托 (Marshal.AllocHGlobal()),而且我找不到任何其他合适的方法。代表本身是 Program 类中的静态字段,因此它们不应该被垃圾收集,但我猜运行时可能会移动它们。

Chris Brumme 的这篇博客文章说,委托在传递到非托管代码之前不需要固定:

显然,非托管函数指针必须指向一个固定地址。如果 GC 重新定位它,那将是一场灾难!这导致许多应用程序为委托创建固定句柄。这是完全没有必要的。非托管函数指针实际上是指我们动态生成以执行转换和封送处理的本机代码存根。此存根存在于 GC 堆外的固定内存中。

但是我不知道当委托是结构的一部分时这是否成立。这确实意味着可以手动固定它们,我对如何执行此操作或关于为什么循环运行 19 次然后突然失败的任何更好建议感兴趣。

谢谢。


编辑回答约翰的问题......

分配结构体的代码如下:

_sendLineFunc = new st_ImageProtocol_sendLine(protocolSendLineStub);

_imageProtocol = new st_ImageProtocol()
                     {
                          //Set some other properties...
                          SendLine = _sendLineFunc
                     };

int protocolSize = Marshal.SizeOf(_imageProtocol);
_imageProtocolPtr = Marshal.AllocHGlobal(protocolSize);
Marshal.StructureToPtr(_imageProtocol, _imageProtocolPtr, true);

其中 _sendLineFunc 和 _imageProtocol 变量都是 Program 类的静态字段。如果我正确理解了它的内部结构,这意味着我将一个指向_imageProtocol 变量副本的非托管指针传递到 C 库中,但该副本包含对静态 _sendLineFunc 的引用。这应该意味着 GC 不会触及副本 - 因为它是非托管的 - 并且委托不会被收集,因为它仍在范围内(静态)。

该结构实际​​上作为另一个回调的返回值传递给库,但作为指针:

private static IntPtr beginCallback(IntPtr localData, en_ImageType imageType)
{
    return _imageProtocolPtr;
}

基本上还有另一种结构类型,它保存图像文件名和指向此回调的函数指针,库确定文件中存储的图像类型,并使用此回调为给定类型请求正确的协议结构。我的文件名结构的声明和管理方式与上面的协议相同,因此可能包含相同的错误,但由于此委托只被调用一次并且调用速度很快,所以我还没有遇到任何问题。


编辑更新

感谢大家的回复,但在又花了几天时间解决这个问题但没有任何进展后,我决定搁置它。万一有人感兴趣,我正在尝试为 Lightwave 3D 渲染应用程序的用户编写一个工具,一个不错的功能是能够查看 Lightwave 支持的所有不同图像格式(其中一些是相当奇特的)。我认为最好的方法是为 Lightwave 用于图像处理的插件架构编写一个 C# 包装器,这样我就可以使用他们的代码来实际加载文件。不幸的是,在针对我的解决方案尝试了一些插件之后,我遇到了各种我无法理解或修复的错误,我的猜测是 Lightwave 没有以标准方式调用插件上的方法,可能是为了提高运行外部代码的安全性(我承认在黑暗中野蛮刺伤)。暂时我将放弃图像功能,如果我决定恢复它,我会以不同的方式处理它。

再次感谢,即使我没有得到我想要的结果,我也通过这个过程学到了很多东西。

4

4 回答 4

2

注册回调委托时我遇到了类似的问题(它会被调用,然后噗!)。我的问题是具有被委派方法的对象正在被 GC 处理。我在一个更全球化的地方创建了这个对象,以防止它被 GC'ed。

如果这样的事情不起作用,这里有一些其他的事情要注意:

作为附加信息,请查看 Marshal 类中的GetFunctionPointerForDelegate。这是你可以做到这一点的另一种方式。只要确保代表没有被 GC 处理。然后,代替结构中的委托,将它们声明为 IntPtr。

这可能无法解决固定问题,但请查看fixed关键字,即使这可能对您不起作用,因为您正在处理比通常使用的更长的生命周期。

最后,看看stackalloc创建非GC内存。这些方法将需要使用unsafe,因此可能会对您的程序集施加一些其他限制。

于 2009-04-17T20:55:54.687 回答
0

您没有确切说明回调是如何在 C 库中声明的。除非明确声明,否则__stdcall您将慢慢破坏您的堆栈。你会看到你的方法被调用(可能参数颠倒了),但在将来的某个时候程序会崩溃。

据我所知,除了在 C# 代码和需要回调的库之间用 C 语言编写另一个回调函数之外,没有其他办法__cdecl

于 2009-04-17T20:26:49.340 回答
0

多了解一点会很有趣:

  • 如何创建 ImageProtocol 结构?它是局部变量还是类成员,还是使用 Marshal.AllocHGlobal 将其分配在非托管内存中?

  • 它是如何发送到 C 函数的?直接作为堆栈变量还是作为指针?


一个非常棘手的问题!感觉就像 GC 移动了委托数据,这导致了访问冲突。有趣的是,委托数据类型是一个引用数据类型,它将其数据存储在 GC 堆上。该数据包含诸如要调用的函数的地址(函数指针)之类的内容,还包含对包含该函数的对象的引用。这应该意味着即使实际的函数代码存储在 GC 堆之外,保存函数指针的数据也存储在 GC 堆中,因此可以被 GC 移动。昨晚我想了很多,但没有想出解决方案....

于 2009-04-06T11:51:21.713 回答
0

如果 c 函数是 __cdecl 函数,则必须在委托声明之前使用属性 [UnmanagedFunctionPointer(CallingConvention.Cdecl)]。

于 2014-05-08T17:56:10.970 回答