如何在64位Windows上从C++返回字符串到C#时避免AccessViolationException?

10
我正在使用一个第三方专有DLL,但我无法获取其源代码。然而,使用SWIG 1.3.39生成的包装器代码对我来说是可用的。这个包装器代码由一个C++文件组成,它使用一些描述DLL的头文件编译为DLL,以及一个C#项目,该项目通过PInvoke调用C++包装器DLL。
根据供应商文档的解释,我将解决方案中的所有内容都编译为x86或x64,具体取决于目标平台。供应商提供专有DLL的32位和64位版本,我已确保在给定的构建中使用正确的版本。我的机器是32位的。在我的机器上测试x86版本的应用程序,无论是发布版还是调试版,似乎都可以正常工作。然而,在64位上,应用程序在调试模式下可以工作,但在发布模式下会出现System.AccessViolationException错误。
我阅读了这篇不错的博客文章,它似乎很好地描述了调试与发布的问题,以及这个问题和答案引起了博客文章。然而,我不确定如何在这种情况下解决问题。
AccessViolationException似乎在尝试返回任何实际长度的字符串(或尝试返回)时第一次发生在C++包装器中。以下是有问题的C#代码:
// In one file of the C# wrapper:
public string GetKey()
{
    // swigCPtr is a HandleRef to an object already created
    string ret = csWrapperPINVOKE.mdMUHybrid_GetKey(swigCPtr);
    return ret;
}

// In the csWrapperPINVOKE class in another file in the C# wrapper:
[DllImport("csWrapper.dll", EntryPoint="CSharp_mdMUHybrid_GetKey")]
public static extern StringBuilder mdMUHybrid_GetKey(HandleRef jarg1);

以下是来自 C++ 封装器的麻烦的 C++ 代码:

SWIGEXPORT char * SWIGSTDCALL CSharp_mdMUHybrid_GetKey(void * jarg1) {
  char * jresult ;
  mdMUHybrid *arg1 = (mdMUHybrid *) 0 ;
  char *result = 0 ;

  arg1 = (mdMUHybrid *)jarg1; 
  result = (char *)(arg1)->GetKey();
  jresult = SWIG_csharp_string_callback((const char *)result); 
  return jresult;
}

SWIGEXPORT已经被定义为__declspec(dllexport)。在调试中,我发现SWIG_csharp_string_callback被定义为:

/* Callback for returning strings to C# without leaking memory */
typedef char * (SWIGSTDCALL* SWIG_CSharpStringHelperCallback)(const char *);
static SWIG_CSharpStringHelperCallback SWIG_csharp_string_callback = NULL;

被设置为委托(在C#封装器中):

static string CreateString(string cString) {
  return cString;
}

我已经尝试过使用Marshal.PtrToStringAut等结构来修改此代码,但无济于事。我该如何排除和/或解决这个问题?

我遇到了完全相同的问题,这篇帖子确实解决了它。然而,我想知道你是如何找到问题代码的。因为当我在发布模式下运行我的项目时,我没有收到任何异常提示。只有当我浏览到发布文件夹并直接从那里执行exe文件时才会出现“访问冲突”的错误提示。 - Simsons
@Subhen 一段时间过去了,我已经换了雇主,所以我不再有访问这个代码的权限。如果我没记错的话,我是在Windows Server实例上运行它的,并且我能够以某种方式使用远程调试器。我可能记错了,但我知道我花了一两天的时间找到了这个问题出在哪里。当然,记录日志也可以帮助你,如果你能记录下你在代码中遇到异常时所处的位置。 - Andrew
1个回答

4
< p > 正确的方法是让托管代码分配非托管代码将写入(字符串数据)的缓冲区。假设由于某种原因这是不切实际的,您需要以一种可以从托管代码中释放的方式来分配字符串数据。

通常的方法是使用LocalAlloc分配内存,然后可以使用Marshal.FreeHGlobal从托管代码中释放。这样,您就不再需要(笨拙且明显无法正常工作的)SWIG_csharp_string_callbackCreateString了。

C++ 代码

SWIGEXPORT HLOCAL SWIGSTDCALL CSharp_mdMUHybrid_GetKey(mdMUHybrid* jarg1)
{
    char const* const str = jarg1->GetKey();
    std::size_t const len = std::strlen(str);
    HLOCAL const result = ::LocalAlloc(LPTR, len + 1u);
    if (result)
        std::strncpy(static_cast<char*>(result), str, len);
    return result;
}

C# 代码:

// In one file of the C# wrapper:
public string GetKey()
{
    return csWrapperPINVOKE.mdMUHybrid_GetKey(swigCPtr);
}

// ...

public static class csWrapperPINVOKE
{
    // ...

    [DllImport("csWrapper.dll")]
    private static extern IntPtr CSharp_mdMUHybrid_GetKey(HandleRef jarg1);

    public static string mdMUHybrid_GetKey(HandleRef jarg1)
    {
        var ptr = CSharp_mdMUHybrid_GetKey(jarg1);
        try
        {
            return Marshal.PtrToStringAnsi(ptr);
        }
        finally
        {
            Marshal.FreeHGlobal(ptr);
        }
    }
}

作为旁注,你展示的那个微小的C++代码片段是一个丑陋的带类C语言遗物;如果这代表了代码库的其余部分,那么只能说,哇... :-/

说实话,这段代码看起来确实是自动生成的(而且是供应商提供的,不是内部的,我们应该感激)。谢谢你的回答;如果可以的话,我明天会试一下。 - Andrew
@ildjarm 这个方法非常好用。谢谢!为了告诉Google的群众,我做了一些调整(例如使用strncpy_s来消除警告),并将大部分内容放入一个单独的函数中,以便我可以在所有返回char*的函数中使用它。在C#方面,我也做了同样的事情。在我没有空闲时间的情况下,我应该考虑为swig做出贡献,因为我从另一个项目中看到至少2.0.1版本产生了类似的代码。再次感谢! - Andrew
函数CSharp_mdMUHybrid_GetKey的效率确实不高,如果可能的话应该避免使用这个额外的层,否则与C++的Interop开销就太大了。我没有使用SWIG的经验,但是你能否通过P/Invoke在C#中使用SWIG调用jarg1->GetKey()呢? - xInterop

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