DllImport 返回空终止字符串 AccessViolationException

Posted

技术标签:

【中文标题】DllImport 返回空终止字符串 AccessViolationException【英文标题】:DllImport Returning null terminated string AccessViolationException 【发布时间】:2011-10-06 05:27:04 【问题描述】:

我正在尝试与实现多个函数的 Dll 进行交互,其中一个函数接受一个以空字符结尾的字符串和一个 int,并返回一个以空字符结尾的字符串。我试图与这样的方法进行交互:

[DllImport(dll_loc)]
[return : MarshalAs(UnmanagedType.LPStr)]
public static extern StringBuilder GetErrorMessage([MarshalAs(UnmanagedType.LPStr)]
                                                       StringBuilder message, 
                                                       int error_code);

然后我尝试像这样调用该方法:

StringBuilder message = new StringBuilder(1000);
StringBuilder out2 = new StringBuilder(1000);
out2 = GetErrorMessage(message, res0);

但是,当我尝试这样做时,会抛出一个 AccessViolationException,告诉我我正在尝试访问受保护的内存。

我成功地声明了一种不同的方法:

[DllImport(dll_loc)]
public static extern int GetVersion([MarshalAs(UnmanagedType.LPStr)]
                                        StringBuilder version);

并以相同的方式调用它,但此方法不适用于当前的函数调用。

我也尝试返回一个 IntPtr,因为从技术上讲,该方法返回一个指向字符串缓冲区第一个字符的指针,但无济于事。

有没有人知道这里可能出了什么问题?这两种方法之间可能有什么不同导致 dll 尝试访问它不应该访问的内存。或者,您建议如何调试此问题?

【问题讨论】:

在调用某些 Windows DLL 时遇到类似问题。我发现参数中的一些细微调整导致了巨大的改进。即使是稍微错误的编组/数据类型/等。产生巨大的差异。您必须仔细查看非托管 DLL 接口的文档。 【参考方案1】:

将所有StringBuilder 替换为System.IntPtr仅分配 message var 为System.Runtime.InteropServices.Marshal.AllocHGlobal

然后将messageout2 转换为带有PtrToStringAuto 的字符串

然后拨打System.Runtime.InteropServices.Marshal.FreeHGlobal

在与char*wchar_t* 打交道时,我总是以这种方式工作。

【讨论】:

我不确定该怎么做。我尝试将函数定义为public static extern IntPtr GetErrorMessage(IntPtr message, int error_code);,然后调用:IntPtr message = new IntPtr(); Marshal.AllocHGlobal(1000); IntPtr out2 = GetErrorMessage(message, res0);,但抛出了相同的异常。我做错了吗? IntPtr message=Marshal.AllocHGlobal(1000);【参考方案2】:

返回字符串的 C 函数是内存管理问题。 C 字符串需要一个数组,并且该数组的内存需要在字符串被消耗后释放。这使得这些函数很难在 C 程序中使用,几乎不可能与 pinvoke 一起使用。这也是一个经典的C bug,返回一个指向栈上字符串的指针。

pinvoke marshaller 将尝试释放返回的字符串,以避免内存泄漏。它将使用 CoTaskMemFree()。这通常不会有好的结果,C 代码实际上很少使用 CoTaskMemAlloc 为数组分配内存。在 XP 上,这将导致无声的内存泄漏。 Vista 和 Win7 有更严格的堆管理器,如果附加了非托管调试器,它们将调用调试中断。然后用 AccessViolation 轰炸程序。

您可以通过将返回类型声明为 IntPtr 并自己编组字符串来避免自动编组行为。通常使用 Marshal.PtrToStringAnsi()。但是你仍然面临着释放内存的任务。你不能,你没有 CRT 堆的句柄,你不能调用 free()。

返回字符串的 C 函数应使用传递字符串缓冲区的参数声明。还有一个参数说明缓冲区有多大。像这样:

 int GetErrorMessage(int errorCode, char* buffer, size_t bufferSize);

现在很简单,调用者可以为缓冲区分配内存并释放它。 C 函数只是将字符串复制到缓冲区中。返回值可用于告知复制了多少字符,或指示需要更大的缓冲区。不要吝啬bufferSize 参数,缓冲区溢出是致命的,并会调用可怕的 FatalExecutionEngineError 异常,该异常与 AV 一样不可调试,因为直到很久以后才检测到 GC 堆损坏。您在 C# 端使用 StringBuilder,并使用非零容量适当地初始化。 bufferSize 的值。

【讨论】:

不幸的是,我无法访问定义我通过 dll 与之交互的函数调用的 C 代码。如果我从托管代码调用函数时忽略返回值,而只是传递一个字符串缓冲区,是否还会存在内存泄漏问题?或者这会解决问题。如果存在内存泄漏,我可以在返回的 IntPtr 上调用 Marshal.FreeHGlobal 以释放该内存吗? 再看一下声明,这个函数可能只是返回调用它的指针。在这种情况下,您没有问题,请在 [DllImport] 声明中声明它返回 void,以便 pinvoke 编组器不再尝试做正确的事情。使用 message.ToString() 恢复返回的字符串。如果它实际上返回了一个不同的字符串指针,那么你就卡住了,你不能调用它。

以上是关于DllImport 返回空终止字符串 AccessViolationException的主要内容,如果未能解决你的问题,请参考以下文章

返回带有零而不是空值的 Access 记录集

为啥在 Access 中返回 835 条记录的查询会在 VBA 代码中返回一个空记录集?

Microsoft Access 2016 查询为链接表中的空日期返回 1/1/1800

字符串不是空终止错误

为啥需要空终止符?

调用从 c# 返回 char* 的 c++ dll 函数。无法使用 DllImport()