如何正确使用 intptr 到 return char* 值从 C++ DLL 到 Vb.net

How to properly use intptr to return char* value from c++ DLL to Vb.net

我已经能够实现一个导入 C++ DLL 函数并正确使用它来获取所需计算值的程序。我正在 return 使用 intptr 指针将 char* 值设为 VB.net。

它工作正常,但是,我似乎无法休息或清除存储函数结果的内存 space。当我第一次调用该函数时,它给了我正确的答案,当它第二次调用时,它给了我第一个和第二个答案。

以下是我的代码的相关部分:
CPM.cpp - 计算 cpp 文件

中的 return 变量的函数
char* CPMfn(char* sdatabase, int project_num)
{
/* Retrieve data from database and calculate CPM for the selected project number*/  

char* testvector = getCPM(sdatabase, project_num);
return testvector;
} 

CPM.h - 导出函数的头文件

#pragma once
#ifdef CPM_EXPORTS
#define CPM_API __declspec(dllexport)
#else
#define CPM_API __declspec(dllimport)
#endif
extern "C" CPM_API char* CPMfn(char*, int);  

VB.net导入DLL,声明函数并使用的代码

'' Import C++ CPM Calculation function from CPM DLL
<DllImport("CPM.dll", CallingConvention:=CallingConvention.Cdecl)>
Private Shared Function CPMfn(ByVal dbstring As Char(), ByVal task As Int32) As System.IntPtr
End Function

'' Get CPM results from DLL function with database location string and selected project number
CPMresults = CPMfn(DBString, Val(Project_IDTextBox.Text))
CPMvalues = Marshal.PtrToStringAnsi(CPMresults)
If CPMvalues.Length() = 0 Then
    MsgBox("No tasks for seleccted project")
Else
    MsgBox(CPMvalues)           ' Show CPM values
End If

当我 运行 时,字符串不断变长,即第 4 次函数调用将 return 项目 1、2、3 和 4 的值。 在过去的几个小时里,我一直在网上查看,试图弄清楚如何从 C++ DLL 中 return char*,然后如何清除 intptr。
我只是对建议的解决方案没有任何运气。我真的很感激一些帮助。 谢谢!

根据以下 MSDN 文档:

Default Marshaling Behavior

The interop marshaler always attempts to free memory allocated by unmanaged code. This behavior complies with COM memory management rules, but differs from the rules that govern native C++.

Confusion can arise if you anticipate native C++ behavior (no memory freeing) when using platform invoke, which automatically frees memory for pointers. For example, calling the following unmanaged method from a C++ DLL does not automatically free any memory.

Unmanaged signature

BSTR MethodOne (BSTR b) {  
     return b;  
}  

However, if you define the method as a platform invoke prototype, replace each BSTR type with a String type, and call MethodOne, the common language runtime attempts to free b twice. You can change the marshaling behavior by using IntPtr types rather than String types.

The runtime always uses the CoTaskMemFree method to free memory. If the memory you are working with was not allocated with the CoTaskMemAlloc method, you must use an IntPtr and free the memory manually using the appropriate method. Similarly, you can avoid automatic memory freeing in situations where memory should never be freed, such as when using the GetCommandLine function from Kernel32.dll, which returns a pointer to kernel memory. For details on manually freeing memory, see the Buffers Sample.

因此,DLL 需要在每次 return 时动态分配一个新的 char* 字符串,并且需要告知 VB 代码如何正确释放该字符串。有几种方法可以解决这个问题:

  • 让 DLL return 一个 char*(或 wchar_t*)字符串分配给 CoTaskMemAlloc(),然后更改 PInvoke 以获取return 值作为 string 编组为 UnmanagedType.LPStr(或 UnmanagedType.LPWStr)。然后,.NET 运行时将使用 CoTaskMemFree().

  • 为您释放内存
  • 将 DLL 更改为 return 分配有 SysAllocString() 的 COM BSTR 字符串,然后更改 PInvoke 以获取 return值作为 string 编组为 UnmanagedType.BStr。然后,.NET 运行时将使用 SysFreeString().

  • 为您释放内存
  • 如果您想要 DLL return 原始 char*(或 wchar_t*)字符串并让 PInvoke 将其视为 IntPtr (因为它不是使用 CoTaskMemAlloc() 分配到 SysAllocString()),那么 .NET 运行时将无法知道字符串是如何分配的,因此无法自动释放内存。所以要么:

    • IntPtr在使用完毕后必须返回给DLL,因为只有DLL知道内存是如何分配的,所以只有DLL能够妥善释放它。

    • 让 DLL 使用 LocalAlloc() 分配 char*(或 wchar_t*)字符串,然后 .NET 代码可以使用 Marshal.PtrToStringAnsi()(或 Marshal.PtrToStringUni()) 从 IntPtr 获得 string,然后在使用完后将 IntPtr 传递给 Marshal.FreeHGlobal()

更多详细信息请参阅以下文章(它是为 C# 编写的,但您可以将其改编为 VB.NET):

Returning Strings from a C++ API to C#

非常感谢@Remy Lebeau。我已经尝试实施 CoTaskMemAlloc() 方法并且它有效!我的代码如下:

cpp 文件中,我编辑了要使用 CoTaskMemAlloc()

分配的 return 值
char* CPMfn(char* sdatabase, int project_num)
{
/* Retrieve data from database and calculate CPM for the selected project number*/
char* CPMvector = getCPM(sdatabase, project_num);

/* Store results in specially allocated memory space that can easily be deallocated when this DLL is called*/
ULONG ulSize = strlen(CPMvector) + sizeof(char);
char* ReturnValue = NULL;

ReturnValue = (char*)::CoTaskMemAlloc(ulSize);
// Copy the contents of CPMvector
// to the memory pointed to by ReturnValue.
int charlen = strlen(CPMvector);
strcpy_s(ReturnValue, charlen + 1, CPMvector);
// Return
return ReturnValue;
}  

VB.net 文件中,我编写了如下 Dllimport 和 Marshalling 代码:

'' Import C++ CPM Calculation function from CPM DLL
<DllImport("CPM.dll", CallingConvention:=CallingConvention.Cdecl, CharSet:=CharSet.Ansi)>
Private Shared Function CPMfn(ByVal dbstring As String, ByVal task As Int32) As <MarshalAs(UnmanagedType.LPStr)> String
End Function  

'' Get CPM results from DLL function
 Dim teststring As String = CPMfn(cDBString, Val(Project_IDTextBox.Text))    

或者 我也尝试使用 GlobalAlloc()Marshal.FreeHGlobal() 函数手动释放分配的内存,但我得到了相同的结果(即第一次调用 = 1,2,3\n,第二次调用 = 1,2,3\n,4,5,6\n 而不仅仅是 4,5,6\n)。
这是我使用 GlobalAlloc() 方法的代码:

在 .cpp 文件中

ReturnValue = (char*)::GlobalAlloc(GMEM_FIXED, ulSize);
strcpy_s(ReturnValue, strlen(CPMvector) + 1, CPMvector);

并且在 VB.net 文件中

<DllImport("CPM.dll", CallingConvention:=CallingConvention.Cdecl)>
Private Shared Function CPMfn(ByVal dbstring As Char(), ByVal task As Int32) As System.IntPtr
End Function    

Dim CPMresults As IntPtr = CPMfn(cDBString, Val(Project_IDTextBox.Text))
Dim CPMvalues As String = Marshal.PtrToStringAnsi(CPMresults)
Marshal.FreeHGlobal(CPMresults)
CPMresults = IntPtr.Zero

感谢迄今为止的所有帮助!