- Esporre funzioni "alla C" da VB (6 e .Net) -
 
COSA SERVE PER QUESTO TUTORIAL
Download | Chiedi sul FORUM | Glossario Una qualunque versione di Visual Studio
Un metodo alternativo di interoperazione VB6-.Net

INTRODUZIONE
Obiettivo dell'articolo: export "alla C" da VB

In questo articolo vedremo come è possibile far interoperare VB6 con .Net senza coinvolgere COM. Per ottenere questo risultato creeremo una libreria di classi in VB.Net (una DLL) della quale alcune funzioni saranno esportate "alla C" (ovvero come si usa fare in C attraverso exports.def). La prima parte del tutorial introdurrà la tecnica utilizzata attraverso un semplice esempio, mentre la seconda spiegherà come impostare un hook globale utilizzando solamente VB (6 e .Net): questo sarebbe normalmente impossibile senza una DLL esterna scritta in C/C++, ma facendo un export C-like attraverso .Net lo faremo.

GETHASHCODE IN VB6
Un semplice esempio di interoperazione tra VB6 e .Net: GetHashCode()

Ogni buon programmatore .Net conosce il metodo GetHashCode della classe String: "restituisce il codice hash per questa stringa". Supponiamo ora che, per qualche motivo, si necessiti di utilizzare l'algoritmo di String.GetHashCode in VB6. Vediamo un modo intelligente per farlo.
Il codice VB6 (progetto VB6Hasher) è estremamente semplice: è composto da un form con due caselle di testo e un bottone. Cliccando sul bottone la seconda casella verrà a contenere il codice hash del testo nella prima.


Private Declare Function JustHash Lib "HashExporter.Net.dll" (ByVal str As String) As Long

Private Sub btnHash_Click()
    txtHash.Text = JustHash(txtInput.Text)
End Sub

Chi ha famigliarità con VB6 avrà già notato l'istruzione Declare che si riferisce ad una semplice funzione in una DLL esterna realizzata in VB.Net (progetto HashExporter.Net):


Public Function JustHash(ByVal str As String) As Integer
    Return str.GetHashCode()
End Function

