Utilizzare codice C/C++ con Visual Basic
a cura di Antonio Giuliana (requisiti: conoscenza avanzata di VB e generica di VC++ )

Introduzione
Lo scopo di questo, e probabilmente, dei prossimi miei articoli su VB e VC, è quello di definire alcune semplici regole per permettere a del codice scritto in questi due linguaggi di interagire in modo sicuro. Cercherò di usare un linguaggio quanto più semplice possibile, per permettere anche a chi si avvicina a queste problematiche da poco tempo di acquisire quante più conoscenze possibili. Tuttavia, mi rendo conto che il lettore che non ha molta confidenza con elementi quali lo stack, i puntatori e quant’altro non strettamente collegato al VB, potrà avere qualche difficoltà a recepire immediatamente le tecniche esposte; anche per questo ho affiancato, alla parte teorica, una pratica, con la costruzione di una DLL d’esempio del cui codice sorgente è possibile effettuare il download. Come sempre, sono a disposizione tramite e-mail (possibilmente nella ML quando il problema esposto potrebbe interessare tutti) per chiarimenti e spiegazioni.

Com’è noto, il codice VB può richiamarne altro scritto in C a patto che quest’ultimo sia contenuto in una DLL creata appositamente.
La necessità di eseguire codice scritto in VC si presenta, in molti casi, tra i quali (ma non solo):

Sebbene, in realtà, sia semplice usare del codice scritto in C da VB, i problemi che si incontrano con maggiore frequenza sono i seguenti:

Anche se è possibile definire alcune regole che risolvono in maniera sicura i problemi esposti, è sempre necessario esaminare, caso per caso, le difficoltà, superandole facendo uso di documentazione adeguata (MSDN, Technet). Informazioni preziose si possono ottenere anche nei file sorgenti messi a disposizione dal VC (in particolare nei file include utilizzabili nei progetti). In essi, infatti, sono definite costanti, strutture e funzioni, molte volte non rintracciabili facilmente nella documentazione a corredo dei vari tool o, quantomeno, corrette e aggiornate.

Un ultimo, ma importante discorso, va fatto per quanto riguarda il debug (inevitabile) di tali applicazioni in quanto, spesso, è necessario controllare il codice C passo passo per individuare dei problemi. A tale proposito, vedremo alcune tecniche che ci permetteranno di gestire e recuperare alcune situazioni di errore.

Creare una DLL correttamente
Sebbene la teoria sia importante (in quanto alla base dei progetti realizzati), è quasi sempre necessario accompagnare tali nozioni ad esempi pratici.
È per tale ragione che, in questo articolo, descriverò fase per fase la creazione, implementazione ed uso di una DLL servendomi di un esempio da cui si potrà prendere spunto per le proprie esigenze. Un esempio pratico è comunque necessario per determinare il ‘contesto’ in cui valgono le regole fornite.

Iniziamo dagli strumenti usati. Essi sono il Visual Basic 6 (Service Pack 5) e il Visual C++ 6 EE anche se, ritengo, altri SP di VB possano andare bene (e lo stesso dicasi per la versione 5 di VB). Eventuali differenze di funzionamento possono essere rilevate con particolari combinazioni di SO, VB e VC; le soluzioni vanno cercate caso per caso. Nell’articolo cercherò di dare delle indicazioni in modo che anche chi non ha molta confidenza con VC possa seguire i concetti espressi.

Il tipo di progetto da creare (dal menu File-New) è quello denominato ‘MFC AppWizard (dll)’ che consente di utilizzare la Microsoft Foundation Class; quindi, si dovrà parlare di codice C++ piuttosto che C.
 
Alla destra della finestra di dialogo inseriremo il nome del progetto (VBTTML) che verrà aggiunto automaticamente alla directory da noi predefinita per i progetti di VC e che determinerà la posizione di tutti i file appartenenti al progetto stesso (Location).
La successiva schermata (l’unica del wizard che crea il progetto) consente di scegliere il tipo di rapporto che la nostra DLL deve avere con la MFC DLL (nel nostro caso, Regular DLL using shared MFC DLL), l’eventuale supporto per l’Automation e i Windows Socket (non usati nel nostro esempio) e, infine, la possibilità di avere inseriti nel codice dei commenti per l’implementazione (utili).

Dopo la conferma della creazione del progetto, nella directory prescelta vengono creati tutti i file necessari all’implementazione base della DLL (file .cpp, .h, .def, .rc) la cui lista è disponibile tramite la FileView (vedi figura).

I file che modificheremo saranno solamente tre e cioè:
  1. VBTTML.cpp – che conterrà il codice C++ per l’implementazione delle funzioni che saranno richiamate da VB;
  2. VBTTML.h – che conterrà la definizione delle costanti e la dichiarazione di funzioni utilizzate nel sorgente C++;
  3. VBTTML.def – che, vedremo, sarà necessario per esportare i nomi delle funzioni disponibili alla nostra applicazione VB.

Una volta creata la DLL tramite il compilatore, questa verrà usata specificandone il path completo in ogni Declare usata nel codice VB (per la dichiarazione di ogni funzione in essa contenuta). Tuttavia, copiando la DLL in una directory di sistema (quale C:\WINDOWS o C:\WINNT), non risulta più necessario specificarne il path completo ma solo il nome.

Definizione di una funzione
I punti piùimportanti per il buon funzionamento del codice C da VB sono tre e precisamente: la definizione della funzione da richiamare, il passaggio dei parametri e la restituzione dei valori.

