Questo sito utilizza cookies solo per scopi di autenticazione sul sito e nient'altro. Nessuna informazione personale viene tracciata. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - Le Reflection Parte IV

Guida al Visual Basic .NET

Capitolo 47° - Le Reflection Parte IV

<< Precedente Prossimo >>


Compilazione di codice a runtime

Bene, ora che sappiamo scrivere del normale codice per una qualsiasi applicazione e che sappiamo come analizzare codice esterno, che ne dite di scrivere programmi che producano programmi? La questione è molto divertente: esistono delle apposite classi, in .NET, che consentono di compilare codice che viene prodotto durante l'esecuzione dal'applicazione stessa, generando così nuovi assembly per gli scopi più vari. Una volta mi sono servito in maniera intensiva di questa capacità del .NET per scrivere un installer: non solo esso creava altri programmi (autoestraenti), ma questi a loro volta creavano altri programmi per estrarre le informazioni memorizzate negli autoestraenti stessi: programmi che scrivono programmi che scrivono programmi! Ma ora vediamo più nel dettaglio cosa usare nello specifico per attivare queste interessanti funzionalità.
Prima di tutto, è necessario importare un paio di namespace: System.CodeDom e System.CodeDom.Compiler. Essi contengono le classi che fanno al caso nostro per il mestiere. Il processo di compilazione si svolge alltraverso queste fasi:
  • Prima si ottiene il codice da compilare, che può essere memorizzato in un file o prodotto direttamente dal programma sottoforma di normale stringa;
  • Si impostano i parametri di compilazione: ad esempio, si può scegliere il tipo di output (*.exe o *.dll), i riferimenti da includere, se mantenere i file temporanei, se creare l'assembly e salvarlo in memoria, se trattare gli warning come errori, eccetera... Insomma, tutto quello che noi scegliamo tramite l'interfaccia dell'ambiente di sviluppo o che ci troviamo già impostato grazie all'IDE stesso;
  • Si compila il codice richiamando un provider di compilazione;
  • Si leggono i risultati della compilazione. Nel caso ci siano stati errori, i risultati conterranno tutta la lista degli errori, con relative informazioni sulla loro posizione nel codice; in caso contrario, l'assembly verrà generato correttamente;
  • Se l'assembly conteneva codice che serve al programma, si usa la Reflection per ottenerne e invocarne i metodi.
Queste cinque fasi corrispondono a cinque oggetti che dovremo usare nel codice:
  • String : ovviamente, il codice memorizzato sottoforma di stringa;
  • CompilerParameters : classe del namespace CodeDom.Compiler. Contiene come proprietà tutte le opzioni che ho esemplificato nella lista precedente;
  • VBCodeProvider : provider di compilazione per il linguaggio Visual Basic. Esiste un provider per ogni linguaggio .NET, anche se può non trovarsi sempre nello stesso namespace. Esso fornirà i metodi per avviare la compilazione;
  • CompilerResults : contiene tutte le informazioni relative all'output della compilazione. Se si sono verificati errori, ne espone una lista; se la compilazion è andata a buon file, riferisce dove si trova l'assembly compilato e, se ci sono, dove sono posti i file temporanei;
  • Assembly : classe che abbiamo già analizzato. Permette di caricare in memoria l'assembly prodotto come output e richiamarne il codice od usarne le classi ivi definite.
