0

Disclaimer: C++/CLI Noob question

I'm attempting to use a PInvoke on a C++ DLL that has a std::string in the signature. At the moment I just testing: my goal is to pass a string to the native DLL, and return it.

The native export looks like this:

#define NATIVE_CPP_API __declspec(dllexport)

NATIVE_CPP_API void hello_std(std::string inp, char* buffer)
{
    const char* data = inp.data();
    strcpy(buffer, data);
}

I'm attempting to PInvoke it in the normal way, with a custom marshaler:

[DllImport("native_cpp.dll", EntryPoint = "?hello_std@@YAPADV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z", CallingConvention = CallingConvention.Cdecl)]
private static extern void hello_std(
    [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(clr_wrapper.string_marshaler))]
        String inp,
        StringBuilder buffer);

static void Main(string[] args)
{
    var buffer = new StringBuilder(100);
    hello_std("abcdefg", buffer);
    Console.WriteLine(buffer);
    Console.ReadLine();
}

The custom marshaler specified here, clr_wrapper.string_marshaler, is an ICustomMarshaler in a C++/CLI project, and is intended to take the System::String input and convert it to the native std::string. My MarshalManagedToNative implementation is a stab in the dark. I've tried a few things, but this is my best guess:

IntPtr string_marshaler::MarshalManagedToNative( Object^ ManagedObj )
{
    String^ val = (String^) ManagedObj;
    size_t size = (size_t)val->Length;
    char* ptr = (char*) Marshal::StringToHGlobalAnsi(val->ToString()).ToPointer();

    std::string * str = new std::string(ptr, size);
    IntPtr retval = (IntPtr) str;
    return retval;
}

Unfortunately, when I attempt to run this, PInvoke call triggers an AccessViolationException.

What am I doing wrong, or is this entire venture ill-conceived?


First Edit, Complete Listing

1. C# Console App

class Program
{
    static void Main(string[] args)
    {
        var buffer = new StringBuilder(100);
        hello_std("abcdefg", buffer);
        Console.WriteLine(buffer);
        Console.ReadLine();
    }

    [DllImport("native_cpp.dll", EntryPoint = "?hello_std@@YAXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PAD@Z", CallingConvention = CallingConvention.Cdecl)]
    private static extern void hello_std(
        [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(clr_wrapper.string_marshaler))]
        [In]
        String inp,
        StringBuilder buffer
    );
}

2. Native C++ DLL project "native_cpp"

native_cpp.h

#ifdef NATIVE_CPP_EXPORTS
#define NATIVE_CPP_API __declspec(dllexport)
#else
#define NATIVE_CPP_API __declspec(dllimport)
#endif

#include <string>

NATIVE_CPP_API void hello_std(std::string inp, char* buffer);

native_cpp.cpp

#include "native_cpp.h"

void hello_std(std::string inp, char* buffer)
{
    const char* data = inp.data();
    strcpy(buffer, data);
}

3. C++/CLI project "clr_wrapper"

clr_wrapper.h

#pragma once

using namespace System;
using namespace System::Runtime::InteropServices;

namespace clr_wrapper {

    public ref class string_marshaler : public ICustomMarshaler
    {
    public:
        string_marshaler(void);

        virtual Object^ MarshalNativeToManaged( IntPtr pNativeData );
        virtual IntPtr MarshalManagedToNative( Object^ ManagedObj );
        virtual void CleanUpNativeData( IntPtr pNativeData );
        virtual void CleanUpManagedData( Object^ ManagedObj );
        virtual int GetNativeDataSize();

        static ICustomMarshaler ^ GetInstance(String ^ pstrCookie)
        {
            return gcnew string_marshaler();
        }

    private:
        void* m_ptr;
        int m_size;
    };
}

clr_wrapper.cpp

#include "clr_wrapper.h"

#include <string>

using namespace clr_wrapper;
using namespace System::Text;

string_marshaler::string_marshaler(void)
{
}


Object^ string_marshaler::MarshalNativeToManaged( IntPtr pNativeData )
{
    return Marshal::PtrToStringAnsi(pNativeData);
}