Per il primo punto, la definizione della funzione all’interno della DLL appena creata, il discorso è molto semplice in quanto è sufficiente seguire due semplici regole:

  1. utilizzare lo standard di chiamata _stdcall
  2. includere il nome della funzione nel file di definizione (.def)

Visual Basic usa lo standard _stdcall nella chiamata di funzioni. Questo prevede che lo spazio utilizzato nello stack sia liberato al termine dell’esecuzione dalla funzione chiamata e non dalla chiamante. Così facendo, il runtime VB non è responsabile dell’utilizzazione dello stack per le funzioni chiamate e nessuna operazione viene effettuata dallo stesso per recuperare lo spazio eventualmente occupato dagli argomenti e dalle variabili locali. Se dichiarate con tale parola chiave, nel codice delle funzioni C (e C++) viene generato codice aggiuntivo per il cleanup dello stack cosa che non viene fatta normalmente.

Per default, infatti, lo standard di chiamata usata dalle funzioni C è il _cdecl che prevede il cleanup dello stack da parte della funzione chiamante. È per questo motivo che, in mancanza di tale specifica, VB evidenzia l’errore

Run-time Error ‘49’:
Bad DLL Calling Convention

(in verità, ciò succede con VB quando si lavora nell’IDE mentre un file eseguibile compilato con VB sembra lavorare correttamente anche con le funzioni esterne dichiarate con lo standard _cdecl; questo comportamento, tuttavia, è stato dichiarato come un bug da Microsoft e su di esso non si devono basare le proprie applicazioni, in quanto suscettibile di correzione nelle prossime release del linguaggio; per maggiori informazioni, può essere interessante leggere l’articolo Q153586 –HOWTO: Call C Functions That Use the _cdecl Call Convention).

Con VC quindi, un esempio di funzione richiamabile da VB, potrebbe essere il seguente,

long _stdcall NomeFunzione(long arg)

in cui viene normalmente dichiarata la funzione NomeFunzione che restituisce un long ed accetta un long, ma che segue lo standard _stdcall; sarà il compilatore a fare il resto. Naturalmente ciò vale solo per le funzioni da esportare (cioè, da richiamare attraverso il codice VB) mentre non è necessario per le funzioni definite all’interno della DLL ed usate internamente (come supporto alle funzioni esportate).

La seconda regola è necessaria per fare in modo che VB possa trovare nell’elenco delle funzioni disponibili nella DLL, quelle definite nelle frasi Declare (come per una qualsiasi API). Sebbene sia possibile utilizzare la _declspec(dllexport) per definire una funzione come esportata, il nome di quest’ultima verrebbe prima decorato dal compilatore (il nome verrebbe cambiato, cioè, in modo da gestire anche gli argomenti per eventuali override della funzione) e ciò porterebbe all’errore

Run-time Error ‘453’:
Can’t find DLL entry point <xxx> in <yyy.dll>

con il quale VB ci informerebbe di non trovare la funzione xxx dichiarata nella dll yyy. È facilmente verificabile tale comportamento utilizzando il tool Dependency Walker di S. P. Miller di Microsoft. Con tale utility, infatti, il nome della funzione esportata con la _declspec (dllexport) apparirebbe incomprensibile.

Per ovviare a tale inconveniente, è sufficiente dichiarare in maniera esplicita il nome che la funzione dovrà avere quando esportata scrivendolo nell’apposita sezione del file .DEF incluso nel progetto (EXPORTS).

La macro AFX_MANAGE_STATE(AfxGetStaticModuleState( )) inserita all’inizio di ogni funzione esportata è necessaria (come ricordato dalle note inserite dallo stesso compilatore) per proteggere la stessa funzione; maggiore documentazione è contenuta in MSDN.

Il passaggio dei parametri
Il passaggio degli argomenti sui quali la funzione C dovrà operare è un’operazione abbastanza delicata e costituisce la maggiore fonte di problemi. Infatti, una errata definizione di tali parametri quasi sempre degenera in eccezioni la cui causa non è facilmente individuabile. Un passaggio di dati per puntatore piuttosto che per valore (o viceversa) può causare l’accesso a zone di memoria errate (e comunque esterne all’area definita per il processo chiamante) che causa sempre errori evidenti (se va bene) o malfunzionamenti subdoli (nella maggioranza dei casi).

Le regole da seguire, per i dati più semplici, non sono molto complesse ma possono diventarlo per i parametri di tipo articolato (anche se la si vede dal punto di vista dell’ottimizzazione del codice).

La situazione più semplice è quando la funzione non accetta parametri in ingresso, nel qual caso è sufficiente usare la parola void nella lista degli argomenti (come tutti i programmatori C sapranno). Una funzione di esempio, che mostrerò in seguito, non accetta parametri e ritorna il nome della directory di sistema di Windows; essa viene dichiarata con

BSTR _stdcall GetWinDir(void)

In questo modo, viene dichiarato che la funzione GetWinDir non accetta alcun argomento (void), segue la convenzione di chiamata _stdcall e restituisce un valore di tipo BSTR (una stringa a lunghezza variabile). Il nome di tale funzione (GetWinDir) verrà anche incluso nel file .DEF per poterlo esportare correttamente e nel codice VB sarà dichiarata con una

Public Declare Function GetWinDir Lib “VBTTML.DLL” ( ) As String

per potere essere correttamente richiamata.

