BETA, la rivista ipertestuale tecnica (colleg. al sito Web)
BETA
BETA, la rivista ipertestuale tecnicaBarra BETA
Barra Sito Beta.it
BETA Numero 20 - Programmazione: COM, ovvero COMplicato  -  Indici | Guida

Clicca qui per maggiori informazioni

COM, ovvero COMplicato

   Continuiamo a parlare delle possibili estensioni di Explorer, la shell di Windows 95/NT4. Poiché Explorer è stato costruito con la tecnologia COM, vale la pena di soffermarsi sulle basi di questa architettura, elencandone alcune definizioni e regole. Si passerà poi alle descrizione di alcune delle interfacce usate da Explorer, nonché dell'interfaccia IUnknown (questa sconosciuta), che è quella da cui derivano tutte le altre interfacce COM.

Stefano Casini
Articolista, BETA

Sommario

Introduzione

Introduzione 

COM (Component Object Model) è alla base di una vasta gamma di servizi basati sulla tecnologia ad oggetti, implementati sulla piattaforma Windows. Gli oggetti COM, anche detti componenti, vengono creati da alcuni programmatori mediante certi strumenti di creazione, per poi essere usati da altri programmatori, che possono utilizzare altri strumenti per manipolarli. L'architettura di COM può, entro certi limiti, essere utilizzata anche su sistemi operativi dversi da Windows. All'atto pratico, creare un componente COM si finalizza con la creazione di uno o piu' file EXE o DLL, ottenuti pero' seguendo delle ben specifiche linee guida.
COM rappresenta l'evoluzione della tecnologia OLE (Object Linking Embedding), che nelle prime versioni di Windows permetteva di incorporare all'interno di un documento generato con una certa applicazione (per esempio un file .doc di Winword) pezzi di documento generati altre applicazioni (per esempio un foglio elettronico di Excel); adesso con COM un'applicazione può incorporare al proprio interno più in generale i servizi forniti da un'altra applicazione.
COM è essenzialmente una serie di linee guida sul come creare, identificare e referenziare un oggetto, il quale può mostrare all'esterno interfacce multiple, ogni interfaccia essendo, all'atto pratico, codificata da una serie di funzioni (o metodi).

Le regole d'oro di COM 

1. COM è un insieme di regole associate con i puntatori a funzione

Nell'architettura COM esiste un pezzo di codice (generalmente un EXE o una DLL) che implementa le funzionalità del componente ed un altro pezzo di codice che utilizza tali funzionalità; il primo pezzo di codice viene anche chiamato "oggetto" o "server", mentre il secondo viene anche detto "applicazione" o "client". La comunicazione tra client e server è basata sui puntatori a funzione [1], che permettono ad uno dei componenti di chiamare l'altro dinamicamente senza dover aggiungere staticamente l'effettivo nome della funzione nel proprio codice.
Possiamo raggruppare una serie di puntatori a funzione all'interno di una struttura (o di una classe, in C++), ottenendo una "function table", come nell'esempio qui sotto:


struct FAR IClassFactory : public IUnknown

{
// *** IUnknown methods ***
virtual HRESULT STDMETHODCALLTYPE QueryInterface(IID FAR& riid, 
   LPVOID FAR* ppvObj) = 0;
virtual HRESULT STDMETHODCALLTYPE AddRef(void) = 0;
virtual HRESULT STDMETHODCALLTYPE Release(void) = 0;
// *** IClassFactory methods ***
virtual HRESULT STDMETHODCALLTYPE CreateInstance(LPUNKNOWN pUnkOuter, 
   IID FAR& riid, LPVOID FAR* ppvObject) = 0;
};

