Свободное выделение неуправляемой памяти из управляемого кода

Приложение.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, который создал память. Вы не можете смешивать и сочетать.

Другие вопросы по тегам