Свободное выделение неуправляемой памяти из управляемого кода
Приложение.NET вызывает C dll. Код C выделяет память для массива char и возвращает этот массив в качестве результата. Приложения.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.
Любая помощь будет оценена.:)
7 ответов
Управляемая строка - это не то же самое, что char*. Под прикрытием происходит то, что код маршалинга на уровне взаимодействия создает копию неуправляемой строки, чтобы преобразовать ее в управляемую строку, но он не может освободить эту память, поскольку не знает, как она была выделена.
Однако вы можете попробовать выделить и вернуть BSTR вместо символа *. Уровень взаимодействия лучше работает с типами данных автоматизации, чем с классическими неуправляемыми типами данных.
Причина, по которой это имеет значение, заключается в том, как char * и BSTR выделяются в памяти.
Буферы char * размещаются в куче среды выполнения C++ с использованием частных процедур выделения / освобождения, о которых CLR ничего не знает, поэтому нет способа удалить эту память. И что еще хуже, буфер, который указывает char *, может быть выделен внутренней реализацией кучи кода dll или даже может указывать на переменную-член в закрытом классе.
BSTR, с другой стороны, распределяются с использованием API-интерфейса WIndows SysAllocString и освобождаются SyFreeStirng, а поскольку уровень взаимодействия CLR знает об этих API-интерфейсах Windows, он знает, как освободить BSTR, полученный из неуправляемого кода.
Маршаллер 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();
Вы не можете освободить неуправляемую память из управляемого кода. Вам нужно написать процедуру на C, которая вызывает free
на указателе, возвращенном Run
функция и P/ вызвать его из.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);
Я читал несколько вопросов о 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;
}
public class Memory
{
[DllImport("kernel32.dll")]
private static extern bool SetProcessWorkingSetSize(IntPtr proc, int min, int max);
//[DllImport("kernel64.dll")]
//private static extern bool SetProcessWorkingSetSize(IntPtr proc, int min, int max);
public String Errores = "";
public void Limpiar()
{
try
{
Process Mem;
Mem = Process.GetCurrentProcess();
SetProcessWorkingSetSize(Mem.Handle, -1, -1);
Mem = null;
}
catch (Exception ex)
{
Errores = ex.ToString() + " " + ex.StackTrace.ToString();
}
}
}
public class LimpiadodeMemoria
{
private Boolean Monitorear;
private Boolean Salida;
private String ElMensajeBitacoras;
private String Error;
public delegate void Errores(string Elerror);
public event Errores OnErrores;
public delegate void Bitacora(string LaBitacora);
public event Bitacora OnBitacora;
public void Iniciar()
{
Monitorear = true;
Salida = false;
ElMensajeBitacoras = "LimpiadodeMemoria - " + DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss") + " Iniciada";
OnBitacora(ElMensajeBitacoras);
while (Monitorear == true)
{
Salida = false;
Memory _Memori = new Memory();
_Memori.Limpiar();
Error = _Memori.Errores;
_Memori = null;
if (Error != "")
{
OnErrores(Error);
}
Salida = true;
System.Threading.Thread.Sleep(1000);
}
}
public void Detener()
{
Monitorear = false;
while (Salida == false)
{
System.Threading.Thread.Sleep(100);
}
ElMensajeBitacoras = "LimpiadodeMemoria - " + DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss") + " Detenida";
OnBitacora(ElMensajeBitacoras);
}
}
Память.NET ДОЛЖНА быть выделена в CLR для очистки GC. Вам нужно добавить функцию для освобождения блока в C DLL.
Не забудьте освободить память в том же экземпляре C DLL, который создал память. Вы не можете смешивать и сочетать.