Questa "function table", che il componente espone all'esterno, viene chiamata "interfaccia"; naturalmente il singolo componente può supportare diversi servizi, e quindi esporre all'esterno più di una interfaccia; sarà compito del progettista del componente studiare la granuralità delle interfacce, in maniera da permettere a chi poi dovrà manipolare il componente di utilizzare in maniera ottimale una piccola parte o tutte le funzionalità o i servizi del componente.
Se il componente risulta essere "standardizzato", il programmatore che vuole usarlo non dovrà riscrivere ogni volta il codice con le chiamate alle funzioni: l'unica avvertenza che dovrà prevedere è quella che non tutti i componenti prodotti da diverse ditte potranno supportare tutte le interfacce standardizzate, ed il codice dovrà contenere le istruzioni necessarie a gestire il mancato supporto di una delle interfacce; naturalmente, chi costruisce i componenti dovrà prevedere una risposta standard da dare al client quando richiede i servizi di un'interfaccia non supportata.
Esempio: prendiamo due interfacce, calendario e orologio; i servizi forniti dal calendario e dall'orologio sono standardizzati, pertanto chi vuole utilizzarli deve chiamare sempre le medesime funzioni, sia che utilizzi il componente orologio con datario, prodotto da Tizio, che implementa entrambe le interfacce, sia che utilizzi il componente prodotto da Caio, che implementa solo l' orologio, sia che utilizzi il componente prodotto da Sempronio, che implementa solo il calendario; i componenti di Caio e Sempronio segnalano, quando viene richiesta l'interfaccia che non supportano, che essa non è disponibile; il programmatore che vuole utilizzare i componenti, quando riceve un messaggio di interfaccia non supportata, si astiene dal cercare di utilizzarla.

2. Le interfacce sono il nostro unico contatto con i componenti

Ricapitolando, un'interfaccia altro non è che un puntatore ad un puntatore ad una serie di funzioni, anche detta "virtual function table"; un componente può implementare più di un'interfaccia, e la medesima interfaccia può essere implementata da diversi componenti.

figura 1
Figura 1 - Schematizzazione della virtual function table

Poiché la richiesta di un'interfaccia del componente proviene dal client a run-time, una delle prime regole di COM è che ogni componente deve implementare una funzione QueryInterface; il parametro passato dal client a QueryInterface è l'identificativo univoco dell'interfaccia richiesta, e la funzione deve ritornare, nel caso la supporti, il puntatore alla relativa function table.
L'interfaccia di base, che tutti i componenti COM devono supportare, è la IUnknown, i cui metodi sono 3: QueryInterface, AddRef e Release. Ogni altra interfaccia è polimorfica rispetto a IUnknown, ovvero deriva da essa e ne eredita i 3 metodi. Oltre ai 3 metodi di base, l'interfaccia contiene le proprie funzioni, ma un'interfaccia non espone mai membri di dati; inoltre, non si può ottenere un puntatore al componente, ma solo ad una (o più) delle sue interfacce.
Quando l'applicazione chiamante ha finito di utilizzare l'interfaccia, deve chiamarne il metodo Release, per rendere noto al componente che la memoria impegnata per l'interfaccia può essere liberata (se l'interfaccia non è attualmente usata da altri client).

3. Ogni cosa (interfaccia o componente) ha un identificatore univoco (GUID)

Abbiamo visto che la richiesta di un'interfaccia del componente proviene dal client a run-time tramite l'identificativo univoco passato come parametro a QueryInterface; questo identificativo è un numero a 128 bit, detto GUID (globally unique identifier); per crearlo i usa in genere un programmino fornito con Visual C++ o Visual Basic, GUIDGEN; essendo basato su una combinazione di data e ora di sistema, numero della scheda di rete, più altri numeri random, è assai improbabile che vengano generati 2 GUID uguali da parte dei costruttori di componenti. Il GUID del componente viene chiamato CLSID, quello dell'interfaccia IID.

4. Il Registro è il database centrale dei componenti

Le applicazioni client possono caricare i componenti usando il CLSID (ID a 128 bit del componente); il Registro contiene mappati sotto la chiave HKEY_CLASSES_ROOT\CLSID gli identificativi dei componenti, con i corrispondenti pathname della DLL o dell'EXE che li implementa effettivamente; in questa maniera la locazione fisica del componente risulta trasparente al client.

5. Per istanziare un componente si usa la fabbrica di classi (Class Factory)

