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 - 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 - 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.
|