La GetWinDir utilizza la API GetWindowsDirectory e la sua utilità è chiaramente limitata (anche se così come è scritta provvede a ripulire la stringa restituita dai caratteri non necessari). Del resto, lo scopo di tale funzione (come di altre che seguiranno) è puramente didattico e deve essere inteso solo come scheletro da cui partire per la realizzazione di funzioni personalizzate.

Per passare un argomento di tipo long, come nella seguente funzione di esempio,

BSTR _stdcall ToBin(LONG Op)

è sufficiente dichiarare l’argomento di tipo LONG (in maiuscolo, predefinito negli include di VC ed equivalente al long) e in VB questo deve essere dichiarato ricevibile ByVal (per valore), così

Public Declare Function ToBin Lib “VBTTML.DLL” (ByVal Op As Long) As String

Anche la ToBin è dimostrativa e restituisce al VB una stringa di 32 caratteri contenente la rappresentazione binaria in complemento a due del valore long passato come parametro.

Per gli altri tipi di dati semplici di VB, le regole sono semplici e la seguente tabella riassume le equivalenze tra i tipi di dati semplici:
Tipo di dato VB
Tipo di dato VC
Note
Boolean
BOOL
(ovvero bool o long)
sia in VB sia in VC il tipo boolean equivale ad un long; il valore True viene rappresentato da –1, il False da 0; può essere passato per valore (ByVal in VB, BOOL in VC) o per indirizzo (ByRef in VB, *BOOL in VC);
Byte
BYTE
(ovvero unsigned char o signed char)
in VB sul tipo Byte non sono effettuati controlli sul segno; è possibile assegnare valori negativi ma anche valori maggiori di 127; è il codice scritto in C che opera tenendo conto del segno; può essere passato per valore (ByVal in VB, BYTE in VC) o per indirizzo (ByRef in VB, *BYTE in VC);
Integer
SHORT
(ovvero short int)
in VB il tipo Integer è segnato e non accetta valori maggiori di 32767; non è quindi possibile utilizzare il tipo unsigned short int; il tipo int non è equivalente in quanto a 32 bit; può essere passato per valore (ByVal in VB, SHORT in VC) o per indirizzo (ByRef in VB, *SHORT in VC);
Long
LONG
  (ovvero int, oppure long)
in VB il tipo Long è segnato e non accetta valori maggiori di 2147483647; non è quindi possibile utilizzare il tipo unsigned long; il tipo int è equivalente al long in quanto, per default, a 32 bit; può essere passato per valore (ByVal in VB, LONG in VC) o per indirizzo (ByRef in VB, *LONG in VC);
Single
FLOAT
(ovvero float)
i due tipi sono equivalenti; può essere passato per valore (ByVal in VB, FLOAT in VC) o per indirizzo (ByRef in VB, *FLOAT in VC); il formato è IEEE
Double
DOUBLE
(ovvero double)
i due tipi sono equivalenti; può essere passato per valore (ByVal in VB, DOUBLE in VC) o per indirizzo (ByRef in VB, *DOUBLE in VC); il formato è IEEE
Variant
VARIANT
i due tipi sono equivalenti; può essere passato per valore (ByVal in VB, VARIANT in VC) o per indirizzo (ByRef in VB, *VARIANT  in VC); per trattare i VARIANT si possono utilizzare le ‘VARIANT Manipulation API Functions’;
String
String *n
BSTR
sia le stringhe a lunghezza variabile sia quelle a lunghezza fissa devono essere passate per valore; in VB le stringhe sono dei tipi BSTR e così vanno dichiarati in VC; (ByVal in VB, BSTR in VC); le stringhe in VB sono in formato UNICODE ma lo stesso VB provvede a trasformarle in formato ANSI prima che il codice C venga eseguito;

Un semplice esempio di funzione che riceve ed elabora una stringa è la Reverse, dichiarata come segue,

BSTR _stdcall Reverse(BSTR Op)

che provvede a invertire il contenuto della stringa ricevuta e a restituirlo in uscita.

In VB la funzione deve essere dichiarata utilizzando il ByVal:

Public Declare Function Reverse Lib “VBTTML.DLL” (ByVal Op As String) As String

Nell’usare da VB la Reverse, si possono usare stringhe a lunghezza sia fissa che variabile.

Per le funzioni che devono accettare in ingresso dei tipi di dati complessi (dati di tipo utente) o degli array, il discorso si fa un tantino più articolato e verrà affrontato nei prossimi paragrafi.

Il passaggio di dati di tipo utente
Il noto costrutto Type...End type del VB provvede (similmente alla struct del C) a dichiarare una struttura dati di tipo personalizzato utilizzabile in molte occasioni. Spesso, infatti, non è sufficiente utilizzare dei dati di tipo semplice e l’utilizzo di variabili strutturate risolve facilmente i problemi. Nel passaggio dei tipi di dato utente (UDT per brevità), bisogna prestare molta attenzione e seguire alcune regole.

Per iniziare, gli UDT devono essere passati per riferimento e non per valore (non deve essere usata la ByVal; per default viene quindi utilizzata la ByRef dal VB). Come conseguenza, nel codice C, i parametri che dovranno ricevere tali informazioni dovranno essere dei puntatori a strutture simili a quelle usate in VB.

Una semplice variabile UDT può contenere dati dei tipi semplici compresi stringhe sia a lunghezza fissa sia a lunghezza variabile. Nel caso delle stringhe bisogna tenere in considerazione il fatto che VB provvede in maniera trasparente a convertirle dal formato UNICODE all’ANSI. Attenzione però che questa conversione automatica non viene effettuata nel caso in cui venga passato un array di UDT (ma per gli array rimando al paragrafo successivo).