Un componente COM non viene mai istanziato o distrutto esplicitamente, poichè il medesimo può essere già caricato in memoria ed in uso da un'altra applicazione; per istanziarlo si chiama una speciale interfaccia del componente, la IClassFactory; questa si prenderà cura di istanziare per noi il componente, attraverso il suo metodo CreateInstance che prende come parametri il CLSID del componente e l'ID dell'interfaccia richiesta, ritornando il puntatore a tale interfaccia (se supportata). Attraverso la class factory, il programma chiamante è svincolato dalla gestione della memoria legata all'interfaccia che viene richiesta al componente, poichè è il componente stesso a mantenere il reference count [2] delle proprie interfacce, distruggendole e liberando la memoria quando quando esse non sono più referenziate (il loro reference count torna a zero).
L'interfaccia IClassFactory deve essere implementata solo per quei componenti che sono referenziati dal Registro tramite il CLSID; i componenti creati da altri componenti sul medesimo server non hanno bisogno di implementare IClassFactory o di avere un CLSID.

6. Le interfacce sono remotabili

Le interfacce altro non sono, in C, che il risultato di una doppia derefenziazione di un puntatore; perciò, se inframezziamo con un pezzo di codice RPC il collegamento tra due calcolatori, potremo avere il client che gira su una macchina ed il componente COM che gira sulla macchina remota, senza dover cambiare una virgola al codice del client.

7. La locazione fisica dei componenti è trasparente alle applicazioni

I componenti COM possono girare sia come parte dello stesso processo dell'applicazione client (componenti in una dll – inproc servers), sia come processi separati sulla medesima macchina (out-of-proc servers), sia  come processi separati su macchine remote (remote servers).

figura 2
Figura 2 - La remotabilità dei componenti COM

I tre metodi di IUnknown 

Prima di parlare delle interfacce usate da Explorer è opportuno citare i 3 metodi di base dell'interfaccia IUnknown, comuni a tutte le interfacce COM da essa derivate.

QueryInterface

Questo è il metodo che permette, una volta che abbiamo ottenuto l'accesso ad un oggetto, di richiamare una delle sue interfacce, identificata dal GUID dell'interfaccia (IID) che passiamo come parametro in ingresso; in uscita avremo il puntatore all'interfaccia cercata dell'oggetto, se questo la supporta.
Qualunque sia l'oggetto, due chiamate successive dell'interfaccia IUnknown fatte attraverso la QueryInterface devono ritornare il medesimo puntatore: questo permette all'applicazione client di capire se due puntatori ad un componente puntano al medesimo oggetto o a due istanze diverse dell'oggetto: nel primo caso i due puntatori ritornati da QueryInterface saranno uguali, nel secondo caso diversi.
Per le chiamate alle interfacce diverse da IUnknown il discorso fatto sopra non è più obbligatorio.
Chi implementa la QueryInterface per un componente deve seguire queste precise regole:
  • il set delle interfacce di un componente accessibili attraverso QueryInterface deve essere statico, non dinamico, ovvero se la chiamata a QueryInterface ha successo la prima volta, deve aver successo anche tutte le volte successive; viceversa, se fallisce la prima volta deve fallire anche tutte le volte successive;
  • deve essere simmetrica: se il client ha in mano il puntatore all'interfaccia X dell'oggetto, e chiede attraverso QueryInterface la medesima interfaccia, la chiamata deve aver successo;
  • deve essere riflessiva: se il client ha in mano il puntatore all'interfaccia X dell'oggetto, e ottiene attraverso QueryInterface l'interfaccia Y, quando chiama l'interfaccia X partendo dalla Y la chiamata deve aver successo;
  • deve essere transitiva: se il client ha in mano il puntatore all'interfaccia X dell'oggetto, e ottiene attraverso QueryInterface l'interfaccia Y, e dal puntatore all'interfaccia Y ottiene attraverso QueryInterface l'interfaccia Z, quando chiama l'interfaccia X partendo dalla Z la chiamata deve aver successo.
