Introduzione alle API Win32

Capitolo 37: COM: alcune API, e le BSTR

Qualsiasi programma, che faccia uso di COM, prima di potere fare qualunque altra chiamata al COM, deve anzitutto chiamare l'API di "inizializzazione" del COM stesso:

HRESULT CoInitialize(LPVOID reserved) il cui unico parametro deve essere 0.

Similmente, alla fine dell'esecuzione di un programma che ha chiamato la CoInitialize, e in particolare dopo qualsiasi altra chiamata al COM, bisogna infine chiamare l'API:

void CoUninitialize();

 

Vi sono vari modi di ottenere un puntatore ad una interfaccia COM di un certo oggetto, ma uno dei più semplici, e più utilizzati, è l'API:

HRESULT WINAPI CoCreateInstance( REFCLSID rclsid, // id di classe (CLSID) dell'oggetto COM LPUNKNOWN pUnkOuter, // puntatore all'oggetto "aggregante" DWORD dwClsContext, // "contesto di esecuzione" REFIID riid, // id dell'interfaccia che interessa LPVOID* ppv // indirizzo del puntatore risultante );

Il "puntatore all'oggetto aggregante", pUnkOuter, sarà sempre 0 nei casi semplici che per ora ci interessano. Il codice dwClsContext sarà normalmente la costante (macro) CLSCTX_SERVER, a indicare che ci va bene qualsiasi tipo di server.

Il riid è l'identificatore dell'interfaccia COM che desideriamo (in C++, sarebbe passato come reference; in C, dovremo invece passarlo come puntatore), quel codice di 128 bit cui già ci siamo riferiti come GUID o UUID (REFIID, CLSID, ecc ecc, sono vari sinonimi, del tutto equivalenti, dello stesso tipo, a seconda del ruolo in cui viene usato); similmente, rclsid sarà l'identificatore della classe COM ("coclass") dell'oggetto che richiediamo, un altro GUID (codice univoco, di 128 bit).

CoCreateInstance, come la gran parte delle chiamate relative a COM, ritorna un HRESULT per indicare successo o errore; il vero e proprio "risultato", in caso di successo, è depositato nel puntatore che viene passato come ultimo parametro. Si noti che il tipo di questo parametro formale è void**, mentre quello che passiamo sarà sempre l'indirizzo di un puntatore-a-interfaccia, non quello di un void* -- quindi, inevitabilmente, sarà necessario un cast su questo ultimo argomento (anche in C, dove void* può essere intercambiabile con qualsiasi altro puntatore, ciò non si applica più se, come qui, si aggiunge un ulteriore livello di indirettezza).

 

L'identificatore di interfaccia deve esserci noto, visto che su quella interfaccia dovremo poi chiamare dei metodi; ad esempio, per un'interfaccia IScriptControl, dovremo usare come identificatore la costante IID_IScriptControl che già abbiamo visto.

Viceversa, l'identificatore di classe può non interessarci; se, sulla macchina su cui gira il nostro programma, fosse istallata una qualche implementazione piu` aggiornata, e quindi presumibilmente "migliore", di questa interfaccia, la cosa non ci darebbe alcun fastidio; in altri termini, gli identificatori di interfaccia sono cruciali, quelli di classe (implementazione) sono secondari.
L'identificatore di classe da usare viene così, normalmente, recuperato dalla registry, partendo dal "nome programmatico" (ProgID) della classe, con l'API:

HRESULT CLSIDFromProgID( LPCOLESTR lpszProgID, // ProgID che interessa LPCLSID pclsid); // ClsID risultante

LPCOLESTR è una stringa di wchar_t ("caratteri larghi", tipicamente di 16 bit, previsti dagli standard C e C++), che adotta la codifica Unicode. In C, o C++, costanti di questo tipo si possono indicare semplicemente prefiggendo una L a quella che per il resto appare come una normale costante stringa.

 

Ecco dunque come si potrebbe presentare il codice iniziale, per fare partire il COM e ottenere un puntatore alla interfaccia che ci interessa, usando per semplicità la macro _S che abbiamo precedentemente definito...:

static IScriptControl* pSC = 0; BOOL IndiEs_Init( const char* sOpzioni, // opzioni-stringa unsigned long lOpzioni // opzioni-intero ) { GUID clsid; HRESULT hr; hr = CoInitialize(0); _S hr = CLSIDFromProgID(L"MSScriptControl.ScriptControl",&clsid); _S hr = CoCreateInstance(&clsid,0,CLSCTX_SERVER, &IID_IScriptControl,(void**)&pSC); _S

 

Stiamo pensando a questo codice come incapsulato nella funzione IndiEs_Init, che già conosciamo; infatti, il ruolo del COM sarà qui, appunto, quello di mettere a nostra disposizione un interprete per le espressioni delle quali intendiamo disegnare il grafico.

Si noti il primo argomento che passiamo alla CLSIDFromProgID: la sua sintassi, L"stringa", è appunto quella da usare per una stringa Unicode; il suo contenuto è il "Prog ID" del Microsoft Script Control (ma non ne precisiamo il numero di versione; qualsiasi coclass che si sia istallata nella registry con questo nome dovrebbe andarci bene!).

Se questo codice funziona correttamente, a questo punto avremo, nella variabile statica pSC, un puntatore all'interfaccia "script control". Tuttavia, essa non è ancora efficace, in quanto nessuno specifico linguaggio di scripting è ancora stato connesso allo "script control"; per poterla effettivamente usare, dobbiamo anzitutto, chiamando il metodo put_Language su pSC, specificare quale linguaggio di scripting ci interessa usare.

A questo scopo possiamo usare il nostro argomento sOpzioni, magari con un default se esso è nullo o vuoto:

if(!sOpzioni || !*sOpzioni) sOpzioni = "VBScript"; A questo punto saremmo "quasi" pronti... se non fosse che sOpzioni è un char*, mentre l'argomento della put_Language deve essere una BSTR.

 

Le BSTR sono "stringhe" particolari; vanno gestite, da C, attraverso API specifiche, come:

BSTR SysAllocString(OLECHAR FAR* sz); per produrne una da una stringa "normale" (già Unicode), e: HRESULT SysFreeString(BSTR bstr); per rilasciare la memoria che una BSTR utilizza.

Il problema, però, è chiaramente, anzitutto, quello di convertire in Unicode delle stringhe che, per comodità e abitudine, il resto del nostro programma ci passa invece in termini di una "normale" codifica, come stringhe di char, cioè dei caratteri di 8, non 16, bit ciascuno.

 

Lo standard C ISO (stranamente, ed erroneamente, da molti chiamato invece "ANSI") fornisce per fortuna una funzione atta proprio a questa trasformazione (anche le API di Windows forniscono funzionalità similari, ma la chiamata Standard C è un poco più semplice, quindi, per ora, useremo quella), dichiarata nell'header file standard stdlib.h:

size_t mbstowcs(wchar_t *wcstr, const char *mbstr, size_t count);

L'input è l'argomento mbstr, una stringa di caratteri di 8 bit con una lunghezza massima di count caratteri (può essere più corta, se terminata dal solito "zero di terminazione"); la nomenclatura "mb" ("multibyte") si riferisce al fatto che, in certe codifiche per le lingue orientali, 2 o più byte (char) possono rappresentare un singolo glifo (ideogramma), ma non faremo uso di questa ulteriore possibilità.

Se wcstr è 0, mbstowcs ritorna il numero di wchar_t di cui ha bisogno per appoggiare il risultato della conversione che effettua; altrimenti, wcstr deve puntare ad un buffer di sufficiente lunghezza per ospitare questo risultato (e mbstowcs ritorna il numero di wchar_t che ha effettivamente scritto, con un massimo, naturalmente, pari a count).

 

Convertire sOpzioni in una BSTR, e passarla al metodo put_Language, si può dunque fare "in più tempi":

int cLen = mbstowcs(0,sOpzioni,strlen(sOpzioni)+1); wchar_t* wOpzioni = (wchar_t*)malloc(cLen*sizeof(wchar_t)); mbstowcs(wOpzioni,sOpzioni,strlen(sOpzioni)+1); BSTR bLinguaggio = SysAllocString(wOpzioni); hr = pSC->lpVtbl->put_Language(pSC, bLinguaggio); SysFreeString(bLinguaggio); free(wOpzioni); _S

Si noti che il controllo del successo di put_Language (con la solita macro _S) è posto dopo la deallocazione delle stringhe nelle due forme -- infatti, è cruciale liberare le stringhe stesse indipendentemente dal successo o dal fallimento di questa chiamata.

Il codice qui visto è dunque, sicuramente, tedioso e ripetitivo (purtroppo, il C non dispone dei comodi modi di "incapsulare" queste sequenze, che il C++ ci mette invece a disposizione), ma, tutto sommato, non difficile.

 

La IndiEs_Init, per come l'avevamo progettata, deve tornare TRUE o FALSE, non un HRESULT; inoltre, è importante che pSC sia liberato (e il puntatore azzerato), se era stato allocato ma la sua inizializzazione è poi fallita. Le ultime poche righe di IndiEs_Init possono dunque essere del tipo:

errore: if(pSC && FAILED(hr)) { pSC->Release(); pSC = 0; } return pSC != 0; }

La chiamata al metodo Release è fondamentale, ancora più importante di quanto non lo sia liberare sistematicamente tutte le altre risorse (memoria, ecc) via via acquisite dal nostro programma; scordandosela, infatti, si potrebbero tenere erroneamente impegnate risorse di sistema (o di rete!) anche al di là della terminazione dell'esecuzione del nostro stesso programma.

Questo spreco di risorse non si verifica con i "serventi in-process", come quello che attualmente Microsoft distribuisce per lo Script Control, ma è una ottima abitudine da prendere, quella di ragionare "come se" qualsiasi servente COM che si acquisisce "potesse", un domani, essere in realtà un server remoto -- infatti, questa possibilità esiste eccome, e il nostro programma potrebbe trovarsi ad utilizzare server remoti invece che locali o in-process, a seconda solo di come le cose sono istallate su una certa macchina/rete, senza neppure venire ricompilato o relinkato...!

 

Se tutto è andato bene, abbiamo dunque a nostra disposizione un servente COM, che ci espone l'interfaccia di "script control" attraverso il puntatore pSC (se qualcosa è andato invece storto, pSC sarà 0). È anche facile, a questo punto, scrivere la funzione di terminazione, la IndiEs_Finis, sulla base di quanto abbiamo già visto -- raccomandiamo quindi calorosamente al lettore, come esercizio, di cimentarsi appunto in questa scrittura, promettendogli intanto, come al solito, "il seguito alla prossima puntata"...!


Capitolo 36: il COM, visto da C (2)
Capitolo 38: Indies: scelte di progetto
Elenco dei capitoli