Le stringhe a lunghezza fissa saranno definite in C come array di CHAR mentre quelle a lunghezza variabile verranno definite di tipo BSTR.
Ecco quindi che un UDT che in VB si presenta in questo modo

  Type Test
  	b as Byte
	s1 as String * 10
	s2 as String
  End Type
  

sarà reso in C con la struttura seguente

  struct Test
    {
		BYTE b;
		CHAR s1[10];
		BSTR s2;
	};
  

tenendo presente che un’ipotetica funzione in C dovrà ricevere il puntatore alla struttura, come nell’esempio seguente

long _stdcall Sample(struct Test *Op)

mentre in VB sarà dichiarata con

Public Declare Function Sample Lib “VBTTML.DLL” (ByRef Op As Test) As Long

Ad uso didattico, ho inserito nella DLL una funzione (la SetVString) che non fa altro che riempire una stringa a lunghezza variabile con un carattere (come farebbe la funzione String di VB) con la differenza che questi due parametri sono passati all’interno di un UDT.
La struttura in VB è la seguente

  Type SS
  	chSet As String * 1
	sToSet As String
  End Type
  

mentre in C è

  struct SS
  	{
	CHAR chSet;
	BSTR sToSet;
	};
  

Da notare nel codice C l’uso della funzione SysStringByteLen per determinare la lunghezza in byte della stringa in quanto trasformata in ANSI (con la SysStringLen si avrebbe un risultato inaspettato dato che questa funzione ritorna il valore in caratteri considerando la stringa in formato UNICODE).

La variabile UDT, come detto in precedenza, viene passata dal VB al VC per riferimento (ByRef nella Declare).

Il passaggio di array
Per passare degli array dal codice VB a quello VC bisogna tenere conto del fatto che gli array in VB non sono affatto equivalenti a quelli del C. VB usa infatti i SAFEARRAY di OLE, che non sono altro che strutture contenenti tutte le informazioni (numero di dimensioni, elementi per dimensione e puntatore ai dati) necessarie a gestire un array; in un array C, invece, tali informazioni non esistono in quanto, essenzialmente, essi sono costituiti da una semplice serie di byte in memoria.

In particolare, la struttura di un SAFEARRAY è la seguente

  typedef struct tagSAFEARRAY
  	{
	USHORT cDims;
	USHORT fFeatures;
	ULONG cbElements;
	ULONG cLocks;
	PVOID pvData;
	SAFEARRAYBOUND rgsabound[ 1 ];
	} SAFEARRAY;
  

e quella della SAFEARRAYBOUND presente in essa è

  typedef struct  tagSAFEARRAYBOUND
  	{
	ULONG cElements;
	LONG lLbound;
	} SAFEARRAYBOUND;

Esiste, inoltre, un certo numero di funzioni associate agli array che costituiscono le Array Manipulation API Functions e tramite cui si possono allocare, modificare e distruggere SAFEARRAY.

Quando si passa un argomento di tipo array da VB a VC, il primo fornisce un puntatore ad un puntatore ad una struttura SAFEARRAY (**SAFEARRAY) che si può utilizzare nel codice C. È buona norma non accedere direttamente alla struttura tramite il doppio puntatore (anzi, è proprio sconsigliabile), ma utilizzare esclusivamente le funzioni di gestione messe a disposizione da Win32 (anzi, dal sottosistema OLE).

Non posso, in questo articolo, soffermarmi sulle funzioni di gestione degli array (per questo è meglio consultare la documentazione adeguata, tipo MSDN), ma qualche piccolo esempio lo vedremo tra due paragrafi. Adesso vedremo invece le differenze e le particolarità riguardanti il passaggio di array a funzioni C, in particolare quando questi sono di tipo stringa o UDT.

Per iniziare, vediamo, con un esempio pratico, come è possibile passare e gestire un array di long ad una funzione C per ordinarne gli elementi molto velocemente. A tal fine ho scritto una semplice funzione che, sfruttando la qsort (della libreria standard del C), ordina molto velocemente l’array. La qsort è usata nei progetti C per ordinare gli array propri del C ma nel codice d’esempio vedremo come è possibile fare in modo che vengano trattati correttamente anche i SAFEARRAY. Cominciamo col vedere che la ArrSortL (che è il nome della funzione che, richiamata da VB, provvede ad ordinare un array di long) è definita come segue

DWORD _stdcall ArrSortL(SAFEARRAY **psa)

Essa accetta, come detto in precedenza, un puntatore a puntatore di un SAFEARRAY che, nel nostro caso, sarà l’array di long ad una dimensione da ordinare. Ho dichiarato che la funzione ritorni un long (DWORD) per potere ottenere un codice d’errore dalla stessa o zero se l’ordinamento ha avuto successo. Con la SafeArrayGetDim ricavo il numero di dimensioni dell’array passato e se è diverso da 1 (mi aspetto un vettore), ritorno al VB il codice d’errore 1. In caso contrario, utilizzo la SafeArrayAccessData per bloccare l’array ed ottenere un puntatore effettivo ai dati (che serve alla qsort); inoltre, usando opportunamente la SafeArrayGetLBound e la SafeArrayGetUBound, ottengo il numero di elementi da ordinare che mi consente (insieme alla grandezza dell’elemento in byte, 4 in questo caso ovvero sizeof(long)) di chiamare la qsort per effettuare l’ordinamento. Al termine, tramite la SafeArrayUnaccessData sblocco l’array e ritorno al VB senza errori. La qsort necessita anche di un puntatore alla funzione (la cmpArrSortL in questo caso) che viene chiamata internamente e che deve restituire il risultato della comparazione dei due elementi scelti, volta per volta, dalla qsort stessa.