Una volta ottenuta un'interfaccia attraverso QueryInterface, il client deve poi chiamare il metodo AddRef su quell'interfaccia per incrementarne il reference count,  ed il metodo Release quando ha finito di utilizzarla.

AddRef

Questo metodo incrementa il reference count dell'interfaccia; deve essere chiamato ogni volta che viene ottenuta o fatta una copia di un puntatore ad una interfaccia di un componente. Il metodo va chiamato anche quando si passa il puntatore come parametro in-out ad una funzione: la funzione chiamata dovrà poi chiamare il metodo Release prima di copiare nel puntatore il valore di uscita.

Release

Questo metodo decrementa il reference count dell'interfaccia; deve essere chiamato ogni volta che non si ha più bisogno di utilizzare l'interfaccia. L'implementazione del metodo Release deve poter essere in grado di liberare la memoria impegnata dall'interfaccia quando il suo reference count scende a zero; quando poi non ci sono più altre interfacce impegnate per un oggetto, l'implementazione del metodo Release deve essere in grado di liberare la memoria impegnata e distruggere l'oggetto.

Le interfacce COM utilizzate da Explorer 

Queste sono, in ordine alfabetico, le principali interfacce che Explorer usa per dialogare con le proprie estensioni, con il namespace, con i collegamenti (shell links) e le barre delle applicazioni (appbars) della shell.

IContextMenu
ICopyHook
IEnumIDList
IExtractIcon
IShellExtInit
IShellFolder
IShellLink
IShellPropSheetExt

Esaminiamole un poco in dettaglio, ricordando che ognuna di esse deve obbligatoriamente implementare, oltre ai propri metodi, i 3 metodi di IUnknown: QueryInterface, AddRef e Release.

IContextMenu

Implementa questi metodi:
QueryContextMenu 
Viene chiamato dalla shell prima di mostrare il menu contestuale (quello che si ottiene cliccando con il tasto destro) di un oggetto del namespace di Explorer; si usa per aggiungere delle nuove voci, oltre a quelle predefinite per l'oggetto, al menu .
InvokeCommand
Esegue l'azione associata ad una voce del menu contestuale, quando la voce viene selezionata.
GetCommandString
Recupera il breve testo d'aiuto che Explorer mostra nella sua barra di stato (status bar), associato alla voce del menu contestuale su cui si ferma il mouse. In alternativa recupera una stringa, indipendente dal linguaggio, che può essere passata al metodo InvokeCommand per eseguire un'azione.

ICopyHook

La shell inizializza questa interfaccia direttamente, senza passare per l'interfaccia IShellExtInit. Implementa un solo metodo:
CopyCallBack
Viene chiamato dalla shell quando sta per avvenire un'operazione di copia, spostamento, cancellazione o ridenominazione per un oggetto di tipo folder.Tramite questo metodo si può modificare o prevenire l'azione di default della shell sul folder. Poichè ad ogni tipo di folder può essere associato più di una estensione della shell, la shell chiama in sequenza tutti i metodi CopyCallBack di ogni estensione che implementa questa interfaccia, prima di eseguire l'operazione.

IEnumIDList 

Designa l'interfaccia utilizzata per enumerare gli item identifier (ID) di un folder della shell; viene creata dal metodo EnumObjects dell'interfaccia IShellFolder. Implementa questi metodi:
Clone
Crea un nuovo oggetto enumeratore che ha lo stesso contenuto e lo stesso stato di quello correntemente usato dall'interfaccia. Torna utile soprattutto per registrare un punto particolare della sequenza di enumerazione, onde poterci ritornare facilmente in seguito.
Next
Recupera uno o più item identifier e avanza la posizione corrente all'interno della sequenza di enumerazione.
Reset
Riporta all'inizio della sequenza di enumerazione.
Skip
Salta uno o più item identifier della sequenza di enumerazione.

IExtractIcon

Designa un'interfaccia che permette alla shell di recuperare le icone per gli oggetti del namespace.
Implementa 2 metodi:
GetIconLocation
Recupera la posizione dell'icona all'interno di un file.
Extract
Estrae l'icona dal file passato in ingresso.

