简体   繁体   中英

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

I have been able to achieve a program that imports a C++ DLL function and uses it correctly to get the required calculated values. I'm returning the char* values to VB.net using an intptr pointer.

It works fine, however, I cant seem to rest or clear the memory space where the result from the function is stored. When i call the function the first time it gives me the right answers, when it is called the second time, it gives me both the first and the second answers.

Here are relevant parts of my code:
CPM.cpp - The function that calculated the return variables in the cpp file

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 - the header file for exporting the function

#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 code to import the DLL, declare the function and use it

'' 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

As I run it consecutively the string just keeps getting longer ie 4th function call would return the values for project 1, 2, 3 and 4. I've checked online for the past few hours trying to figure out how to return char* from C++ DLLs and then how to clear the intptr.
I just haven't had any luck with the solutions suggested. I would really really appreciate some help. Thank you!

Per the following MSDN documentation:

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 .

So, the DLL needs to dynamically allocate a new char* string each time it returns, and the VB code needs to be told how to free that string properly. There are a few ways to handle this:

  • have the DLL return a char* (or wchar_t* ) string that is allocated with CoTaskMemAlloc() , and then change the PInvoke to take the return value as a string marshaled as an UnmanagedType.LPStr (or UnmanagedType.LPWStr ). The .NET runtime will then free the memory for you using CoTaskMemFree() .

  • change the DLL to return a COM BSTR string that is allocated with SysAllocString() , and then change the PInvoke to take the return value as a string marshaled as an UnmanagedType.BStr . The .NET runtime will then free the memory for you using SysFreeString() .

  • if you want to have the DLL return a raw char* (or wchar_t* ) string and have PInvoke treat it as an IntPtr (because it is not allocated using CoTaskMemAlloc() to SysAllocString() ), then the .NET runtime will have no way of knowing how the string was allocated and so cannot free the memory automatically. So either:

    • the IntPtr will have to be passed back to the DLL when done being used, since only the DLL will know how the memory was allocated, so only the DLL will be able to free it properly.

    • have the DLL allocate the char* (or wchar_t* ) string using LocalAlloc() , and then the .NET code can use Marshal.PtrToStringAnsi() (or Marshal.PtrToStringUni() ) to get a string from the IntPtr , and then pass the IntPtr to Marshal.FreeHGlobal() when done using it.

See the following article for more details (it is written for C#, but you can adapt it for VB.NET):

Returning Strings from a C++ API to C#

Thanks a lot @Remy Lebeau. I've tried implementing the CoTaskMemAlloc() method and it works!. My code is as follows:

In the cpp file i've edited the return value to be allocated using the 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;
}  

In the VB.net file I've written the Dllimport and Marshalling code as follows:

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

Alternatively I've also tried freeing up the allocated memory manually using the GlobalAlloc() and Marshal.FreeHGlobal() functions but i'm getting the same results (ie first call = 1,2,3\\n, second call = 1,2,3\\n,4,5,6\\n instead of just 4,5,6\\n).
Here's my code with the GlobalAlloc() method:

In .cpp file

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

And in VB.net file

<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

Thanks for all the help so far!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM