Nel .NET Framework l’unità di deployment (distribuzione) è l’assembly; l’assembly è l’unità minima a cui si può applicare un numero di versione.
Un assembly è costituito fisicamente da uno o più file, chiamati moduli, alcuni dei quali possono contenere codice IL e tipi .NET.
Un assembly non specifica il contesto di esecuzione. Il contesto di esecuzione è definito da un application domain. Un application domain sta a .NET come un processo sta al sistema operativo Windows. Fisicamente un processo Win32 può ospitare uno o più application domain.
I moduli che possono far parte di un assembly sono di due tipi:
- file PE (Portable Executable): contengono metadati e possono ospitare codice IL; normalmente hanno estensione .EXE, .DLL o .NETMODULE, anche se non è obbligatorio (vedi sezione Estensioni .EXE e .DLL e .NETMODULE);
- altri file: non contengono metadati e non contengono codice IL, possono contenere risorse o altri dati (tali moduli possono essere inclusi nell’assembly o collegati ad esso restando file separati).
I file PE associati ad un assembly contengono sempre i metadati del modulo: nei metadati sono contenute tutte le informazioni su tipi, metodi, campi, proprietà, eventi e parametri del codice managed (codice IL) contenuto nel modulo.
La definizione dell’assembly è contenuta nel manifest, che è un insieme di informazioni che specificano l’elenco dei moduli che compongono l’assembly, le dipendenze da altri assembly, i requisiti di versione, le informazioni sulla security ed in maniera indiretta i tipi esposti dai moduli dell’assembly.
Il manifest è contenuto nei metadati di uno dei file PE che fanno parte dell’assembly; il più delle volte il manifest è contenuto nel file che contiene le classi più utilizzate dell’assembly, ma è anche possibile definire un modulo che contiene unicamente il manifest, senza altri metadati né codice IL.
Tutti i file che costituiscono un assembly devono risiedere nella stessa directory.
Un assembly può avere dipendenze da altri assembly; tali dipendenze sono specificate nel manifest. E’ possibile definire dei riferimenti circolari tra assembly.
Un modulo in un assembly può essere condiviso da più assembly (resta comunque il vincolo per cui tutti i moduli di un assembly devono risiedere nella stessa directory).
Spesso è conveniente creare assembly a modulo singolo, dove manifest, codice, tipi e risorse risiedono sullo stesso file fisico. Gli assembly che contengono più di un file prendono il nome di assembly multi-modulo; torneremo a parlare di questi ultimi più avanti.
Costruiamo un assembly
Di solito un assembly corrisponde ad un modulo con un manifest, il tutto incluso in un file .EXE o .DLL.
Questa potrebbe essere la prima realizzazione:
.assembly fooasm {}
.assembly extern mscorlib {}
.method static public void main() il managed {
.entrypoint
.maxstack 1
ldstr "Hello World from IL!"
call void [mscorlib]System.Console::WriteLine( class System.String )
ret
}
Scrivendo questo codice in un file FOOASM.IL e compilandolo con il comando
> ILASM FOOASM.IL
otteniamo un assembly a modulo singolo, che contiene un manifest e una funzione che è anche il punto di ingresso del programma (notare che conta la direttiva .entrypoint piuttosto che il nome della funzione, main).
Dopo un po’ inevitabilmente il nostro programma crescerà e potremmo avere la necessità di separare il codice in più file, magari per coordinare meglio il lavoro di un team di sviluppo. Istintivamente si vorrà creare una DLL che contenga le funzioni e le classi che ci interessano. Il codice che segue implementa una funzione statica test() all’interno della classe SimpleClass.
.assembly foomod {}
.assembly extern mscorlib {}
.class public SimpleClass {
.method static public void test() il managed {
.maxstack 1
ldstr "Hello World from SimpleClass test function!"
call void [mscorlib]System.Console::WriteLine( class System.String )
ret
}
}
Per compilare questo file in una DLL si usa il comando
> ILASM /DLL FOOMOD.IL
In questo modo la DLL compilata può essere chiamata dal codice del primo esempio, referenziando opportunamente il nuovo assembly appena creato (con .assembly extern foodmod) ed aggiungendo la chiamata alla funzione statica SimpleClass::test().
.assembly fooasm {}
.assembly extern foomod {}
.assembly extern mscorlib {}
.method static public void main() il managed {
.entrypoint
.maxstack 1
ldstr "Hello World from IL!"
call void [mscorlib]System.Console::WriteLine( class System.String )
call void [foomod]SimpleClass::test()
ret
}
La compilazione avviene sempre con
> ILASM FOOASM.IL
A questo punto abbiamo due diversi file, un .EXE e una .DLL, ciascuno dei quali costituisce però un assembly a modulo singolo, separato ed indipendente. Se è ciò che desideriamo, possiamo fermarci. Se invece la dipendenza tra questi due moduli è molto stretta e non pensiamo, ad esempio, che la DLL possa essere distribuita separatamente dal file .EXE, possiamo decidere di unire i due moduli in un solo assembly.
Fisicamente su disco non cambierà niente (resteranno due file diversi), ma il manifest sarà presente soltanto in uno dei due file, mentre l’altro diventerà un modulo “esterno” che fa parte dell’assembly.
Vediamo come cambiano i due sorgenti. In FOOMOD.IL cambia solo la prima riga, che con la direttiva .module al posto di .assembly definisce la compilazione di un modulo che non avrà nei metadati nessun manifest.
.module foomod.dll
.assembly extern mscorlib {}
.class public SimpleClass {
.method static public void test() il managed {
.maxstack 1
ldstr "Hello World from SimpleClass test function!"
call void [mscorlib]System.Console::WriteLine( class System.String )
ret
}
}
In FOOASM.IL invece la direttiva .assembly extern che faceva riferimento a FOOMOD va sostituita con due righe, .module extern e .file, che definiscono il collegamento dell’assembly con il modulo esterno FOOMOD.DLL. Le due direttive sono necessarie perché corrispondono a due diversi campi del manifest (ModuleRef e File). Da notare che anche la chiamata alla funzione SimpleClass::test() è cambiata, perché viene preceduta da [.module foomod.dll] che identifica il modulo dell’assembly che contiene tale funzione; il nome del modulo va sempre specificato completo di estensione del file (che può anche essere diverso da .DLL).
.assembly fooasm {}
.module extern foomod.dll
.file foomod.dll
.assembly extern mscorlib {}
.method static public void main() il managed {
.entrypoint
.maxstack 1
ldstr "Hello World from IL!"
call void [mscorlib]System.Console::WriteLine( class System.String )
call void [.module foomod.dll]SimpleClass::test()
ret
}
I comandi per la compilazione non cambiano:
> ILASM /DLL FOOMOD.IL
> ILASM FOOASM.IL
Quando può avere senso fare una cosa simile? Creare assembly a modulo singolo è certamente la soluzione più semplice e gestibile, perché ciascun file è distribuibile ed aggiornabile indipendentemente dagli altri. Nel caso di assembly multi-modulo, invece, il deployment di tutti i file dell’assembly deve essere simultaneo, perché non si può aggiornare solo un modulo di un assembly. In realtà ciò è possibile in fase di sviluppo, poiché il controllo dei moduli avviene solo per gli assembly con strong name (firmati digitalmente); i moduli di assembly senza strong name possono quindi essere sostituiti in qualsiasi momento.
Assembly multi-modulo
Uno dei vantaggi nell’uso di assembly multi-modulo può essere il fatto che solo i moduli effettivamente utilizzati vengono caricati in memoria (e scaricati da un server remoto se è là che risiedono).
La divisione dovrebbe comunque essere più funzionale che legata a problemi di dimensioni o distribuzione. Un assembly rappresenta un insieme di classi, una libreria, un oggetto che fornisce servizi in maniera indipendente. Tutti gli elementi di un assembly condividono un livello di visibilità interna che isola i componenti dell’assembly dal mondo esterno. Uno dei motivi che possono portare a creare un assembly multi-modulo è anche il fatto che si vuole creare un assembly composto da moduli realizzati con linguaggi diversi.
Mentre il manifest di un assembly conosce tutti i moduli di cui è composto l’assembly, un singolo modulo non sa a quale assembly appartiene, potrebbe anzi essere condiviso da più assembly.
Estensioni .EXE, .DLL e .NETMODULE
L’estensione nel nome di un file non è determinante per stabilire il suo ruolo all’interno di un assembly. In altre parole si possono creare assembly in un singolo file dal nome FOO.XYZ, così come si possono avere assembly multi-modulo dove alcuni file contenenti codice, tipi e metadati hanno estensioni diverse dal comune .NETMODULE (estensione convenzionalmente usata per moduli di un assembly diversi da quello che contiene il manifest).
La direttiva .assembly extern cerca automaticamente un file con estensione .DLL; se questi non esiste, cerca un file con estensione .EXE. E’ possibile specificare esplicitamente l’estensione del file in questa direttiva, rendendo utilizzabili assembly con un’estensione diversa; tale nome completo di estensione deve poi essere riportato in tutti i riferimenti a tale assembly.
Non è consigliabile modificare in modo non standard l’estensione del file di un assembly che contiene il manifest. I moduli che fanno parte di un assembly multi-modulo e non contengono il manifest hanno per convenzione un’estensione .NETMODULE, proprio per scoraggiare i tentativi di copiare tali file separatamente dal resto dell’assembly, pensando che possano funzionare. Il formato PE di un file .NETMODULE è infatti lo stesso di un file .DLL, ma usare .DLL sia per i moduli con il manifest, che identificano l’assembly, sia per quelli senza manifest, cioè i moduli secondari, porterebbe a una grande confusione.
Per chiarezza è bene usare l’estensione .EXE per gli assembly che hanno un punto d’ingresso (.entrypoint), e .DLL per gli assembly senza un punto d’ingresso.
Riassumendo, il consiglio pratico è quello di usare sempre .DLL o .EXE come estensione dei file contenenti il manifest di un assembly (di solito il file principale), e usare .NETMODULE per i moduli che appartengono ad un assembly.