Introduzione alle API Win32

Capitolo 35: il COM, visto da C (1)

L'approccio Windows alla collaborazione fra componenti separati è il Component Object Model, COM; sia su di una sola macchina (entro lo stesso processo, o fra più processi separati), o fra macchine in rete ("DCOM"), il modello di programmazione di COM è equivalente.

Su questa tecnologia fondamentale ne poggiano mille altre, a seconda delle specifiche esigenze: interfaccia allo Shell di Windows, alla telefonia, alla grafica, al suono, e chi più ne ha, più ne metta. In particolare, fra le tecnologie "di secondo livello" basate su COM (e a volte confuse terminologicamente con esso, anche per colpa di passati errori del Marketing Microsoft!) ci sono OLE, Active/X, e Automation.

COM è indipendente dal linguaggio di programmazione usato, ma la sua orientazione ad oggetti lo rende scomodo (ma non certo impossibile!) da usare da C, linguaggio che ad oggetti non è; inoltre, nelle interfacce COM pubblicate è comune l'uso di tipi particolari (BSTR, per le stringhe, in Unicode; VARIANT, per "unioni discriminate"; SAFEARRAY, per i vettori di vari tipi), che sono "nativi" a certi linguaggi (Visual Basic) e possono agevolmente essere incapsulati in altri (C++, Java) in modo assai comodo in classi di oggetti opportune, mentre in C vanno gestiti chiamando sempre le varie API apposite. Insomma, diciamocelo: il COM lo si userebbe in C più o meno solo "per scommessa"...!

Tuttavia, anche capire "ma cosa c'è poi sotto" può essere un motivo valido, alternativo alle scommesse:-). Anche un utente C++ non ha necessariamente (anche se dovrebbe averle!) le idee chiare sul modello di "cosa succeda nella macchina" a fronte dei vari costrutti del linguaggio -- men che meno, in tutta probabilità, un programmatore Java, o Visual Basic, linguaggi più ad alto livello, che meglio "schermano" il programmatore dalla macchina sottostante.

In C, invece, è più probabile che l'utente debba, anche per necessità, mantenere un modello concettuale lucido e rispondente (anche se macro e funzioni qualcosa possono pur sempre nascondere); può dunque valer la pena di sopportare un poco di scomodità, a puro fine didattico, per poi passare, naturalmente, al C++ o altri linguaggi più adatti, quando si vorrà fare uso pratico dei concetti appresi.

 

Il modello di programmazione proposto da COM si impernia sulle "interfacce": una interfaccia COM, dal punto di vista del C, è un vettore di puntatori a funzioni, ciascuna delle quali condivide varie caratteristiche:

