La classe System.CodeDom.Compiler.CodeDomProvider è una classe astratta dalla quale derivano classi specifiche utilizzate dal .NET Framework per implementare i compilatori dei vari linguaggi da esso supportati. Il compilatore del linguaggio C#, ad esempio, è basato sulla classe Microsoft.CSharp.CSharpCodeProvider che eredita dalla classe astratta CodeDomProvider e permette di accedere ad una istanza del compilatore C# per la gestione del codice sorgente in detto linguaggio, così come il compilatore del linguaggio Visual Basic .NET è basato sulla classe Microsoft.VisualBasic.VBCodeProvider. Utilizzando tali classi è possibile compilare il codice sorgente, proveniente ad esempio da un file di script, per generare ed eseguire codice managed direttamente in memoria oppure creando un assembly. Ciò si rileva molto utile quando è necessario provare il funzionamento di porzioni di codice sorgente senza dover creare una soluzione da salvare su disco, come ad esempio accade se si utilizza l'ambiente Visual Studio .NET, oppure per fornire ad una applicazione la possibilità di integrazioni software da implementare a livello di scripting. Il primo passaggio consiste nel creare una istanza del compilatore C# oppure VB .NET rispettivamente attraverso la classe CSharpCodeProvider e VBCodeProvider, e di ottenere quindi un oggetto ICodeCompiler che rappresenta una interfaccia la cui implementazione da parte dello specifico compilatore permette di compilare dinamicamente il codice sorgente. E' opportuno inoltre creare una istanza della classe CompilerParameters per impostare le opzioni di compilazione appropriate.
C# // questo codice utilizza le seguenti istruzioni using // using Microsoft.CSharp; // using System.CodeDom.Compiler; CSharpCodeProvider CSharpProvider = new CSharpCodeProvider(); ICodeCompiler Comp = CSharpProvider.CreateCompiler(); CompilerParameters CompParameters = new CompilerParameters();
VB .NET ' questo codice utilizza le seguenti istruzioni Imports ' Imports Microsoft.VisualBasic ' Imports System.CodeDom.Compiler Dim VBNetProvider As New VBCodeProvider Dim Comp As ICodeCompiler = VBNetProvider.CreateCompiler Dim CompParameters As New CompilerParameters
Alcune di queste opzioni permettono ad esempio di generare un eseguibile oppure una dll, di includere i simboli di debug, di creare un assembly in memoria o di produrre un file su disco (in questo caso è possibile assegnare un nome specifico al file attraverso la proprietà OutputAssembly). Una proprietà importante della classe CompilersParameters è ReferencedAssemblies, ovvero un oggetto di tipo collezione contenente la lista degli assemblies impostati come riferimento dell'assembly in compilazione. Occorre prevedere almeno gli assembly di uso più comune, altrimenti l'utilizzo di una classe senza il riferimento all'assembly che la contiene produce un errore.
C# // questo codice utilizza la seguente istruzione using // using System.CodeDom.Compiler; // sarà generato file .EXE in memoria con i simboli di debug caricati CompParameters.GenerateExecutable=true; CompParameters.GenerateInMemory = true; CompParameters.IncludeDebugInformation = true; // aggiunta delle referenze CompParameters.ReferencedAssemblies.Add("System.Security.dll"); CompParameters.ReferencedAssemblies.Add("System.dll"); CompParameters.ReferencedAssemblies.Add("System.Data.dll"); CompParameters.ReferencedAssemblies.Add("System.Xml.dll"); CompParameters.ReferencedAssemblies.Add("System.Drawing.dll"); CompParameters.ReferencedAssemblies.Add("System.Management.dll");
VB .NET ' questo codice utilizza la seguente istruzione Imports ' Imports System.CodeDom.Compiler ' sarà generato file .EXE in memoria con i simboli di debug caricati CompParameters.GenerateExecutable = True CompParameters.GenerateInMemory = True CompParameters.IncludeDebugInformation = True ' aggiunta delle referenze CompParameters.ReferencedAssemblies.Add("System.Security.dll") CompParameters.ReferencedAssemblies.Add("System.dll") CompParameters.ReferencedAssemblies.Add("System.Data.dll") CompParameters.ReferencedAssemblies.Add("System.Xml.dll") CompParameters.ReferencedAssemblies.Add("System.Drawing.dll") CompParameters.ReferencedAssemblies.Add("System.Management.dll")
A questo punto è possibile leggere il codice sorgente da compilare, tipicamente da un file di testo, e costruire l'entry point dell'assembly insieme al metodo Main che sarà invocato dinamicamente:
C# // questo codice utilizza la seguente istruzione using // using System.IO; // using System.Text; // lettura del codice sorgente e costruzione dell'entry point scripter e del membro Main: StreamReader sr =new StreamReader( fileName); string Line, CodeExec; string Header = "namespace CSharpScript\n" + "{\n" + "public class scripter\n" + "{\n" + "public static void Main()\n" + " {\n"; StringBuilder SourceCode = new StringBuilder(); StringBuilder Using = new StringBuilder(); while ( (Line = sr.ReadLine()) != null) { if (Line.Equals("")) continue; // lettura della direttiva using if (Line.ToLower().StartsWith("using") && Line.IndexOf("(") < 0) { Using.Append(Line); } else { SourceCode.Append(Line); } } CodeExec = Using.ToString() + "\n" + Header + "\n" + SourceCode.ToString() + "\n}\n}\n}\n"; sr.Close();
VB .NET ' questo codice utilizza le seguenti istruzioni Imports ' Imports System.IO ' Imports System.Text ' Imports System.Windows.Forms ' lettura del codice sorgente e costruzione dell'entry point scripter e del membro Main: Dim sr As New StreamReader(fileName) Dim Line, CodeExec As String Dim Header As String = "public class scripter" & _ ControlChars.CrLf & _ ControlChars.CrLf & _ "public shared sub Main()" & ControlChars.CrLf Dim SourceCode As New StringBuilder Dim Import As New StringBuilder Line = sr.ReadLine() Do While Not (Line Is Nothing) ' lettura della direttiva imports If Line.ToLower().StartsWith("imports") Then Import.Append(Line) Else SourceCode.Append(Line & ControlChars.CrLf) End If Line = sr.ReadLine() Loop CodeExec = Import.ToString() & ControlChars.CrLf & Header _ & ControlChars.CrLf & SourceCode.ToString() & _ ControlChars.CrLf & "End Sub " & ControlChars.CrLf _ & "End Class" sr.Close()
Per compilare il codice è necessario invocare il metodo CompileAssemblyFromSource esposto dall'interfaccia ICodeCompiler passandogli l'oggetto CompParameters creato in precedenza e la stringa contenente il codice. Questo metodo restituisce un oggetto CompilerResults che permette di verificare se la compilazione ha prodotto errori o avvertimenti mediante la collection Errors, la quale espone proprietà per individuare la riga e la colonna contenente l'istruzione che ha prodotto l'errore, il numero dell'errore stesso e la sua descrizione, oltre alla proprietà HasErrors che consente immediatamente di verificare se è presente almeno un errore:
C# // questo codice utilizza la seguente istruzione using // using System.CodeDom.Compiler; // compilazione del codice sorgente: CompilerResults CompResults = Comp.CompileAssemblyFromSource( CompParameters, CodeExec); // verifica se ci sono errori di compilazione if (CompResults.Errors.HasErrors ) { foreach( CompilerError Err in CompResults.Errors) { if (! Err.IsWarning) { Console.WriteLine("\nErrore riga {0} colonna {1} \n\t" + "Errore numero: {2} {3}", Err.Line.ToString(), Err.Column.ToString(), Err.ErrorNumber.ToString(), Err.ErrorText); } } }
VB .NET ' questo codice utilizza le seguenti istruzioni Imports ' Imports System.CodeDom.Compiler ' compilazione del codice sorgente: Dim CompResults As CompilerResults CompResults = Comp.CompileAssemblyFromSource(CompParameters, _ CodeExec) ' verifica se ci sono errori di compilazione If CompResults.Errors.HasErrors Then For Each Err As CompilerError In CompResults.Errors If Not Err.IsWarning Then Console.WriteLine(ControlChars.CrLf & "Errore " & _ "riga {0} colonna {1}" _ & _ ControlChars.CrLf & ControlChars.Tab & _ "Errore numero: {2} _ {3}",Err.Line.ToString, Err.Column.ToString, _ Err.ErrorNumber.ToString, Err.ErrorText) End If Next End If
Nel caso di compilazione terminata correttamente si effettua un ciclo sui tipi che appartengono all'assembly appena compilato e si ricava un riferimento al tipo "scripter" che costituisce l'entry point dell'assembly e, mediante l'uso di Reflection, si invoca dinamicamente il metodo Main:
C# // questo codice utilizza la seguente istruzione using // using System.CodeDom.Compiler; // estrazione dell'entry point 'scripter' Type t; for (int i = 0; i<= CompResults.CompiledAssembly.GetTypes().Length -1; i++) { if (CompResults.CompiledAssembly.GetTypes()[i].Name == "scripter") { t = CompResults.CompiledAssembly.GetTypes()[0]; break; } } // invocazione dinamica del membro Main t.InvokeMember("Main", System.Reflection.BindingFlags.InvokeMethod, null , null , null);
VB .NET ' questo codice utilizza le seguenti istruzioni Imports ' Imports System.CodeDom.Compiler ' Estrazione dell'entry point "scripter" For i As Integer = 0 To CompResults.CompiledAssembly.GetTypes().Length - 1 If CompResults.CompiledAssembly.GetTypes()(i).Name = "scripter" Then t = CompResults.CompiledAssembly.GetTypes()(0) Exit For End If Next ' invocazione dinamica del membro Main t.InvokeMember("Main", System.Reflection.BindingFlags.InvokeMethod, _ Nothing, Nothing, Nothing)
Infine, è possibile registrare nel sistema l'applicazione impostando una particolare estensione di file, ad esempio "cssc" per indicare uno script in linguaggio C#, oppure "vbsc" per uno script in Visual Basic .NET. In tal modo attraverso un semplice doppio click sul file con tale estensione sarà eseguito il codice in esso contenuto, in modo simile ad un file di script Visual Basic.