IShellExtInit

Designa un'interfaccia utilizzata per inizializzare un'estensione della property sheet, del menu contestuale o di un handler per il drag & drop. Implementa 1 solo metodo:
Initialize
Questo è il primo metodo che la shell chiama quando ha creato un'istanza di una property sheet extension, di un menu contestuale o di un handler per il drag & drop; nel suo parametro IDataObject * è contenuta la lista degli oggetti selezionati in quel momento nel folder, sui quali andrà ad agire l'estensione della shell. IDataObject è l'interfaccia che fornisce le capacità di trasferimento di dati e di notifica di cambiamenti nei dati.

IShellFolder

Questa è certamente l'interfaccia più importante, perchè è quella usata per determinare il contenuto di un folder. I suoi metodi sono:
BindToObject
E' il metodo usato per recuperare l'interfaccia IShellFolder di un folder figlio del folder corrente. Il subfolder viene individuato tramite il suo piddle [3] relativo al folder padre.
BindToObject
Questo metodo è riservato per un uso futuro e attalmente non è implementato.
CompareIDs
E' il metodo usato per determinare l'ordinamento relativo tra due oggetti contenuti in un folder, individuati tramite i propri piddle.
CreateViewObject
Questo metodo è riservato per un uso futuro e attalmente non è implementato.
EnumObjects
Crea un'interfaccia IEnumIDList, necessaria per enumerare gli oggetti contenuti nel folder.
GetAttributesOf
Recupera gli attributi di uno o più oggetti o subfolder contenuti in un folder; gli oggetti sono individuati tramite i propri piddle. Tra gli attributi che possiamo richiedere se sono supportati dall'oggetto, citiamo: la possibilità di copiarlo, rinominarlo, cancellarlo; se è un collegamento, se è un folder, se ha dei subfolder, se ha la property sheet, se fa parte del file system, ecc.
GetDisplayNameOf
Recupera il "display name" per l'oggetto. Il display name è quella stringa che compare nelle finestre di Explorer accanto all'icona che individua l'oggetto; anche per gli oggetti del file system il display name può non coincidere con il nome lungo del file (per esempio, impostando Explorer nel non visualizzare le estensioni per i file registrati, al file pippo.txt corrisponde il display name pippo); per gli altri oggetti (per esempio il nome di una connessione di Accesso Remoto) non esiste un file fisico corrispondente all'oggetto, ed il display name viene gestito dalla DLL che è associata al folder padre.
SetNameOf
Cambia il "display name" per l'oggetto, modificandone contemporaneamente la sua item identifier list.
GetUIObjectOf
Crea un'interfaccia per poter compiere alcune operazioni su un oggetto o su un subfolder, quali ad esempio creare un menu contestuale o supportare operazioni di drag & drop; infatti le interfacce che possono essere richieste tramite questo metodo sono la IExtractIcon, la IContextMenu, la IDataObject e la IDropTarget.
ParseDisplayName
Traduce il "display name" di un oggetto o di un folder nella sua corrispondente item identifier list. Il piddle ritornato dal metodo è relativo al folder padre.

IShellLink