Il prossimo esempio costituisce uno dei casi più evidenti di quando conviene rivolgersi alla reflection, e sono sicuro che potrebbe tornarvi utile in futuro:
Module Module1

    'Questa classe rappresenta una funzione matematica:
    'ne ho racchiuso il nome tra parentesi quadre poiché Function
    'è una keyword del linguaggio, ma in questo caso la si
    'vuole usare come identificatore. In generale, si possono usare
    'le parentesi quadre per trasformare ogni keyword in un normale
    'nome.
    Class [Function]
        Private _Expression As String
        Private EvaluateFunction As MethodInfo

        'Contiene l'espressione matematica che costruisce il valore
        'della funzione in base alla variabile x
        Public Property Expression() As String
            Get
                Return _Expression
            End Get
            Set(ByVal value As String)
                _Expression = value
                Me.CreateEvaluator()
            End Set
        End Property

        'La prossima è la procedura centrale di tutto l'esempio.
        'Il suo compito consiste nel compilare una libreria di
        'classi in cui è definita una funzione che, ricevuto
        'come parametro un x decimale, ne restituisce il valore
        'f(x). Il corpo di tale funzione varia in base
        'all'espressione immessa dall'utente.
        Private Sub CreateEvaluator()
            'Crea il codice della libreria: una sola classe
            'contenente la sola funzione statica Evaluate. Gli {0}
            'verranno sostituti con caratteri di "a capo", mentre
            'in {1} verrà posta l'espressione che produce
            'il valore desiderato, ad esempio "x ^ 2 + 1".
            Dim Code As String = String.Format( _
            "Imports Microsoft.VisualBasic{0}" & _
            "Imports System{0}" & _
            "Imports System.Math{0}" & _
            "Public Class Evaluator{0}" & _
            "  Public Shared Function Evaluate(ByVal X As Double) As Double{0}" & _
            "    Return {1}{0}" & _
            "  End Function{0}" & _
            "End Class", Environment.NewLine, Me.Expression)

            'Crea un nuovo oggetto CompilerParameters, per
            'contenere le informazioni relative alla compilazione
            Dim Parameters As New CompilerParameters
            
            With Parameters
                'Indica se creare un eseguibile o una libreria di
                'classi: in questo caso ci interessa la seconda,
                'quindi impostiamo la proprietà su False
                .GenerateExecutable = False

                'Gli warning vengono considerati come errori
                .TreatWarningsAsErrors = True
                'Non vogliamo tenere alcun file temporaneo: ci
                'interessa solo l'assembly compilato
                .TempFiles.KeepFiles = False
                'L'assembly verrà tenuto in memoria temporanea
                .GenerateInMemory = True

                'I due riferimenti di cui abbiamo bisogno, che si
                'trovano nella GAC (quindi basta specificarne il
                'nome). In questo caso, si richiede anche
                'l'estensione (*.dll)
                .ReferencedAssemblies.Add("Microsoft.VisualBasic.dll")
                .ReferencedAssemblies.Add("System.dll")
            End With

            'Crea un nuovo provider di compilazione
            Dim Provider As New VBCodeProvider
            'E compila il codice seguendo i parametri di
            'compilazione forniti dall'oggetto Parameters. Il
            'valore restituito dalla funzione
            'CompileAssemblyFromSource è di tipo 
            'CompilerResults e viene salvato in CompResults
            Dim CompResults As CompilerResults = _ 
                Provider.CompileAssemblyFromSource(Parameters, Code)

            'Se ci sono errori, lancia un'eccezione
            If CompResults.Errors.Count > 0 Then
                Throw New FormatException("Espressione non valida!")
            Else
                'Altrimenti crea un riferimento all'assembly di
                'output. La proprietà CompiledAssembly di
                'CompResults contiene un riferimento diretto a
                'quell'assembly, quindi ci è molto comoda.
                Dim Asm As Reflection.Assembly = CompResults.CompiledAssembly
                'Dall'assembly ottiene un OT che rappresenta
                'l'unico tipo ivi definito, e da questo ne
                'estrae un MethodInfo con informazioni sul
                'metodo Evaluate (l'unico presente).
                Me.EvaluateFunction = _ 
                    Asm.GetType("Evaluator").GetMethod("Evaluate")
            End If
        End Sub

        'Per richiamare la funzione, basta invocare il metodo
        'Evaluate estratto precedentemente. Al pari delle
        'proprietà e dei campi, che possono essere letti o
        'scritti dinamicamente, anche i metodi possono essere
        'invocati dinamicamete attraverso MethodInfo. Si usa
        'la funzione Invoke: il primo parametro da
        'passare è l'oggetto da cui richiamare il metodo, mentre
        'il secondo è un array di oggetti che indicano i
        'parametri da passare a tale metodo.
        'In questo caso, il primo parametro è Nothing poiché
        'Evaluate è una funzione statica e non ha bisgno di nessuna
        'istanza per essere richiamata.
        Public Function Apply(ByVal X As Double) As Double
            Return EvaluateFunction.Invoke(Nothing, New Object() {X})
        End Function

    End Class

    Sub Main()
        Dim F As New [Function]()

        Do
            Try
                Console.Clear()
                Console.WriteLine("Inserisci una funzione: ")
                Console.Write("f(x) = ")
                F.Expression = Console.ReadLine
                Exit Do
            Catch ex As Exception
                Console.WriteLine("Espressione non valida!")
                Console.ReadKey()
            End Try
        Loop

        Dim Input As String
        Dim X As Double

        Do
            Try
                Console.Clear()
                Console.WriteLine("Immettere 'stop' per terminare.")
                Console.WriteLine("Il programma calcola il valore di f in X: ")
                Console.Write("x = ")
                Input = Console.ReadLine
                If Input <> "stop" Then
                    X = CType(Input, Double)
                    Console.WriteLine("f(x) = {0}", F.Apply(X))
                    Console.ReadKey()
                Else
                    Exit Do
                End If
            Catch Ex As Exception
                Console.WriteLine(Ex.Message)
                Console.ReadKey()
            End Try
        Loop
    End Sub
