2009-12-19 17 views
6

A .NET la aplicación llama a C dll. El código C asigna memoria para una matriz char y devuelve esta matriz como resultado. Las aplicaciones .NET obtienen este resultado como una cadena.Asignación gratuita de memoria no administrada desde el código administrado

El código C:

extern "C" __declspec(dllexport) char* __cdecl Run() 
{ 
    char* result = (char*)malloc(100 * sizeof(char)); 
    // fill the array with data 
    return result; 
} 

El código 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 

Algunas pruebas de que muestran que el recolector de basura no libera la memoria asignada por el código C.

Cualquier ayuda será apreciada. :)

Respuesta

6

Gestionado no es lo mismo como char *. Lo que sucede en secreto es que el código de cálculo en la capa de interoperabilidad hace una copia de la cadena no administrada para convertirla en una cadena administrada, pero no puede liberar esa memoria ya que no sabe cómo fue asignada.

Sin embargo, puede intentar asignar y devolver un BSTR en lugar de un char *. La capa de interoperabilidad trata mucho mejor con los tipos de datos de automatización que los tipos de datos clásicos no gestionados.

La razón por la que esto importa es la forma en que se asignan los caracteres * y BSTR en la memoria.

Los búfers char * se asignan en el montón del tiempo de ejecución de C++ utilizando rutinas privadas de asignación/desasignación de las que el CLR no sabe nada, por lo que no hay forma de que pueda eliminar esa memoria. Y para empeorar las cosas, el buffer que los puntos char * pueden ser asignados por una implementación de heap interno del código dll, o incluso podría apuntar a una variable miembro en una clase privada.

Los BSTR por otro lado se asignan utilizando la API SysAllocString de API de Windows y se liberan mediante SyFreeStirng y, como la capa de interoperabilidad CLR conoce estas API de Windows, sabe cómo liberar un BSTR que obtuvo del código no administrado.

+0

Gracias. Funciona bien. – Alex

6

No puede liberar la memoria no administrada del código administrado. Debe escribir una rutina en C que llame al free en el puntero devuelto por la función Run y P/Invocarlo desde .NET.

Otra opción es asignar memoria no administrado en .NET, pase el puntero a la función C que llenarlo con los datos y, finalmente libre de este puntero:

IntPtr ptr = Marshal.AllocHGlobal(100 * sizeof(char)); 
SomeUnmanagedFunc(ptr); 
Marshal.FreeHGlobal(ptr); 
+0

¿Podría dar un ejemplo sencillo, por favor. – Alex

+1

La asignación de memoria en la aplicación .NET asume que conocemos el tamaño de la matriz por adelantado. En la solución real (el problema) es misterio total :). – Alex

+0

@Darin - ¿Cómo crees que funcionará el primer método de manera confiable? ¿Hay algún truco o algo que deba implementarse? ¿Debo ajustar la llamada a la operación libre de memoria no administrada en el método IDisposable para mi clase que maneja la memoria no administrada? –

-1

memoria .NET deben asignarse dentro del CLR para ser aprobado por el GC. Debe agregar una función para liberar el bloque dentro de la DLL de C.

Recuerde liberar la memoria dentro de la misma instancia de la DLL de C que creó la memoria. No puedes mezclar y combinar.

+1

Eso es incorrecto. El CLR sabe cómo liberar ciertos tipos de bloques de memoria asignados desde un código no administrado, por ejemplo, la memoria asignada mediante SysAllocString o CoTaskMemAlloc. –

3

Otra forma de hacerlo sería pasar una cadena administrada (una instancia de StringBuilder) a través de P/Invoke (como un parámetro para su función Run).

De esta forma, no se realizan asignaciones en el lado no administrado.

En otras palabras, usted tiene algo así como:

extern "C" __declspec(dllexport) void __cdecl Run(char* data) 
{ 
    // fill the array with data 
    // no return value (void) 
} 

y lo llaman así:

cadena
[DllImport("Unmanaged.dll", CharSet = CharSet.Ansi)] 
static extern void Run(StringBuilder result); 

StringBuilder result = new StringBuilder(100); 
Run(result); 
+0

Según la respuesta de Darin, esta funcionaría si se conoce el tamaño de la memoria necesaria. –

7

El Marshaller P/Invoke asumirá que la memoria para el tipo de devolución se asignó con CoTaskMemAlloc() y llamará a CoTaskMemFree() para liberarlo. Si esto no se hizo, el programa fallará con una excepción en Vista y Win7 pero silenciosamente perderá memoria en XP. Se puede hacer que SysAllocString() funcione, pero debe anotar el tipo de devolución en el atributo [DllImport]. No hacerlo todavía causará una fuga, sin un diagnóstico en Win7. Un BSTR es no un puntero a un bloque de memoria asignado por CoTaskMemAlloc, hay 4 bytes delante de la dirección apuntada que almacena el tamaño de cadena.

Cualquiera de las siguientes combinaciones funcionará correctamente:

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(); 

O:

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(); 

usted debe considerar lo que permite el código de cliente para pasar un buffer para que no haya problemas de gestión de memoria. Eso debería parecerse a esto:

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(); 
2

Estaba leyendo algunas preguntas sobre PInvoke y me detuve aquí. No sé si el problema aún es relevante para usted, pero decidí publicar mi respuesta para los lectores futuros.

Se trata de su último comentario a la respuesta de Darin Dimitrov. Cuando se desconoce el tamaño de la memoria asignada, la solución típica es llamar a la función no administrada con un puntero nulo y recibir el tamaño en un parámetro de salida. Luego, asignamos el espacio necesario y volvemos a llamar a la función no administrada.

Exemple a continuación:

//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; 
} 
+0

Enfoque bastante interesante, pero supone que el tiempo de ejecución del código administrado es muy rápido, por lo que podemos llamarlo dos veces. En la práctica, el código administrado podría a) ejecutarse lentamente b) devolver conjuntos de resultados ligeramente diferentes para que la memoria asignada no sea suficiente –

Cuestiones relacionadas