【问题标题】:How to properly use intptr to return char* value from c++ DLL to Vb.net如何正确使用 intptr 将 char* 值从 c++ DLL 返回到 Vb.net
【发布时间】:2018-11-06 20:58:52
【问题描述】:

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

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

以下是我的代码的相关部分:
CPM.cpp - 在 cpp 文件中计算返回变量的函数

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 个函数调用将返回项目 1、2、3 和 4 的值。 过去几个小时,我在网上查了一下,试图弄清楚如何从 C++ DLL 返回 char*,然后如何清除 intptr。
我只是对建议的解决方案没有任何运气。我真的很感激一些帮助。 谢谢!

【问题讨论】:

  • 与其自己编组,为什么不让框架为你做呢?我不确定它作为返回类型是如何工作的,但我相信char* 可以映射到StringBuilderconst char* 映射到String)。我不认为改变它会解决你的实际问题(但我可能是错的),也许getCPM 重用了相同的字符数组?你有这种方法的某种文档吗?
  • char* 可以映射到string 而不是StringBuilder,如果它被分配了CoTaskMemAlloc() 并被编组为UnmanagedType.LPStr

标签: c++ vb.net visual-c++ char marshalling


【解决方案1】:

根据以下 MSDN 文档:

Default Marshaling Behavior

互操作封送拆收器总是尝试释放非托管代码分配的内存。此行为符合 COM 内存管理规则,但不同于管理本机 C++ 的规则。

如果您在使用平台调用时预期本机 C++ 行为(无内存释放)可能会出现混淆,平台调用会自动为指针释放内存。例如,从 C++ DLL 调用以下非托管方法不会自动释放任何内存。

非托管签名

BSTR MethodOne (BSTR b) {  
     return b;  
}  

但是,如果将方法定义为平台调用原型,将每个BSTR 类型替换为String 类型,并调用MethodOne,公共语言运行时会尝试释放b 两次。您可以通过使用 IntPtr 类型而不是 String 类型来更改编组行为。

运行时总是使用CoTaskMemFree 方法来释放内存。如果您正在使用的内存不是通过CoTaskMemAlloc 方法分配的,您必须使用IntPtr 并使用适当的方法手动释放内存。同样,您可以避免在永远不应该释放内存的情况下自动释放内存,例如使用来自Kernel32.dllGetCommandLine 函数时,该函数返回指向内核内存的指针。有关手动释放内存的详细信息,请参阅Buffers Sample

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

  • 让 DLL 返回分配有 CoTaskMemAlloc()char*(或 wchar_t*)字符串,然后更改 PInvoke 以将返回值作为 string 编组为 UnmanagedType.LPStr (或UnmanagedType.LPWStr)。然后,.NET 运行时将使用 CoTaskMemFree() 为您释放内存。

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

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

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

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

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

Returning Strings from a C++ API to C#

【讨论】:

  • 我不知道.NET 还负责清理这样的非托管内存!直到知道我还没有真正知道 UnmanagedType.LPStr 的实际用途,并且文档的描述 “指向以 null 结尾的 ANSI 字符数组的指针。” 信息量不是很大。谢谢你教我今天的智慧之珠!
  • 嗨 Remy,非常感谢您的解释和包含的链接。真的帮助我更好地理解了一点。
  • 我遇到了一些我无法找到解决方案的问题。我在下面发布了我的代码
  • @SayeedaO 应该作为编辑添加到您的问题中,而不是作为答案发布。
  • 哦,谢谢,不知道如何显示更新后的代码。
【解决方案2】:

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

cpp 文件中,我编辑了要使用 CoTaskMemAlloc() 分配的返回值

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

感谢到目前为止的所有帮助!

【讨论】:

  • 您的 C++ 代码错误。具体来说,如果charlen 为0,您将泄漏您分配的内存,并返回一个指向未使用CoTaskMemAlloc() 分配的内存的指针。只需摆脱对 0 的检查并无条件调用strcpy_s(或者,至少将ReturnValue = "" 替换为*ReturnValue = '\0')。另外,你需要释放getCPM()返回的内存吗?
  • 另外,在VB代码中,ByVal dbstring As Char()应该是ByVal dbstring As String
  • @RemyLebeau:char* 作为参数不应该被声明为StringBuilder 吗? (或至少编组为UnmanagedType.LPStr?)
  • 哦,非常感谢。我已经编辑了代码并无条件地调用了strcopy_s。并且还将 dbstring 字符类型更改为String。它现在工作正常,但不幸的是仍然做同样的事情并在每次调用函数时将结果与之前调用的结果聚合..
  • @VisualVincent StringBuilder 可以使用,但String 也可以使用。这两种类型都可以作为空终止字符串封送到非托管代码(事实上,这是StringBuilder 支持的唯一封送处理选项,而String 可以作为 NTS 或BSTR 封送处理)。
猜你喜欢
  • 2010-12-17
  • 2017-01-05
  • 2019-07-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-01
  • 1970-01-01
相关资源
最近更新 更多