Как правильно использовать intptr для возврата значения char * из c ++ DLL в Vb.net - PullRequest
0 голосов
/ 06 ноября 2018

Мне удалось создать программу, которая импортирует функцию C ++ DLL и правильно использует ее для получения требуемых рассчитанных значений. Я возвращаю значения char * на VB.net, используя указатель intptr .

Работает нормально, однако я не могу восстановить или очистить пространство памяти, где хранится результат функции. Когда я вызываю функцию в первый раз, она дает мне правильные ответы, когда она вызывается во второй раз, она дает мне и первый, и второй ответы.

Вот соответствующие части моего кода:
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. Я проверил онлайн последние несколько часов, пытаясь выяснить, как вернуть char * из C ++ DLL, а затем как очистить intptr.
Мне просто не повезло с предложенными решениями. Я был бы очень признателен за помощь. Спасибо!

Ответы [ 2 ]

0 голосов
/ 07 ноября 2018

Большое спасибо @ Реми Лебо. Я попытался реализовать метод 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

Спасибо за всю помощь!

0 голосов
/ 07 ноября 2018

Согласно следующей документации MSDN:

Маршалинг по умолчанию

Маршалер взаимодействия всегда пытается освободить память, выделенную неуправляемым кодом . Это поведение соответствует правилам управления памятью COM, но отличается от правил, управляющих собственным C ++.

Может возникнуть путаница, если вы ожидаете собственного поведения C ++ (без освобождения памяти) при использовании вызова платформы, который автоматически освобождает память для указателей. Например, вызов следующего неуправляемого метода из DLL C ++ не приводит к автоматическому освобождению памяти.

Неуправляемая подпись

BSTR MethodOne (BSTR b) {  
     return b;  
}  

Однако, если вы определите метод как прототип вызова платформы, замените каждый тип BSTR типом String и вызовите MethodOne, общеязыковая среда выполнения попытается освободить b дважды. Вы можете изменить поведение маршалинга, используя типы IntPtr вместо типов String.

Среда выполнения всегда использует метод CoTaskMemFree для освобождения памяти . Если память, с которой вы работаете, не была выделена методом CoTaskMemAlloc, вы должны использовать IntPtr и освободить память вручную, используя соответствующий метод. Точно так же вы можете избежать автоматического освобождения памяти в ситуациях, когда память никогда не должна освобождаться, например, при использовании функции GetCommandLine из Kernel32.dll, которая возвращает указатель на память ядра. Подробнее об освобождении памяти вручную см. Образец буфера .

.

Таким образом, DLL необходимо динамически выделять новую строку char* каждый раз, когда она возвращается, и коду VB нужно указать, как правильно освободить эту строку. Есть несколько способов справиться с этим:

  • заставляет DLL возвращать строку char* (или wchar_t*), которая выделяется с помощью CoTaskMemAlloc(), а затем изменяет PInvoke, чтобы принимать возвращаемое значение как string, маршалируемое как UnmanagedType.LPStr (или UnmanagedType.LPWStr). Среда выполнения .NET освободит для вас память, используя CoTaskMemFree().

  • измените DLL, чтобы она возвращала строку COM BSTR, которая была выделена с помощью SysAllocString(), а затем измените PInvoke, чтобы получить возвращаемое значение как string, маршалированное как UnmanagedType.BStr. Среда выполнения .NET освободит для вас память, используя SysFreeString().

  • , если вы хотите, чтобы DLL возвращала необработанную строку char* (или wchar_t*) и PInvoke рассматривал ее как IntPtr (поскольку она не выделяется с помощью CoTaskMemAlloc() для SysAllocString() ), тогда среда выполнения .NET не сможет узнать, как была распределена строка, и поэтому не сможет автоматически освободить память. Так что либо:

    • IntPtr необходимо будет вернуть обратно в DLL, когда она будет использована, поскольку только DLL будет знать, как было выделено пространство, поэтому только DLL сможет правильно ее освободить.

    • чтобы DLL выделяла строку char* (или wchar_t*), используя LocalAlloc(), а затем .NET-код может использовать Marshal.PtrToStringAnsi() (или Marshal.PtrToStringUni()) для получения string из IntPtr, а затем передайте IntPtr в Marshal.FreeHGlobal(), когда закончите с его использованием.

Для получения более подробной информации см. Следующую статью (она написана для C #, но вы можете адаптировать ее для VB.NET):

Возвращение строк из C ++ API в C #

...