End Module
In questo esempio ho utilizzato solo alcuni dei membri esposti dalle classi sopra menzionate. Di seguito elenco tutti quelli più rilevanti, che potrebbero servirvi in futuro: CompilerParameters
CompilerOptions : contiene sottoforma di stringhe dei parametri aggiuntivi da passare al compilatore. Vedremo solo più avanti di cosa si tratta e di come possano generalmente essere modificati dall'IDE nell'ambito del nostro progetto; EmbeddedResources : una lista di stringhe, ognuna delle quali indica il percorso su disco di un file di risorse da includere nell'assembly compilato. Di questi file parlerò nella sezione B; GenerateExcutable : determina se generare un eseguibile o una libreria di classi; GenerateInMemory : determina se non salvare l'assembly generato su un supporto permanente (disco fisso o altre memorie non volatili); IncludeDebugInformations : determina se includere nell'eseguibile anche le informazioni relative al debug. Di solito questo non è molto utile perché è possibile accedere prima a queste informazioni tramite l'IDE facendo il debug del codice stesso che compila altro codice XD; MainClass : imposta il nome della classe principale dell'assembly. Se si sta compilando una libreria di classi, questa proprietà è inutile. Se, invece, si sta compilando un programma, questa proprietà indica il nome della classe dove è contenuta la procedura Main, il punto di ingresso nell'applicazione. Generalmente il compilatore riesce ad individuare da solo tale classe, ma nel caso ci siano più classi contenenti un metodo Main bisogna specificarlo esplicitamente. Nel caso l'applicazione da compilare sia di tipo windows form, come vedremo nella sezione B, la MainClass può anche indicare la classe che rappresenta la finestra iniziale; OutputAssembly : imposta il percorso dell'assembly da generare. Nel caso questa proprietà non venga impostata prima della compilazione, sarà il provider di compilazione a preoccuparsi di creare un nome casuale per l'assembly e di salvarlo nella stessa cartella del nostro programma; ReferencedAssemblis : anche questa è una collezione di stringhe, e contiene il nome degli assemblies da includere come rierimeneto per il codice corrente. Dovete sempre includere almeno System.dll (quello più importante). Gli altri assemblies pubblici sono facoltativi e variano in funzione del compito da svolgere: se doveste usare file xml, importerete anche System.Xml.dll, ad esempio; TempFiles : collezione che contiene i percorsi dei file temporanei. Espone qualche proprietà e metodo in più rispetto a una normale collezione; TreatWarningsAsErrors : tratta gli warning come se fossero errori. Questo impedisce di portare a termine la compilazione quando ci sono degli warning; WarningLevel : livello da cui il compilatore interrompe la compilazione. La documentazione su questa proprietà non è molto chiara e non si capisce bene cosa intenda. È probabile che ogni warning abbia un certo livello di allerta e questo valore dovrebbe comunicare al compilatore di visualizzare solo gli warning con livello maggiore o uguale a quello specificato... solo ipotesi, tuttavia.

