.NET Framework
Il framework .net e’ l’ambiente che consente la creazione e l’esecuzione di programmi di nuova generazione e di web services. L’obiettivo e’ quello di fornire un ambiente orientato agli oggetti in modalita’ locale e remota, di minimizzare conflitti di versoni e problemi in fase di distribuzione del software, di consentire una esecuzione sicura del codice, di rendere coerente per lo sviluppatore il lavoro sia su apps web che desktop.
Le 2 componenti principali del framework sono il CLR (Common Language Runtime) e la libreria di classi.
Il framework e’ passato attraverso una serie di [ri|e]voluzioni, ad oggi (18/11/2008) l’ultima versione e’ la 3.5SP1; da 4guysfromrolla prendo uno schema utile per comprendere le componenti:

Il CLR
Visto in ottica Java, il CLR equivale alla JVM, puo’ essere visualizzato come il nucleo del framework. Si occupa dell’attivazione degli oggetti, dei controlli di sicurezza su di essi, della gestione della memoria.
Nell’ambiente .NET il CLR permette l’esecuzione dei cosiddetti PE (Portable Executable) file, che sono i famosi assembly, cioe’ l’unita’ minima di sviluppo sui .NET, costituita da un file MANIFEST, uno o piu’ moduli ed eventualmente un insieme di risorse. I PE possono essere EXE o DLL che consistono di Metadati e di codice.
PE - Portable Executable
Gli eseguibili .NET differiscono dai classici exe windows in quanto, oltre al codice e ai dati, essi contengono anche i metadati, che sono info relative a una risorsa, si potrebbero definire dati sui dati e consistono in dettagli sui contenuti, formato, dimensione o altre caratteristiche dei dati.
Supponendo di scrivere il classico Hello World in:
C#:
using System;
public class MainApp
{
public static void Main( )
{
Console.WriteLine("C# Hello, World!");
}
}
VB.NET:
Imports System
Public Module MainApp
Public Sub Main( )
Console.WriteLine ("VB Hello, World!")
End Sub
End Module
Dopo la compilazione (csc hello.cs oppure vbc /t:exe /out:hello.exe hello.vb) viene generato il file .exe
conforme al formato PE File, che deriva dalle specifiche del COFF (Common Object File Format), esteso in modo da include metadati e codice IL.
Formato di un file PE:
PE/COFF Header
———————
CLR Header
———————
CLR Data (Metadata e IL)
———————
Native img section (.data,.rdata,.rsrc,.text)
Facendo un dumpbin.exe del nostro PE, si ottiene questo.
METADATI
Si e’ detto che i metadati sono info relative a una risorsa, si potrebbero definire dati sui dati e consistono in dettagli sui contenuti, formato, dimensione o altre caratteristiche dei dati; per fare dialogare due componenti una con l’altra e’ necessario che almeno una di esse conosca le caratteristiche dell’altra; prima di .NET cio’ era patrimonio esclusivo del C++ attraverso le type libraries, adesso tutto cio’ e’ fattibile grazie ai metadati.
in .NET i metadati sono un mezzo (o un dialetto) che il compilatore, il runtime e tutti i tools possono usare per ottenere informazioni su tutti i tipi che sono esposti da un particolare assembly .NET. I metadati descrivono un assembly in dettaglio a partire da info sulla sua identita’ (nome, versione. culture, key) e su tutti i tipi da lui referenziati, esportati, su tutti i moduli, classi, metodi, proprieta, eventi, ecc
I metadati garantiscono l’interoperabilita dei linguaggi in quanto tutti i linguaggi devono usare gli stessi oggetti per poter generare un PE valido; si puo’ affermare che non ci sarebbe .NET senza metadati.
Per poter esaminare un PE esiste un tool chiamato ILDASM che consente di vedere sia i metadati che il codice IL; il dump del nostro hello.exe produce questo output ; ILDASM utilizza il namespace System.Reflection per lavorare sull’assembly, looppare sui suoi metodi e vederne i metadati.
E’ interessante notare che possiamo vedere il codice di ogni assembly pur senza possederne il codice sorgente, a meno che non siano state usate forme di offuscamento o encrypt del codice.
Assemblies e manifest
Gli assembly sono l’unita’ minima di sviluppo sui .NET, costituita da un file MANIFEST, uno o piu’ moduli ed eventualmente un insieme di risorse;se nel mondo COM per garantire l’univocita’ di un EXE o di una DLL era necessario ricorrere a incomprensibili GUIDs, su .NET ci si riferisce a un assembly tramite il suo nome e il suo namespace; se cio’, a livello globale, non e’ sufficiente, si ricorre a una coppia di chiavi pubblica/privata; il creatore puo’ firmare il suo assembly con una chiave privata e ognuno puo’ visualizzarne la firma digitale attraverso la sua chiave pubblica. Dal momento che nessun altro possiede la sua chiave privata, nessuno puo’ generare lo stesso assembly.
In fase di compilazione, il compilatore genera un hash, lo firma con la chiave privata e salva la firma in una sezione riservata del PE; la chiave pubblica e’ altresi’ salvata nell’assembly. Per verificarne la firma digitale il CLR usa la chiave pubblica per decriptarla usando l’hash originale. In piu’ il CLR usa le info nel manifest per per generare dinamicamente un nuovo hash, se i 2 hash coincidono e’ tutto ok, altrimenti vuol dire che qualcuno ha smanettato sull’assembly e questo non viene lanciato.
Gli assemblies possono essere:
Static: PE creati a compile time, tramite tools quali csc, vbc, cl, eccc
Dynamic: creati dinamicamente a runtime usando le classi del NS System.Reflection.Emit
Private: assembly static usati da una specifica applicazione
Shared: hanno un unico metodo condiviso e possono essere usati da tutte le applicazioni
Per cio’ che riguarda le versioni, un assembly ha il seguente formato: <major_ver>.<minor_ver>.<build_num>.<revision>
Dal momento che un assembly manifest contiene info anche su eventuali risorse esterne non e’ piu’ necessario ricorrere al registry, usando le info del manifest il CLR carica l’assembly corretto.
Sicurezza
.NET introduce un nuovo concetto, quello di code identity, tramite cui anche un pezzo di codice ha un’identita (nome, versione, cultura, ecc), di conseguenza viene introdotto anche il concetto di code access, in base a cui l’assembly puo’ o meno accedere a determinate risorse in base a particolari policies. Il CLR si occupa di verificare tali policies e, in caso negativo, impedisce l’esecuzione sollevando un’eccezione di sicurezza.
DLL hell
Quando si compila una applicazione che usa uno shared assembly, il manifest dell’assembly viene appeso a quello dell’applicazione e parallelamente l’hash a 8 bytes dell’assembly viene aggiunto a quello dell’applicazione; con questa coppia di hash il CLR conosce perfettamente l’identita’ dell’assembly usato dall’app.
Per essere considerati shared, gli assembly vanno registrati nella GAC (Global Assembly Cache) tramite il gacutil.exe
IL - Intermediate Language
Per poter eseguire un assembly e’ necessario tradurre il codice sorgente in MSIL (MicroSoft Intermediate Language) che e’ un insieme di istruzioni indipendenti dall CPU che poi puo’ essere convertito in modo efficiente in codice nativo. La presenza del MSIL rende possibile la creazione di assembly partendo da piu’ linguaggi. Dall’MSIL si avra’ poi la conversione in native code grazie al JIT compiler
CTS - Common Type System
Si e’ visto che per .NET tutti i linguaggi sono equivalenti, nel senso che una classe in C# e’ equivalente a una in VB; per consentire cio’, MS ha introdotto un sistema comune di tipi di dato a cui ogni linguaggio si deve attenere.
Value type
Il CLR supporta due tipi: value type e reference type; i value type sono allocati sullo stack, non possono essere nulli e devono necessariamente contenere dati. Se si passa un parametro di tipo value a una funzione significa che viene inviata una copia di tale valore; cio’ significa che il valore originale non viene variato, qualsiasi cosa succeda dentro la funzione. In realta’, su oggetti di grandi dimensioni, questo modo di operare consuma memoria perche’ l’operazione di copia comporta continue allocazioni di spazio.
Reference type
Per ovviare al suddetto problema esistono i reference type, cosi’ chiamati perche’ contengono riferimenti a oggetti nella heap e possono essere nulli. Quanto succede che si passi un reference type a una funzione,
in realta’ viene passato un puntatore (l’indirizzo) all’oggetto. Oltre al fatto che il parametro puo’ essere usato come output parameter, il vantaggio e’ che non viene fatta nessuna copia, vantaggio tanto piu’ grande quanto piu l’oggetto e’ grosso; in .NET il problema e’ che i reference type sono allocati nella managed heap(), cosa che implica piu’ cicli di CPU perche’ deve essere gestito e garbage collected dal CLR, i cui tempi di esecuzione possono essere lunghi e portare quindi a un degrado delle prestazioni.
Esempi di reference type sono classi, interfacce, arrays, delegates, ecc..
Boxing e unboxing
Tutto in .NET e’ un oggetto. int deriva da System.Int32 che a sua volta deriva da System.ValueType; e’ allocato per default sullo stack ma puo’ essere sempre convertito in uno heap-based, reference type object. Questa operazione e’ detta boxing
int i;
object box=i;
Quando si boxa un oggetto se ne ottiene uno su cui si possono invocare metodi, props ed eventi; se ho convertito un int in oggetto, posso invocare metodi definiti in System.Object come ToString(), ecc…
Il contrario e’ l’unboxing:
int j=(int)box;
Classi
Il CLR fornisce supporto per tutti i concetti di OOP e ne inserisce di nuovi come proprieta’, indexers e eventi.
Una proprieta’ e’ simile a un campo(membro) con l’eccezione che ci sono metodi getter e setter:
using System;
public class Car
{
private string make;
public string Make
{
get { return make; }
set { make = value; }
}
public static void Main( )
{
Car c = new Car( );
c.Make = "Acura"; // Use setter.
String s = c.Make; // Use getter.
Console.WriteLine(s);
}
}
La parola chiave value rappresenta l’unico argomento del metodo setter.
Un indexer e’ sintatticamente simile a una proprieta’ e consente l’accesso array-like agli elementi di un oggetto.
In pratica permette di accedere a un oggetto come si accede a un array.
using System;
public class Car
{
Car( )
{
wheels = new string[4];
}
private string[] wheels;
public string this[int index]
{
get { return wheels[index]; }
set { wheels[index] = value; }
}
public static void Main( )
{
Car c = new Car( );
c[0] = "LeftWheel"; // c[0] can be an l-value or an r-value.
Console.WriteLine(c[0]);
}
}
Delegates
I delegates sono la versione type safe dei puntatori a funzione C
using System;
class TestDelegate
{
// 1. Define callback prototype.
delegate void MsgHandler(string strMsg);
// 2. Define callback method.
void OnMsg(string strMsg)
{
Console.WriteLine(strMsg);
}
public static void Main( )
{
TestDelegate t = new TestDelegate( );
// 3. Wire up our callback method.
MsgHandler f = new MsgHandler(t.OnMsg);
// 4. Invoke the callback method indirectly.
f("Hello, Delegate.");
}
}
Nell’esempio si definisce una callback function prototype e la parola chiave delegate diceal compilatore
che si vuole un puntatore alla funzione. Il compilatore genera una classe MsgHandler che deriva
da System.MulticastDelegate che supporta tanti receivers. Una volta definito il prototype si deve
implementare un metodo con una signature che coincida col prototype e chiamarlo passando la funzione
al costruttore del delegate. Al termine. invocare la callback direttamente
L’esecuzione nel CLR
Le principali entita’ coinvolte nell’esecuzione di un .NET executable sono:
.NET PE Files (metadata e IL)
———————————
Class Loader
———————————
Verifier
———————————
JIT compiler
———————————
Execution support & management
(gc, security engine,code manager,
exceptionmanager, thread support, ecc)
Quando si esegue un programma .NET, l’OS riconosce che si tratta di codice gestito e lo passa al CLR, che primacercal’entry point (tipicamente Main) e lo esegue per lanciare l’app. Ma prima di cio’, il ClassLoader deve trovare la classe che espone Main e la carica. In piu’ quando Main istanzia un oggetto di una certa classe il ClassLoader la carica; in pratica il ClassLoader esegue il suo compito la prima volta in cui si referenzia un tipo.
Per trovare la classe, il ClassLoader cerca nel .config dell’app, nella curdir, nella GAC e cerca i metadata del manifest. Dopo che la classe e’ stata trovata e caricata, viene cachata. Il ClassLoader segna con uno stub il metodo per informare il JIT Compiler che sull’oggetto deve essere fatta la compilazione.
Verifier
Il verifier si occupa di controllare che non ci siano problemi di type safety; questa verifica e’ fatta a runtime e cio’ da’ la garanzia di prevenire codice non sicuro.
Quindi, dopo che il ClassLoader ha caricato lesue classi, il verifier controlla che:
I metadati siano ben formati
Il codice IL sia type safe
Solo se questi criteri vengono passati si passa il controllo al JIT Compiler.
JIT Compiler
Visto che i PE files contengono IL e metadati e non codice nativo, e’ necessario il compilatore JIT, che converte appunto IL in codice nativo che possa essere eseguito dal SO. Un vantaggio del JIT compiler e’ che puo’ compilare dinamicamente codice ottimizzato a seconda della macchina (double processor rispetto a single). Per ogni metodo passato dal ClassLoader e dal verifier, il JITComp compilera’ il metodo e lo tradurra’ in codice nativo.
Il JIT compiler, per motivi di ottimizzazione, esegue il suo compito solo la prima volta in cui un metodo e’ invocato leggendo lo stub nel metodo, messo dal ClassLoader, che indica che il metodo necessita di compilazione; al termine della compilazione il JITComp inserisce l’indirizzo del metodo nativo nello stub in modo da impedire successive inutili ricompilazioni.
Il codice compilato giace in memoria fino al chiudersi del processo o fino all’operazione di GC associata al processo. Cio’ vuol dire che la prox-prima volta viene di nuovo invocato.
Se si desidera evitare il costo del JIT e compilare una volta per tutte, occorre usare un tool chiamato ngen.exe
Execution support and management
Durante l’esecuzione dell’applicazione vengono coinvolte numerose importanti entita’, quali ad es:
Garbage collection: il CLR gestisce automaticamente il ciclo di vita degli oggetti .NET, rilevando quando un oggetto non e’ piu’ referenziato e libera la memoria inutilizzata
Exception handling: Il CLR supporta un meccanismo di gestione delle eccezioni per tutti i linguaggi, caratteristica non presente per esempio nel vecchio VB.
Debugging
Interop support