Allo stesso modo, ho implementato le funzioni ArrSortFS e ArrSortVS che trattano, rispettivamente, array di stringhe a lunghezza fissa e array di stringhe a lunghezza variabile. Le relative funzioni di supporto sono chiaramente differenti; quest’ultima, nel caso dei long, riceve due puntatori a long e ne confronta i valori puntati

  int cmpArrSortL(long *arg1, long *arg2)
  	{
	return (*arg1-*arg2);
	}

Nel caso delle stringhe a lunghezza fissa, vengono forniti i puntatori a stringhe di tipo WCHAR e confrontate le stringhe relative in quanto, in questo caso, la conversione da UNICODE ad ANSI non avviene. Ecco perché è necessario trattare i dati come puntatori a WCHAR ed usare la funzione wcscmp per confrontare le stringhe UNICODE

  int cmpArrSortFS(WCHAR *arg1, WCHAR *arg2)
  	{
	return wcscmp(arg1 , arg2);
	}

Infine, nel caso di un array di stringhe a lunghezza variabile, questo è costituito da un elenco di BSTR e quindi, nella funzione di supporto alla qsort, devono essere dichiarati come argomenti dei puntatori a BSTR. La comparazione, tuttavia, avviene con una semplice funzione strcmp in quanto, in questo caso, la conversione da UNICODE ad ANSI viene prima effettuata dal VB

  int cmpArrSortVS(BSTR *arg1, BSTR *arg2)
  	{  
	return strcmp((LPSTR)*arg1 , (LPSTR)*arg2);  
	}

Anche quando deve essere richiamata la funzione qsort, nei tre casi visti in precedenza esiste una piccola differenza. La qsort infatti ammette un parametro che contiene la dimensione in byte di ogni elemento dell’array da ordinare (per potere scambiare correttamente due elementi se necessario). Mentre nel caso dei long e delle stringhe a lunghezza variabile tale parametro corrisponde alla dimensione dei rispettivi tipi di dati (determinata tramite sizeof(long) e sizeof(BSTR)), nel caso delle stringhe a lunghezza variabile esso è determinato tramite una apposita API per il trattamento degli array e precisamente la SafeArrayGetElemsize.

Le funzioni così ottenute risultano molto efficienti; dalle prove fatte sul sistema di test (Pentium III a 450 MHz con 128 M di RAM), ho rilevato che la funzione ArrSortL riesce ad ordinare 100.000 long in circa 0,44 secondi e la ArrSortFS ordina 100.000 stringhe di 50 caratteri ciascuna (trattando circa 5 M di dati) in circa 2,42 secondi.

Nel caso di array di strutture definite dall’utente (array di UDT), vale quanto detto in precedenza per gli altri tipi di array e, in più, bisogna considerare che, se l’UDT contiene stringhe a lunghezza fissa queste sono in formato UNICODE (e vanno dichiarate in C con il tipo WCHAR). Le stringhe a lunghezza variabile devono invece essere definite come BSTR. La funzione ArrSortU (simile alle precedenti ma per gli UDT) provvede ad ordinare dei dati definiti in una struttura contenente un long, una stringa a lunghezza variabile e una a lunghezza fissa in base al valore del primo long.

La funzione di supporto diventa

  int cmpArrSortU(VCitt *arg1, VCitt *arg2)
  	{  
	return (arg1->Venduto - arg2->Venduto);  
	}

e la grandezza degli elementi dell’array va indicata con sizeof(VCitt) che altro non è che la dimensione in byte della struttura utente.
 
L'unica raccomandazione per evitare disallineamenti degli elementi dell’array in memoria è quella di adattare il numero di byte usati per l’allineamento delle strutture usato da C a quello di VB. Nel caso specifico, mentre C usa per default un allineamento di 8 byte, VB usa 4 byte ed è questo valore che deve essere assegnato a tale parametro tramite l’apposita opzione (Project -> Settings -> C/C++ -> Code Generation -> Struct member alignment).

Lavorare con le collection
Un interessante capitolo riguardante le interazioni tra codice VB e VC riguarda il passaggio, la gestione e la restituzione di dati contenuti nelle collection. L’argomento è sicuramente degno di attenzione, ma è irto di difficoltà ed ostacoli (non insuperabili) legati alla relativa complessità che inizia ad assumere il codice. In questo paragrafo, con tre funzioni d’esempio inserite nella DLL VBTTML, mostrerò le ‘basi’ per gestire i dati nelle collection; lascio al lettore il compito di migliorare ed ampliare le funzionalità del codice.

La prima funzione (GetProcList) provvede a restituire al VB la lista dei processi attivi con il relativo ID tramite una collection passata come unico argomento. La GetProcList sfrutta alcune funzioni della DLL ToolHelp32, quali la CreateToolhelp32Snapshot, la Process32First e la Process32Next, che non è disponibile per Windows NT. Per quest’ultimo sistema operativo è infatti disponibile la DLL PSAPI che mette a disposizione altre funzioni per l’enumerazione dei processi (vedi bibliografia).

La GetProcList è dichiarata in C con

LONG _stdcall GetProcList(IUnknown *pUnk)