CompilerResults
CompiledAssembly : restituisce un oggetto Assembly in riferimento all'assembly compilato; Errors : collezione di oggetti di tipo CompilerError. Ognuno di questi oggetti espone delle proprietà utili a identificare il luogo ed il motivo dell'errore. Alcune sono: Line e Column (linea e colonna dell'errore), IsWarning (se è un warning o un errore), ErrorNumber (numero identificativo dell'errore), ErrorText (testo dell'errore) e FileName (nome del file in cui si è verificato); NativeCompilerReturnValue : restituisce il valore che a sua volta il compilatore ha restituito al programma una volta terminata l'esecuzione. Vi ricordo, infatti, che compilatore, editor di codice e debugger sono tre programmi differenti: l'ambiente di sviluppo integrato fornisce un'interfaccia che sembra unirli in un solo applicativo, ma rimangono sempre entità distinti. Come tali, un programma può restituire al suo chiamante un valore, solitamente intero: proprio come si comporta una funzione; PathToAssembly : il percorso su disco dell'assembly generato; TempFiles : i file temporaneai rimasti. Avrete notato che anche VBCodeProvider espone molti metodi, ma la maggior parte di questi servono per la generazione di codice. Questo meccanismo permette di assemblare una collezione di oggetti ognuno dei quali rappresenta un'istruzione di codice, e poi di generare codice per un qualsiasi linguaggio .NET. Sebbene sia un funzionalità potente, non la tratterò in questa guida.


Generazione di programmi

Il prossimo sorgente è un esempio che, secondo me, sarebbe stato MOLTO più fruttuoso se usato in un'applicazione windows forms. Tuttavia siamo nella sezione A e qui si fa solo teoria, perciò, purtroppo, dovrete sorbirvi questo entusiasmante esempio come applicazione console.
Ammettiamo che un'impresa abbia un software di gestione dei suoi materiali, e che abbastanza spesso acquisti nuove tipologie di prodotti o semilavorati. Gli addetti al magazzino dovranno introdurre i dati dei nuovi oggetti, ma per far ciò, è necessario un programma adatto per gestire quel tipo di oggetti: in questo modo, ogni volta, serve un programma nuovo. L'esempio che segue è una possibile soluzione a questo problema: il programma principale richiede di immettere informazioni su un nuovo tipo di dato e crea un programma apposta per la gestione di quel tipo di dato (notate che ho scritto cinque volte programma sulla stessa colonna XD).
Module Module1

    'Classe che rappresenta il nuovo tipo di dato, ed
    'espone una funzione per scriverne il codice
    Class TypeCreator
        Private _Fields As Dictionary(Of String, String)
        Private _Name As String

        'Fields è un dizionario che contiene come
        'chiavi i nomi delle proprietà da definire
        'nel nuovo tipo e come valori il loro tipi
        Public ReadOnly Property Fields() As Dictionary(Of String, String)
            Get
                Return _Fields
            End Get
        End Property

        'Nome del nuovo tipo
        Public Property Name() As String
            Get
                Return _Name
            End Get
            Set(ByVal value As String)
                _Name = value
            End Set
        End Property

        Public Sub New()
            _Fields = New Dictionary(Of String, String)
        End Sub

        'Genera il codice della proprietà
        Public Function GenerateCode() As String
            Dim Code As New Text.StringBuilder()

            Code.AppendLine("Class " & Name)
            For Each Field As String In Me.Fields.Keys
                Code.AppendFormat("Private _{0} As {1}{2}", _
                    Field, Me.Fields(Field), Environment.NewLine)
                Code.AppendFormat( _
                "Public Property {0} As {1}{2}" & _
                "  Get{2}" & _
                "    Return _{0}{2}" & _
                "  End Get{2}" & _
                "  Set(ByVal value As {1}){2}" & _
                "    _{0} = value{2}" & _
                "  End Set{2}" & _
                "End Property{2}", _
                Field, Me.Fields(Field), Environment.NewLine)
            Next
            Code.AppendLine("End Class")

            Return Code.ToString()
        End Function

    End Class

    'Classe statica contenente la funzione per scrivere
    'e generare il nuovo programma
    Class ProgramCreator

        'Accetta come input il nuovo tipo di dato
        'da gestire. Restituisce in output il percorso
        'dell'eseguibile creato 
        Public Shared Function CreateManagingProgram(ByVal T As TypeCreator) As String
            Dim Code As New Text.StringBuilder()

            Code.AppendLine("Imports System")
            Code.AppendLine("Imports System.Collections.Generic")
            Code.AppendLine("Module Module1")
            Code.AppendLine(T.GenerateCode())

            Code.AppendLine("Sub Main()")
            Code.AppendLine("  Dim Storage As New List(Of " & T.Name & ")")
            Code.AppendLine("  Dim Cmd As Char")
            Code.AppendLine("  Do")
            Code.AppendLine("  Console.Clear()")
            Code.AppendLine("  Console.WriteLine(""Inserimento di oggetti " & T.Name & """)")
            Code.AppendLine("  Console.WriteLine()")
            Code.AppendLine("  Console.Writeline(""Scegliere un'operazione: "")")
            Code.AppendLine("  Console.WriteLine("" i - inserimento;"")")
            Code.AppendLine("  Console.WriteLine("" e - elenca;"")")
            Code.AppendLine("  Console.WriteLine("" u - uscita."")")
            Code.AppendLine("  Cmd = Console.ReadKey().KeyChar")
            Code.AppendLine("  Console.Clear()")
            Code.AppendLine("  Select Case Cmd")
            Code.AppendLine("    Case ""i"" ")
            Code.AppendLine("      Dim O As New " & T.Name & "()")
            Code.AppendLine("      Console.WriteLine(""Inserire i dati: "")")
            'Legge ogni membro del nuovo tipo. Usa la CType
            'per essere sicuri che tutto venga interpretato nel
            'modo corretto.
            For Each Field As String In T.Fields.Keys
                Code.AppendFormat("Console.Write(""  {0} = ""){1}", Field, Environment.NewLine)
                Code.AppendFormat("O.{0} = CType(Console.ReadLine(), {1}){2}", Field, T.Fields(Field), Environment.NewLine)
            Next
            Code.AppendLine("      Storage.Add(O)")
            Code.AppendLine("      Console.WriteLine(""Inserimento completato!"")")
            Code.AppendLine("    Case ""e"" ")
            Code.AppendLine("      For I As Int32 = 0 To Storage.Count - 1")
            Code.AppendLine("        Console.WriteLine(""{0:000} + "", I)")
            'Fa scrivere una linea per ogni proprietà
            'dell'oggetto, mostrandone il valore
            For Each Field As String In T.Fields.Keys
                Code.AppendFormat("Console.WriteLine(""    {0} = "" & Storage(I).{0}.ToString()){1}", Field, Environment.NewLine)
            Next
            Code.AppendLine("      Next")
            Code.AppendLine("      Console.ReadKey()")
            Code.AppendLine("  End Select")
            Code.AppendLine("  Loop Until Cmd = ""u""")

            Code.AppendLine("End Sub")
            Code.AppendLine("End Module")

            Dim Parameters As New CompilerParameters

            With Parameters
                .GenerateExecutable = True
                .TreatWarningsAsErrors = True
                .TempFiles.KeepFiles = False
                .GenerateInMemory = False
                .ReferencedAssemblies.Add("Microsoft.VisualBasic.dll")
                .ReferencedAssemblies.Add("System.dll")
            End With

            Dim Provider As New VBCodeProvider
            Dim CompResults As CompilerResults = _
                Provider.CompileAssemblyFromSource(Parameters, Code.ToString())

            If CompResults.Errors.Count = 0 Then
                Return CompResults.PathToAssembly
            Else
                For Each E As CompilerError In CompResults.Errors
                    Stop
                Next
                Return Nothing
            End If
        End Function

    End Class

    Sub Main()
        Dim NewType As New TypeCreator()
        Dim I As Int16
        Dim Field, FieldType As String

        Console.WriteLine("Creazione di un tipo")
        Console.WriteLine()

        Console.Write("Nome del tipo = ")
        NewType.Name = Console.ReadLine

        Console.WriteLine("Inserisci il nome del campo e il suo tipo:")

        I = 1
        Do
            Console.Write("Nome campo {0}: ", I)
            Field = Console.ReadLine

            If String.IsNullOrEmpty(Field) Then
                Exit Do
            End If

            Console.Write("Tipo campo {0}: ", I)
            FieldType = Console.ReadLine

            'Dovrete immettere il nome completo e con
            'le maiuscole al posto giusto. Ad esempio:
            '  System.String
            'e non string o system.String o STring.
            If Type.GetType(FieldType) Is Nothing Then
                Console.WriteLine("Il tipo {0} non esiste!", FieldType)
                Console.ReadKey()
            Else
                NewType.Fields.Add(Field, FieldType)
                I += 1
            End If
        Loop

        Dim Path As String = ProgramCreator.CreateManagingProgram(NewType)

        If Not String.IsNullOrEmpty(Path) Then
            Console.WriteLine("Programma di gestione per il tipo {0} creato!", NewType.Name)
            Console.WriteLine("Avviarlo ora? y/n")
            If Console.ReadKey().KeyChar = "y" Then
                Process.Start(Path)
            End If
        End If
    End Sub
End Module


<< Precedente Prossimo >>
A proposito dell'autore

C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...