Ovviamente questa sintassi non è sufficiente per realizzare un export "alla C" (ovvero accessibile da VB6 attraverso l'istruzione Declare), infatti normalmente non è possibile fare questo genere di export in VB.Net, né in C# o in qualunque altro linguaggio .Net, eccetto ILAsm (IL assembly language).

COME FARE EXPORT C-LIKE IN .NET
Modificare il codice ILAsm del programma per creare un export.

Dopo aver compilato il progetto HashExporter.Net, aprite il Visual Studio Command Prompt (Start menu, Programmi, Microsoft .NET Framework SDK, SDK Command Prompt), raggiungete la cartella che contiene HashExporter.Net.dll e digitate:

ildasm /out:HashExporter.Net.il HashExporter.Net.dll

Questo comando estrae il codice ILAsm dalla DLL compilata: con un "dir" potrete vedere che sono stati generati i file HashExporter.Net.il e HashExporter.Net.res. Aprite ora con un qualunque editor di testo (il Blocco Note andrà benissimo) il file HashExporter.Net.il. La prima cosa da fare è individuare la linea che comincia con ".corflags" e quindi rimpiazzarla con ".corflags 0x00000002" (in genere il valore iniziale è 0x00000001): questo significa che il codice funzionerà solamente sotto win32. È inoltre necessario aggiungere dopo questa linea quanto segue:


.vtfixup [1] int32 fromunmanaged at VT_01
.data VT_01 = int32(0)

Questo funzionerebbe nel nostro caso perché necessitiamo di esportare una funziona sola, ma naturalmente è possibile eseguire quanti export si desidera:


.vtfixup [1] int32 fromunmanaged at VT_01
.vtfixup [1] int32 fromunmanaged at VT_02
.vtfixup [1] int32 fromunmanaged at VT_03
.data VT_01 = int32(0)
.data VT_02 = int32(0)
.data VT_03 = int32(0)

A questo punto individuate la dichiarazione della classe che contiene il metodo da esportare (es. ".class private auto ansi sealed HashExporter.Net.mdlHashExporter"), all'interno del blocco della classe cercate ".method public static int32 JustHash(string str) cil managed", e appena dopo l'aperta parentesi graffa aggiungete il seguente codice:


.vtentry 1:1
.export [1] as JustHash

o più in generale:


.vtentry #:1
.export [#] as exported_function_name

Dove # indica il numero dell'export (il primo 1, il secondo 2 e così via) e exported_function_name rappresenta il nome con cui la funzione sarà esposta.
A questo punto è possibile ricompilare il codice da linea di comando:

ilasm "HashExporter.Net.il" /DLL /OUT:"HashExporter.Net.dll" /RESOURCE:"HashExporter.Net.res"

Se avrete compiuto tutti i passaggi correttamente, aprendo con Dependency Walker (un tool di Visual Studio) la nuova DLL (HashExporter.Net.dll) vedrete sulla destra il nome della funzione esportata!
Come avrete notato questo processo non è comodo, soprattutto considerando che, testando un progetto, la DLL viene ricompilata moltissime volte e ripetere la procedura così spesso risulta impossibile, è dunque consigliabile automatizzare questo processo, ma se non si desidera farlo da sé si può usare questo tool.
Ora possiamo provare ad avviare VB6Hasher.exe e vedere cosa succede.

QUALCOSA DI PIÙ COMPLESSO: GLI HOOK
Impostare hook globali o a thread di processi esterni da VB6 grazie a .Net

Gli hook di Windows sono una sorta di metodi di subclassing che non si associa ad una singola finestra ma ad un intero thread o addirittura all'intero sistema (inteso come desktop). Si tratta in sostanza di un filtro dei messaggi di Windows che permette di modificare o aggiungere funzionalità alla ricezione di un particolare messaggio. Ecco la definizione che MSDN riporta di hook:

A hook is a point in the system message-handling mechanism where an application can install a subroutine to monitor the message traffic in the system and process certain types of messages before they reach the target window procedure.

Per impostare un hook si usa la funzione SetWindowsHookEx contenuta in user32.dll. Eccone la dichiarazione per VB6:


Private Declare Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" _
    (ByVal idHook As Long, ByVal lpfn As Long, ByVal hmod As Long, ByVal dwThreadId As Long) As Long

Chi ha provato ad usare SetWindowsHookEx in VB6, molto probabilmente saprà quale limitazione questo linguaggio impone. Infatti MSDN dice:

If the dwThreadId parameter is zero or specifies the identifier of a thread created by a different process, the lpfn parameter must point to a hook procedure in a dynamic-link library (DLL).

Creando un hook bisogna passare a SetWindowsHookEx un puntatore ad una funzione (parametro lpfn) che riceverà i messaggi filtrandoli. Come ognuno saprà per supplire alla mancanza di puntatori VB6 fornisce l'operatore AddressOf, ma esso risulterebbe utile solamente nel caso in cui si stia tentando di impostare un hook su un thread del processo corrente. Infatti come è scritto su MSDN se il thread specificato non appartiene al processo chiamante oppure è 0 (indica quindi l'intero sistema), il parametro lpfn deve riferirsi ad una funzione in una DLL esterna, ma è noto a tutti che VB6 non può esporre funzioni "alla C". Per questo motivo risulta impossibile creare un hook globale o associato al thread di un altro processo.

ENTRA IN GIOCO .NET
La DLL esterna

Il progetto HookCExport.Net contiene una funzione chiamata HookWndProc che è definita esattamente come CallWndProc. Ripetiamo dunque su di essa il processo spiegato nella prima parte del tutorial:

  1. Compilare il progetto HookCExport.Net;
  2. Usare ildasm su HookCExport.Net.DLL;
  3. Apportare le necessarie modifiche a HookCExport.Net.il come visto in precedenza;
  4. Ricompilare con ilasm;
  5. Controllare con Dependency Walker se in HookCExport.Net.DLL è visibile HookWndProc;

Importante: c'è un altro fondamentale passaggio per evitare il crash del sistema. Se si imposta un hook globale la funzione che riceve i messaggi verrà richiamata moltissime volte al secondo e per questo HookWndProc deve essere molto veloce, altrimenti tutto si rallenterà fino al crash completo. Come noto le applicazioni .Net sono compilate alla prima esecuzione dal compilatore JIT, dunque anche la nostra DLL. Questo è molto pericoloso poiché, mentre la libreria di classi viene compilata, vengono eseguite decine o centinaia di chiamate alla stessa funziona al suo interno, portando alla paralisi del sistema. La soluzione è forzare la compilazione manualmente dal prompt dei comandi:

ngen install "HashExporter.Net.dll"
TORNIAMO A VB6
L'interfaccia VB6 con la DLL esterna

Questo è il cuore del progetto VB6HookListener, che si trova nel modulo mdlHooking:


'Crea l'hook
Public Sub Hook(hThread As Long)
    Dim hProc As Long
    'Carica la DLL esterna
    hModule = Chk(LoadLibrary("HookCExport.Net.DLL"))
    'Prende l'indirizzo della funzione di callback della DLL esterna
    hProc = Chk(GetProcAddress(hModule, "HookWndProc"))
    'Imposta l'hook usando l'handle della DLL e il puntatore della funzione appena
    'ottenuto
    hHook = Chk(SetWindowsHookEx(WH_CALLWNDPROC, hProc, hModule, hThread))
    [...]
End Sub

Come abbiamo già detto, il puntatore a funzione passato a SetWindowsHookEx deve essere in una DLL esterna, per questo abbiamo usato LoadLibrary e GetProcAddress che rispettivamente caricano la libreria in memoria e prendono un puntatore alla funzione specificata (nel nostro caso HookWndProc). A questo punto è lecito chiedersi perché non abbiamo utilizzato una semplice istruzione Declare, la risposta è semplice: in primo luogo non è possibile utilizzare AddressOf su una funzione dichiarata con Declare, inoltre non sarebbe possibile ottenere l'handle (l'indirizzo) del modulo caricato, che è richiesto da SetWindowsHookEx.
L'hook è stato così impostato. La spiegazione del resto del codice trascende lo scopo di questo articolo.

CONCLUSIONI
Cosa abbiamo ottenuto

In questo articolo abbiamo visto come fare uso delle potenzialità di .Net da VB6 per sfruttare tecniche altrimenti inutilizzabili, senza pesanti wrapper COM, ma solamente esponendo alcuni metodi in maniera C-like attraverso VB .Net e ILAsm. Questa tecnica è davvero utile ogni volta che si necessita di usare le possibilità a basso livello di .Net (proprio come impostare hook globali). Non sarà mai più necessario chiedere aiuto a papà-C!
 

<< INDIETRO by VeNoM00