2009-12-19 13 views
6

Un'applicazione .NET chiama C dll. Il codice C alloca la memoria per un array di caratteri e restituisce questo array come risultato. Le applicazioni .NET ottengono questo risultato come una stringa.Assegnazione memoria non gestita gratuita dal codice gestito

Il codice C:

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

Il codice 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 

Alcuni test di esso mostrano che il garbage collector non libera la memoria allocata dal codice C.

Qualsiasi aiuto sarà apprezzato. :)

risposta

6

gestito non è la stessa di char *. Quello che succede sotto copertura è che il codice di marshaling nel livello di interoperabilità fa una copia della stringa non gestita per convertirla in una stringa gestita, ma non può liberare quella memoria poiché non sa come è stata allocata.

Tuttavia, è possibile provare a allocare e restituire un BSTR anziché un carattere *. Lo strato di interoperabilità interagisce meglio con i tipi di dati di automazione rispetto ai tipi di dati non gestiti classici.

Il motivo per cui ciò è importante è il modo in cui char * e BSTR sono allocati nella memoria.

I buffer char * sono allocati nell'heap del runtime C++ utilizzando le routine di allocazione/deallazione private di cui CLR non sa nulla, quindi non è possibile in alcun modo eliminare tale memoria. E per rendere le cose ancora peggiori, il buffer che i punti char * possono essere allocati da un'implementazione interna dell'heap del codice dll o potrebbe persino puntare a una variabile membro in una classe privata.

Le BSTR invece vengono allocati utilizzando l'API di Windows SysAllocString e vengono liberati dai SyFreeStirng e dal momento che lo strato di interoperabilità CLR conosce queste API di Windows, si sa come liberare un BSTR ha ottenuto da codice non gestito.

+0

Grazie. Funziona bene. – Alex

6

Non è possibile liberare memoria non gestita dal codice gestito. È necessario scrivere una routine in C che chiami free sul puntatore restituito dalla funzione Run e P/Richiamalo da .NET.

Un'altra opzione è quella di allocare memoria non gestita in NET, passare il puntatore alla funzione C che riempirà con i dati e finalmente libera questo puntatore:

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

Potrebbe fornire un semplice esempio, per favore. – Alex

+1

L'allocazione della memoria nell'applicazione .NET presuppone che conosciamo la dimensione dell'array in anticipo. Nella soluzione reale (il problema uno) è un mistero totale :). – Alex

+0

@Darin - Come pensi che il primo metodo funzionerà in modo affidabile? Ci sono trucchi o qualcosa che deve essere implementato. Devo eseguire il wrap su un'operazione non gestita da memoria libera in metodo IDisposable per la mia classe che gestisce la memoria non gestita? –

-1

memoria NET DEVE essere assegnato nel CLR essere cancellato dal GC. È necessario aggiungere una funzione per liberare il blocco all'interno della DLL C.

Ricordarsi di liberare la memoria all'interno della stessa istanza della DLL C che ha creato la memoria. Non puoi mescolare e abbinare.

+1

Che non è corretto. Il CLR sa come liberare determinati tipi di blocchi di memoria allocati dal codice non gestito, ad esempio memoria allocata usando SysAllocString o CoTaskMemAlloc. –

3

Un altro modo per farlo sarebbe passare una stringa gestita (un'istanza StringBuilder) tramite P/Invoke (come parametro della funzione Run).

In questo modo non vengono effettuate assegnazioni sul lato non gestito.

In altre parole, si avrebbe qualcosa di simile:

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

e chiamare in questo modo:

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

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

Come risposta di Darin, questo funzionerebbe se si conoscesse la dimensione della memoria necessaria. –

7

Il marshaller P/Invoke assumerà che la memoria per il tipo restituito sia stata allocata con CoTaskMemAlloc() e chiamerà CoTaskMemFree() per rilasciarlo. Se ciò non è stato fatto, il programma fallirà con un'eccezione su Vista e Win7 ma silenziosamente perderà memoria su XP. È possibile utilizzare SysAllocString() per funzionare, ma è necessario annotare il tipo restituito nell'attributo [DllImport]. Non farlo causerà comunque una perdita, senza una diagnosi su Win7. Un BSTR è non un puntatore a un blocco di memoria allocato da CoTaskMemAlloc, ci sono 4 byte davanti all'indirizzo puntato che memorizza la dimensione della stringa.

Una delle seguenti combinazioni di funzionare correttamente:

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

Oppure:

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

Si dovrebbe considerare che consente il codice del client di passare un buffer quindi non ci sono problemi di gestione della memoria. Questo dovrebbe essere simile a questo:

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

Stavo leggendo alcune domande su PInvoke e mi sono fermato qui. Non so se il problema è ancora rilevante per te, ma ho deciso di postare la mia risposta ai futuri lettori.

Riguarda il tuo ultimo commento alla risposta di Darin Dimitrov. Quando la dimensione della memoria allocata non è nota, la soluzione tipica è chiamare la funzione non gestita con un puntatore nullo e ricevere la dimensione in un parametro out. Quindi, assegniamo lo spazio necessario e richiamiamo nuovamente la funzione non gestita.

Esempio di seguito:

//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

Un approccio abbastanza interessante, ma presuppone che il tempo di esecuzione del codice gestito sia molto veloce, quindi possiamo chiamarlo due volte. In pratica il codice gestito potrebbe a) eseguire lentamente b) restituire set di risultati leggermente diversi in modo che la memoria allocata non sia sufficiente –

Problemi correlati