I parametri dell'attributo
DllImport
Nell'ultimo post riguardante l'esame 70-536, abbiamo
visto come utilizzare l'attributo DllImport per poter
utilizzare una funzione esportata da una libreria unmanaged. Nella sua
forma base, DllImport richiede un solo parametro: una stringa
che contiene il nome del file DLL che esporta la funzione che ci interessa
utilizzare nella nostra applicazione managed. L'ultima volta avevamo
fatto un piccolo esempio, considerando la funzione GetTempPath, esportata da
kernel32.dll. la dichiarazione nella nostra classe managedera:
[DllImport("kernel32.dll")]
private static extern uint GetTempPath(uint nBufferLength,
[Out] StringBuilder lpBuffer);
L'attributo DllImport può avere qualche parametro
aggiuntivo. Ad esempio, nel codice qui sopra il nome della funzione GetTempPath
corrisponde al nome della funzione contenuta in kernel32.dll. Se per qualche
motivo il nome della funzione managed contenuta nel prototipo
differisce dal nome della funzione contenuta nella libreria unmanaged,
dobbiamo fornire al framework l'entrypoint corretto. Ad
esempio:
[DllImport("kernel32.dll", EntryPoint = "GetTempPath")]
private static extern uint GetTemporaryPath(uint nBufferLength,
[Out] StringBuilder lpBuffer);
Il parametro EntryPoint di
DllImport nella dichiarazione qui sopra dice semplicemente che
il nome della funzione in managed code è GetTemporaryPath,
che punta all'entrypoint GetTempPath della libreria kernel32.dll. Un
secondo possibile parametro è CharSet, che riveste
un'importanza fondamentale nel caso in cui dobbiamo lavorare con le stringhe:
impostando questo parametro su CharSet.Auto, ci assicuriamo che
le stringhe vengono passate correttamente dal mondo managed al mondo
unmanaged, e viceversa. Per il marshaling, il CLR si basa sul sistema
operativo su cui sta girando l'assembly.
Il default marshaling e l'attributo MarshalAsAttribute
Il
default marshaling è il comportamento di default che .NET assume ogni volta che
deve passare oggetti tra il mondo managed ed il mondo
unmanaged. Tecnicamente, è espresso da una serie di tabelle che
esprimono come un tipo managed viene convertito nel tipo
unmanaged. Ad esempio, questa tabella considera i value-typed di .NET. Tener
presente l'attributo MarshalAs, grazie al
quale possiamo modificare questo comportamento, comunicando esplicitamente
al CLR come effettuare il marshaling in un particolare frangente. Caso tipico
sono le stringhe, che in base al tipo unmanaged (LPStr, BStr, LPWStr,
etc.) possono subire il marshaling verso il contesto managed secondo
diverse tipi di destinazione.
Fare il marshaling di strutture
Esiste una pagina su MSDN che descrive accuratamente questo
scenario. Fino ad adesso, abbiamo trattato con tipi di dato semplici, ad
esclusione delle stringhe dove abbiamo a che fare innanzitutto con il CharSet,
in secondo luogo con stringhe a lunghezza fissa o variabile (dove quindi
dobbiamo usare il tipo string oppure la classe StringBuilder).
Cosa succede se dobbiamo invece trasferire intere strutture dati? Ne dobbiamo
fare ovviamente il marshaling, ma come? Dobbiamo capire essenzialmente che
il codice del mondo managed ed il codice del mondo
unmanaged formattano i dati in memoria in modo diverso. Dobbiamo
trovare quindi una strada per dire al CLR di non mappare i dati in memoria come
vorrebbe lui, ma di rispettare determinate logiche. Questa strada è disponibile,
ancora una volta, con un attributo .NET dedicato: StructLayout. Questo attributo prende
come parametro un valore dell'enum LayoutKind, che esprime la
modalità con cui i campi della nostra struttura vengono mappati in memoria. I
valori di questo enum più interessanti sono principalmente due:
- Explicit: siamo noi che indichiamo al CLR come mappare
(uno ad uno) i campi della struttura. Attraverso l'attributo FieldOffset, esprimiamo in
bytes la posizione fisica di un campo all'interno di una classe o di una
struttura. Questo vale ovviamente sia nel mondo managed, che in
unmanaged.
- Sequential: i campi vengono mappati in memoria
sequenzialmente, nello stesso ordine con cui questi servono dal lato
unmanaged della nostra applicazione. I campi possono essere non
contigui
Funzioni di callback
In altri scenari serve una funzione
di callback, ovvero una funzione scritta in managed code che viene
eseguita al termine dell'esecuzione di una funzione unmanaged. In altre
parole, dal nostro codice .NET eseguiamo una funzione nativa attraverso
P/Invoke, la quale deve sapere cosa eseguire quando la sua esecuzione termina.
In realtà, come vedremo fra poco, non è detto che la callback serva solamente al
termine dell'esecuzione: la funzione unmanaged potrebbe utilizzare la
callback quando necessario, magari per avvisare il mondo .NET. Anche in questo
caso, possiamo fare riferimento a questa pagina su MSDN per maggiori dettagli. La tecnica consiste
nell'utilizzare un delegate, che non è nient'altro che un puntatore a funzione
in ambiente .NET. L'esempio su MSDN tratta un caso molto particolare: attraverso
P/Invoke, utilizziamo la funzione EnumWindows, la quale
richiede una callback, che viene eseguita ogni volta che la funzione
unmanaged lo richiede. Come facciamo a capire
quando una funzione unmanaged richiede una callback?
Bisogna leggere con attenzione il prototipo della funzione stessa, perchè dal
tipo e dal nome dei parametri possiamo arrivare a capirlo. Il prototipo C++
della funzione EnumWindows è il seguente:
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam)
Ciò significa che se vogliamo usare questa funzione unmanaged
dobbiamo semplicemente convertire il primo parametro in un delegate:
public delegate bool CallBack(int hwnd, int lParam);
e conseguente utilizzare questo delegate come funzione di callback.