A. Сетевое приложение называет C dll. Код C выделяет память для массива символов и возвращает этот массив как результат. Приложения.NET получают этот результат как строку.
Код C:
extern "C" __declspec(dllexport) char* __cdecl Run()
{
char* result = (char*)malloc(100 * sizeof(char));
// fill the array with data
return result;
}
Код C#:
[DllImport("Unmanaged.dll")]
private static extern string Run();
...
string result = Run();
// do something useful with the result and than leave it out of scope
Некоторые тесты его показывают, что сборщик "мусора" не освобождает память, выделенную кодом C.
Любая справка будет цениться.:)
Managed string is not the same as char*. What happens undercover is that the marshaling code in the interop layer makes a copy of the unmanaged string in order to convert it to a managed string, but it can't free that memory as it does not know how it was allocated.
However, you could try allocating and returning a BSTR instead of a char*. The interop layer deals way better with automation datatypes than classic unmanaged data types.
The reason why that matters is the way char* and BSTRs are allocated in the memory.
The char* buffers are allocated on the heap of the C++ runtime using private allocation/deallocation routines that the CLR knows nothing about, so there's no way it can delete that memory. And to make things even worse, the buffer that char* points can be allocated by an internal heap implementation of the dll code, or it might even point to a member variable in a private class.
The BSTRs on the other hand are allocated using the WIndows API SysAllocString and are freed by SyFreeStirng and since the CLR interop layer knows about these Windows APIs, it knows how to free a BSTR it got from unmanaged code.
Вы не можете освободить неуправляемую память из управляемого кода. Вам нужно написать процедуру на C, которая вызывает free
для указателя, возвращаемого функцией Run
, и P / Invoke его из .NET.
Другой вариант - выделить неуправляемую память в .NET передайте указатель на функцию C, которая заполнит его данными и, наконец, освободит этот указатель:
IntPtr ptr = Marshal.AllocHGlobal(100 * sizeof(char));
SomeUnmanagedFunc(ptr);
Marshal.FreeHGlobal(ptr);
Другой способ сделать это - передать управляемую строку (экземпляр StringBuilder) через P / Invoke (в качестве параметра функции Run
).
Таким образом, на неуправляемой стороне не происходит выделения.
Другими словами, вы должны иметь что-то вроде:
extern "C" __declspec(dllexport) void __cdecl Run(char* data)
{
// fill the array with data
// no return value (void)
}
и называть это так:
[DllImport("Unmanaged.dll", CharSet = CharSet.Ansi)]
static extern void Run(StringBuilder result);
StringBuilder result = new StringBuilder(100);
Run(result);
Память .NET ДОЛЖНА быть выделена в среде CLR, чтобы быть очищено GC. Вам нужно добавить функцию для освобождения блока в C DLL.
Не забудьте освободить память в том же экземпляре C DLL, который создал память. Нельзя смешивать и сочетать.
Маршаллер P/Invoke будет считать, что память для возвращаемого типа была выделена с помощью CoTaskMemAlloc() и для ее освобождения вызовет CoTaskMemFree(). Если этого не сделать, то программа выйдет из строя с исключением на Vista и Win7, но беззвучной утечкой памяти на XP. Использование SysAllocString() можно заставить работать, но необходимо аннотировать тип возврата в атрибуте [DllImport]. Если этого не сделать, все равно произойдет утечка, без диагностики на Win7. BSTR - это а не указатель на блок памяти, выделенный CoTaskMemAlloc, перед указанным адресом находятся 4 байта, по которым хранится размер строки.
Любая из следующих комбинаций будет работать корректно:
extern "C" __declspec(dllexport)
BSTR __stdcall ReturnsAString() {
return SysAllocString(L"Hello world");
}
[DllImport(@"c:\projects\cpptemp1\debug\cpptemp1.dll")]
[return: MarshalAs(UnmanagedType.BStr)] // NOTE: required!
private static extern string ReturnsAString();
или:
extern "C" __declspec(dllexport)
const wchar_t* __stdcall ReturnsAString() {
const wchar_t* str = L"Hello world";
wchar_t* retval = (wchar_t*)CoTaskMemAlloc((wcslen(str)+1) * sizeof(wchar_t));
wcscpy(retval, str);
return retval;
}
[DllImport(@"c:\projects\cpptemp1\debug\cpptemp1.dll", CharSet=CharSet.Auto)]
private static extern string ReturnsAString();
Нужно подумать о том, чтобы разрешить клиентскому коду передавать буфер, чтобы не возникало проблем с управлением памятью. Это должно выглядеть аналогично:
extern "C" __declspec(dllexport)
void __stdcall ReturnsAString(wchar_t* buffer, size_t buflen) {
wcscpy_s(buffer, buflen, L"Hello world");
}
[DllImport(@"c:\projects\cpptemp1\debug\cpptemp1.dll", CharSet=CharSet.Auto)]
private static extern void ReturnsAString(StringBuilder buffer, int buflen);
...
StringBuilder sb = new StringBuilder(256);
ReturnsAString(sb, sb.Capacity);
string s = sb.ToString();
Я читал несколько вопросов о PInvoke и остановился на этом. Не знаю, актуальна ли эта проблема для вас, но я решил опубликовать свой ответ для будущих читателей.
Речь идет о вашем последнем комментарии к ответу Дарина Димитрова. Когда размер выделенной памяти неизвестен, типичное решение - вызвать неуправляемую функцию с нулевым указателем и получить размер в параметре out. Затем мы выделяем необходимое пространство и снова вызываем неуправляемую функцию.
Пример ниже:
//MANAGED SIDE
IntPtr ptr = IntPtr.Zero;
int size = 0;
myFunc(ptr, out size);
ptr = Marshal.AllocHGlobal(size);
myFunc(ptr, out size);
//Do your work..
Marshal.FreeHGlobal(ptr);
//UNMANEGED SIDE
int myFunc(void* dest, size_t& size){
if(dest==NULL)
//calculate de size..
size = 'resul'
return 0;
}
// create the array and copy all elements
memcopy(dest, ... , size);
//free all allocated space in unmanaged and return success
return 0;
}