Prima di analizzare WinRT bisogna tornare indietro nel tempo e ripercorrere l'evoluzione dei linguaggi.
Il linguaggio C ha consolidato un modo standard per interoperare in due diversi modi:
- A livello sorgente grazie alla standardizzazione ANSI del 1989 (e successive)
- A livello binario, grazie alle calling convention dell'architettura x86 (stdcall, fastcall, cdecl) che stabilisce il modo in cui sono passati i parametri (ed altro).
Ad oggi questo rimane il modo più diffuso di fare interoperabilità tra linguaggi e, anche eseguiamo chiamate PInvoke con il Framework.NET stiamo di fatto utilizzando queste calling convention.
Quando utilizziamo le Win32, stiamo chiamando le API con la calling convention "stdcall".
Come nota di colore, il mio primo beta testing fu del compilatore MASM (Macro Assembler) di Microsoft come conseguenza di un bug che trovai nella versione 5.1 proprio nelle calling convention del compilatore interoperando con il compilatore C dell'epoca (parliamo degli anni 80).
Con l'affermazione di C++ l'interoperabilità è sempre stata solo a livello sorgente. La commissione ISO (oggi presieduta da Herb Sutter) ha stabilito il primo standard nel 1998 (posteriore all'uscita di Visual C++ 6.0 che perciò è fuori standard) e successivi, fino ad arrivare all'attuale C++11 di questo Agosto 2011.
La commissione non ha mai stabilito alcuno standard a livello binario tanto che non è neppure possibile utilizzare librerie che provengono da due versioni differenti della stessa marca di compilatore. È sempre necessario ricompilare tutto con la stessa versione del compilatore C++.
Ecco alcuni dei problemi di uno standard binario per un linguaggio object oriented:
- definizione di un type system (pensiamo a quanti tipi 'string' sono stati creati in C e C++)
- gestione del ciclo di vita degli oggetti (costruzione e distruzione)
- definizione di uno standard per la "decorazione" applicata dal compilatore per differenziare le funzioni in overload
- definizione di incapsulamento a livello binario (C++ supporta l'ereditarietà private o protected, per cui solo il compilatore conosce l'esatto layout della classe. Da qui la necessità di separare i concetti di interfaccia binaria (virtual table) dalle sue possibili implementazioni in oggetti binari differenti
Parlando di sistemi distribuiti, il problema è ancora più complesso. Pensiamo ad esempio di dover invocare una funzione remota che ha per parametro un blob (Binary Large OBject). In linguaggio C questo buffer è definito da un puntatore che però non ha l'informazione della lunghezza del blob. In C++ il blob può essere incapsulato in una classe ma C++ non fornisce la semantica necessaria per dichiarare, durante l'invocazione della chiamata remota, il blob sia in ingresso, uscita o entrambe le direzioni.
In un sistema distribuito attraversare un "boundary" (confine) ha un costo. Se il blob fosse un parametro di un web service, il costo in termini di banda per spostarlo in una o l'altra direzione non potrebbe essere ignorato.
Per colmare queste lacune nasce IDL (Interface Definition Language a cura dell'Open Software Foundation Distributed Computing Environment Remote Procedure Call) che viene adottato da Microsoft per realizzare COM e dal mondo Unix per realizzare CORBA. La versione binaria dei metadati generata grazie a IDL, le type library, sono le moderne librerie binarie che permettono anche l'interoperabilità binaria tra linguaggi differenti.
Non posso biasimare Don Box quando affermava "COM is Love" indicando (mia interpretazione) finalmente COM come una reale soluzione pratica, vincente e performante per i problemi di compatiblità binaria, interoperabilità e comunicazione distribuita. Tutt'ora è una soluzione che funziona egregiamente in Windows e la più performante (escludendo ovviamente le API "C" in quanto non forniscono una soluzione ai problemi risolti da COM). Nota di colore: Nei corridoi di //build/, al termine della sessione di Herb Sutter ho visto passare Don Box e gli ho detto "So COM is still love" ed è stato un piacere vederlo sorridere.
Fino a qui siamo agli anni 90 al termine dei quali emergono diversi problemi:
- Il type system è inadeguato e troppo complesso (BSTR, VARIANT, SAFEARRAY, …) e spesso richiede un custom marshaler (un oggetto che sia in grado di descrivere come copiare il layout di memoria di un tipo custom)
- Le collection non sono native
- Il sistema di eventi basato sui connection point è complesso
- Il ciclo di vita degli oggetti basato sul reference counting deve essere gestito manualmente ed è complesso
- La registrazione di interfacce e classi (registry)
In C++ alcuni di questi problemi vengono colmati dalle librerie ATL (Active Template Library) ma rimane pur sempre una libreria disponibile solo in C++ e complessa da utilizzare.
Potenza hardware
A partire dalla fine anni 90 la potenza dei PC superava abbondantemente i requisiti di una applicazione standard. Questo è stato il trigger che ha visto la nascita di Java e poi del Framework.NET. In quel periodo storico l'overhead dovuto ai runtime e alla garbage collection era minimo rispetto ai benefici di produttività e manutenibilità. Inoltre, in assenza di un moderno standard C++ che ha visto la luce solo questo Agosto 2011, linguaggi come C# hanno avuto giustamente una vasta diffusione.
Il Framework.NET, basato nella sua infrastruttura su COM, riafferma diversi concetti che abbiamo già analizzato:
- Definisce un type system moderno
- Provvede alle specifiche CLI che permettono di utilizzare più linguaggi
- Definisce la compatibilità a livello binario
- Aumenta la ricchezza dei metadati rispetto a quelli forniti da IDL in COM (pensiamo a reflection)
- Definisce un Intermediate Language (IL) che permette il disaccoppiamento rispetto all'architettura di sistema (x86 / x64 / …)
In fondo il vero plus del Framework.NET è quello del runtime, da tanti vituperata, che ha comunque diminuito drasticamente il numero di bug nelle applicazioni moderne e un aumento incredibile della produttività che si traduce direttamente nella possibilità di creare applicazioni più sofisticate. Il resto erano già concetti presenti in COM anche se espressi in modo più complesso.
Non più solo PC
Siamo quindi ai giorni nostri e i requisiti sono nuovamente cambiati. Gli utenti infatti chiedono dispositivi sempre più piccoli e potenti (ma che per ovvi motivi non possono essere tanto potenti quanto lo è un PC). Questo requisito si soddisfa principalmente con due fattori: minore consumo di CPU e (come conseguenza) di batteria.
Se prima avevamo cicli da buttare, oggi il discorso è cambiato quel tanto da non poterlo più ignorare.
Un altro fattore non indifferente è che l'introduzione del runtime implica la gestione della sua versione. Garantire che una applicazione che gira su una versione di runtime possa girare anche su una diversa versione di runtime è molto complesso, forse una chimera.
I requisiti cambiano ancora:
- uso di codice nativo
- conservare la produttività della programmazione con .NET
- type system moderno
- performance
- eterogenità di linguaggi
- flessibilità nella gestione della versione
Il Windows RunTime (WinRT) getta le basi su questi concetti e ne aggiunge di nuovi che vedremo in prossimi post quali per esempio il meccanismo di attivazione e le chiamate asincrone.
WinRT non poteva che ripartire da un'infrastruttura ben consolidata nel sistema operativo quale COM, ma apportando una serie di modifiche utili a superare i problemi che abbiamo analizzato in precedenza.
WinRT conserva i meccanismi di layout degli oggetti, IUnknown con le sue AddRef, Release e QueryInterface. Niente più IDispatch che ha dato tanti grattacapi agli sviluppatori per fornire invece la IInspectable. Può sembrare paradossale come queste due interfacce, tra le altre cose, svolgano ruoli simili e cioè la scoperta dei metdati.
Non so se esistano altri precedenti nel mondo dei sistemi operativi, ma l'esposizione dei servizi del sistema operativo in modo object oriented è certamente un grosso salto in avanti. A partire dal prossimo post comincerò ad analizzare le varie parti di WinRT.