Questa interfaccia permette di creare e risolvere i collegamenti della shell, ovvero quegli oggetti che contengono come informazione solo un puntatore ad un altro oggetto del namespace. Questi sono i suoi metodi, quasi tutti accoppiati tra Get e Set.
GetArguments / SetArguments
Recupera / cambia gli argomenti della linea di comando associati all'oggetto puntato dal collegamento.
GetDescription / SetDescription
Recupera / cambia la stringa di descrizione dell'oggetto puntato dal collegamento.
GetHotkey / SetHotkey
Recupera / cambia la hot key per l'oggetto puntato dal collegamento.
GetIconLocation / SetIconLocation
Recupera / cambia la locazione (pathname del file ed indice all'interno del file) dell'icona per l'oggetto puntato dal collegamento.
GetWorkingDirectory / SetWorkingDirectory
Recupera / cambia il nome della directory di lavoro per l'oggetto puntato dal collegamento.
GetIDList / SetIDList
Recupera / cambia la item identifier list dell'oggetto puntato dal collegamento.
GetPath / SetPath
Recupera / cambia il path ed il filename dell'oggetto puntato dal collegamento.
GetShowCmd / SetShowCmd
Recupera / cambia il comando di visualizzazione (SW_*) con cui viene visualizzata la finestra dell'applicazione associata all'oggetto puntato dal collegamento.
Resolve
Risolve il collegamento cercando l'oggetto puntato ed eventualmente modificando il path e la item identifier list dell'oggetto puntato, qualora venga trovato in una locazione diversa. Infatti, quando il metodo viene chiamato, se l'oggetto si trova nella locazione indicata dal collegamento, il collegamento si intende risolto; altrimenti cerca, nella stessa directory, un oggetto con la medesima data di creazione e attributi del file; questo permette di risolvere il link se l'oggetto puntato ha semplicemente cambiato nome. Se ancora il link non è risolto, la ricerca viene estesa a tutte le subdirectory di quella di partenza, per un file avente o lo stesso nome o la stessa data di creazione; infine, in caso il collegamento ancora non si risolva, viene mostrata una finestra di dialogo che invita l'utente ad inserire a mano il pathname delll'oggetto cercato.
SetRelativePath
Cambia il path relativo al folder padre dell'oggetto puntato dal collegamento. 

IShellPropSheetExt 

Designa un'interfaccia che permette di aggiungere o modificare delle pagine nella property sheet di un oggetto della shell. Implementa 2 metodi:
AddPages
Aggiunge una o più pagine nella property sheet di un oggetto del namespace. Explorer chiama questo metodo prima di mostrare la property sheet, e lo chiama tante volte quante sono le estensioni (property sheet handler) che sono registrate per l'oggetto.
ReplacePages
Sostituisce una pagina nella property sheet di un oggetto del Pannello di Controllo.
 

Conclusioni 

Abbiamo visto le regole basilari di COM, e dato un'occhiata alle principali interfacce usate da Explorer. Se tutto va bene, la prossima volta proveremo a costruire una estensione della shell, usando le nozioni fin qui acquisite. Posso assicurarvi che la cosa non è molto immediata, è difficile da debuggare, però quando si è riusciti a farne una bene è un piacere farne tante altre per personalizzare il proprio ambiente di lavoro.

Note e riferimenti

Dallo stesso autore, sono stati trattati i seguenti argomenti sulla rivista:

[3] Reference counted classes, BETA 1998.2
[2] I piddle del namespace, BETA 1898.1
[1] Come funzionano i puntatori a funzione, BETA 0198 (numero 16)


Stefano Casini, ingegnere, è Articolista di BETA dal 1995 e svolge un lavoro che non ha nulla a che fare con la programmazione; è raggiungibile su Internet tramite la redazione oppure all'indirizzo etngh@tin.it

Copyright © 1999 Stefano Casini, tutti i diritti sono riservati. Questo Articolo di BETA, insieme alla Rivista, è distribuibile secondo i termini e le condizioni della Licenza Pubblica Beta, come specificato nel file LPB.

Clicca qui per maggiori informazioni


BETA 2099.3: Sommario | Internet ID | Redazione | Liste/Forum | Informazioni | Indici di BETA | Installazione | Licenza Pubbl. Beta | Browser | Mirror ufficiali | Guida | Ricerche | Stampa


Beta.it (http://www.beta.it)email info@beta.it
Barra Sito Beta.it

Copyright © 1994-99 Beta, tutti i diritti sono riservati. Documento Lpb. BETA sul Web: http://www.beta.it
Sommario Internet Id Redazione Liste/Forum Informazioni Browser Mirror ufficiali Beta Home Page Beta Home Page english Beta News BETA Rivista Articoli BETA Beta Edit, pubblicazioni Beta Logo, premi Beta Lpb, Licenza Pubblica e Articoli Lpb Beta Navigatore Beta Online Beta Library Beta Info Gruppo Beta