in quanto restituisce un long come codice d’errore. La collection viene passata dal VB tramite un puntatore ad interfaccia IUnknown ed è tale l’argomento dichiarato in C. In VB la funzione deve essere semplicemente dichiarata nel modo seguente

Public Declare Function GetProcList Lib “VBTTML.DLL” (ByVal Iunk as Collection) As Long

Tramite il metodo QueryInterface della IUnknown si ottiene un puntatore all’interfaccia IDispatch della collection e, per mezzo del metodo GetIDsOfNames di quest’ultima, si ottiene un ID del metodo Add. Quest’ultimo servirà quando si utilizzerà il metodo InvokeHelper dell’oggetto di classe ColeDispatchDriver (che implementa il lato client dell’automazione OLE) all’interno del ciclo di enumerazione dei processi.

Da notare che la stringa aggiunta nella collection all’interno del ciclo, viene passata tramite il membro bstrVal di una variabile VARIANT (così come richiesto dalla InvokeHelper). Ma la bstrVal deve puntare ad una stringa in formato UNICODE (e per tale motivo è allocata tramite la SysAllocString) mentre quella ottenuta nel ciclo è in formato ANSI; per tale motivo viene usata la TO_OLE_STRING (ovvero la funzione ConvertToUnicode) che effettua la trasformazione prima dell’allocazione (ricordo che VB tratta le stringhe in UNICODE). Per usare senza errori la GetProcList la collection passata deve essere istanziata (perché venga passato un puntatore corretto dal VB). La ConvertToUnicode (come anche la ConvertToAnsi, che vedremo in seguito) è una funzione di supporto usata internamente; per questo motivo non è necessario che segua lo standard di passaggio degli argomenti _stdcall, né che sia esportata nel file .DEF.

Le altre due funzioni implementate per dimostrare il passaggio e l’uso di collection tra VB e VC sono la ArrToColl e la CollToArr. Il loro scopo (puramente didattico) è quello di passare le informazioni contenute in un array ad una dimensione in una collection e viceversa.

Il loro funzionamento è intuitivo, ma bisogna sottolineare alcune parti importanti del codice. In particolare, per la ArrToColl, il fatto che nell’array le stringhe siano state trasformate in ANSI dal VB e vadano ritrasformate prima di assegnarle alla collection con la solita TO_OLE_STRING (ConvertToUnicode). Per la CollToArr invece, la trasformazione in senso inverso (da UNICODE ad ANSI) deve essere effettuata perché gli elementi della collection sono memorizzati come BSTR in UNICODE mentre la SafeArrayPutElement che li assegna all’elemento dell’array, se li aspetta in ANSI per ritrasformarli all’atto della restituzione al VB.

Inoltre, in VB, è necessario (come nel caso della GetProcList) che la collection sia istanziata, mentre l’array non deve essere dimensionato, ma solamente dichiarato, nel seguente modo

  Dim ArrS( ) As String

Bisogna tenere ben presente che il codice usato in queste funzioni è stato realizzato a fini didattici e quindi non è esente da bug e sicuramente migliorabile. Molto dipende dalle necessità di chi lo intende usare e le modifiche sono sempre da effettuarsi valutandole caso per caso.

La restituzione di valori
Le procedure C possono restituire dei valori al VB come risultato delle operazioni svolte, ad esempio, come codice d’errore. Se il codice C non deve ritornare alcun valore, come nel caso di mancanza di parametri, questa verrà dichiarata di tipo void, come nell’esempio seguente

  void _stdcall Sample (LONG Op)

e in VB dovrà essere dichiarata come Sub (non come Function)

Public Declare Sub Sample Lib “VBTTML.DLL” (ByVal Op As Long)

In tutti gli altri casi, in VB andrà dichiarata come Function e dovrà essere specificato il tipo di dato restituito. Per i dati di tipo semplice, non esistono problemi in quanto dovranno essere dichiarati in maniera simile a quella dei parametri.

Anche i variant potranno essere restituiti ma, se necessario, dovranno essere inizializzati nel codice C, come nel seguente esempio

  VARIANT v;
  V_VT(&v)=VT_I4;
  V_I4(&v)=0x1234;
  return(v);

in cui un variant viene inizializzato con un valore long e restituito al codice VB chiamante.

Per quanto riguarda le stringhe, possono essere restituite solamente quelle a lunghezza variabile; in questo caso, la funzione C dovrà restituire un dato di tipo BSTR. È compito del programmatore allocare la stringa nel codice C prima di restituirla alla funzione VB chiamante, come nel caso della ToBin inserita nella DLL; il codice dovrà essere simile al seguente

  BSTR wd;
  ...
  wd = SysAllocStringByteLen(NULL, 32);       // per una stringa di 32 caratteri
  ...
  return(wd);

Tenete presente che la stringa restituita dovrà essere in formato ANSI (per tale motivo viene allocata con la SysAllocStringByteLen) in quanto verrà trasformata in UNICODE da VB in maniera trasparente.

Anche gli UDT possono essere restituiti a patto di gestire correttamente i dati in essi contenuti (stringhe fisse e variabili, valori numerici, variant) prima di restituirli al VB.

La funzione Mul64 è il primo esempio nella DLL VBTTML, che mostra come restituire un valore a 64 bit tramite una struttura utente composta da due long (tipo di dato LONGLONG). Tale funzione provvede a moltiplicare due long per ottenere un valore a 64 bit.