IntPtr string_marshaler::MarshalManagedToNative( Object^ ManagedObj )
{
    String^ val = (String^) ManagedObj;
    size_t size = (size_t) val->Length;

    char* ptr = (char*) Marshal::StringToHGlobalAnsi(val->ToString()).ToPointer();

    std::string * str = new std::string(ptr, size);
m_size = sizeof(str*);

    m_ptr = (void*) str;

    IntPtr retval = (IntPtr) str;

    return retval;
}

void string_marshaler::CleanUpNativeData( IntPtr pNativeData )
{
    //Marshal::FreeHGlobal(pNativeData);
    delete (std::string*) m_ptr;
}

void string_marshaler::CleanUpManagedData( Object^ ManagedObj )
{
}

int string_marshaler::GetNativeDataSize()
{
    return m_size;
}

End First Edit

McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • Not really a C# guy but it looks like you're passing a pointer to a string to a function that takes a `std::string` by value. Plus `std:string` in C# may not have the same layout as `std::string` in C++. – Captain Obvlious Jun 14 '13 at 01:10
  • It is a valiant effort and nothing yells wrong. But interop with std::string and friends is notoriously difficult, it can only work if your C++/CLI assembly and your C++ DLL use the exact same shared CRT version. Not enough code here to try it ourselves. – Hans Passant Jun 14 '13 at 01:53
  • Can you alter the C++ code or was it given to you? In light of @Hans Passant's comment, it might be easier to use a character array? – chessofnerd Jun 14 '13 at 02:24
  • 4
    Step 1: shoot the author of the DLL – David Heffernan Jun 14 '13 at 10:54
  • @HansPassant thanks - I've updated my question with the complete code listing. – McGarnagle Jun 14 '13 at 17:10
  • @chessofnerd I can't modify the DLL, although by all accounts that would be best. Basically, I'm trying to make a good-faith effort to "make it work", so that our team has some substance to go back at the authors of the DLL. – McGarnagle Jun 14 '13 at 17:12
  • Never never never pass standard library components across a DLL boundary. – Ben Voigt Jun 15 '13 at 16:49

1 Answers1

1

If you can build the C++/CLI dll with the exact same compiler version, packing, class member alignment, calling convention, CRT linkage, library options like _ITERATOR_DEBUG_LEVEL, debug/release configuration etc with the native DLL, then you can pass an STL class over the DLL boundary. and a wrapper function like this may work:

public ref class Wrapper
{
    void hello_std_managed(String^ inp, array<Byte>^ buffer)
    {
        IntPtr inpCopy = Marshal::StringToHGlobalAnsi(inp);
        std::string inpNative(static_cast<const char*>(inpCopy.ToPointer()));
        pin_ptr<BYTE> nativeBuffer = &buffer[0];
        hello_std(inpNative,nativeBuffer);
        Marshal::FreeHGlobal(inpCopy);
    }
}

However since this is a big IF, you may want to ask the DLL's author to change the method's signature to primitive C/COM types like char* or BSTR. It is actually better this way, the DLL is now consumable regardless of language or build configuration.

Community
  • 1
  • 1
Sheng Jiang 蒋晟
  • 15,125
  • 2
  • 28
  • 46
  • Thanks for your answer, and for the useful link. This approach requires building against either the native library's header file, or against its .lib static library, does it not? For my task, this will be my fallback, and it is a fine solution. But my preference is to use the DLL directly via a PInvoke, and my question is really, is it possible to do this. – McGarnagle Jun 14 '13 at 17:18
  • possible via explicit pinvoke if you wrap up this code in 2 custom marshallers (one class per parameter, first one takes care of the round trip of the string and second one takes care of roundtrip of the byte array), but why bother doing all this when the compiler is designed to performs implicit pinvoke for you? – Sheng Jiang 蒋晟 Jun 14 '13 at 17:32
  • To your question -- it's an issue of build complexity + build time. Using explicit Pinvoke from my C# project, all I have to do then is drop the DLL into the project output. Using implicit Pinvoke, I have to add another wrapper C++/CLI project that builds against the native C++ source, with all its dependencies. – McGarnagle Jun 14 '13 at 17:58
  • 1
    The original native dll is not usable via pinvoke, you need a C++/CLI bridge anyways. – Sheng Jiang 蒋晟 Jun 14 '13 at 18:11