Search code examples
c++.net-4.0pinvokeswigaccess-violation

How do I prevent AccessViolationException when returning a string from C++ to C# on 64-bit Windows?


I am using a third-party, proprietary DLL for which the source code is not available to me. Wrapper code that appears to have been auto-generated using SWIG 1.3.39 is, however, available to me. The wrapper code consists of a C++ file that compiles (using some headers that describe the DLL) to a DLL and of a C# project that makes PInvoke calls to the C++ wrapper DLL.

Per my interpretation of the vendor's documentation, I have compiled everything in the solution as either x86 or x64, depending on the target platform. The vendor provides both 32-bit and 64-bit versions of the proprietary DLL, and I have ensured that I use the correct one for the given build. My machine is 32-bit. Testing the x86 version of my application on my machine, in either release or debug builds, seems to work fine. On 64-bit, however, the application works in Debug mode but fails with System.AccessViolationException in Release mode.

I have read this nice blog entry that seems to describe the Debug vs. Release problem well, as well as this question and answer that gave rise to the blog post. However, I am unsure how to troubleshoot the problem in this case.

The AccessViolationException seems to occur the first time a string of any real length is returned (or attempted to be returned) from the C++ wrapper. Here is the offending C# code:

// 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);

And the troublesome C++ code from the C++ wrapper:

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 had already been defined as __declspec(dllexport). In debugging, I discovered that SWIG_csharp_string_callback, defined as:

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

was being set to the delegate (in the C# wrapper):

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

I have tried messing with this code to use constructs such as Marshal.PtrToStringAut to no avail. How do I troubleshoot and/or fix this problem?


Solution

  • The proper way to do this is to have your managed code allocate the buffer to which the unmanaged code will be writing (the string data). Assuming that's impractical for some reason, what you need to do is allocate the string data in a way that can be deallocated from managed code.

    The usual approach is to allocate the memory with LocalAlloc, which can then be deallocated from managed code using Marshal.FreeHGlobal. This way you no longer need the (kludgy and obviously non-functional) SWIG_csharp_string_callback and CreateString.

    C++ code:

    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# code:

    // 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);
            }
        }
    }
    

    As an aside, that tiny snippet of C++ code you showed is a hideous C-with-classes relic; if that's representative of the rest of the codebase, then just, wow... :-/