如何编写以引用字符串作为参数的托管 C# dll 的 C++ 包装器

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

我正在使用 C++/CLR 编写包装器。托管 C# class 具有如下函数签名

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

我必须为这个函数编写一个 C++ 包装器,类似于下面的签名

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

我的问题是:如何处理从 C# 中的 "ref string" 到 C++ 中的 char* 的转换?或者我如何处理需要从 C# 中获取可在 C/C++ 中使用的引用字符串的情况?提前谢谢了。

在高级溢出中,您需要 C++/CLI(使用托管 .NET 代码的 C++)

好的,您获取类型 System::String (.NET) 的句柄并获得其长度 属性。使用该值分配大小为 2 个字符的新缓冲区,使用 malloc 和 memset 将其清零。锁定字符串,复制其内容并再次解锁。

有一个转换运算符可以从 System::String ^ 到 MFC 的 CString,如果有帮助的话。它将使代码成为单行代码

是的。但是,

CString unmanaged = CString(System::String ^) 为您完成这一切。

我将添加一些我今天早上编写的代码示例。一般来说,当谈到 returning 对象时(在广义上,即使 char* 字符串也是一个对象),C/C++ 中的大问题是:

  • 谁分配内存
  • 需要多少元素
  • 内存是如何分配的(使用哪个分配器)
  • 作为推论,必须如何释放内存
  • 最后一个可选问题是内存是否必须真正释放:一个方法可以 return 一个指向内部对象的指针,该对象的生命周期等于程序的生命周期,并且不应该被释放。例如:

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

    您不能释放由 Message() 编辑的 return 内存!

当您编写将被其他程序使用的库(dll)时,这个问题会变得更加复杂:dll 中使用的 mallocnew 可以是different/distinct 来自主程序(或另一个 dll)使用的 mallocnew,所以你不应该 free 你的(主程序)释放由 dll malloc(编辑) 的内存。

这个特定问题有三种可能的解决方案:

  • 使用共享分配器,例如 OS 给出的分配器。 Windows 给出 LocalAllocCoTaskMemAlloc。它们甚至可以从 .NET 访问(Marshal.AllocHGlobalMarshal.AllocCoTaskMem)。通过这种方式,主应用程序可以释放 dll
  • 分配的内存
  • 你的dll的API有一个Free()方法必须用来释放dll分配的内存
  • 你的 dll 的 API 有一些方法,比如 SetAllocator(void *(*allocator)(size_t))SetFree(void (*free)(void*)),所以存储函数指针的方法,主应用程序可以使用它来设置分配器和释放由 dll 使用,以便它们在主应用程序和 dll 之间共享。 dll 将使用这些分配器。请注意 SetAllocator(malloc); SetFree(free) 如果由主应用程序完成是完全合法的:现在 dll 将使用主应用程序的 malloc,而不是 dll 的 malloc!
  • 我将给出一些示例中使用的快捷方式:该方法将分配器(函数指针)作为参数,然后将使用该分配器

作为一个重要的旁注:我们现在是 2018 年。你应该至少在 15 年之前忘记了 char* 用于 C 中的字符串 Windows。使用 wchar_t。总是。

最后是一些代码:-)

现在...给定(C# 代码):

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

调用 WriteToInstrument 然后将 response 结果复制到 ansi 字符串 (char*) 的简单方法。缓冲区由调用者分配,大小为 length。执行该方法后,length 包含使用的字符数(包括终止 [=54=])。 response 总是 [=54=] 终止。这里的问题是 response 可能会被截断 and/or 调用者可能会分配一个太大的缓冲区(如果不幸的话,这并不能真正保护它免受截断问题的影响:-))。我在这里重复一遍:在 2018 年对字符串使用 char* 是古老的技术。

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

使用示例:

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;

或(使用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;

调用 WriteToInstrument 然后将 response 结果复制到 unicode 字符串 (wchar_t*) 的简单方法。缓冲区由调用者分配,大小为 length。执行该方法后,length 包含使用的字符数(包括终止 [=54=])。 response 总是 [=54=] 终止。

// in input length is the size of response, in output the number of characters (not bytes!) written to response
// (INCLUDING THE [=15=]!). 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] = '[=15=]';
    *length++;

    return res;
}

使用示例:

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;

或(使用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;

接下来的所有示例都将使用 char 而不是 wchar_t。转换它们很容易。我会在这里重复一遍:在 2018 年对字符串使用 char* 是古老的技术。这就像使用 ArrayList 而不是 List<>


调用 WriteToInstrument 的简单方法,使用 CoTaskMemAlloc 分配 response 缓冲区并将结果复制到 ansi 字符串 (char*)。调用者必须 CoTaskMemFree 分配的内存。 response 总是 [=54=] 终止。

// 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 [=18=]
    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;
}

使用示例:

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

调用 WriteToInstrument 的简单方法,使用 "private" "library" 分配器分配 response 缓冲区并将结果复制到 ansi 字符串(char* ).调用者必须使用库释放器 MyLibraryFree 来释放分配的内存。 response 总是 [=54=] 终止。

// 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 [=20=]
    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;
}

使用示例:

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

调用 WriteToInstrument 的简单方法,使用可设置的(通过 SetLibraryAllocator/SetLibraryFree 方法)分配器分配 response 缓冲区(有一个默认值如果没有选择特殊分配器则使用)并将结果复制到 ansi 字符串(char*)。调用者必须使用库解除分配器 LibraryFree(使用由 SetLibraryFree 选择的分配器)来释放分配的内存,或者如果它设置了不同的分配器,则可以直接使用该解除分配器。 response 总是 [=54=] 终止。

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 [=22=]
    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;
}

使用示例:

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

void MyLocalFree(void *p)
{
    LocalFree(p);
}

然后:

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

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

更复杂的方法调用 WriteToInstrument 但具有作为参数的 allocator 将用于分配 response 缓冲区。有一个附加参数 par 将传递给 allocator。然后该方法会将结果复制为 ansi 字符串 (char*)。调用者必须根据使用的 allocator 使用特定的释放器释放内存。 response 总是 [=54=] 终止。

// 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 [=26=]
    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;
}

使用示例(显示多个分配器:vector<>mallocnew[]unique_ptr<>):

注意 par 参数的使用。

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

然后(注意有时传递给 WriteToInstrumentWrap6 的参数之一是 useless 因为我们已经有一个指向缓冲区的指针):

{
    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<>
}