Un altro esempio, è costituito dalla funzione GetCPUCycle che ritorna, sempre in un LONGLONG (valore a 64 bit) il numero di cicli di clock trascorsi dall’accensione del sistema. Questa funzione sfrutta l’istruzione macchina RDTSC della CPU; non tutte le CPU però, dispongono di tale istruzione e quindi la funzione, in qualche caso, potrebbe fallire. Per questa funzione, il codice deve essere inserito in assembler sfruttando la direttiva _asm di VC. La parte fondamentale della funzione è costituita dalle seguenti istruzioni

  __asm {
  	rdtsc
	lea ebx,res
	mov [ebx],eax
	mov [ebx+4],edx
	}

tramite le quali viene generato il codice per leggere il numero di cicli della CPU il quale viene depositato (attraverso i registri eax e edx della CPU) in un LONGLONG restituito al VB.

Ancora, è possibile vedere, con la funzione FileTimeComp, come ricevere per indirizzo due strutture di tipo FILETIME e restituire un valore (positivo, negativo o nullo) al VB come risultato del confronto tra i due valori.

Un array, per finire, può essere restituito se incapsulato in un VARIANT (vedi bibliografia). Per mostrare un esempio pratico, ho realizzato una piccola funzione (puramente didattica) il cui scopo è quello di restituire un array di 2 x 2 elementi di tipo long, il primo dei quali assume il valore passato, mentre gli altri ricevono lo stesso valore via via incrementato di 1.

La funzione C dovrà essere dichiarata di tipo VARIANT, mentre l’array dovrà essere creato tramite l’apposito metodo (Create) della classe COleSafeArray di cui avremo creato un’istanza. Il metodo PutElement provvederà ad inserire i valori nell’array mentre il metodo Detach consente di restituire il VARIANT associato al ColeSafeArray in modo che venga trattato dal VB.

In VB, invece, la Declare della funzione dovrà prevedere un variant come dato restituito ma, come nel codice che segue,

  Dim A As Variant
  ...
  A = Set2x2Arr(5)
  lst.AddItem A(0, 0)

dopo avere usato la funzione, la variabile variant potrà essere trattata come un array.

Un modo alternativo per restituire degli array, anche se poco ortodosso (e non documentato), prevede il passaggio di un array non inizializzato (un SAFEARRAY, vedere paragrafo riguardante il passaggio dei parametri) alla funzione C chiamata, la quale, dopo avere allocato e riempito un proprio array, ne assegna il puntatore a quello passato da VB. È proprio quest’ultima fase che è un po' sporca ma, dopo tante prove, posso dire ben funzionante. Un piccolo esempio pratico di questa tecnica è la funzione GetPit, inserita nella DLL VBTTML, la quale provvede a creare e riempire una tabella pitagorica di long (10 x 10). Qualche piccolo commento a questo codice è necessario per apprezzarne il funzionamento.

Per cominciare, bisogna notare che la funzione C non ritorna alcun valore (in quanto modifica il parametro ricevuto per ottenere lo stesso risultato) ed è quindi di tipo void; in VB sarà quindi una Sub (per quanto detto in precedenza). Essa accetta un puntatore a puntatore di SAFEARRAY (secondo lo standard di passaggio parametri di VB) al quale, al termine, viene assegnato il valore del puntatore al SAFEARRAY creato all’interno. Quest’ultimo viene allocato con la SafeArrayCreate specificando il tipo degli elementi (VT_I4, cioè long), il numero di dimensioni (2, è bidimensionale) e un array (pit) di tipo SAFEARRAYBOUND contenente, per ogni dimensione, il numero massimo di elementi e la base di partenza.

Con la SafeArrayAccessData, in seguito, si ottiene un puntatore ai dati allocati che vengono riempiti con due classici cicli for nidificati. La SafeArrayUnaccessData decrementa il contatore di lock dell’array per permetterne il successivo uso da parte del VB.

Se necessario, è possibile restituire molti valori (long, byte e altro) dichiarandoli in VB come argomenti passati per riferimento in VB (tramite ByRef) e dichiarati in C come puntatori al tipo di dato utilizzato (LONG *, BYTE * e simili). La funzione può quindi essere dichiarata di tipo void (Sub in VB) e può restituire il valore al VB tramite il puntatore passato. Cautela deve essere posta nel passare stringhe tramite puntatori.

Il debug
Durante il lavoro con codice misto (VB/VC) risulta fondamentale conoscere ed adottare alcune tecniche per il debug. Se con il solo codice VB si manifesta un numero consistente di inconvenienti da testare e risolvere, nel caso di programmi con codice misto tale numero aumenta esponenzialmente e la fase di correzione degli errori diventa in assoluto quella che impegna maggiormente il programmatore. Purtroppo non esiste una ‘bacchetta magica’ tramite la quale individuare e risolvere gli errori che si presentano e molto è invece affidato all’esperienza del programmatore e alla sua conoscenza degli strumenti e ambienti operativi.

Sebbene si possa pensare diversamente, la fase prevalente (in termini di tempo e risorse impegnate) è quella dell’individuazione del problema e non quella della sua correzione. È infatti, ad esempio, molto più difficile capire le cause che fanno assumere il valore NULL ad un puntatore (che causa in seguito un errore) che correggere tale comportamento apportando le opportune modifiche al codice.

