Introduzione
P/Invoke è
l'abbreviazione di Platform Invoke e consiste
nella capacità da parte del nostro managed code di utilizzare DLL di
sistema ed eseguire le funzioni contenute. Ho trovato di grande comodità il sito
www.pinvoke.net, che ci aiuta con una
grande raccolta di tutte le librerie di sistema e come poterle importare in una
classe managed per rendere accessibile da C#, VB.NET e da tutti gli altri
linguaggi della famiglia .NET. Anche in questo caso, come nel mio ultimo post,
abbiamo a che fare con una classe
wrapper: sarà questa che il codice managed eseguirà.
Tale classe deve essere decorata dall'attributo DllImport, specificando il
nome del file DLL che si vuole utilizzare in questa chiamata. Il namespace di
riferimento per tutto quello che riguarda P/Invoke è System.Runtime.InteropServices.
Una chiamata semplice a GetTempPath
La funzione
GetTempPath è funzione contenuta nel file kernel32.dll di Windows. Partendo da
questo presupposto, in C# possiamo dichiarare una classe statica PInvoke ed
inserire un metodo definito così:
static class PInvoke
{
[DllImport("kernel32.dll")]
public static extern uint GetTempPath(uint nBufferLength,
[Out] StringBuilder lpBuffer);
}
Il metodo è marcato come statico e come extern, che esplicita il fatto che il
metodo è implementato esternamente rispetto all'assembly. Il secondo parametro è
un oggetto StringBuilder che verrà restituito (notare la
keyword out ) e conterrà il path per i
files temporanei restituito dall'OS. Questo è un caso estremamente semplice.
L'unica verrà difficoltà è alla fin fine mappare correttamente i tipi tra il mondo
managed ed il mondo unmanaged. Per questo risorse on-line come
www.pinvoke.net sono estremamente
importanti ed utili, perchè hanno fatto gran parte del lavoro e ci basta copiare
& incollare.
Torniamo a noi. Una volta dichiarata la classe ed il metodo, chiamarlo non è
per nulla diverso a qualsiasi altro metodo statico scritto in C#.
StringBuilder bld = new StringBuilder();
PInvoke.GetTempPath(255, bld);
string tempPath = bld.ToString();
Quando ha senso usare
P/Invoke? In primo luogo, quando una
certa funzionalità non è esposta dal Framework
. Ad esempio, la libreria kernel32.dll
espone la funzione CopyFileEx, che però è direttamente utilizzabile da codice
managed. P/Invoke è molto utile, ripeto, se vogliamo interagire
con componenti COM che al contrario non sono disponibili in .NET. Questo articolo su CodeProject ad esempio dimostra come usare P/Invoke con
Windows Media Player, cosa che può essere fatta tra l'altro con le tecniche
viste ieri (ovvero aggiungere come riferimento il file WMP.dll ed accedere così
all'object model).
Ovviamente P/Invoke non funziona solo con le librerie di sistema, ma
con qualsiasi libreria COM scritta in C++, comprese quindi le nostre.
Usiamo P/Invoke, ma usiamolo
pulito!
In alcune occasioni, P/Invoke è davvero necessario, per cui
siamo obbligati per forza di cose a servirci delle sue potenzialità. Però
dobbiamo pensare bene alle
complicazioni ed agire di conseguenza.
Le Windows API ritornano le condizioni di errori come banali costanti. Questo
è in netta contrapposizione con la logica delle Exception messa in campo da .NET
e dal framework. Per ovviare a questo inconveniente, è opportuno costruire una
classe wrapper che converta un HRESULT restituito da Windows nella Exception più
opportuna. Questa classe wrapper può inoltre nascondere l'accesso al
mondo unmanaged: in pratica, una best practice da applicare è rendere
privata la dichiarazione del metodo extern e creare invece un metodo public di
buffer che sarà quello che useremo realmente nel nostro codice.
Vediamo di applicare nel concreto queste best practices. Vogliamo sempre
utilizzare la funzione API GetTempPath esportata dalla libreria
kernel32.dll. Ho creato una classe PInvoke, il cui codice
è:
sealed class PInvoke
{
public static string GetTemporaryPath()
{
StringBuilder bld = new StringBuilder();
uint ret = GetTempPath(255, bld);
return(bld.ToString());
}
[DllImport("kernel32.dll")]
private static extern uint GetTempPath(uint nBufferLength,
[Out] StringBuilder lpBuffer);
}
Ho riportato due metodi. Un metodo pubblico
GetTemporaryPath, scritto completamente in codice
managed. Un metodo privato GetTempPath che invece è
extern, ed è in unmanaged code. Tale classe sarà accessibile dal
chiamante solo attraverso il metodo pubblico, come è normale che sia. Questo
modus operandi rende possibili alcune migliorie al codice che abbiamo
scritto:
- quando il Framework renderà eventualmente disponibile la funzionalità alla
quale adesso accediamo attraverso P/Invoke, ci basterà modificare
l'implementazione del metodo pubblico GetTemporaryPath ed il
gioco è fatto.
- così facendo, isoliamo tutto quello che riguarda P/Invoke.
Questo ci permette di limitare le porzioni di codice interessate ad eventuali
crash o a problemi dovuti ad Interop.
- come dicevo prima (ed è domanda di esame) possiamo mascherare gli errori
ritornati da Win32 in opportune exception. Nel codice qui sopra, per esempio,
memorizza nella variabile ret di tipo uint il valore
ritornato dalla chiamata: mi basterebbe implementare un costrutto switch per
intercettare eventuali errori ed agire di conseguenza.
Come usare DllImport
L'attributo DllImport ci permette di
indicare in quale libreria viene esportata una certa funzione. Questo attributo
ha diverse proprietà importanti. EntryPoint ci permette di
specificare il nome della funzione contenuta nella libreria, nel caso in cui il
nome sia diverso rispetto a quello che indichiamo noi nella dichiarazione del
metodo. CharSet va impostato su CharSet.Auto, e ha importanza
solo se la funzione esportata utilizza le stringhe; questo permette al CLR di
utilizzare il set di caratteri corretto in base all'OS su cui si sta eseguendo
il codice.