Il "cliente" di funzionalità esportate via COM le invoca solo e sempre attraverso "puntatori a interfacce", che egli ottiene in vari possibili modi (ad esempio, attraverso varie API). Un "oggetto" COM può "esporre" varie interfacce, e in questo caso il cliente può "navigare" fra esse attraverso un metodo (una delle funzioni dell'interfaccia) che tutte le interfacce COM devono supportare. Il cliente è inoltre responsabile di "rilasciare" l'interfaccia (e anche ciò si fa chiamando l'apposito metodo, sempre presente) quando ha finito di usarla.

L'HRESULT, ritornato da tutti i metodi COM e da parecchie delle API che riguardano COM, è una parola di 32 bit da interpretarsi come codice di errore, o di successo; può essere "interrogato" attraverso delle macro, FAILED(hr) che torna TRUE se l'HRESULT hr rappresenta un codice d'errore, e SUCCEEDED(hr) che equivale a !FAILED(hr).

 

Gli header file di windows (implicitamente "assorbiti" da un #include <windows.h>) comprendono inoltre varie macro che possono essere usate per dichiarare una interfaccia COM in modo tale da poterla "espandere" correttamente, sia in C (con l'equivalenza "al nudo metallo"), sia in C++ (con la comodità di considerarla una "classe astratta", da utilizzarsi con i comuni idiomi C++, e senza alcun aggravio computazionale rispetto all'uso in C). Per "espandere" queste macro "alla C", pur scrivendo in C++, basta inoltre porre un:

#define CINTERFACE subito prima dell'#include <windows.h>.

Ecco un esempio di uso di queste macro, che definisce, appunto, una ricca interfaccia COM...:

 

enum ScriptControlStates { }; const GUID IID_IScriptControl = {0x0e59f1d3,0x1fbe,0x11d0,{0x8f,0xf2,0x00,0xa0,0xd1,0x00,0x38,0xbc}}; #undef INTERFACE #define INTERFACE IScriptControl DECLARE_INTERFACE_(IScriptControl, IDispatch) { /* i tre metodi di IUnknown */ STDMETHOD(QueryInterface)(THIS_ REFIID riid, LPVOID FAR* ppvObj) PURE; STDMETHOD_(ULONG, AddRef)(THIS) PURE; STDMETHOD_(ULONG, Release)(THIS) PURE; /* poi i cinque di IDispatch ("Automation") */ STDMETHOD(GetTypeInfoCount)(THIS_ UINT FAR* pctinfo) PURE; STDMETHOD(GetTypeInfo)(THIS_ UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo) PURE; STDMETHOD(GetIDsOfNames)(THIS_ REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID lcid, DISPID FAR* rgdispid) PURE; STDMETHOD(Invoke)(THIS_ DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr) PURE; /* infine i (tanti!) specifici dell'interfaccia "Script Control" */ STDMETHOD(get_Language)(THIS_ BSTR* pbstrLanguage) PURE; STDMETHOD(put_Language)(THIS_ BSTR pbstrLanguage) PURE; STDMETHOD(get_State)(THIS_ enum ScriptControlStates* pssState) PURE; STDMETHOD(put_State)(THIS_ enum ScriptControlStates pssState ) PURE; STDMETHOD(put_SitehWnd)(THIS_ long phwnd) PURE; STDMETHOD(get_SitehWnd)(THIS_ long* phwnd) PURE; STDMETHOD(get_Timeout)(THIS_ long* plMilliseconds) PURE; STDMETHOD(put_Timeout)(THIS_ long plMilliseconds) PURE; STDMETHOD(get_AllowUI)(THIS_ VARIANT_BOOL* pfAllowUI) PURE; STDMETHOD(put_AllowUI)(THIS_ VARIANT_BOOL pfAllowUI) PURE; STDMETHOD(get_UseSafeSubset)(THIS_ VARIANT_BOOL* pfUseSafeSubset) PURE; STDMETHOD(put_UseSafeSubset)(THIS_ VARIANT_BOOL pfUseSafeSubset) PURE; STDMETHOD(get_Modules)(THIS_ interface IScriptModuleCollection** ppmods) PURE; STDMETHOD(get_Error)(THIS_ interface IScriptError** ppse) PURE; STDMETHOD(get_CodeObject)(THIS_ IDispatch** ppdispObject) PURE; STDMETHOD(get_Procedures)(THIS_ interface IScriptProcedureCollection** ppdispProcedures) PURE; STDMETHOD(_AboutBox)(THIS) PURE; STDMETHOD(AddObject)(THIS_ BSTR Name, IDispatch* Object, VARIANT_BOOL AddMembers) PURE; STDMETHOD(Reset)(THIS) PURE; STDMETHOD(AddCode)(THIS_ BSTR Code) PURE; STDMETHOD(Eval)(THIS_ BSTR Expression, VARIANT* pvarResult) PURE; STDMETHOD(ExecuteStatement)(THIS_ BSTR Statement) PURE; STDMETHOD(Run)(THIS_ BSTR ProcedureName, SAFEARRAY** Parameters, VARIANT* pvarResult) PURE; };

Il tipo GUID, usato per dichiarare la costante IID_IScriptControl, è una struttura che comprende 128 bit "arbitrari"; il nome del tipo è una sigla, che sta per "Globally Unique ID" (si usa spesso anche il sinonimo UUID, "Universally Unique ID") -- il suo ruolo è solo ed esclusivamente quello di fare da "vero nome, inconfondibile" per una qualche entità COM (in questo caso, un'interfaccia), al di là del "nome umanamente leggibile" che gli viene solitamente affibbiato -- il GUID "garantisce" (almeno statisticamente) la univocità dell'identificazione (esso viene generato da apposite API, che raramente un programmatore ha bisogno di chiamare, in quanto sono "rivestite" da comodi programmi con nomi come guidgen o uuidgen -- la generazione di GUID, infatti, normalmente serve, non mentre un programma sta girando, ma quando lo si sta scrivendo, e solo, inoltre, se esso definisce e implementa, invece di limitarsi ad usare, delle entità COM).

Il nome dell'interfaccia che stiamo definendo è assegnato con il #define INTERFACE, e poi, di nuovo, nella macro DECLARE_INTERFACE (in quest'ultima è associato alla sua "interfaccia di base" -- di solito IDispatch, la interfaccia fondamentale di "Automation", che, come accennavamo, è una importante tecnologia di secondo livello che poggia su COM -- essa serve, sostanzialmente, per facilitare l'uso di COM da parte di linguaggi puramente interpretativi, come, ad esempio, quelli di "scripting"). Questo fa sì che, in C, il nome dell'interfaccia divenga (grazie a una typedef) quello di un tipo: precisamente, una struct, contenente (sempre dal punto di vista del C...) il solo campo lpVtbl, che, a sua volta, punta a una tabella di puntatori a funzioni, tabella che descriviamo nel "corpo" della DECLARE_INTERFACE.

Di questo "corpo", cioè dell'elenco delle funzioni che costituiscono la "tabella di puntatori" (quindi, che costituiscono in effetti l'interfaccia che stiamo descrivendo) ci occuperemo alla prossima puntata.


Capitolo 34: il grafico: ultimi dettagli
Capitolo 36: il COM, visto da C (2)
Elenco dei capitoli