Note per l'impaginatore: - "boopsi" si scrive in minuscolo - le parole racchiuse tra barre sono in /italico/ Implementazione e Uso delle Classi boopsi ***************************************** Premessa ======== Leggendo questo articolo incontrerete molti riferimenti ai concetti ed alla terminologia che sono alla base del modello di programmazione orientata agli oggetti. L'OOP e' di fatto un argomento piuttosto vasto, la cui trattazione richiede normalmente un intero libro. Cercare di spiegarne le basi in poche righe sarebbe pertanto impensabile. I libri adatti allo scopo sono facilmente reperibili nel reparto di informatica di ogni libreria. 1. Introduzione =============== La programmazione orientata agli oggetti viene spesso associata all'uso di linguaggi che offrono una grammatica studiata per semplificare la creazione e la manipolazione di classi e oggetti. Tuttavia e' bene ricordare che l'OOP e' innanzitutto una "filosofia" di progettazione del software, non un linguaggio. Semmai il linguaggio utilizzato puo' rendere piu' agevole l'implementazione delle classi e la manipolazione degli oggetti, fornendo al programmatore dei costrutti vicini al modello OOP che hanno il pregio di rendere piu' chiaro il codice e di garantire che le interfacce definite per le classi vengano rispettate in tutti i casi. L'esempio che segue e' cosi' leggibile che non ha bisogno di commenti: if (TheBook.IsOpen) { for (page = TheBook.FirstPage; page < TheBook.LastPage; page++) TheBook.ReadPage (page); TheBook.Close(); } La comodita' di una sintassi naturale come quella del C++ non e' comunque indispensabile. Niente ci impedisce di scrivere applicazioni OOP in ANSI C, anche se il compilatore non conosce in modo nativo il concetto di classe e quindi non puo' garantire che le interfacce tra le classi vengano sempre rispettate. Analogamente, anche se nel C non esiste un tipo "stringa alfanumerica" come nel BASIC e il Pascal, usando degli array di caratteri e alcune funzioni quali strcpy() e strlen() si possono ottenere facilmente i medesimi risultati. Cosi', anche senza avere un supporto diretto per l'OOP nel linguaggio, un programmatore puo' scrivere il codice del proprio programma rispettando le regole per l'accesso ai dati che egli stesso ha stabilito in fase di progettazione. Con la versione 2.0 di AmigaOS, il gruppo di ricerca di West Chester decise di aggiungere ad Intuition un sistema OOP. I progettisti lo battezzarono Basic Object Oriented Programming System for Intuition, in breve 'boopsi'. Come per la maggior parte dei progetti software realizzati dai laboratori R&D Commodore in quel periodo, boopsi e' sorprendente per la sua flessibilita' e per l'ingegnosita' della sua implementazione, perfettamente integrata con il resto del sistema operativo. boopsi consente di scrivere oggetti indipendenti dalle applicazioni che li utilizzano e dal linguaggio di programmazione utilizzato. Sono supportate anche caratteristiche avanzate come l'ereditarieta' multipla ed il polimorfismo. Diversamente da quanto accade con il C++, non e' necessario ricompilare le applicazioni ogni volta che una classe viene modificata. E' inoltre possibile creare librerie di classi che vengono caricate in memoria soltanto quando serve. I Datatypes e le interfacce utente MUI, Triton e BGUI e ClassAct sono alcuni esempi di applicazioni complesse che sono state costruite sulla base del modello boopsi. Ovviamente tutti questi vantaggi hanno un prezzo. La manipolazione di un oggetto boopsi e' generalmente piu' lenta del suo equivalente C++, perche' i primi sono totalmente separati dalle applicazioni che li utilizzano, mentre gli oggetti C++ lo sono solamente a livello logico. In pratica i compilatori C++, conoscendo la struttura interna delle classi al momento della compilazione, possono generare codice che accede direttamente ai dati privati di un oggetto ed inserire inline il codice dei metodi piu' semplici. Le chiamate alle funzioni membro degli oggetti avvengono tramite dei semplici puntatori a funzione, mentre nel caso di un oggetto boopsi, come vedremo, il meccanismo e' piu' complesso. 2. Metodi, attributi e ereditarieta' ==================================== Ogni classe possiede dei /metodi/ e degli /attributi/. I primi sono l'equivalente delle member functions del C++ e costituiscono il principale mezzo di per manipolare un oggetto. Per fare un esempio, la classe "File" potrebbe implementare i metodi "Leggi", "Scrivi", "Apri", etc. Un metodo puo' richiedere degli argomenti e puo' ritornare un risultato, allo stesso modo di una funzione. Ad ogni metodo e' associato un valore numerico che lo identifica univocamente e permette alla classe di distinguerlo dagli altri. Nel gergo boopsi, un metodo con i suoi parametri viene chiamato /messaggio boopsi/. Questi messaggi sono delle strutture composte dall'ID seguito da un numero di parametri variabile. Per convenzione tutti i parametri contenuti nel messaggio sono di lunghezza pari a 4 byte (LONG), pertanto un messaggio puo' essere trattato, a seconda delle necessita', come se fosse una struttura oppure come se fosse un array di LONG. Questa dualita' permette di costruire messaggi boopsi "al volo", passandoli sullo stack a funzioni che accettano un numero variabile di argomenti. Gli attributi sono invece l'equivalente boopsi delle variabili membro del C++, oppure dei campi contenuti nelle strutture del C. E' possibile leggere il valore di un attributo, impostare un nuovo valore e fare in modo che un oggetto notifichi altri oggetti quando il valore di uno dei suoi attributi cambia. Ogni attributo di una classe e' identificato da un diverso tag, cioe' da un valore numerico a 32bit (ULONG). I gruppi di attributi vengono passati agli oggetti sotto forma di TagList in cui ogni TagItem contiene una coppia attributo/valore, percio' possono essere manipolati facilmente usando le funzioni di supporto della utility.library. Nella programmazione orientata agli oggetti, le classi sono organizzate in una gerarchia in cui i figli (sottoclassi) ereditano gli attributi ed i metodi dei genitori (superclassi). Una sottoclasse, o classe derivata, puo' definire nuovi metodi e nuovi attributi oppure modificare il comportamento di quelli che ha ereditato dalla propria superclasse. Per esempio, se la classe "animale" definisce i metodi "mangia" e "dormi", anche le sue sottoclassi "cane", "gatto" e "pesce" sono capaci sia di mangiare che di dormire. La classe "pesce" inoltre puo' definire dei nuovi metodi, per esempio "nuota". Per convenzione i nomi simbolici associati ai metodi e agli attributi boopsi hanno un prefisso che permette di distinguere la classe a cui appartengono. Per esempio, GM_HANDLEINPUT e' un metodo della gadgetclass (GM sta per Gadget Method), mentre PGA_Visible e' un attributo (A) di un prop gadget (PG). I codici di ritorno speciali hanno invece un prefisso che termina per R, come nel caso di GMR_MEACTIVE. Il sistema boopsi ha anche alcune caratteristiche peculiari che non trovano riscontro nelle caratteristiche standard dei linguaggi di programmazione orientati agli oggetti. Una di queste e' la notifica, che permette di collegare tra loro degli oggetti in modo che ognuno aggiorni gli attributi degli altri. In pratica gli oggetti si "parlano" trasmettendosi reciprocamente delle TagList di attributi, senza alcun intervento da parte dell'applicazione. Per esempio, un gadget colorwheel puo' aggiornare automaticamente la posizione di tre gadget proporzionali che mostrano la quantita' di rosso, verde e blu. Viceversa, ognuno dei gadget proporzionali puo' notificare il gadget colorwheel quando l'utente sposta l'indicatore. Grazie alla notifica boopsi, l'interfaccia utente puo' reagire autonomamente all'input dell'utente, liberando l'applicazione da un pesante carico gestionale. 3. La rootclass =============== Tutte le classi boopsi derivano da una classe universale chiamata "rootclass". Questa classe definisce dei metodi di base che tutte le altre classi ereditano (vedi tabella 1). Non viene invece definito alcun attributo. La rootclass di per se' non e' molto utile: alcuni dei suoi metodi esistono soltanto per poter essere ridefiniti dalle sue sottoclassi. In C++ si direbbe che la rootclass e' una "classe virtuale". Quando una classe non implementa un metodo, lo deve passare alla propria superclasse perche' lo gestisca: e' cosi' che funziona l'ereditarieta' nelle classi boopsi. La rootclass fa eccezione a questa regola perche', non avendo alcuna superclasse a cui passarare i metodi non supportati, ritorna immediatamente un errore che si propaga fino all'applicazione che ha invocato il metodo sconosciuto. [tabella 1: metodi definiti dalla rootclass. (Il prefisso OM_ significa "Object Method").] OM_NEW crea una nuova istanza della classe OM_DISPOSE distrugge un oggetto OM_SET imposta il valore di un attributo OM_GET legge il valore di un attributo OM_NOTIFY notifica il cambiamento di uno o piu' attributi OM_UPDATE aggiorna il valore di uno o piu' attributi (simile a OM_SET) OM_ADDMEMBER aggiunge un figlio alla lista privata di un oggetto OM_REMMEMBER rimuove un figlio dalla lista privata di un oggetto OM_ADDTAIL inserisce l'oggetto in una lista OM_REMOVE rimuove l'oggetto da una lista I metodi OM_NEW e OM_DISPOSE servono rispettivamente per creare una nuova istanza di una classe e per liberarne una esistente. Corrispondono rispettivamente ai costruttori e ai distruttori delle classi C++. OM_NEW permette anche di specificare una lista di attributi da impostare al momento della creazione dell'oggetto. OM_SET e OM_GET consentono di modificare e di leggere il valore degli attributi di un oggetto. I metodi OM_NOTIFY e OM_UPDATE costituiscono il meccanismo di comunicazione tra oggetti boopsi che avviene tramite lo scambio di TagList contenenti gli attributi da aggiornare. Nella documentazione delle classi, per ogni attributo vengono specificati dei flags di applicabilita'. Questi flags indicano se l'attributo puo' essere utilizzato al momento dell'inizializzazione (I), con il metodo OM_GET (G), con OM_SET (S), con OM_NOTIFY (N) e con OM_UPDATE (U). Un attributo che puo' essere usato in tutti i casi ha quindi applicabilita' (IGSNU), mentre un attributo che puo' essere solo inizializzato alla creazione dell'oggetto o impostato con OM_SET ha applicabilita' (IS). Gli ultimi quattro metodi - OM_ADDMEMBER, OM_REMMEMBER, OM_ADDTAIL e OM_REMOVE - non sono implementati nella rootclass e servono come supporto per le classi che permettono di costruire liste e alberi di oggetti. 4. Le classi di Intuition ========================= Intuition mantiene internamente una lista delle classi pubbliche che sono state rese disponibili a tutte le applicazioni. Una classe pubblica puo' essere trovata per nome come avviene con le librerie di Exec. Intuition contiene anche alcune classi built-in, la cui gerarchia e' mostrata nello schema 1. Per completezza, lo schema comprende anche le classi dei Datatypes, che non fanno parte di Intuition, ma vengono aggiunte durante il boot dal comando AddDataTypes. Le classi built-in di Intuition sono intenzionalmente generiche e dunque piuttosto limitate, perche' servono come base per la creazione di classi piu' complesse. Un gadget proporzionale che riporta un indicatore numerico della posizione corrente, come lo SLIDER_KIND della gadtools.library, puo' essere facilmente ottenuto collegando un oggetto 'propgclass' con un oggetto 'strgclass'. Le classi boopsi sono studiate in modo da poter notificare altri oggetti quando uno dei loro attributi viene modificato, senza alcun intervento da parte dell'applicazione. [Inserire figura 1] Intuition fornisce alcune funzioni che semplificano la manipolazione degli oggetti da parte della applicazioni: Creazione e distruzione di oggetti: object = NewObjectA (classPtr, classID, tagList) - crea una nuova istanza di una classe, usando gli la TagList per l'inizializzazione degli attributi DisposeObject (object) - distrugge un oggetto creato con NewObject() Lettura e scrittura di attributi: result = GetAttr (attrID, object, storagePtr) - chiede ad un oggett il valore di uno dei suoi attributo result = SetAttrsA (object, tagList) - imposta il valore di uno o piu' attributi di un oggetto Creazione di classi: class = MakeClass (classID, superClassID, superClassPtr, instanceSize, flags) - crea una nuova classe derivata AddClass (classPtr) - rende pubblica una classe aggiungendola alla lista delle classi pubbliche di Intuition RemoveClass (classPtr) - rimuove una classe dalla lista delle classi pubbliche success = FreeClass (classPtr) - tenta di liberare una classe. La liberazione fallisce se ci sono ancora degli oggetti istanziati dalla classe Funzioni specifiche per la gadgetclass: result = DoGadgetMethodA (gadget, window, requester, message) - invoca un di un gadget boopsi, passando all'oggetto delle informazioni addizionali sul contesto result = SetGadgetAttrsA (gadget, window, requester, tagList) - imposta uno o piu' attributi di un gadget boopsi, passando all'oggetto delle informazioni addizionali sul contesto Funzioni di supporto di amiga.lib: result = CallHookA (hookPtr, obj, message) - Chiama una funzione usando il protocollo di chiamata standard per gli hook result = CallHook (hookPtr, obj, ...) - Versione a parametri variabili di CallHook() result = DoMethodA (obj, message) - Invoca un metodo su un oggetto result = DoMethod (Object *obj, MethodID, ...) - Versione a parametri variabili di DoMethodA() result = CoerceMethodA (class, obj, message) - Invoca un metodo su un oggetto come se l'oggetto appartenesse alla classe specificata result = CoerceMethod (class, obj, MethodID, ...) - Versione a parametri variabili di CoerceMethodA() result = DoSuperMethodA (class, obj, message) - Invoca un metodo su un oggetto utilizzando la superclasse della classe specificata result = DoSuperMethod (class, obj, MethodID, ...) - Versione a parametri variabili di DoSuperMethodA() result = SetSuperAttrs (class, obj, Tag1, ...) - Imposta uno o piu' attributi di un oggetto utilizzando la superclasse della classe specificata La maggior parte di queste funzioni si limita ad invocare un metodo su un oggetto (OM_NEW, OM_SET, OM_GET, OM_DISPOSE...). La gadgetclass e le sue sottoclassi prevedono delle funzioni specifiche (SetGadgetAttrs() e DoGadgetMethod()). Le applicazioni devono evitare la manipolazione diretta degli oggetti appartenenti a queste classi ed usare sempre le funzioni di Intuition, che si prendono cura di alcuni dettagli implementativi come l'arbitraggio dell'accesso ai gadget tra applicazione e input.device. Il sistema MUI ed i Datatypes forniscono ulteriori funzioni di questo tipo come "wrappers" per i metodi piu' comuni. Per una descrizione dettagliata di tutte le funzioni qui elencate vi rimandiamo alla documentazione ufficiale (Gli autodocs contenuti nel 3.1 Native Developer Toolkit e la terza edizione dell'Amiga ROM Kernel Reference Manual: Libraries). 5. Operazioni di base con gli oggetti ===================================== La funzione NewObject() consente di creare oggetti invocando sulle loro classi il metodo OM_NEW (detto anche "costruttore" della classe). Il primo parametro di questa funzione e' un puntatore alla classe da istanziare: oggetto = NewObjectA (ClassePrivata, NULL, NULL); Se invece la classe e' pubblica, il puntatore deve essere NULL ed il secondo parametro deve contenere l'ID della classe: oggetto = NewObjectA (NULL, "nomeclasse", NULL); E' possibile specificare una TagList di attributi che verranno passati al metodo OM_NEW per inizializzare l'oggetto. Si possono usare soltanto gli attributi che hanno applicabilita' (I): immagine = (struct Image *) NewObject (NULL, "sysiclass", SYSIA_Which, AMIGAKEY, SYSIA_DrawInfo, drawinfo, SYSIA_Size, SYSISIZE_HIRES, TAG_DONE); Un'immagine boopsi cosi' creata puo' essere utilizzata ovunque Intuition richieda una struttura Image. La principale differenza con un'immagine normale e' che l'oggetto di solito non contiene una bitmap statica. Un'immagine boopsi sa come disegnare se' stessa sullo schermo in funzione dell'ambiente circostante. Per esempio, vengono usate le penne e la risoluzione corrette per lo schermo, e l'immagine puo' cambiare quando viene selezionata o disabilitata. La funzione DisposeObject() distrugge un oggetto invocando su di esso il metodo OM_DISPOSE (distruttore): DisposeObject (oggetto); Alcune classi, oltre a liberare la propria istanza, liberano anche tutti gli oggetti figli. Per esempio, gli oggetti della groupgclass inviano un messaggio OM_DISPOSE a tutti i gadget che fanno parte del loro gruppo. In questo modo e' possibile distruggere un'intera gerarchia di oggetti con un'unica chiamata. Invece le immagini ed i frame boopsi attaccati ai gadgets non vengono liberati, perche' e' possibile condividere un'unica istanza di un'immagine boopsi tra piu' gadgets. SetAttrs() consente di passare una TagList di attributi ad un oggetto. Per esempio, per cambiare la posizione e le dimensioni di un'immagine boopsi: SetAttrs (image, IA_Left, left, IA_Top, top, IA_Width, width, IA_Height, height, TAG_DONE); I gadget boopsi che sono stati aggiunti ad una finestra necessitano di una versione specifica di SetAttrs() chiamata SetGadgetAttrs(), che fornisce al gadget delle informazioni addizionali che permettono l'aggiornamento visivo della finestra. Per esempio, per cambiare il contenuto di uno string gadget boopsi: SetGadgetAttrs ((Object *)gadget, window, NULL, STRINGA_TextVal, "The quick brown fox jumps over the lazy dog", TAG_DONE); Viene da domandarsi come mai in un modello orientato agli oggetti i gadget boopsi non si comportano come le altre classi nella gestione di OM_SET, metodo che viene ereditato dalla rootclass. I progettisti di Intuition sono dovuti ricorrere a questa forzatura perche' i gadget boopsi dovevano rimanere compatibili con quelli classici. Le applicazioni possono infatti aggiungere e rimuovere un gadget boopsi ad una finestra usando AddGadget() e RemoveGadget() come di consueto. Per questo motivo, gli oggetti della gadgetclass non sono in grado di tenere traccia della finestra o del requester di cui fanno parte e hanno bisogno di ottenere queste informazioni ogni volta che devono aggiornare il loro contenuto. Inoltre i metodi dei gadget boopsi possono essere chiamati contemporaneamente sia dall'applicazione che li ha creati che da Intuition, quando l'utente interagisce con il gadget. Per mantenere consistenti i dati contenuti nell'istanza e' necessario arbitrare l'accesso all'oggetto con un semaforo. SetGadgetAttrs() e DoGadgetMethod() implementano questo meccanismo di accesso e devono percio' essere utilizzate anche nel caso di operazioni che non alterano il l'aspetto grafico del gadget. DoMethod() e' il modo piu' diretto di invocare un metodo qualsiasi su un oggetto boopsi: result = DoMethod (oggetto, MethodID, ...); Normalmente le applicazioni non usano DoMethod(), perche' la maggior parte dei metodi di uso comune nelle classi boopsi di Intuition viene chiamato usando funzioni specifiche come NewObject() e SetAttrs(). Come abbiamo detto, in genere tutti i parametri che compongono un messaggio boopsi occupano 4 byte ciascuno. Per esempio, i metodi OM_NOTIFY e OM_UPDATE richiedono i seguenti parametri (il prefisso "op" significa "object packet"): struct opUpdate { ULONG MethodID; struct TagItem *opu_AttrList; struct GadgetInfo *opu_GInfo; ULONG opu_Flags; }; Per invocare il metodo OM_NOTIFY si deve allocare una struttura opUpdate in memoria, riempirne i campi e quindi passarla a DoMethodA(). Piu' semplicemente, e' possibile costruire al volo la struttura opUpdate sullo stack usando la variante di DoMethodA() con argomenti variabili: DoMethod (obj, OM_NOTIFY, taglist, ginfo, flags); 6. Finestre con Gadget boopsi ============================= L'applicazione tipica del sistema boopsi e' la costruzione di interfacce utente. La gestione dei gadget e delle immagini boopsi e' molto simile a quella dei loro corrispondenti tradizionali, percio' in una finestra possono coesistere senza problemi gadget di tipo diverso (normali, GadTools e boopsi). Gli oggetti appartenenti ad una sottoclasse della gadgetclass possono essere aggiunti ad una finestra o ad un requester con AddGadget() e AddGList(), oppure tramite il tag WA_Gadget a OpenWindowTags(). Quasi tutte le immagini e i gadget boopsi richiedono una struttura DrawInfo per poter disegnare nella finestra in cui vengono inseriti. La struttura DrawInfo di uno schermo pubblico puo' essere ottenuta in questo modo: struct Screen *screen; struct DrawInfo *drawinfo; screen = LockPubScreen (nome_schermo) if (screen) { drawinfo = GetScreenDrawInfo(); [...apertura delle finestre...] FreeScreenDrawInfo (drawinfo); UnlockPubScreen (NULL, screen); } LockPubScreen() permette di ottenere l'indirizzo dello schermo pubblico su cui si desidera aprire la finestra assicurandosi che nel frattempo lo schermo non venga chiuso. La struttura DrawInfo deve essere passata al gadget o all'immagine al momento della creazione, usando gli attributi GA_DrawInfo e SYSIA_DrawInfo. I gadget boopsi builtin di Intuition sono piuttosto generici. La buttongclass permette di creare bottoni contenenti un testo o un'immagine (quindi anche dei checkbox o dei radio button). La sua sottoclasse frbuttongclass aggiunge la possibilita' di specificare un'immagine boopsi da usare come bordo (frame). Un bottone boopsi e' di solito composto da tre elementi: un gadget (frbuttongclass), un bordo (frameiclass) e un'immagine boopsi (una sottoclasse della imageclass). E' possibile utilizzare un unico bordo per piu' bottoni, perche' i bordi boopsi sono capaci di circondare automaticamente un oggetto usando il metodo IM_DRAWFRAME. [inserire figura 2] La strgclass e la propgclass corrispondono rispettivamente ai gadget stringa e ai gadget proporzionali. Sfortunatamente queste due classi non sono in grado di gestire automaticamente il proprio frame. Si devono quindi costruire dei frame boopsi appropriati e posizionarli correttamente rispetto al contenuto del gadget. Non e' comunque difficile scrivere delle sottoclassi della strgclass e della progclass che svolgano automaticamente questa funzione. Purtroppo alcune classi interne di Intuition hanno gravi problemi nel posizionamento relativo ai bordi (GA_RelRight, GA_RelBottom, GA_RelWidth e GA_RelHeight). Inoltre alcuni attributi non funzionano come documentato oppure come ci si aspetterebbe. Quel che e' peggio e' che i bug si presentano in modo diverso a seconda della versione del sistema operativo (V37, V39 e V40), il che rende ancora piu' difficoltoso aggirarli correttamente. La soluzione adottata da molti programmatori e' scrivere delle classi equivalenti a quelle di sistema, ma prive di questi problemi. E' questo il caso di ClassAct, che purtroppo non e' liberamente distribuibile. Per costruire una lista di gadget boopsi da aggiungere ad una finestra si utilizza l'attributo GA_Previous: PrimoGadget = NewObject (NULL, "frbuttongclass", GA_ID, ID_PRIMOGADGET, GA_Left, 50, GA_Top, 50, GA_Width, font->tf_XSize * 7 + 8, GA_Height, font->tf_YSize + 6, GA_DrawInfo, drawinfo, GA_Image, ButtonFrame, GA_Text, "boopsi!", TAG_DONE); SecondoGadget = NewObject (NULL, "frstrgclass", GA_Previous, PrimoGadget, GA_ID, ID_SECONDOGADGET, GA_Left, 160, GA_Top, 50, GA_Width, font->tf_XSize * 10 + 12, GA_Height, font->tf_YSize + 6, GA_DrawInfo, drawinfo, TAG_DONE); Costruire liste di gadget in questo modo e' il sistema piu' semplice di disporre degli oggetti boopsi all'interno di una finestra. Gli oggetti possono anche essere organizzati in strutture piu' sofisticate. La maggior parte dei sistemi GUI orientati agli oggetti suddividono il contenuto delle finestre usando dei gruppi di gadget che a loro volta possono contenere altri gruppi. In pratica si tratta di una gerarchia ad albero. Ogni gruppo e' in grado di posizionare automaticamente i propri figli in orizzontale, in verticale o in una griglia. In questo modo si ottengono delle interfacce utente completamente scalabili e sensibili ai font utilizzati. La groupgclass di Intuition implementa solo un supporto parziale per questo sistema di layout. Manca del tutto il posizionamento automatico degli oggetti, che presuppone anche una collaborazione da parte di tutti gli oggetti. Per poter assegnare la posizione e le dimensioni corrette ad ogni figlio e' necessario che ogni classe implementi un metodo che ne calcola le dimensioni minime e massime, come avviene per le classi MUI. [inserire figura 3] Alcuni gadget boopsi possono inviare dei normali messaggi IDCMP_GADGETUP alla porta IDCMP della finestra di cui fanno parte. Per ricevere questi messaggi, l'attributo GA_RelVerify (che corrisponde al flag GACT_RELVERIFY nella struttura Gadget) deve impostato a TRUE. Gli eventi IDCMP_GADGETUP vengono inviati soltanto quando l'utente rilascia il tasto del mouse su un bottone o su un bottone o su un gadget proporzionale, oppure quando preme il tasto RETURN in un gadget stringa. In questo modo l'applicazione puo' controllare lo stato dell'oggetto e reagire di conseguenza. Il funzionamento e' analogo a quello dei gadget GadTools. Gli oggetti boopsi possono anche comunicare con l'applicazione inviando eventi di tipo IDCMP_IDCMPUPDATE. Questi messaggi contengono il puntatore ad una TagList di attributi nel campo IAddress della struttura IntuiMessage. A differenza degli eventi gli eventi IDCMP_GADGETUP, che vengono inviati solamente quando l'utente rilascia il gadget, gli eventi IDCMP_IDCMPUPDATE vengono trasmessi anche mentre l'utente sta interagendo con il gadget. Per esempio, un gadget proporzionale invia un messaggio IDCMP_IDCMPUPDATE ogni volta che il valore dell'attributo PGA_Top cambia. Per ottenere questo tipo di messaggio l'applicazione deve innanzitutto includere IDCMP_IDCMPUPDATE tra i flags passati al tag WA_IDCMPFlags di OpenWindowTags(), oppure usando la funzione ModifyIDCMP(). Tutti i gadget boopsi supportano due attributi della icclass, ICA_TARGET e ICA_MAP, che consentono di specificare quale oggetto deve essere avvisato dei cambiamenti in tutti gli attributi con applicabilita' N (Notify). Il sistema di notifica boopsi e' un argomento piuttosto complesso che verra' affrontato successivamente. Per adesso bastera' dire che per ricevere eventi di tipo IDCMP_IDCMPUPDATE ogni volta che lo stato di un oggetto cambia, si deve impostare l'attributo ICA_TARGET con il valore ICTARGET_IDCMP. Nel loop di gestione degli eventi l'applicazione puo' gestire i messaggi IDCMP_IDCMPUPDATE in questo modo: switch (IntuiMsg->Class) { case IDCMP_IDCMPUPDATE: { struct TagItem *ti, *tstate; tstate = IntuiMsg->IAddress; while (ti = NextTagItem (&tstate)) switch (ti->ti_Tag) { case ATTRIBUTO1: attributo1 = ti->ti_Data; ... break; case ATTRIBUTO2: attributo2 = ti->ti_Data; ... break; ... default: break; } } ... } In alternativa, se l'applicazione e' interessata ad un solo attributo, si possono utilizzare le funzioni FindTagItem() e GetTagData() della utility.library. Per un gran numero di attributi e' invece migliore il metodo proposto sopra, perche' chiamando ripetutamente FindTagItem() o GetTagData() tutti gli elementi della TagList vengono esaminati diverse volte inutilmente. 7. Il dispatcher ================ Ogni classe boopsi possiede una funzione chiamata dispatcher. Questa funzione ha il compito di smistare ed eseguire i messaggi inviati agli oggetti della classe in questione, oppure di passarli al dispatcher della propria superclasse. La sintassi di chiamata di un dispatcher e' la seguente: ULONG MyClassDispatcher (Class *cl, Object *g, Msg msg); A0 A2 A1 Il dispatcher analizza l'ID che e' contenuto nella prima long word del messaggio ed esegue la funzione di gestione corretta per il metodo che e' stato chiamato. Se il metodo non e' implementato nella classe, deve essere inviato alla superclasse. Un dispatcher scritto in C ha grossomodo questa struttura base: ULONG HOOKCALL MyClassDispatcher ( REG(a0, Class *cl), REG(a2, Object *obj), REG(a1, Msg msg)) { ULONG result; switch (msg->MethodID) { case METODO1: result = MyClass_Metodo1 (cl, obj, msg); break; case METODO2: result = MyClass_Metodo2 (cl, obj, msg); break; ... default: /* Metodo non supportato: passare alla superclasse */ result = DoSuperMethodA (cl, obj, msg); } return result; } Il parametro obj corrisponde all'handle che e' stato passato a DoMethod() per invocare il dispatcher ed e' l'equivalente boopsi della variabile "this" del C++. Il codice dei metodi piu' semplici puo' essere implementato direttamente nel corpo del blocco switch(), ma e' preferibile mantenere tutti i metodi separati per una maggiore chiarezza e riutilizzabilita' del codice (in ogni caso il compilatore potra' comunque generare codice inline). HOOKCALL e ASM() sono due macro dipendenti dal compilatore. La prima contiene gli attributi da usare per le funzioni Hook, la seconda permette di specificare i registri in cui vengono passati i parametri. L'header "CompilerSpecific.h" che accompagna questo articolo contiene le definizioni corrette per SAS/C e GCC. Nella progettazione di un dispatcher bisogna tenere presente che quest'ultimo puo' essere chiamato in un contesto diverso da quello del task di cui fa parte. Per esempio, alcuni metodi dei gadget boopsi vengono invocati direttamente da Intuition (cioe' dall'input.device), o anche da parte di altri oggetti. Alcuni compilatori possono richiedere attenzioni particolati in casi come questo. Innanzitutto, e' necessario disabilitare il controllo e l'estensione automatica dello stack, che non puo' funzionare quando il dispatcher viene chiamato nel contesto di un altro task. Lo stesso vale anche per tutte le funzioni che vengono chiamate dal dispatcher. Inoltre alcuni compilatori permettono di generare codice che accede alle variabili globali usando degli offset a 16bit rispetto ad un indirizzo base, anziche' dei riferimenti assoluti a 32bit. Questo modello viene chiamato comunemente "base relative addressing" o anche "small data model", e generalmente produce codice piu' veloce e compatto nell'accesso ai dati contenuti negli hunk DATA e BSS del programma. In questo modello il compilatore assume che un registro indirizzi (tipicamente A4) contenga sempre il puntatore alla base della sezione dati, cosa che pero' non e' assicurata nel caso di un dispatcher boopsi, perche' come abbiamo gia' detto quest'ultimo puo' essere chiamato anche da altri task. Prima di poter accedere alle variabili globali e' dunque necessario ricaricare l'indirizzo base della sezione dati. Nel caso del SAS/C e del GCC questo si ottiene specificando l'attributo __saveds nella definizione della funzione. Anche se il dispatcher non ha riferimenti espliciti a variabili esterne, possono esservene nelle funzioni usate dal dispatcher o nelle funzioni di libreria del compilatore. Le chiamate alle librerie dinamiche come Intuition nascondono sempre un riferimento ad una variabile esterna: la base della libreria stessa. Inoltre, per i metodi dei gadget che vengono invocati da Intuition (GM_RENDER, GM_HANDLEINPUT, etc) il dispatcher non puo' attendere dei segnali o chiamare funzioni che attendono Intuition, pena il blocco totale dell'interfaccia utente. Il passaggio dei parametri per il dispatcher rispetta le convenzioni degli Hook della utility.library, percio' un metodo puo' essere invocato anche usando CallHookPkt() (oppure CallHook() di amiga.lib) al posto della canonica DoMethod(). Quando il dispatcher e' scritto in un linguaggio di alto livello che non permette di specificare in quali registri vengono passati i tre parametri, e' sempre possibile ricorrere ad uno stub in assembler che esegue le operazioni necessarie prima di chiamare la funzione vera e propria. La struttura Hook contiene a questo scopo il campo h_SubEntry, che consente di memorizzare il puntatore al dispatcher vero e proprio. La funzione HookEntry() di amiga.lib puo' essere usata come stub per i linguaggi in cui gli argomenti delle funzioni vengono passati in ordine inverso sullo stack, come nel caso del C. Gli autodoc suggeriscono inoltre che la chiamata all'hook puo' essere fatta direttamente, eliminando cosi' l'overhead di CallHookPkt(). L'header "BoopsiStubs.h", incluso nei sorgenti che accompagnano questo articolo, contiene delle implementazioni inline di DoMethod(), DoSuperMethod() e CoerceMethod(). Le versioni proposte funzionano con SAS/C e GCC, ma possono essere facilmente adattate ad altri compilatori. L'uso delle funzioni inline al posto delle chiamate alle funzioni di amiga.lib consente di risparmiare il piccolo overhead dovuto al passaggio dei parametri. Preoccuparsi di simili dettagli puo' sembrare superfluo, ma gran parte dell'overhead gestionale degli oggetti boopsi rispetto ai sistemi OOP compilati e' causato proprio dal passaggio dei metodi tra applicazione e dispatcher e tra il dispatcher e le sue superclassi. Nel caso di un sistema composto da un gran numero di oggetti complessi interconnessi tra loro, come nel caso di una tipica applicazione MUI, il numero di metodi chiamati diviene cosi' elevato che piccole migliorie come questa possono produrre un grande aumento di velocita'. Un altro fattore determinante nella velocita' di esecuzione e' la capacita' del compilatore di generare codice ottimizzato per il blocco switch() del dispatcher. 8. Struttura interna degli oggetti ================================== Le applicazioni non dovrebbero mai dipendere dall'assunto che i meccanismi interni del sistema boopsi rimangano inalterati per sempre. La programmazione orientata agli oggetti e prima ancora quella struttuata si basano sul principio di /information hiding/ con lo scopo di conferire al codice una maggiore robustezza, riutilizzabilita' ed interoperabilita'. Tuttavia, conoscere i dettagli implementativi puo' essere utile sia per chi scrive applicazioni che usano le classi boopsi, sia per chi desidera implementarne una. L'importante e' tenere ben distinte le informazioni "ufficiali" da quelle "ufficiose" e non scrivere mai codice basato sulle seconde. Ogni classe boopsi viene definita da una struttura IClass: struct IClass { struct Hook cl_Dispatcher; ULONG cl_Reserved; struct IClass *cl_Super; ClassID cl_ID; UWORD cl_InstOffset; UWORD cl_InstSize; ULONG cl_UserData; ULONG cl_SubclassCount; ULONG cl_ObjectCount; ULONG cl_Flags; }; /* Il campo cl_Dispatcher contiene una struttura Hook: */ struct Hook { struct MinNode h_MinNode; ULONG (*h_Entry)(); ULONG (*h_SubEntry)(); APTR h_Data; }; /* l'ID di una classe e' una stringa */ typedef UBYTE * ClassID; /* Il tipo "Class" e' un'abbreviazione per "struct IClass" */ typedef struct IClass Class; Le classi pubbliche vengono aggiunte ad una lista mantenuta da Intuition usando il nodo contenuto nella struttura Hook. La struttura Hook contiene inoltre il puntatore al dispatcher della classe. Il campo cl_Super punta alla superclasse. cl_ID punta ad una stringa che contiene il nome della classe (per esempio "groupgclass"). cl_ObjectCount indica il numero di oggetti che sono stati istanziati dalla classe, mentre cl_SubclassCount tiene traccia del numero di sottoclassi derivate da essa. cl_ObjectCount viene incrementato da NewObject() e decrementato da DispospeObject(). cl_SubclassCount viene aggiornato nello stesso modo da MakeClass() e FreeClass(). Quando si tenta di liberare una classe, entrambi questi contatori devono essere a zero, perche' ovviamente non e' possibile distruggere una classe che possiede delle sottoclassi o degli oggetti ancora attivi. Per comprendere il significato dei campi cl_InstOffset e cl_InstSize e' necessario vedere prima come e' strutturata l'istanza di una classe. Ogni volta si crea una nuova copia di un oggetto invocando il metodo OM_NEW, la rootclass alloca una certa quantita' di memoria che serve a contenere gli attributi ed i dati privati dell'oggetto stesso. Per esempio, ogni oggetto della classe "rettangolo" deve memorizzare nella sua istanza gli attributi Left, Top, Width e Height. Questi attributi vengono raggruppati in una struttura che l'oggetto puo' recuperare ogni volta che deve accedere ai propri dati privati. Il campo cl_InstSize contiene la dimensione in byte di questa struttura. Ogni oggetto possiede dunque nella sua istanza i dati definiti da tutte le classi da cui deriva. Poniamo per esempio che i quattro attributi della classe "rettangolo" occupino 4 byte ciascuno, per un totale di 16 byte. Una classe derivata da "rettangolo pieno", che oltre ai quattro attributi della sua superclasse definisce anche un attributo "colore", avra' la sua istanza in coda a quella di rettangolo. cl_InstOffset indica appunto l'offset dei dati privati di una classe all'interno del blocco di memoria allocato per l'oggetto, e nel caso di "rettangolo pieno" conterrebbe il valore 16. Il valore preciso di questo offset dipende dalla somma dei campi cl_InstSize di tutte le superclassi dell'oggetto. Piu' precisamente: myclass->cl_InstOffset = myclass->cl_Super->cl_InstOffset + myclass->cl_Super->cl_InstSize; NewObject() ritorna un handle astratto che l'applicazione puo' usare come riferimento all'oggetto. Questo handle altro non e' che un puntatore al primo byte dell'istanza. Le sottoclassi derivate direttamente dalla rootclass hanno cl_InstOffset = 0, percio' la loro istanza coincide con l'handle ed e' possibile accedervi direttamente. E' questo il caso dei gadget e delle immagini boopsi: gadget = (struct Gadget *) NewObject (NULL, "gadgetclass", NULL); image = (struct Image *) NewObject (NULL, "imageclass", NULL); Questa caratteristica viene chiamata "white box instance", letteralmente "istanza a scatola bianca", in contrapposizione al concetto di "scatola nera" che e' molto comune nell'OOP. L'utilita' di poter accedere direttamente all'istanza di un oggetto e' discutibile. Nel caso dei gadget e delle immagini, si tratta di una necessita' progettuale, perche' gli oggetti boopsi di questo tipo devono rimanere compatibili con i loro corrispettivi classici (gadget e immagini normali). L'altro motivo per cui l'accesso diretto ai dati contenuti nell'istanza puo' essere conveniente e' che l'operazione e' piu' semplice e quindi piu' veloce rispetto all'uso dell'interfaccia standard OOP. Nella maggior parte dei casi il piccolo aumento di velocita' che si ottiene usando queste "scorciatoie" si paga con la perdita della generalita' e della compatibilita' tipiche dell'OOP, percio' e' fortemente sconsigliato. La classe "rettangolo" potrebbe definire i quattro attributi RECTA_Left, RECTA_Top, RECTA_Width e RECTA_Height, ma internamente la clesse potrebbe esprimere le dimensioni del rettangolo in una forma differente, per sempio tramite le coordinate XY di due vertici opposti. Non e' necessario che vi sia una corrispondenza diretta tra gli attributi pubblici di una classe e la loro rappresentazione interna. Questa distinzione consente a chi implementa una classe di scegliere per i dati privati l'organizzazione piu' adatta alle operazioni che la classe dovra' svolgere su di essi. Gli oggetti che non sono derivati direttamente dalla rootclass hanno la loro porzione di istanza collocata di seguito alle istanze delle loro superclassi. Per ottenere l'indirizzo iniziale di questi dati, si utilizza la macro INST_DATA, cosi' definita: #define INST_DATA(cl, obj) ((VOID *) (((UBYTE *)obj)+cl->cl_InstOffset)) dove obj e' l'handle dell'oggetto e cl e' la classe di cui si vuole ricavare l'istanza. La distinzione tra classe "corrente" e classe "vera" (true class) dell'oggetto e' sottile. Ci sono alcune analogie con la possibilita' del C++ di manipolare un oggetto come se appartenesse ad una classe diversa da quella che e' stata dichiarata al momento della sua creazione. Un oggetto boopsi ha un dispatcher diverso per ognuna delle classi classi da cui deriva. Normalmente i metodi vengono passati al dispatcher dalla true class dell'oggetto, la quale passa quelli non implementati alla propria superclasse usando DoSuperMethod(). Al dispatcher della superclasse viene passato il puntatore alla propria struttura IClass, che verra' utilizzato per accedere ai dati privati dell'istanza tramite la macro INST_DATA. Ogni oggetto boopsi deve contenere inoltre il riferimento alla struttura IClass da cui e' stato istanziato perche' si possa risalire alla classe di un oggetto conoscendone soltanto l'handle. In viene definita una macro che svolge questa funzione: struct IClass *classe = OCLASS (oggetto); Come funziona questa macro? Abbiamo visto che la struttura dell'istanza di un oggetto contiene soltanto dei dati privati che dipendono dalla classe. In alcuni casi l'istanza corrisponde addirittura a delle strutture preesistenti (Gadget e Image) che non possono essere alterate per accogliere un puntatore alla classe. Non e' neanche possibile aggiungere dei dati in coda all'istanza perche' la sua dimensione e' variabile e dipende dalla somma delle dimensioni delle istanze definite da tutte le superclassi dell'oggetto. Rimane dunque una sola possibilita': memorizzare il puntatore alla superclasse ad un offset negativo rispetto all'handle dell'oggetto. Il funzionamento e' simile a quello dei vettori di salto delle librerie di Exec, che si trovano appunto ad un offset negativo rispetto alla base. I dati memorizzati agli offset negativi dell'istanza possono essere considerati come la parte di istanza definita dalla rootclass, perche' tutti gli oggetti ne sono provvisti. La definizione corrente di questi dati e': struct _Object { struct MinNode o_Node; struct IClass *o_Class; }; o_Class si trova all'offset -4 rispetto all'handle dell'oggetto e contiene il puntatore alla true class. La struttura o_Node e' privata e non deve essere usata. Se in futuro la struttura _Object venisse modificata, l'unico campo che verra' sicuramente mantenuto e' o_Class (che sara' sempre l'ultimo membro della struttura). Un errore molto comune nella programmazione delle classi boopsi e' tentare di accedere alla propria porzione di istanza in questo modo: myinstdata = INST_DATA (OCLASS(o), o); Questo metodo funziona perfettamente per gli oggetti istanziati direttamente dalla classe stessa. Quando pero' l'oggetto appartiene ad una classe derivata, OCLASS(o) non coincide piu' con la classe di cui si vuole ottenere l'istanza e myinstdata viene cosi' inizializzato con un'indirizzo errato. Il diagramma in figura 3 mostra la struttura interna di un oggetto istanziato dalla classe "propgclass" (un gadget proporzionale). Lo schema mostra anche che la classe propgclass e' derivata dalla gadgetclass, la quale a sua volta e' una sottoclasse della rootclass. La struttura gadget e' immediatamente accessibile utilizzando l'handle, mentre il resto dell'istanza e' privato e non puo' essere manipolato direttamente. [inserire figura 4] 9. OM_NEW e OM_DISPOSE ====================== OM_NEW costruisce ed inizializza una nuova istanza di una classe. In genere questo metodo viene chiamato usando la funzione NewObject() di Intuition. OM_NEW deve ritornare un puntatore all'handle dell'oggetto o NULL quando la creazione fallisce. Il pacchetti di parametri di OM_NEW e di OM_SET sono identici: struct opSet { ULONG MethodID; /* Contiene l'ID OM_NEW o OM_SET */ struct TagItem *ops_AttrList; /* attributi da inizializzare */ struct GadgetInfo *ops_GInfo; /* NULL per OM_NEW */ }; OM_NEW differisce da tutti gli altri metodi perche' viene chiamato prima ancora che l'oggetto esista. Il secondo parametro ricevuto dal dispatcher non puo' quindi contenere il puntatore all'handle dell'oggetto, ed e' invece un puntatore alla true class dell'oggetto. La prima cosa che il dispatcher deve fare e' ottenere dalla propria superclasse l'indirizzo del nuovo oggetto. Questo viene fatto passando il messaggio OM_NEW al dispatcher della propria superclasse, che a sua volta si comportera' nello stesso modo, finche' il messaggio non raggiungera' il dispatcher della rootclass. Quando riceve il messaggio OM_NEW, la rootclass incrementa il contatore di utilizzo della true class e alloca un blocco di memoria sufficiente a contenere i dati per l'istanza della true class e di tutte le sue superclassi. Ecco un esempio generico della gestione del metodo OM_NEW: Object * MYCLASS_OMNew (Class *cl, Class *trueclass, struct opSet *msg) { Object *obj; /* Passa il messaggio OM_NEW alla superclasse */ obj = DoSuperMethodA (cl, (Object *)trueclass, (Msg) msg); if (obj) { struct MyClassData *myclassdata; /* Ottiene il puntatore ai dati dell'istanza */ myclassdata = (struct MyClassData *) INST_DATA (cl, obj); /* Inizializza i dati dell'istanza */ myclassdata->Attributo1 = GetTagData ( MYCLASS_Attributo1, ATTRIBUTO1_DEFAULT, msg->ops_AttrList); myclassdata->Attributo2 = GetTagData ( MYCLASS_Attributo2, ATTRIBUTO2_DEFAULT, msg->ops_AttrList); ... } return obj; } Le classi che non hanno dati da inizializzare nelle prorie istanze possono anche non implementare affatto OM_NEW e lasciare che le proprie superclassi se ne prendano cura al loro posto. La memoria allocata dalla rootclass per i dati dell'istanza non viene in alcun modo inizializzata automaticamente, percio' il codice di gestione del il metodo OM_NEW deve assicurarsi per lo meno di aver azzerato tutti i campi della propria struttura di dati. Per gestire gli attributi che hanno applicabilita' (IS), gli attrubiti che possono cioe' essere specificati sia in fase di inizializzazione che con il metodo OM_SET, la classe puo' invocare su se' stessa il metodo OM_SET usando CoerceMethod(). Il metodo OM_DISPOSE libera le risorse allocate dall'oggetto. Dopo aver inviato il metodo OM_DISPOSE alla superclasse, il dispatcher non puo' piu' accedere ai dati contenuti nella propria istanza. Le classi che non possiedono altri dati oltre quelli contenuti nell'istanza possono anche non implementare questo metodo, perche' la liberazione dell'oggetto viene gestita autonomamente dalla rootclass. Il metodo OM_DISPOSE non ha altri parametri oltre l'ID e non definisce alcun valore di ritorno. 10. OM_SET e OM_GET =================== Usando il metodo OM_SET e' possibile impostare contemporaneamente diversi attributi, alcuni dei quali possono essere ereditati da una delle superclassi dell'oggetto. Il dispatcher analizza uno per volta gli attributi contenuti nella TagList del messaggio opSet e per ognuno di essi decide cosa fare. Se la TagList contiene anche degli attributi sconosciuti, il dispatcher deve passarla alla propria superclasse in modo che abbia la possibilita' di aggiornare di conseguenza i dati contenuti nella propria istanza. ULONG MYCLASS_OMSet (Class *cl, Object *obj, struct opSet *msg) { struct MyClassData *myclassdata; struct TagItem *ti, *tstate; BOOL do_super_method; do_super_method = FALSE; myclassdata = (struct PIPData *) INST_DATA (cl, obj); tstate = msg->opu_AttrList; while (ti = NextTagItem (&tstate)) switch (ti->ti_Tag) { case MYCLASS_Attributo1: ... myclassdata->Attributo1 = ti->ti_Data; ... break; case MYCLASS_Attributo2: ... myclassdata->Attributo2 = ti->ti_Data; ... break; default: /* La TagList contiene degli attributi non gestiti */ do_super_method = TRUE; } /* Invia il messaggio OM_SET alla superclasse, * solamente se necessario. */ if (do_super_method) return DoSuperMethodA (cl, (Object *)g, (Msg) msg); return 0; } Il risultato di OM_SET non e' definito esplicitamente, ma in genere le classi ritornano il valore 0 quando gli attributi impostati non hanno causato alcun cambiamento visivo nell'oggetto, un valore diverso da zero in caso contrario. OM_GET e' il metodo che permette alle applicazioni di leggere il valore di un attributo. Viene invocato utilizzando la funzione di Intuition GetAttr(). Il messaggio associato a questo metodo e' cosi' definito: struct opGet { ULONG MethodID; ULONG opg_AttrID; ULONG *opg_Storage; } Il dispatcher deve copiare nel buffer puntato da opg_Storage il valore dell'attributo opg_AttrID. Per convenzione il buffer fornito deve essere abbastanza grande da contenere una variabile LONG, e tutti i tipi di dimensione inferiore devono essere promossi a LONG. Se l'attributo richiesto non viene riconosciuto, il dispatcher deve sottoporre il messaggio OM_GET alla propria superclasse. La documentazione sul codice di ritorno corretto per questo metodo e' contradditoria: l'autodoc di GetAttr() sostiene che la funzione ritorna FALSE quando l'attributo non e' stato riconosciuto, mentre l'appendice B del RKRM dichiara che il risultato di OM_GET non e' definito. Nel dubbio, conviene attenersi all'autodoc di GetAttrs() e ritornare un valore diverso da zero per gli attributi riconosciuti dal dispatcher. 11. Notifica ============ Il sistema di notifica boopsi permette di trasmettere ad un oggetto detto /target/ tutti i cambiamenti che avvengono negli attributi di un altro oggetto. I tre elementi che entrano in gioco nella notifica sono: * i metodi OM_NOTIFY e OM_UPDATE, definiti dalla rootclass * le classi di interconnessione (la icclass e la sua sottoclasse modelclass) * gli attributi ICA_TARGET e ICA_MAP, definiti dalla "icclass" e dalla gadgetclass. Il metodo OM_NOTIFY trasmette una TagList di attributi all'oggetto target specificato dall'attributo ICA_TARGET. Gli oggetti chiamano questo metodo su se' stessi quando il valore di alcuni attributi con applicabilita' N (Notify) viene alterato. In genere le sottoclassi della gadgetclass non implementano direttamente il metodo OM_NOTIFY, percio' il messaggio viene passato alla superclasse finche' non raggiunge un dispatcher che lo riconosce ed invia all'oggetto target un messaggio OM_UPDATE contenente gli stessi parametri di OM_NOTIFY. Il pacchetto di parametri di OM_NOTIFY e di OM_UPDATE e' simile a quello di OM_SET: struct opUpdate { ULONG MethodID; /* OM_NOTIFY o OM_UPDATE */ struct TagItem *opu_AttrList; /* lista dei nuovi attributi */ struct GadgetInfo *opu_GInfo; /* contesto del gadget */ ULONG opu_Flags; /* flags */ }; Il campo aggiuntivo opu_Flags serve a contenere contiene il flag OPUF_INTERIM, che viene impostato quando il messaggio contiene un aggiornamento "intermedio" degli attributi, ed e' azzerato per gli aggiornamenti finali. Per esempio, uno slider invia molti messaggi intermedi mentre l'utente sta spostando l'indicatore, ed un messaggio finale quando l'utente rilascia il gadget. Alcuni oggetti target possono essere interessati soltanto agli aggiornamenti finali e non a quelli intermedi. Dal momento che OM_UPDATE e OM_SET sono molto simili, in genere le classi utilizzano la stessa routine per gestire entrambi questi metodi. Quando un oggetto riceve un messaggio OM_UPDATE, non deve far altro che esaminare la TagList ed impostare i propri attributi di conseguenza, esattamente come nel caso di OM_SET. Certe volte puo' capitare che il cambiamento di un attributo ricevuto con OM_UPDATE scateni l'invio di una seconda notifica da parte dell'oggetto. In questo caso, l'oggetto *deve* copiare il contenuto del campo flags del messaggio OM_UPDATE nel campo flags del messaggio OM_NOTIFY. In caso contrario, tra due oggetti potrebbero verificarsi dei ping-pong di notifiche che porterebbe inevitabilmente ad un blocco totale del sistema. Per evitare che cio' accada, la icclass implementa un sistema in grado di riconoscere i loop e fermarli, utilizzando dei flags non documentati nel campo opu_Flags di opUpdate. Dal momento che classi diverse utilizzano attributi diversi, quando si collegano due oggetti e' spesso necessario operare una traduzione negli ID degli attributi. ICA_MAP permette di specificare una TagList in cui ogni tagitem e' una coppia di valori che associa il un attributo del primo oggetto all'attributo corrispondente per il secondo oggetto. Per esempio, uno string gadget non conosce il tag PGA_Top, che rappresenta la posizione dell'indicatore di un gadget proporzionale. Lo string gadget conosce invece l'attributo STRINGA_LongVal. Per collegare questi due oggetti dobbiano quindi tradurre l'attributo PGA_Top in STRINGA_LongVal e viceversa, lasciando inalterato il valore numerico associato a questi due attributi. Una traduzione biunivoca di questo tipo si ottiene creando le seguenti TagList: static ULONG MapPropToString[] = { PGA_Top, STRINGA_LongVal, TAG_DONE }; static ULONG MapStringToProp[] = { STRINGA_LongVal, PGA_Top, TAG_DONE }; e collegando tra loro gli oggetti in questo modo: SetGadgetAttrs (prop, window, requester, ICA_TARGET, string, ICA_MAP, MapPropToString, TAG_DONE); SetGadgetAttrs (string, window, requester, ICA_TARGET, prop, ICA_MAP, MapStringToProp, TAG_DONE); Si puo' usare SetAttrs() al posto di SetGadgetAttrs() soltanto se i gadget non sono ancora stati aggiunti ad una finestra. ICA_MAP utilizza la funzione MapTags() della utility.library per tradurre gli attributi. Gli attributi che non compaiono nella TagList di ICA_MAP vengono lasciati passare inalterati. Un effetto collaterale di questa implementazione e' che gli ID dei tag passati al metodo OM_NOTIFY vengono modificati, percio' non si possono usare delle TagList definite in modo statico, perche' dopo il primo utilizzo i tag in esse contenuti sarebbero permanentemente alterati. Nel precedente esempio il valore numerico degli attributi rimane invariato. Altri casi potrebbero richiedere una traduzione piu' complessa. Per esempio, uno slider che imposta la profondita' di uno schermo potrebbe essere collegato ad un oggetto che indica il numero di colori in funzione del numero di bitplanes (2^n). La icclass non fornisce alcun metodo per risolvere questo problema. Si puo' ottenere un risultato analogo creando una classe derivata dell'indicatore numerico, definendo un nuovo attributo, oppure definendo una sottoclasse della icclass che svolga automaticamente la traduzione del valore. La icclass e la gadgetclass permettono di specificare un solo target per la notifica. La modelclass e' una sottoclasse della icclass che trasmette notifiche a piu' target contemporaneamente. Per fare questo, la modelclass mantiene una lista di oggetti icclass, ed invia in sequenza ad ognuno di essi una copia della TagList contenente gli attributi da ritrasmettere. Gli oggetti icclass vengono aggiunti e rimossi al model usando i metodi OM_ADDMEMBER e OM_REMMEMBER. Quando un oggetto modelclass viene distrutto, vengono liberati anche tutti i suoi membri. 12. Classi con caratteristiche peculiari ======================================== Sappiamo gia' che un oggetto deve contenere nella sua istanza i dati della propria classe e di tutte le sue superclassi. Quando si progetta una nuova classe derivandola da una preesistente, bisogna tener conto del fatto che l'istanza non si trova ad un offset fisso rispetto all'handle e che la nostra classe puo' essere a sua volta derivata per ottenere classi piu' specializzate. Una classe ben progettata deve poter essere facilmente adattata per poter essere usata con una superclasse diversa da quella originale. Il sistema boopsi consente di dichiarare la superclasse a run-time (la superclasse e' un argomento della funzione MakeClass()), percio' e' possibile creare classi che possono essere usate per derivare una qualsiasi altra classe. Per esempio, sarebbe possibile scrivere una "debugclass" che utilizzi le funzioni della debug.lib per fare un dump via seriale di tutti i metodi ricevuti prima di passarli alla propria superclasse. Si potrebbe anche realizzare una classe "trackclass" che ridefinisca i metodi OM_NEW e OM_DISPOSE di tutte le classi esistenti per implementare il resource tracking su tutti gli oggetti e liberarli automaticamente quando l'applicazione che li ha creati viene chiusa. Sarebbe comunque meglio implementare questa caratteristica direttamente nella rootclass, ma dal momento che non ne possediamo i sorgenti, questo non e' possibile. In realta' non e' difficile scrivere un patch per il dispatcher di una classe esistente, sostituendo il dispatcher originale con uno differente. Cio' che rende complicato aggiungere a posteriori del resource tracking alla rootclass e' la mancanza di spazio all'interno dell'istanza per memorizzare i dati che servono. Nel caso di ereditarieta' multipla (multiple inheritance), il meccanismo di passaggio dei metodi non implementati alla superclasse diviene piu' complesso. Un oggetto puo' avere una sola superclasse, ma puo' incorporare al suo interno oggetti che appartengono ad altre classi. Basta crearli con NewObject() e memorizzarne l'handle da qualche parte nell'istanza. La classe a questo punto puo' provare a passare i metodi sconosciuti a tutti questi oggetti prima di passarli alla propria superclasse. In questo modo la classe si comporta come se possedesse contemporaneamente anche tutti i metodi delle sue superclassi fittizie. Una osservazione interessante e' che questo sistema permette di supportare automaticamente anche i metodi che verranno definiti in versioni future delle superclassi che si vogliono avere, senza alcun bisogno di modificare o ricompilare la classe stessa. L'unico requisito indispensabile e' che i valori numerici associati ai metodi siano sempre distinti tra due diverse classi. Questa caratteristica e' garantita per tutte le classi built-in di Intuition, e /dovrebbe/ esserlo anche per quelle progettate da terze parti. Il condizionale e' d'obbligo perche' per evitare conflitti di questo tipo la Commodore aveva stabilito che gli sviluppatori di nuove classi pubbliche richiedessero al gruppo di West Chester l'assegnazione dei valori da usare per i nuovi metodi e per gli attributi. Un sistema simile era stato adottato dalla Electronic Arts per l'assegnazione dei chunk ID per i file IFF. Purtroppo il fallimento di Commodore ed il conseguente smantellamento del laboratorio ricerca ha costretto gli sviluppatori esterni ad "inventare" dei numeri, facendo molta attenzione ad evitare conflitti con tutte le classi gia' esistenti. Comunque, per quante precauzioni si prendano, la mancanza di centralizzazione non puo' garantire l'assenza totale di conflitti di questo tipo. A cosa puo' servire un oggetto con due o piu' superclassi? Un esempio banale e' un gadget simile al LISTVIEW_KIND della GadTools, che riporta in uno string gadget la selezione corrente. Anziche' creare due oggetti distinti ed interconnetterli manualmente ogni volta, puo' risultare piu' comodo creare una classe "strlistviewgclass" che ne incapsuli la gestione. Una classe di questo tipo dovrebbe implementare tutti i metodi e gli attributi delle classi "listviewgclass" e "strgclass". 13. Tecniche di debug ===================== Il debug di una classe boopsi puo' essere piu' difficoltoso rispetto ad un programma qualsiasi. Le tecniche classiche di debug sono spesso inefficaci, percio' e' necessario usare qualche accorgimento che permetta di trovare piu' facilmente i bug. Innanzitutto, dal momento che alcuni metodi dei gadget vengono chiamati sotto il contesto dell'input.device, usando un normale debugger non e' possibile bloccarne l'esecuzione inserendo dei breakpoints e tanto meno eseguire il codice un'istruzione per volta. Il motivo e' semplice: se l'input.device si blocca, si blocca anche l'interfaccia utente e non e' piu' possibile controllare il debugger. La soluzione piu' semplice per questo problema consiste nell'usare un cross-debugger su un secondo computer che comunica con il primo tramite un cavo null-modem o un altro tipo collegamento bidirezionale. Il cprx del SAS/C e' probabilmente l'unica scelta possibile, sebbene abbia la tendenza ad inchiodarsi quasi piu' spesso dei programmi che pretende di debuggare. Enforcer, Mungwall e gli altri tool di sviluppo canonici possono essere di aiuto soltanto se il loro output viene inviato ad un terminale remoto, sempre via seriale. Non e' neppure possibile scrivere informazioni di debug usando le normali funzioni di I/O della dos.library e tantomeno quelle della libreria standard del C, perche' input.device e' un task e non un processo, quindi non puo' usare funzioni della dos.library. Di nuovo, un terminale remoto e' l'unica soluzione possibile. La libreria linkabile debug.lib contiene delle funzioni come kprintf(), che permettono di ottenere facilmente un output formattato sulla seriale interna presente in tutti i modelli di Amiga. Se non si dispone di un computer da usare come terminale, e' sempre possibile utilizzare la porta parallela linkando con la ddebug.lib [con due 'd', non e' un errore]. Una comune stampante Centronics collegata alla porta parallela e' adatta allo scopo. Il file "Debug.h" che trovate nei sorgenti che accompagnano questo articolo contiene qualche macro di uso generale. Durante la scrittura dei metodi conviene inserire il maggior numero possibile di controlli di validita' sui parametri usando le macro ASSERT() e CHECK_PTR(). Queste macro producono un output soltanto quando non viene soddisfatta la condizione specificata oppure quando un puntatore non e' valido. I controlli vengono completamente eliminati quando si compila senza debug. Una causa molto comune di problemi con gli oggetti boopsi e' l'overflow dello stack. Il task input.device ha uno stack di soli 4KB, che bastano a malapena per far girare un complesso network di oggetti che comunicano tra loro scambiandosi dei messaggi costruiti sullo stack. All'interno del dispatcher di un oggetto boopsi evitate di dichiarare variabili automatiche di grandi dimensioni e di chiamare funzioni in modo ricorsivo. In caso di bisogno, e' possibile estendere lo stack usando StackSwap(). Bibliografia ============ Amiga ROM Kernel Reference Manual: Libraries (3rd edition). 3.1 Native Developer Toolkit Amiga Developer CD 1.1 boopsi Reference, http://www.cs.utah.edu/~stack/boopsi.html Bernardo Innocenti