Search code examples
c#c++clr

How to write a C++ wrapper of managed C# dll having ref string as its parameter


I am writing wrappers using C++/CLR. The managed C# class has a function signature as below

//C#
int WriteToInstrument(string command, ref string response, int stage);

I have to write a C++ wrapper to this function in something like the following signature

//C++
int WriteToInstrumentWrap(const char * command, char * response, int stage);

My question is: how can I handle the conversion from "ref string" in C# to char* in C++? Or how can I handle the situation that requires to take a ref string from C# that can be used in C/C++? Many thanks in advance.


Solution

  • I'll add some examples of code I've written this morning. In general, when speaking of returning objects (in the broad meaning where even a char* string is an object), the big questions in C/C++ are:

    • Who allocates the memory
    • How many elements are needed
    • How is the memory allocated (which allocator is used)
    • And as a corollary, how the memory must be freed
    • One last optional question is if the memory must be really freed: a method could return a pointer to an internal object that has a lifetime equal to the lifetime of the program and that mustn't be freed. For example:

      const char* Message()
      {
          return "OK";
      }
      

      You mustn't free the memory returned by Message()!

    This questions get even more complex when you are writing a library (a dll) that will be used by other programs: the malloc and the new that are used in a dll can be different/distinct from the malloc and the new used by the main program (or by another dll), so that you shouldn't free with your (main program) free the memory that is malloc(ed) by a dll.

    There are three possible solutions to this particular problem:

    • Use a shared allocator, for example one given by the OS. Windows gives LocalAlloc and CoTaskMemAlloc. They are even accessible from .NET (Marshal.AllocHGlobal and Marshal.AllocCoTaskMem). In this way the main application can free the memory allocated by the dll
    • The API of your dll has a Free() method that must be used to free the memory allocated by the dll
    • The API of your dll has some methods like SetAllocator(void *(*allocator)(size_t)) and SetFree(void (*free)(void*)), so methods that store a function pointer, that the main application can use to set the allocator and free to be used by the dll, so that they are shared between the main application and the dll. The dll will use those allocators. Note that SetAllocator(malloc); SetFree(free) if done by the main application is perfectly legal: now the dll will use the main application's malloc, and not the dll's malloc!
    • Shortcut used in some example I'll give: the method has as a parameter the allocator (a function pointer) that will then be used

    As an important sidenote: we are in 2018. It is at least 15 years that you should have forgotten of char* for strings in C for Windows. Use wchar_t. Always.

    And finally some code :-)

    Now... given (C# code):

    int WriteToInstrument(string command, ref string response, int stage)
    {
        response = "The quick brown fox jumps over the lazy dog";
        return 0;
    }
    

    Simple method that calls WriteToInstrument and then copies the response result to an ansi string (char*). The buffer is allocated by the caller, and is of size length. After the method is executed, length contains the number of characters used (including the terminating \0). The response is always \0 terminated. The problem here is that the response could get truncated and/or the caller could allocate a buffer too much big (that won't really protect it from the truncation problem, if it is unlucky :-) ). I'll repeat myself here: using char* for strings in 2018 is ancient technology.

    // Utility method to copy a unicode string to a fixed size buffer
    size_t Utf16ToAnsi(const wchar_t *wstr, char *str, size_t length)
    {
        if (length == 0)
        {
            return 0;
        }
    
        // This whole piece of code can be moved to a method
        size_t length2 = WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, (int)length, nullptr, nullptr);
    
        // WideCharToMultiByte will try to write up to *length characters, but
        // if the buffer is too much small, it will return 0, 
        // **and the tring won't be 0-terminated**
    
        if (length2 != 0)
        {
            return length2;
        }
    
        // Buffer too much small
        if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
        {
            // We add a terminating 0
            str[length - 1] = 0;
            return length;
        }
    
        // Big bad error, shouldn't happen. Return 0 but terminate the string
        str[0] = 0;
        return 0;
    }
    

    Example of use:

    char response[16];
    size_t length = sizeof(response) / sizeof(char); // useless / sizeof(char) == / 1 by definition
    WriteToInstrumentWrap1("cmd1", response, &length, 1);
    std::cout << "fixed buffer char[]: " << response << ", used length: " << length << std::endl;
    

    or (using std::vector<>/std::array<>)

    //Alternative: std::array<char, 16> response;
    std::vector<char> response(16);
    size_t length = response.size();
    WriteToInstrumentWrap1("cmd1", response.data(), &length, 1);
    std::cout << "fixed buffer vector<char>: " << response.data() << ", used length: " << length << std::endl;
    

    Simple method that calls WriteToInstrument and then copies the response result to an unicode string (wchar_t*). The buffer is allocated by the caller, and is of size length. After the method is executed, length contains the number of characters used (including the terminating \0). The response is always \0 terminated.

    // in input length is the size of response, in output the number of characters (not bytes!) written to response
    // (INCLUDING THE \0!). The string is always correctly terminated.
    int WriteToInstrumentWrap2(const wchar_t *command, wchar_t *response, size_t *length, int stage)
    {
        auto str1 = gcnew String(command);
        String ^str2 = nullptr;
        int res = WriteToInstrument(str1, str2, 5);
    
        pin_ptr<const Char> ppchar = PtrToStringChars(str2);
        const wchar_t *pch = const_cast<wchar_t*>(ppchar);
    
        *length = (size_t)str2->Length < *length ? str2->Length : *length - 1;
        memcpy(response, pch, *length * sizeof(wchar_t));
        response[*length] = '\0';
        *length++;
    
        return res;
    }
    

    Example of use:

    wchar_t response[16];
    size_t length = sizeof(response) / sizeof(wchar_t);
    WriteToInstrumentWrap2(L"cmd1", response, &length, 1);
    std::wcout << L"fixed buffer wchar_t[]: " << response << L", used length: " << length << std::endl;
    

    or (using std::vector<>/std::array<char, 16>)

    //Alternative: std::array<wchar_t, 16> response;
    std::vector<wchar_t> response(16);
    size_t length = response.size();
    WriteToInstrumentWrap2(L"cmd1", response.data(), &length, 1);
    std::wcout << L"fixed buffer vector<wchar_t>: " << response.data() << ", used length: " << length << std::endl;
    

    All the next examples will use char instead of wchar_t. It is quite easy to convert them. I'll repeat myself here: using char* for strings in 2018 is ancient technology. It is like using ArrayList instead of List<>


    Simple method that calls WriteToInstrument, allocates the response buffer using CoTaskMemAlloc and copies the result to an ansi string (char*). The caller must CoTaskMemFree the allocated memory. The response is always \0 terminated.

    // Memory allocated with CoTaskMemAlloc. Remember to CoTaskMemFree!
    int WriteToInstrumentWrap3(const char *command, char **response, int stage)
    {
        auto str1 = gcnew String(command);
        String ^str2 = nullptr;
        int res = WriteToInstrument(str1, str2, 5);
    
        pin_ptr<const Char> ppchar = PtrToStringChars(str2);
        const wchar_t *pch = const_cast<wchar_t*>(ppchar);
    
        // length includes the terminating \0
        size_t length = WideCharToMultiByte(CP_ACP, 0, pch, -1, nullptr, 0, nullptr, nullptr);
        *response = (char*)CoTaskMemAlloc(length * sizeof(char));
        WideCharToMultiByte(CP_ACP, 0, pch, -1, *response, length, nullptr, nullptr);
    
        return res;
    }
    

    Example of use:

    char *response;
    WriteToInstrumentWrap3("cmd1", &response, 1);
    std::cout << "CoTaskMemFree char: " << response << ", used length: " << strlen(response) + 1 << std::endl;
    // Must free with CoTaskMemFree!
    CoTaskMemFree(response);
    

    Simple method that calls WriteToInstrument, allocates the response buffer using a "private" "library" allocator and copies the result to an ansi string (char*). The caller must use the library deallocator MyLibraryFree to free the allocated memory. The response is always \0 terminated.

    // Free method used by users of the library
    void MyLibraryFree(void *p)
    {
        free(p);
    }
    
    // The memory is allocated through a proprietary allocator of the library. Use MyLibraryFree() to free it.
    int WriteToInstrumentWrap4(const char *command, char **response, int stage)
    {
        auto str1 = gcnew String(command);
        String ^str2 = nullptr;
        int res = WriteToInstrument(str1, str2, 5);
    
        pin_ptr<const Char> ppchar = PtrToStringChars(str2);
        const wchar_t *pch = const_cast<wchar_t*>(ppchar);
    
        // length includes the terminating \0
        size_t length = WideCharToMultiByte(CP_ACP, 0, pch, -1, nullptr, 0, nullptr, nullptr);
        *response = (char*)malloc(length);
        WideCharToMultiByte(CP_ACP, 0, pch, -1, *response, length, nullptr, nullptr);
    
        return res;
    }
    

    Example of use:

    char *response;
    WriteToInstrumentWrap4("cmd1", &response, 1);
    std::cout << "Simple MyLibraryFree char: " << response << ", used length: " << strlen(response) + 1 << std::endl;
    // Must free with the MyLibraryFree() method
    MyLibraryFree(response);
    

    Simple method that calls WriteToInstrument, allocates the response buffer using a settable (through the SetLibraryAllocator/SetLibraryFree methods) allocator (there is a default that is used if no special allocator is selected) and copies the result to an ansi string (char*). The caller must use the library deallocator LibraryFree (that uses the allocator selected by SetLibraryFree) to free the allocated memory or if it has setted a different allocator, it can directly use that deallocator. The response is always \0 terminated.

    void *(*libraryAllocator)(size_t length) = malloc;
    void (*libraryFree)(void *p) = free;
    
    // Free method used by library
    void SetLibraryAllocator(void *(*allocator)(size_t length))
    {
        libraryAllocator = allocator;
    }
    
    // Free method used by library
    void SetLibraryFree(void (*free)(void *p))
    {
        libraryFree = free;
    }
    
    // Free method used by library
    void LibraryFree(void *p)
    {
        libraryFree(p);
    }
    
    // The memory is allocated through the allocator specified by SetLibraryAllocator (default the malloc of the dll)
    // You can use LibraryFree to free it, or change the SetLibraryAllocator and the SetLibraryFree with an allocator
    // of your choosing and then use your free.
    int WriteToInstrumentWrap5(const char *command, char **response, int stage)
    {
        auto str1 = gcnew String(command);
        String ^str2 = nullptr;
        int res = WriteToInstrument(str1, str2, 5);
    
        pin_ptr<const Char> ppchar = PtrToStringChars(str2);
        const wchar_t *pch = const_cast<wchar_t*>(ppchar);
    
        // length includes the terminating \0
        size_t length = WideCharToMultiByte(CP_ACP, 0, pch, -1, nullptr, 0, nullptr, nullptr);
        *response = (char*)libraryAllocator(length);
        WideCharToMultiByte(CP_ACP, 0, pch, -1, *response, length, nullptr, nullptr);
    
        return res;
    }
    

    Example of use:

    void* MyLocalAlloc(size_t size)
    {
        return LocalAlloc(0, size);
    }
    
    void MyLocalFree(void *p)
    {
        LocalFree(p);
    }
    

    and then:

    // Using the main program malloc/free
    SetLibraryAllocator(malloc);
    SetLibraryFree(free);
    char *response;
    WriteToInstrumentWrap5("cmd1", &response, 1);
    std::cout << "SetLibraryAllocator(malloc) char: " << response << ", used length: " << strlen(response) + 1 << std::endl;
    // Here I'm using the main program free, because the allocator has been set to malloc
    free(response);
    

    or

    // Using the Windows LocalAlloc/LocalFree. Note that we need to use an intermediate method to call them because
    // they have a different signature (stdcall instead of cdecl and an additional parameter for LocalAlloc)
    SetLibraryAllocator(MyLocalAlloc);
    SetLibraryFree(MyLocalFree);
    char *response;
    WriteToInstrumentWrap5("cmd1", &response, 1);
    std::cout << "SetLibraryAllocator(LocalAlloc) char: " << response << ", used length: " << strlen(response) + 1 << std::endl;
    // Here I'm using diretly the Windows API LocalFree
    LocalFree(response);
    

    More complex method that calls WriteToInstrument but has as a parameter an allocator that will be used to allocate the response buffer. There is an addition parameter par that will be passed to the allocator. The method then will copy the result as an ansi string (char*). The caller must free the memory using a specific deallocator based on the allocator used. The response is always \0 terminated.

    // allocator is a function that will be used for allocating the memory. par will be passed as a parameter to allocator(length, par)
    // the length of allocator is in number of elements, *not in bytes!*
    int WriteToInstrumentWrap6(const char *command, char **response, char *(*allocator)(size_t length, void *par), void *par, int stage)
    {
        auto str1 = gcnew String(command);
        String ^str2 = nullptr;
        int res = WriteToInstrument(str1, str2, 5);
    
        pin_ptr<const Char> ppchar = PtrToStringChars(str2);
        const wchar_t *pch = const_cast<wchar_t*>(ppchar);
    
        // length includes the terminating \0
        size_t length = WideCharToMultiByte(CP_ACP, 0, pch, -1, nullptr, 0, nullptr, nullptr);
        *response = allocator(length, par);
        WideCharToMultiByte(CP_ACP, 0, pch, -1, *response, length, nullptr, nullptr);
    
        return res;
    }
    

    Examples of use (multiple allocator showed: vector<>, malloc, new[], unique_ptr<>):

    Note the use of the par parameter.

    template<typename T>
    T* vector_allocator(size_t length, void *par)
    {
        std::vector<T> *pvector = static_cast<std::vector<T>*>(par);
        pvector->resize(length);
        return pvector->data();
    }
    
    template<typename T>
    T* malloc_allocator(size_t length, void *par)
    {
        return (T*)malloc(length * sizeof(T));
    }
    
    template<typename T>
    T* new_allocator(size_t length, void *par)
    {
        return new T[length];
    }
    
    template<typename T>
    T* uniqueptr_allocator(size_t length, void *par)
    {
        std::unique_ptr<T[]> *pp = static_cast<std::unique_ptr<T[]>*>(par);
        pp->reset(new T[length]);
        return pp->get();
    }
    

    and then (note the fact that sometimes one of the parameter passed to WriteToInstrumentWrap6 is useless because we already have a pointer to the buffer):

    {
        std::vector<char> response;
        char *useless;
        WriteToInstrumentWrap6("cmd1", &useless, vector_allocator<char>, &response, 1);
        std::cout << "vector char: " << response.data() << ", used length: " << response.size() << std::endl;
        // The memory is automatically freed by std::vector<>
    }
    
    {
        char *response;
        WriteToInstrumentWrap6("cmd1", &response, malloc_allocator<char>, nullptr, 1);
        std::cout << "malloc char: " << response << ", used length: " << strlen(response) + 1 << std::endl;
        // Must free with free
        free(response);
    }
    
    {
        char *response;
        WriteToInstrumentWrap6("cmd1", &response, new_allocator<char>, nullptr, 1);
        std::cout << "new[] char: " << response << ", used length: " << strlen(response) + 1 << std::endl;
        // Must free with delete[]
        delete[] response;
    }
    
    {
        std::unique_ptr<char[]> response;
        char *useless;
        WriteToInstrumentWrap6("cmd1", &useless, uniqueptr_allocator<char>, &response, 1);
        std::cout << "unique_ptr<> char: " << response.get() << ", used length: " << strlen(response.get()) + 1 << std::endl;
        // The memory is automatically freed by std::unique_ptr<>
    }