Search code examples
c#c++marshalling

type conversion problem for marshaling datatypes from C# to C++


I'm currently working on an C# (.NET Framework 4.7.2) application using some business logic from an unmanaged C++ library. I try to pass data (interop) back and forth from C# to C++. I may not use C++/CLI, no common language runtime allowed in my project.

It works fine for int. Unfortunately as soon as I try to send another datatype I'm getting an conversion error e.g. float 4.2f becomes 1 and string "fourtytwo" turns into -1529101360.

My C# code looks like this:

// works fine, creates an instance of TestClass    
var test = TestProxy.Wrapper_Create("test"); 

// int, works fine, a = 42
var a = TestProxy.TryInt(test, 42);

// float, problem, b = 1
var b = TestProxy.TryFloat(test, 4.2f);

// string, problem, c = -159101360
var c = TestProxy.TryString(test, "fourtytwo");

My C# Interop Proxy class to call the native (unmanaged) C++ code looks like this:

public static class TestProxy
    {
        private const string coreDLL = "test.core.dll";

        [DllImport(coreDLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr Wrapper_Create(string name);

        [DllImport(coreDLL, EntryPoint = "?TryInt@TestClass@@XXX@X", CallingConvention = CallingConvention.ThisCall)]
        public static extern int TryInt(IntPtr instance, int n);

        [DllImport(coreDLL, EntryPoint = "?TryFloat@TestClass@@XXX@X", CallingConvention = CallingConvention.ThisCall)]
        public static extern int TryFloat(IntPtr instance, float n);

        [DllImport(coreDLL, EntryPoint = "?TryString@TestClass@@XXX@X", CallingConvention = CallingConvention.ThisCall)]
        public static extern int TryString(IntPtr instance, string n);

    }

My native (unmanaged) C++ looks like that: the header file:

#ifdef TESTCORE_EXPORTS
#define TESTCORE_API __declspec(dllexport)
#endif

#pragma once

extern "C"
{
    class TESTCORE_API TestClass
    {
    private:
        char* name;
    public:
        TestClass(char*);
        int TryInt(int);
        float TryFloat(float);
        char* TryString(char*);
    };

    TESTCORE_API TestClass* Wrapper_Create(char* name);
}

the implementation file:

#include "stdafx.h"

#include "TESTCore.h"

TestClass::TestClass(char* n) 
{
    name = n;
}

int TestClass::TryInt(int n) 
{
    return n; // works fine
}

float TestClass::TryFloat(float n)
{
    return n; // something goes wrong here
}

char* TestClass::TryString(char* n)
{
    return n; // something goes wrong here
}

extern "C"
{
    TESTCORE_API TestClass * Wrapper_Create(char* name)
    {
        return new TestClass(name);
    }

    TESTCORE_API int TryInt(TestClass * instance, int n) 
    {
        if (instance != NULL) 
        {
            return instance->TryInt(n);
        }
    }

    TESTCORE_API float TryFloat(TestClass * instance, float n) 
    {
        if (instance != NULL) 
        {
            return instance->TryFloat(n);
        }
    }

    TESTCORE_API char* TryString(TestClass * instance, char* n) 
    {
        if (instance != NULL)
        {
            return instance->TryString(n); 
        }
    }
}

Do you know how to correctly marshal float, string from C# to C++ and back?

Thank you!


Solution

  • C++ doesn't have standard ABI. It's rarely a good idea to use C++ classes across DLLs, even when you have same language on both sides.

    There're better ways.

    1. Replace your __thiscall class methods with global functions, cdecl or stdcall whichever you like (but note C# and C++ have different defaults, if you'll do nothing C++ will use cdecl, C# will import as stdcall). You can pass "this" pointer of the class in the first argument, IntPtr in C#, just like you're doing now. Also if you'll write extern "C" or use a module definition file, they will have human-readable names.

    2. If you want objects, use COM. Declare an interface that inherits from IUnknown, implement it in C++ (I usually use ATL), and export a global function to create an instance of that object (2 lines in ATL, CComObject<T>::CreateInstance followed by AddRef). No need to register, type libraries, you just need to implement IUnknown (but see this if you want to use them from multiple threads)

    Update: strings are indeed harder. Apply [MarshalAs(UnmanagedType.LPTStr)] to the argument. Apply [return: MarshalAs(UnmanagedType.LPTStr)] to the function. Specify PreserveSig=true in your DllImport. Finally, modify the C++ code to return a copy of the string, i.e. call strlen then CoTaskMemAlloc (don't forget about the '\0') then strcpy.

    Easier way to deal with strings is like this:

    HRESULT TryString( TestClass *instance, BSTR i, BSTR *o )
    

    At least there're CComBSTR and _bstr_t built-in classes to deal with memory management.