Molte volte la documentazione (MSDN, Technet) aiuta a comprendere il perché di alcuni ‘strani’ comportamenti del codice, ma, purtroppo, si ricorre ad essa solo dopo molto tempo che ci si è impegnati a tentare di risolvere il problema da soli. Sarebbe bene, invece, documentarsi prima riguardo al codice che si intende scrivere, cercando gli articoli e le note che, se presenti, facilitano enormemente il compito evitando di incorrere in problemi noti.

Quando si presenta un problema con codice misto, quasi sempre si è davanti all’uso improprio di un puntatore tramite il quale si tenta di accedere a zone di memoria di cui il processo non è proprietario (o che comunque sono vietate). Le cause per cui un puntatore assume valori che possono compromettere il successivo funzionamento del codice sono svariate e l’unico metodo per determinarle è esaminare l’esecuzione del codice nella zona in cui il puntatore dovrebbe assumere il valore che poi risulta errato. Per fare ciò, uno degli espedienti più usati è l’inserimento di un break all’interno del codice sorgente C, in modo da fermarne l’esecuzione nel punto voluto ed esaminare, con gli strumenti messi a disposizione da VC, il contenuto di variabili, registri e stack.
Il break si ottiene inserendo una istruzione macchina int 3 tramite la parola riservata ­_asm di VC, come nella riga seguente

  ...
  _asm int 3 // break software
  ...

In realtà esistono due funzioni (poco usate e documentate), la DebugBreak e la AfxDebugBreak che altro non fanno che eseguire l’istruzione macchina di cui ho parlato. La prima è proprio una funzione che viene eseguita e che ritorna al chiamante dopo il break; usando la seconda invece, il break viene inserito nel codice nel punto in cui è inserita la funzione stessa. Nulla vieta (come normalmente faccio io) di inserire l’istruzione macchina direttamente.
 
Quando il codice VB chiama quello contenuto nella DLL e si incontra il break, viene visualizzata una finestra che evidenzia l’eccezione che si è manifestata; tramite il tasto Annulla è possibile avviare il VC in modo da visualizzare il punto in cui l’esecuzione del codice si è interrotta (user breakpoint) e controllare lo stato delle variabili e registri.
Nell’esempio in figura, si vede come, una volta attivata questa tecnica per la GetProcList (vedere la int 3 in testa) si arrivi alla causa dell’errore evidenziando il fatto che il puntatore pUnk è nullo (linea di codice segnata dalla freccia gialla) in quanto, nel codice VB, la collection non era stata istanziata.
Questo e altri errori possono essere individuati, ma solo con un po' di pazienza.
Per controllare il contenuto di alcune variabili, durante il funzionamento del codice (senza bloccarlo con il break), si possono usare le tre seguenti linee di codice
	char Line[100];
	wsprintf(Line, "Dati: %d %d", var1,var2, 0);
	MessageBox(NULL, Line, "Message", 0);
	
che si possono inserire quando serve e che visualizzano una finestra in cui vengono mostrati i dati desiderati. Questa tecnica è valida sia che la DLL sia compilata in modo debug sia in modo release.

Un controllo dello stato di allocazione della memoria (per evitare i cosiddetti memory leaks) può essere effettuato, infine, usando degli oggetti di classe CMemoryState ed i relativi metodi Checkpoint e Difference. In poche parole, all’inizio del codice da controllare si inseriscono le seguenti linee

  CMemoryState oldMemState, newMemState, diffMemState;
  oldMemState.Checkpoint();
  ...

e, alla fine, queste altre

  ...
  newMemState.Checkpoint();
  if( diffMemState.Difference( oldMemState, newMemState ) )
  	MessageBox(NULL, "Memory Leak", "Message", 0);
  else
  	MessageBox(NULL, "Memory OK", "Message", 0);

in modo da evidenziare mancati rilasci di memoria allocata in precedenza (memory leak) che possono causare malfunzionamenti difficili da individuare.

A corredo di queste tecniche (ed altre per le quali rimando alla documentazione, specialmente quella di MFC), esistono gli strumenti classici di debug di VC, quali l’esecuzione passo passo del codice, il controllo sempre efficiente e funzionale di variabili, memoria, stack e registri della CPU.

Conclusioni
Quanto scritto non può certo esaurire tutti gli argomenti relativi all’interscambio di dati tra codice VB e VC e, in ogni caso, è limitato alle situazioni in cui tale scambio avviene all’interno della memoria posseduta da uno stesso processo. È comunque una buona base per iniziare a cimentarsi con il codice misto.

In altre circostanze (come nel caso di codice interagente con ActiveX Exe) il discorso si fa molto più complesso ed esula dallo scopo di questo articolo.

Altri argomenti andrebbero affrontati come, ad esempio, lo scambio ed il trattamento di recordset, la gestione dei file e delle comunicazioni tramite codice C interfacciato a VB; forse lo farò in seguito anche se la cosa necessiterà di un bel po' di tempo, unica risorsa veramente mancante :-).

Bibliografia
Molti documenti possono essere utili per approfondire ed integrare quanto detto nel mio articolo. Segue un elenco di quelli che ritengo più adatti.

Codice a corredo
Questo articolo è corredato dal sorgente completo del progetto Visual C++ (VBTTML.dsp) della VBTTML.DLL, fornita anche compilata, dal sorgente completo del progetto Visual Basic (Test.vbp) per testare la libreria stessa, pure esso fornito anche nella forma eseguibile.
Il tutto è scaricabile dall'Area Download (si raccomanda di leggere il file leggimi.txt).

FeedBack
Per ogni chiarimento, critica, suggerimento, potete fare riferimento all'autore, Antonio Giuliana, anche attraverso la Mailing List.