latform
P Invoke e COM
Interoperability
Raffaele Rialdi
Visual Developer Security MVP
[email protected]
MVP Profile: http://snipurl.com/f0cv
http://mvp.support.microsoft.com
Agenda
• Platform Invocation Services
• COM
•
•
•
•
Usare oggetti COM in dotnet
Usare Activex in dotnet
Esporre oggetti COM da dotnet
Esporre Activex da dotnet
• Reverse P/Invoke
Platform Invoke
• Servizio del framework che permette di chiamare le API
esportate in una DLL "C-style"
• Gli attrezzi del mestiere sono dentro System.Runtime.InteropServices
• Il protagonista è l'attributo DllImportAttribute che svolge (tra le
altre cose) le classiche operazioni di:
• LoadLibrary (carica la dll in memoria) – Free (scarica la dll)
• GetProcAddress (ottiene un puntatore alla funzione richiesta)
• Marshalling (mapping e passaggio parametri)
• Molto aiuto viene dal wiki www.pinvoke.net con molte
dichiarazioni già pronte per la API di Windows
DllImportAttribute
step-by-step
[DllImport("PowrProf.dll")]
extern static bool IsPwrHibernateAllowed();
<DllImport("PowrProf.dll")> _
Function IsPwrHibernateAllowed() As Boolean
End Function
• Ottenere la
documentazione
(MSDN, ...)
• Cercare la
funzione
prototipo
• Cercare il nome
della dll dove è
implementata
DllImportAttribute
step-by-step
[DllImport("User32.dll")]
extern static int MessageBox(IntPtr hWnd,
string Text, string Caption, int uType);
MSDN
dichiarare con const oppure enum (meglio)
A MANO
ma a volte su:
www.pinvoke.net
public enum MessageBoxUType
{
MB_OK = 0x00000000,
MB_OKCANCEL = 0x00000001,
MB_ABORTRETRYIGNORE = 0x00000002,
MB_YESNOCANCEL = 0x00000003,
MB_YESNO = 0x00000004,
MB_RETRYCANCEL = 0x00000005,
MB_CANCELTRYCONTINUE = 0x00000006
}
DllImportAttribute
Nomi delle API
• Le API di Windows che usano stringhe esistono in due versioni:
MessageBoxA (Ansi), MessageBoxW (Wide = Unicode)
• ExactSpelling = false aggiunge automaticamente questo suffisso (A o W)
• Il default che è DllImport("MessageBox") cerca in sequenza:
• MessageBox, MessageBoxA, MessageBoxW, _MessageBox@N (N=numero
che rappresenta la dimensione dei parametri del metodo)
• È possibile cambiare il nome esatto tramite "EntryPoint"
[DllImport("User32.dll", EntryPoint="MessageBoxW", ExactSpelling=true, CharSet=CharSet.Auto)]
extern static int MBox(IntPtr hWnd, string Text, string Caption, int uType);
• A volte aiuta il
"dependancy walker"
(http://www.dependencywalker.com/)
DllImportAttribute
Calling Convention
• Le calling convention sono una sorta di contratto alla
compilazione che prevede:
• come passare gli argomenti delle funzioni ed il valore di ritorno
• quali registri della CPU devono essere salvati
• Le quattro convenzioni più usate oggi sono:
•
•
•
•
__cdecl
__stdcall
__fastcall
__thiscall
usato dalle librerie C e numerose API
conosciuta anche come "pascal", usata dalle Win32 API
usa i registri per passare gli argomenti
default per le chiamate a funzioni membro in C++
• Perché chiamata a funzione abbia successo la calling
convention deve essere corretta (StdCall è il default)
[DllImport("User32.dll", CallingConvention=CallingConvention.StdCall)]
extern static int MessageBox(IntPtr hWnd, string Text, string Caption, int uType);
DllImportAttribute
Gestione errori
• Le Win32 mantengono una variabile di stato per gli errori
• Problema: se dopo l'API con errore ne vengono eseguite altre senza
errore, questo va perso
• Usare via PInvoke la funzione GetLastError() è sbagliato
• DLLImport provvede alla proprietà SetLastError
• L'errore viene letto subito dal CLR e messo da parte
• Può essere letto con il metodo Marshal.GetLastWin32Error()
[DllImport("User32.dll", SetLastError=true)]
extern static int MessageBox(IntPtr hWnd, string Text, string Caption, int uType);
Uno sguardo in memoria
• Gli oggetti del CLR
devono stare nel
managed heap
• I buffer, blob, strutture
dati 'classiche' devono
stare nell'unmanaged
heap
• Il problema è quindi il
marshalling dei dati, cioè
copiare e/o mappare i
dati da un mondo all'altro
• La bella notizia è che le
tecniche di marshalling
valgono anche per
l'interoperabilità COM
Il problema del marshalling
"Come mappare i tipi?"
Unmanaged heap
char[...]
Managed heap
String
Bitmap
byte[...]
BSTR
FILETIME
int
ref Bitmap
ref String
DateTime
Int32
Managed Stack
Marshalling: blittable types
• I blittable types sono i tipi che hanno una rappresentazione
identica sia nel mondo managed che unmanaged:
•
•
•
•
•
•
•
•
•
•
•
Byte, SByte
Int16, UInt16
Int32, UInt32
Int64, UInt64
IntPtr, UIntPtr
Single
Double
Array monodimensionali (UInt32[], Byte[], ...)
Value types (strutture) contenenti i tipi appena citati
Array e classi contenenti blittable types vengono "pinned" in memoria invece di
copiare il loro valore nel mondo unmanaged.
Anche se "pinned" richiedono comunque l'applicazione degli attributi direzionali [in]
o [out] quando richiesti
Marshalling: le stringhe
• CharSet influenza la scelta Ansi / Unicode di tutti i parametri
• Si può applicare [MarshalAs(...)] al parametro
• La direzione del parametro è fondamentale
• Se la stringa è [in] nella API, si usa String
• Se la stringa è [out] o [in, out] si usa StringBuilder
preallocando lo spazio
MSDN
[DllImport("Kernel32.dll")]
extern static uint GetCurrentDirectory(uint Size, StringBuilder Buffer);
[DllImport("User32.dll)]
extern static int MessageBox(IntPtr hWnd, string Text, string Caption, int uType);
• Da MSDN:
Normalmente Win9x
Normalmente WinNT +
Marshalling: strutture di dati
• Il CLR non garantisce l'ordine e dimensione dei dati nella struttura
• il CLR ottimizza l'allineamento dei dati guadagnando spazio e performance
• la dimensione di un oggetto managed è sempre e solo presunta
(Marshal.SizeOf indica la dimensione dell'oggetto dopo il marshalling)
• a questo proposito: http://forum.ugidotnet.org/b.asp?m=13367
• Per ovviare a questo "problema" esiste StructLayoutAttribute
• Pack consente di imporre l'allineamento
(il default è 8)
• Size consente di imporre la dimensione
della struttura (lasciare spazio, ...)
• Si possono anche realizzare le "Union"
grazie a "Explicit"
• Si usa ref o out (ByRef) quando si vuole
passare il puntatore all'oggetto.
Diversamente viene passato by value
[StructLayout(LayoutKind.Sequential)]
struct MyStruct
{
char c;
int i;
short s;
}
union MyUnion
{
int x;
float f;
}
[StructLayout
(LayoutKind.Explicit)]
struct MyUnion
{
[FieldOffset( 0 )]
int x;
[FieldOffset( 0 )]
float f;
}
Marshalling di array
• Possono esistere più dichiarazioni PInvoke valide per una stessa chiamata
• Usare la falsa riga di dichiarazioni come quelle di www.pinvoke.net
• Array di stringhe
int f(char **ppArray, int size);
[ DllImport("...")]
public static extern int f([in, out]string[] strings, int size);
• Array di strutture
int f(POINT *pPOINT , int size);
[ DllImport( "..." )]
public static extern int f([In, Out] MyPoint[] points, int size);
• L'attributo MarshalAs permette di informare il CLR quando la dimensione è
prefissata
[MarshalAs(UnmanagedType.ByValArray, SizeConst=128)]
• A volte è necessario fare manualmente
• Il parametro viene dichiarato IntPtr (puntatore)
• Si usano i metodi StructureToPtr e PtrToStructure della classe Marshal
• Alla fine li vedremo in un esempio
•
Tip: In caso di NullReferenceException, provare a imporre il parametro ref (ByRef)
Marshalling di callback
• Le Callback usate nelle Win32 API sono puntatori a funzioni
• .NET non ha puntatori a funzioni, ma i delegate che sono type-safe
• Esempio EnumDesktopWindows
[DllImport("User32.dll")]
public extern static bool EnumDesktopWindows(IntPtr hDesktop,
EnumWindowsProcDelegate lpfn, uint lParam);
public delegate bool EnumWindowsProcDelegate(IntPtr hwnd, uint lParam);
Pinning della memoria
managed
• Quando si passa un reference type viene copiato il reference
e fatto il pinning della memoria nel managed heap
• il pinning automatico funziona spesso ...
• ... ad esempio non può funzionare per le chiamate asincrone
• Quando si passa un value type il valore viene copiato nella
memoria unmanaged
• Se si può usare un value type, questo risolve il problema di cui sopra
• Per eseguire il pinning da C# e VB si usa GCHandle
• Free rilascia l'handle e permette al GC di muovere il blocco di memoria
string s = "blah blah";
GCHandle pin = GCHandle.Alloc(s, GCHandleType.Pinned);
try { ... }
finally { pin.Free(); }
Agenda
• Platform Invocation Services
• COM
•
•
•
•
Usare oggetti COM in dotnet
Usare Activex in dotnet
Esporre oggetti COM da dotnet
Esporre Activex da dotnet
• Reverse P/Invoke
Il mondo COM
• Ci sono molte differenze
• Serve quindi un wrapper
• I wrapper sono diversi a seconda se si espone un oggetto COM a
.NET o viceversa
COM
.NET
CreateInstance
new
Reference count
Garbage Collector
QueryInterface
cast
Errori via HRESULT
Eccezioni
Guid
Strong name
Type library
Metadati
Apartments
MTA-style
Usare oggetti COM da .NET
• Visual Studio.NET
• TLBimp
• Una volta disponibile l'assembly di interop
• Gli oggetti COM si usano come fossero managed (si usa il wrapper)
• Si istanziano con new e si chiamano proprietà e metodi
• addref/release sono gestiti dall'infrastruttura (RCW)
Dietro le quinte
• RCW = Runtime Callable Wrapper
•
•
•
•
•
creato al runtime per parlare con il mondo com
si occupa di eseguire il marshalling dei tipi
esiste un RCW per ogni oggetto (non interfaccia) COM
si occupa di chiamare Addref/Release/QueryInterface
L'oggetto referenziato da RCW è scaricato dalla memoria se il suo
reference counter è 0 e cioè se:
• RCW viene scaricato dal Garbage Collector oppure
• viene fatto un esplicito Marshal.ReleaseComObject
• RCW wrappa IEnumVariant, IDispatch, IUnknown, IErrorInfo
.NET
RCW
COM
Come customizzare RCW
• Si può customizzare RCW in due modi
• Modificando la type library
• si ricava l'IDL (se non è dispobile si può importare con il typelib viewer di
Oleview.exe)
• si aggiungono gli attributi IDL grazie alla keyword custom che permette di
modificare namespace e il nome dotnet del wrapper:
• si ricompila con midl /Oicf
• si importa con TLBImp oppure VS.net
• Modificando l'RCW
• si decompila l'assembly generato da TLBImp
• si modifica l'IL
• si ricompila
COM da .NET: TLBImp
[ComImport, ClassInterface((short) 0), Guid("EF70C490-E7FE-11D2-AB0D-00A02457BBE9"), TypeLibType((short) 2),
ComSourceInterfaces("mailctl.interop._IMailerEvents\0")]
public class MailerClass : IMailer, Mailer, _IMailerEvents_Event
{
// Methods
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType=MethodCodeType.Runtime), DispId(3)]
public virtual extern void IsValidName([In, MarshalAs(UnmanagedType.BStr)] string Recipient);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType=MethodCodeType.Runtime), DispId(2)]
public virtual extern void Logoff();
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType=MethodCodeType.Runtime), DispId(1)]
public virtual extern void Logon([In, MarshalAs(UnmanagedType.BStr)] string Profile, [In] int bUI);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType=MethodCodeType.Runtime), DispId(4)]
public virtual extern void SendMail([In, MarshalAs(UnmanagedType.BStr)] string Subject,
[In, MarshalAs(UnmanagedType.BStr)] string Message,
[In, MarshalAs(UnmanagedType.BStr)] string OriginatorAddress,
[In, MarshalAs(UnmanagedType.BStr)] string AddrTo,
[In, MarshalAs(UnmanagedType.BStr)] string AddrCc,
[In, MarshalAs(UnmanagedType.BStr)] string AddrCcn,
[In, MarshalAs(UnmanagedType.BStr)] string PathNames,
[In] int bReceipt, [In] int bUI);
// Properties
[DispId(5)]
public virtual int MapiIsInstalled {
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType=MethodCodeType.Runtime), DispId(5)] get; }
}
TLBImp usa
la classe
TypeLibConverter
Usare ActiveX in dotnet
• Winform può solo usare controlli che derivano da Control
• Ci vuole quindi un wrapper 
• Il controllo wrapper è AxHost che deriva da Control e ospita l'ActiveX
• Come si genera il wrapper?
• Aggiungendo alla toolbar dei controlli l'ActiveX
• Usando da command prompt l'utility AxImp.exe
• Si può customizzare il comportamento?
• si, anche se serve raramente
• il punto di partenza è generare il wrapper e guardarlo con reflector
• Si può usare un ActiveX facendo Add-Reference?
• dipende dal controllo ma nella maggior parte dei casi no perchè
l'ActiveX esegue il dispatch dei messaggi tramite message pump della
finestra che viene creata solo se è 'visuale'
Primary Interop Assembly
PIA
•
Devono essere fornite dall'autore del componente COM
•
•
•
•
•
In caso di componenti esterni referenziati
•
•
•
Deve esistere una sola PIA per ogni oggetto COM
Una sola PIA può gestire più versioni dello stesso oggetto COM
Devono essere strong-named
Devono esporre tutti gli oggetti COM originali con gli stessi guid
Non li devono ridefinire
Devono referenziare le loro PIA
Devono essere marcati con l'attributo PrimaryInteropAssemblyAttribute
1. Usare TLBImp
2. Creare il wrapper manualmente assegnandogli:
•
•
•
strong name
GuidAttribute (LIBID guid)
attributo PrimaryInteropAssemblyAttribute
Agenda
• Platform Invocation Services
• COM
•
•
•
•
Usare oggetti COM in dotnet
Usare Activex in dotnet
Esporre oggetti COM da dotnet
Esporre Activex da dotnet
• Reverse P/Invoke
Esporre oggetti COM da .NET
• CCW = COM Callable Wrapper
•
•
•
•
creato al runtime
si occupa di eseguire il marshalling dei tipi
esiste un CCW per ogni oggetto dotnet
l'oggetto dotnet come sempre sarà dato al GC solo se tutti i reference
(del CCW ed altri) vengono rilasciati
• CCW espone sempre una "dual interface" (IUnknown + IDispatch)
• CCW espone IEnumVariant (for each di vb6) per i tipi dotnet che
implementano IEnumerable, IErrorInfo/HRESULT per le eccezioni e
ovviamente IDispatch/IUnknown
.NET
CCW
COM
COM object step-by-step
• Creare una class library con strong name
• Dare una versione custom all'oggetto
• La differenza di versioning è un problema rilevante con COM
• Scrivere la classe dotnet
• Decorarla con gli attributi
di interop che customizzano
il comportamento del CCW
• Registrarla con Regasm
• Registrarla nella GAC
• Esportare la Type Library
regasm addercomserver.dll
gacutil -i addercomserver.dll
tlbexp addercomserver.dll /out:addercomserver.tlb
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
[Guid("D0C1F90F-59B2-4e06-B36F-CC835B14C2EC")]
public class Adder
{
public Adder()
{
}
public int Add(int x, int y)
{
return x + y;
}
public int Mul(int x, int y)
{
return x * y;
}
}
Registration-free
• Disponibile solo in Windows XP - SP2, e Windows 2003
• Evita la registrazione nel registry dei componenti COM
• È necessario scrivere un manifest Win32
• Vantaggi:
• più versioni dello stesso componente nello stesso PC
• xcopy deployment
•
http://msdn.microsoft.com/library/default.asp?url=/library/enus/cpguide/html/cpconConfiguringNET-BasedComponentsForRegistrationFreeActivation.asp
Realizzare ActiveX in dotnet
• Activex  specifiche OC96 + libro "Inside OLE"
• Per ActiveX si intende quindi oggetti 'visuali' e non i comuni oggetti
COM
• Task NON supportato ufficialmente da Microsoft
• Fattibile ma con non poche difficoltà. Strada sconsigliata.
• Per IE esiste la possibilità di mostrare controlli Winform in una
pagina html
• migliore sicurezza
• nessuna dialog di richiesta certificato
• tecnica poco diffusa in attesa della diffusione del framework
• Per le migrazioni di UI conviene il contrario
• Fare l'hosting di Winform dentro MFC, etc.
Performance
• PInvoke  10 istruzioni x86
• COM  50 istruzioni x86
• Ma poi c'è il marshalling (che è il più) dei parametri
•
•
•
•
Riduzione del numero di parametri
Riduzione del numero di funzioni da chiamare
Usare [in] e [out] per ridurre il numero di marshalling
In DllImport usare SetLastError=true solo se poi si chiama
Marshal.GetLastWin32Error()
Agenda
• Platform Invocation Services
• COM
•
•
•
•
Usare oggetti COM in dotnet
Usare Activex in dotnet
Esporre oggetti COM in dotnet
Esporre Activex in dotnet
• Reverse P/Invoke
Reverse P/Invoke
• MSDN KB 318804 dice:
.... This requires a DLL export, which .NET Framework does not support.
Managed code has no concept of a consistent value for a function pointer
because these function pointers are proxies that are built dynamically.
• A che servono "DLL Export"?
•
•
•
•
Global hook
Control Panel Applets
...
Applicazione unmanaged chiama una API esposta da un assembly
• Si, si può fare! MA .... (c'è sempre un "ma")
• Certamente con C++, ma è troppo scontato 
• Non con VB.NET, neppure con C# ... ok, neanche con J#
Reverse P/Invoke
1.
2.
3.
4.
Creare un progetto Class Library (C# o VB.NET)
Scrivere la funzione da esportare come API
Compilarla e disassemblarla con ILDasm (ildasm /out=revsimple.il revsimple.dll)
Modificare l'IL come segue:
•
•
•
•
.corflags 0x00000002 invece di .corflags 0x00000001
Calling
Aggiungere in cima:
.vtfixup [1] int32 fromunmanaged at VT_01
Convention
.data VT_01 = int32(0)
Dentro la funzione da esportare aggiungere:
.method public hidebysig static int32
modopt([mscorlib]System.Runtime.InteropServices.CallConvStdCall)
CPlApplet(native int hwndCPl, unsigned int32 uMsg,
[in][out] native int lParam1, [in][out] native int lParam2) cil managed
{
Rinomina la
.vtentry 1 : 1
funzione esportata
.export [1] as CPlApplet
...........
5. Riassemblare: ilasm /out=hello.dll revsimple.il /dll
Reverse P/Invoke
Control panel managed applet
1. Class library con una funzione chiamata:
public static int CPlApplet(
IntPtr hwndCPl, uint uMsg, [In,Out] IntPtr lParam1, [In,Out] IntPtr lParam2)
2. Dichiarazione di CPLINFO, NEWCPLINFO, CPL_...
3. Marshalling manuale dei parametri perché la lParam1 e
lParam2 possono puntare a strutture diverse (CPLINFO o
NEWCPLINFO)
case CPLMessages.CPL_INQUIRE:
AppletNumber = lParam1.ToInt32();
Info = (CPLINFO)Marshal.PtrToStructure(lParam2, typeof(CPLINFO));
Info.idIcon = 101;
Info.idName = 201;
Info.idInfo = 202;
Info.lData = (IntPtr)1;
numeri delle risorse (prox slide)
Marshal.StructureToPtr(Info, lParam2, true);
return 0;
Control panel managed applet
Il problema delle risorse
4. Preparazione del file di risorse "resources.rc":
101
ICON
DISCARDABLE
"Italy.ico"
icona applet
STRINGTABLE DISCARDABLE
BEGIN
nome sotto l'icona
201
"Managed Applet"
202
"Mostra le opzioni del managed applet"
END
descrizione a fianco all'icona
5. Compilazione delle risorse in res
(command prompt di Visual Studio):
rc resources.rc
6. Eventualmente aggiungere al file resources.rc le informazioni
di versione perché quelle di vs.net vanno perse
Reverse P/Invoke
Control panel managed applet
7. Embedding di risorse Win32 nell'assembly:
c:\dev.net\Demo\Interop\RevPInvoke>csc
/resource:obj\Debug\RevPInvoke.InteropWorkshop.resources
risorse unmanaged (win32)
/win32res:resources.res
dll
/target:library
nome dll
/out:bin\debug\RevPInvoke.dll
compilare tutti i sorgenti
*.cs
risorse
managed
8. In alternativa usare "Resource Hacker" importando il file .res:
http://www.download.com/3000-2352-10178588.html
9. Registrarlo:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Control Panel\Cpls
ManagedApplet = path del .cpl
Domande?
Scarica

- Microsoft