Skriptmotor för C# och VB.NET - del 1
Förord
Denna artikeln visar ett exempel på hur du enkelt kan skriva en inkapsling av .NETs kompilatorer så att det blir möjligt att köra scripter i C# eller VB.NET. Med ett enkelt "språktillägg" i form av en #language pre-kompilator-flagga kan din skriptkompilator enkelt bestämma vilken .NET kompilator som ska användas.Innehåll
»»
»
»
»
»
»
Relaterade artiklar
» Skriptmotor för C# och VB.NET - del 2
Inledning och uppdelning
</h3>
Innan vi börjar så måste vi bestämma oss för vad ett skript egentligen är. Givetvis ska skriptet bestå av antingen C# eller VB-kod men vi behöver också en mekanism för att kunna peka ut var skriptet börjar. Dessutom, för att ett skript ska vara meningsfullt ska det givetvis ha tillgång till delar av ”moderapplikationens” olika objekt så vi behöver också ett sätt att skicka information till skript-koden. Detta kan förstås göras på mängder av olika sätt och du kan säkert komma på flera som passar dina behov bättre men detta är den modell jag har valt för denna artikeln: Skriptet består av en klass som implementerar ett interface. Interfacet deklarerar en egenskap, Parametrar, av typen IDictionary som används som det låter. Skriptkoden, som alltså blir en del av klassen som implementerar vårt interfejs, får tillgång till parametrarna på ungefär samma sätt som en Windows-applikation.
Hela övningen kommer att bestå av ett klassbibliotek och en testapplikation. Först och främst placerar vi hela scripting-logiken i klassbiblioteket och i ett eget namespace. Därefter kodar vi en enkel testapplikation (Windows applikation) så att vi kan provköra hela rasket så enkelt som möjligt.
Skriptkompilatorn letar efter en klass som implementerar ett känt interface kallat IScript. Interfejset är väldigt enkelt och beskriver bara tillräckligt med funktionalitet för att kunna funka som ett enkelt kod-gränssnitt.
Interfejset IScript
public interface IScript
{
object Owner { get; set; }
IDictionary Parameters { get; set; }
object Execute();
}
Ok, ett tämligen enkelt interface alltså. En klass som implementerar detta interfejset förväntas placera sin ”start-kod” (entry point) i metoden Execute() och kan sedan använda parametrarna i egenskapen Parameters för att ta emot data ifrån moder-applikationen. Jag kastade dessutom in en ”Owner” egenskap för att kunna peka ut vilken kontext skriptet kör i. Owner-egenskapen använder jag inte i detta exemplet men du kan antagligen ha stor nytta av den. (Givetvis kan kontexten skickas som en parameter men koden blir lättare att läsa på detta sättet.)
Tanken är nu att vi placerar vårt interfejs i ett separat klassbibliotek (dll) som sedan lätt kan refereras av alla dina projekt som behöver skript-funktionalitet.
Dags att koda...
Skapa en ny tom solution (OBS jag har bara den engelsktalande versionen av VS.NET så jag befattar mig inte med ev svenska namn på saker och ting) och därefter ett C# class library som du döper till ”Scripting”. VS.NET skapar automatiskt en classfil (Class1.cs) och denna döper du bara om till ”ScriptObject.cs”. Klassen som autmatgenererades (Class1) döper du om till ”ScriptObject” (mer om detta senare) och därefter kopierar du bara in interfejset från ovan (IScript).
Som du ser i IScript så behöver vi tillgång till .NET interfejset ”IDictionary” (för egenskapen Parameters) så vi måste lägga till using System.Collections;. För skojs skull definierar vi hela vår skript-logik i en egen namespace som vi kallar My.Scripting.
Slutligen, klassen du just döpte om (ScriptObject) blir vår egen implementation av IScript, vilket förenklar saker och ting i framtiden. Hela koden ska nu se ut så här:
Implementering av IScript
using System;
using System.Collections;
namespace My.Scripting
{
///
/// Summary description for IScript.
///
public interface IScript
{
object Owner { get; set; }
IDictionary Parameters { get; set; }
object Execute();
}
public abstract class ScriptObject : IScript
{
public object Owner { get {return getOwner();} set {setOwner(value);} }
public IDictionary Parameters
{
get {return getParameters();}
set {setParameters(value);}
}
public abstract object Execute();
#region Property accessors
protected object getOwner()
{
return owner;
}
protected void setOwner(object value)
{
owner = value;
}
protected IDictionary getParameters()
{
return parameters;
}
protected void setParameters(IDictionary value)
{
parameters = value;
}
#endregion
#region Fields
object owner;
IDictionary parameters;
#endregion
}
}
Skriptkompilatorn (kod)
</h3>
I nästa steg skriver vi inkapslingen av .NETs kompilatorer i form av en klass som vi helt enkelt döper till ScriptCompiler. Skriptkompilatorn löser ett antal uppgifter:
- Möjlighet att skripta i både C# och Visual Basic
- Hantering av felaktigheter i skripter
- Hantering av felaktigheter i egendefinierade språkelement
- Kompilering till minnet och hantering av temporära filer
Skriptkompilatorn lägger vi i en egen fil som vi helt enkelt kallar ScriptCompiler.cs. När du har skapat filen skriver du över dess innehåll med denna koden (håll i hatten :o)...
using System;
using System.IO;
using Microsoft.CSharp;
using Microsoft.VisualBasic;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
using System.Collections;
namespace My.Scripting
{
public enum ScriptLanguage {None=0, CSharp=1, VisualBasic=2}
public class ScriptCompiler
{
///
/// Initializes a new instance of the ScriptCompiler class with no
/// default script language.
///
public ScriptCompiler()
{
defaultLanguage = ScriptLanguage.None;
}
///
/// Initializes a new instance of the ScriptCompiler class, using a
/// default script language.
///
///
public ScriptCompiler(ScriptLanguage defaultLanguage)
{
this.defaultLanguage = defaultLanguage;
}
public static string ScriptLanguageName_CSharp = "CSharp";
public static string ScriptLanguageName_VisualBasic = "VisualBasic";
#region Properties
public static object ThreadRoot { get {return threadRoot;} }
public ScriptLanguage DefaultLanguage
{
get {return defaultLanguage;}
set {defaultLanguage = value;}
}
public Exception Error
{
get {return error;}
}
#endregion
#region Errors and error handling
static void Error_ExpectedScriptLanguageStatement()
{
throw new UnknownScriptLanguageException(
"Expected language definition at beginning of"+
" script (syntax: "+getLanguageStatementSyntax()+")"
);
}
static void Error_LanguageStatementSyntaxError()
{
throw new UnknownScriptLanguageException(
"Cannot interpret the language statement "+
(syntax: "+getLanguageStatementSyntax()+")"
);
}
static void Error_CompilationFailed(string message)
{
throw new ScriptCompilerException("Compilation failed:\n"+message);
}
static void Error_IScriptNotImplemented()
{
throw new ScriptNotImplementedException(
"Interface "+typeof(IScript).FullName+
" must be implemented!"
);
}
static string getLanguageStatementSyntax()
{
return "#language = {" + ScriptLanguageName_CSharp +
" | " + ScriptLanguageName_VisualBasic +"}";
}
void addError(Exception value)
{
error = value;
}
#endregion
///
/// Compiles a script and returns an instance of the first class found to
/// implement the IScript interface, if no such class can be found a
/// ScriptException is raised.
/// On compilation, the compiler will look for a #language statement.
/// If no proper #language statement is found, and the DefaultLanguage
/// property is not set (set to ScriptLanguage.None), a ScriptException is raised.
/// Compilation errors can be found in the Error property.
///
///
/// The script source code object's Execute() method.
///
///
/// A script object (the first class found to implement the IScipt interface)
///
///
/// Thrown when no #language statement was found and DefaultLanguage was
/// set to ScriptLanguage.None.
///
///
/// Thrown when no class in the script source code was found to implement the
/// IScript interface.
///
public IScript Compile(string script)
{
CodeDomProvider codeProvider;
try
{
ScriptLanguage language = getScriptLanguage(ref script);
switch (language)
{
case ScriptLanguage.CSharp :
codeProvider = new CSharpCodeProvider();
break;
case ScriptLanguage.VisualBasic :
codeProvider = new VBCodeProvider();
break;
default:
Error_ExpectedScriptLanguageStatement();
return null; // keeps compiler happy :o/
}
ICodeCompiler compiler = codeProvider.CreateCompiler();
CompilerParameters cParams = new CompilerParameters();
cParams.GenerateExecutable = false;
cParams.GenerateInMemory = true;
cParams.OutputAssembly = getTemporaryOutputAssemblyName(language);
cParams.MainClass = "**not used**";
cParams.IncludeDebugInformation = false;
// allow all referenced assemblies to be used by the script...
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
cParams.ReferencedAssemblies.Add(asm.Location);
CompilerResults results = compiler.CompileAssemblyFromSource(cParams, script);
if (results.Errors.Count > 0)
{
StringBuilder errors = new StringBuilder();
foreach (CompilerError err in results.Errors)
{
errors.Append(err.ToString() + "\n");
}
Error_CompilationFailed(errors.ToString());
return null; // keeps compiler happy :o|
}
else
{
// remove temporary files...
if (File.Exists(cParams.OutputAssembly))
File.Delete(cParams.OutputAssembly);
// get the first class that implements the IScript interface...
IScript scriptObject = getScriptObject(results.CompiledAssembly);
if (scriptObject == null)
Error_IScriptNotImplemented();
return scriptObject;
}
}
catch (ScriptCompilerException e)
{
addError(e);
return null;
}
}
#region Internal stuff
ScriptLanguage defaultLanguage;
Exception error;
static long tmpOutputAssemblyID = 0;
static object threadRoot = new object();
///
/// Returns the script language, as defined by first #-statement in
/// script or the DefaultLanguage.
/// If the #language statement is found it will be removed from the
/// script on return.
///
/// The script source code
///
///
ScriptLanguage getScriptLanguage(ref string script)
{
if (script.Length == 0 || !script.Substring(0,9).Equals("#language"))
{
if (DefaultLanguage == ScriptLanguage.None)
Error_ExpectedScriptLanguageStatement();
return defaultLanguage;
}
string statement = script.Substring(0, script.IndexOf(System.Environment.NewLine));
string[] tokens = statement.Split('=');
if (tokens.Length < 2)
Error_LanguageStatementSyntaxError();
string languageToken = tokens[1].Trim();
script = script.Substring(statement.Length);
if (languageToken.Equals(ScriptLanguageName_CSharp))
return ScriptLanguage.CSharp;
else if (languageToken.Equals(ScriptLanguageName_VisualBasic))
return ScriptLanguage.VisualBasic;
else
return ScriptLanguage.None;
}
///
/// Creates a thread safe temporary assembly name.
/// The method is primarily intended for on-the-run, in-memory, compilation.
///
/// The script language
/// A temporary assembly name.
static string getTemporaryOutputAssemblyName(ScriptLanguage language)
{
long result;
lock (ThreadRoot)
{
result = ++tmpOutputAssemblyID;
}
if (language == ScriptLanguage.VisualBasic)
return "temp_asm"+result + ".dll";
else
return "temp_asm"+result.ToString();
}
///
/// Returns the first class in the assembly that implements the IScript interface.
///
///
/// The assembly that's expected to contain the IScript implementor.
///
/// An instance of the first class found to implement the IScript interface.
/// If no such class exists a null value is returned instead
IScript getScriptObject(Assembly asm)
{
Type[] types = asm.GetTypes();
foreach (Type type in types)
{
if (type.IsClass && type.GetInterface("IScript") != null)
return (IScript)Activator.CreateInstance(type);
}
return null;
}
#endregion
}
public class ScriptCompilerException : System.Exception
{
public ScriptCompilerException(string message) : base(message) {}
}
public class UnknownScriptLanguageException : ScriptCompilerException
{
public UnknownScriptLanguageException(string message) : base(message) {}
}
public class ScriptNotImplementedException : ScriptCompilerException
{
public ScriptNotImplementedException(string message) : base(message) {}
}
}
Skriptkompilatorn (förklaring)
</h3>
Ok, jag antar att det är dags att förklara litet grand... (Tips: Högerklicka i koden och välj Outlining->Collapse to definitions för att lättare hitta. Veckla sedan bara ut de olika delarna efterhand som vi berör dem.)
Först och främst definierar vi en enum kallad ScriptLanguage. Denna använder vi på ett par ställen där vi behöver veta om skriptet är C# eller Visual Basic. Detta tillvägagångssättet gör det också enkelt att lägga till fler språk senare om du skulle vilja. Inga konstigheter så långt alltså.
Sist i filen deklarerar vi tre typer av Exceptions: ScriptCompilerException, UnknownScriptLanguageException och ScriptNotImplementedException. Dessa behövs för att en användare av skriptkompilatorn lätt ska kunna hantera motsvararande fel. Mer om detta senare. Nu till själva skriptkompilatorn...
Vi har två konstruktorer: En utan parametrar och en där den instansierande koden kan bestämma vilket skriptspråk som ska förutsättas om inget annat sägs. Därefter fortsätter vi med att deklarera syntaxen för vårt egenkomponerade språkelement: #language = {CSharp | VisualBasic}. Namnet på de två språken definierar vi i två statiska strängar: ScriptLanguageName_CSharp och ScriptLanguageName_VisualBasic.
Efter detta deklarerar vi tre publika egenskaper: ThreadRoot, DefaultLanguage och Error. ThreadRoot behövs i metoden getTemporaryOutputAssemblyName() (finns i regionen “Internal stuff”) som har till uppgift att skapa ett temporärt namn på det assembly som kompilatorn bygger under arbetet. Egenskapen DefaultLanguage sätts eventuellt av konstruktorn men den kan också sättas efter instansieringen och före kompileringen av skriptet. Före och under kompileringen kan det uppstå fel och dessa lagras då av egenskapen Error.
I regionen ”Errors and error handling” hittar du en del stödmetoder som helt enkelt bara kastar rätt undantag (exception) beroende på vilka fel vi påträffar. Cirkulera, här finns inget att se...
Nu till den intressanta biten: Metoden Compile(string):
Metoden börjar med att deklarera en CodeDomProvider. Denna använder vi för att komma åt en kompilator men först måste vi ta reda på vilket skriptspråk det handlar om. Den egendefinierade metoden getScriptLanguage() kikar på skriptet innan kompileringen och letar efter direktivet #language=X (där X alltså kan vara antingen ”CSharp” eller ”VisualBasic”). Om direktivet inte finns så kollar metoden om vi har ett standardspråk varpå detta i så fall returneras. Har vi inget standardspråk så kastar metoden istället ett UnknownScriptLanguageException-undantag. Förutsatt att språkdirektivet inte innebar några problem så fortsätter vi med att instantiera rätt ”codeProvider” för antingen C# eller VB via en switch, vilken vi till sist använder för att skapa oss en kompilator (på rad 107). Beväpnad med rätt kompilator är det dags att förbereda kompileringen. Vi kommer att kompilera i minnet så någon assembly i form av en EXE eller DLL behövs alltså inte. Istället ställer vi in kompilatorn att använda en temporär assembly-fil som vi kommer att ta bort efter kompileringen. På rad 111 använder vi alltså metoden getTemporaryOutputAssemblyName(ScriptLanguage) som på ett trådsäkert sätt genererar ett lagom meningslöst namn på vårt tillfälliga assembly.
Innan vi till sist kompilerar så ger vi skriptet tillgång till de (assembly) referenser som moderapplikationen själv använder via foreach-loopen på rad 116. Det kan förstås vara så att den anropande koden vill styra detta för att inte dela ut för mycket men i denna versionen finlirar vi inte :o). På rad 119 anropar vi till sist kompilatorns metod CompileAssemblyFromSource() som alltså kompilerar skriptet med de gällande parametrarna. Observera att nästan hela metoden ScriptCompiler.Compile() omfattas av ett try-catch block som fångar allt vi vill hantera och placerar det i egenskapen Error så att den anropande koden ska kunna presentera felen efter eget godtycke.
Efter kompileringen kollar vi om kompilatorn stötte på några fel och i så fall snyggar vi till dessa och kastar slutligen ett ScriptCompilerException via metoden Error_CompilationFailed().
På rad 134 är det fortfarande ”all systems go” och vi kan nu radera den tillfälliga filen som kompilatorn har genererat. Vi måste nu leta upp den klass i det färdiga assemblyt som implementerar interfejset IScript vilket vi gör med hjälp av den egna metoden getScriptObject() som använder reflection för att lösa uppgiften och returnerar objektet eller null om den inte hittade något. Utan ett skript är förstås matchen över och i så fall kastar vi ett ScriptNotImplementedException undantag på rad 139. Förutsatt att vi nu slutligen har ett skript-objekt så returnerar vi till sist detta och arbetet är klart. Nu är det upp till den anropande koden att köra skriptet genom att ställa in lämpliga parametrar och anropa Execute().
Sammanfattning
</h3>
Som du ser är det ganska enkelt att använda scriptning i .NET. Detta exemplet motsvarar bara en enkel skriptkompilator men jag skulle tro att du har sett tillräckligt med förslag på lösningar för att kunna gå vidare på egen hand. I nästa artikel skriver vi en mycket enkel kod-editor som använder vår kompilator för att köra dina skripter.
Kända problem
</h3>
Ett potentiellt problem som jag hittils inte har djupdykt i kommer om du kompilerar VB.NET-kod två gånger efter varandra. Kompilatorn genererar ju ett assembly som sagt men i fallet med VB.NET så görs detta, av för mig okänd anledning, som en DLL-fil. Förutom att den hanteras annorlunda så får jag dessutom ett IO-fel med ”File not found” när jag ska radera assemblyt efter andra kompileringen. Jag har inte bekymrat mig om de bakomliggande orsakerna ännu men om du vet mer så dela gärna med dig.
Inledning och uppdelning
Interfejset IScript
public interface IScript
{
object Owner { get; set; }
IDictionary Parameters { get; set; }
object Execute();
}
Implementering av IScript
using System;
using System.Collections;
namespace My.Scripting
{
///
/// Summary description for IScript.
///
public interface IScript
{
object Owner { get; set; }
IDictionary Parameters { get; set; }
object Execute();
}
public abstract class ScriptObject : IScript
{
public object Owner { get {return getOwner();} set {setOwner(value);} }
public IDictionary Parameters
{
get {return getParameters();}
set {setParameters(value);}
}
public abstract object Execute();
#region Property accessors
protected object getOwner()
{
return owner;
}
protected void setOwner(object value)
{
owner = value;
}
protected IDictionary getParameters()
{
return parameters;
}
protected void setParameters(IDictionary value)
{
parameters = value;
}
#endregion
#region Fields
object owner;
IDictionary parameters;
#endregion
}
}
Skriptkompilatorn (kod)
</h3>I nästa steg skriver vi inkapslingen av .NETs kompilatorer i form av en klass som vi helt enkelt döper till ScriptCompiler. Skriptkompilatorn löser ett antal uppgifter:
- Möjlighet att skripta i både C# och Visual Basic
- Hantering av felaktigheter i skripter
- Hantering av felaktigheter i egendefinierade språkelement
- Kompilering till minnet och hantering av temporära filer
Skriptkompilatorn lägger vi i en egen fil som vi helt enkelt kallar ScriptCompiler.cs. När du har skapat filen skriver du över dess innehåll med denna koden (håll i hatten :o)...
using System;
using System.IO;
using Microsoft.CSharp;
using Microsoft.VisualBasic;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
using System.Collections;
namespace My.Scripting
{
public enum ScriptLanguage {None=0, CSharp=1, VisualBasic=2}
public class ScriptCompiler
{
///
/// Initializes a new instance of the ScriptCompiler class with no
/// default script language.
///
public ScriptCompiler()
{
defaultLanguage = ScriptLanguage.None;
}
///
/// Initializes a new instance of the ScriptCompiler class, using a
/// default script language.
///
///
public ScriptCompiler(ScriptLanguage defaultLanguage)
{
this.defaultLanguage = defaultLanguage;
}
public static string ScriptLanguageName_CSharp = "CSharp";
public static string ScriptLanguageName_VisualBasic = "VisualBasic";
#region Properties
public static object ThreadRoot { get {return threadRoot;} }
public ScriptLanguage DefaultLanguage
{
get {return defaultLanguage;}
set {defaultLanguage = value;}
}
public Exception Error
{
get {return error;}
}
#endregion
#region Errors and error handling
static void Error_ExpectedScriptLanguageStatement()
{
throw new UnknownScriptLanguageException(
"Expected language definition at beginning of"+
" script (syntax: "+getLanguageStatementSyntax()+")"
);
}
static void Error_LanguageStatementSyntaxError()
{
throw new UnknownScriptLanguageException(
"Cannot interpret the language statement "+
(syntax: "+getLanguageStatementSyntax()+")"
);
}
static void Error_CompilationFailed(string message)
{
throw new ScriptCompilerException("Compilation failed:\n"+message);
}
static void Error_IScriptNotImplemented()
{
throw new ScriptNotImplementedException(
"Interface "+typeof(IScript).FullName+
" must be implemented!"
);
}
static string getLanguageStatementSyntax()
{
return "#language = {" + ScriptLanguageName_CSharp +
" | " + ScriptLanguageName_VisualBasic +"}";
}
void addError(Exception value)
{
error = value;
}
#endregion
///
/// Compiles a script and returns an instance of the first class found to
/// implement the IScript interface, if no such class can be found a
/// ScriptException is raised.
/// On compilation, the compiler will look for a #language statement.
/// If no proper #language statement is found, and the DefaultLanguage
/// property is not set (set to ScriptLanguage.None), a ScriptException is raised.
/// Compilation errors can be found in the Error property.
///
///
/// The script source code object's Execute() method.
///
///
/// A script object (the first class found to implement the IScipt interface)
///
///
/// Thrown when no #language statement was found and DefaultLanguage was
/// set to ScriptLanguage.None.
///
///
/// Thrown when no class in the script source code was found to implement the
/// IScript interface.
///
public IScript Compile(string script)
{
CodeDomProvider codeProvider;
try
{
ScriptLanguage language = getScriptLanguage(ref script);
switch (language)
{
case ScriptLanguage.CSharp :
codeProvider = new CSharpCodeProvider();
break;
case ScriptLanguage.VisualBasic :
codeProvider = new VBCodeProvider();
break;
default:
Error_ExpectedScriptLanguageStatement();
return null; // keeps compiler happy :o/
}
ICodeCompiler compiler = codeProvider.CreateCompiler();
CompilerParameters cParams = new CompilerParameters();
cParams.GenerateExecutable = false;
cParams.GenerateInMemory = true;
cParams.OutputAssembly = getTemporaryOutputAssemblyName(language);
cParams.MainClass = "**not used**";
cParams.IncludeDebugInformation = false;
// allow all referenced assemblies to be used by the script...
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
cParams.ReferencedAssemblies.Add(asm.Location);
CompilerResults results = compiler.CompileAssemblyFromSource(cParams, script);
if (results.Errors.Count > 0)
{
StringBuilder errors = new StringBuilder();
foreach (CompilerError err in results.Errors)
{
errors.Append(err.ToString() + "\n");
}
Error_CompilationFailed(errors.ToString());
return null; // keeps compiler happy :o|
}
else
{
// remove temporary files...
if (File.Exists(cParams.OutputAssembly))
File.Delete(cParams.OutputAssembly);
// get the first class that implements the IScript interface...
IScript scriptObject = getScriptObject(results.CompiledAssembly);
if (scriptObject == null)
Error_IScriptNotImplemented();
return scriptObject;
}
}
catch (ScriptCompilerException e)
{
addError(e);
return null;
}
}
#region Internal stuff
ScriptLanguage defaultLanguage;
Exception error;
static long tmpOutputAssemblyID = 0;
static object threadRoot = new object();
///
/// Returns the script language, as defined by first #-statement in
/// script or the DefaultLanguage.
/// If the #language statement is found it will be removed from the
/// script on return.
///
/// The script source code
///
///
ScriptLanguage getScriptLanguage(ref string script)
{
if (script.Length == 0 || !script.Substring(0,9).Equals("#language"))
{
if (DefaultLanguage == ScriptLanguage.None)
Error_ExpectedScriptLanguageStatement();
return defaultLanguage;
}
string statement = script.Substring(0, script.IndexOf(System.Environment.NewLine));
string[] tokens = statement.Split('=');
if (tokens.Length < 2)
Error_LanguageStatementSyntaxError();
string languageToken = tokens[1].Trim();
script = script.Substring(statement.Length);
if (languageToken.Equals(ScriptLanguageName_CSharp))
return ScriptLanguage.CSharp;
else if (languageToken.Equals(ScriptLanguageName_VisualBasic))
return ScriptLanguage.VisualBasic;
else
return ScriptLanguage.None;
}
///
/// Creates a thread safe temporary assembly name.
/// The method is primarily intended for on-the-run, in-memory, compilation.
///
/// The script language
/// A temporary assembly name.
static string getTemporaryOutputAssemblyName(ScriptLanguage language)
{
long result;
lock (ThreadRoot)
{
result = ++tmpOutputAssemblyID;
}
if (language == ScriptLanguage.VisualBasic)
return "temp_asm"+result + ".dll";
else
return "temp_asm"+result.ToString();
}
///
/// Returns the first class in the assembly that implements the IScript interface.
///
///
/// The assembly that's expected to contain the IScript implementor.
///
/// An instance of the first class found to implement the IScript interface.
/// If no such class exists a null value is returned instead
IScript getScriptObject(Assembly asm)
{
Type[] types = asm.GetTypes();
foreach (Type type in types)
{
if (type.IsClass && type.GetInterface("IScript") != null)
return (IScript)Activator.CreateInstance(type);
}
return null;
}
#endregion
}
public class ScriptCompilerException : System.Exception
{
public ScriptCompilerException(string message) : base(message) {}
}
public class UnknownScriptLanguageException : ScriptCompilerException
{
public UnknownScriptLanguageException(string message) : base(message) {}
}
public class ScriptNotImplementedException : ScriptCompilerException
{
public ScriptNotImplementedException(string message) : base(message) {}
}
}
Skriptkompilatorn (förklaring)
</h3>
Ok, jag antar att det är dags att förklara litet grand... (Tips: Högerklicka i koden och välj Outlining->Collapse to definitions för att lättare hitta. Veckla sedan bara ut de olika delarna efterhand som vi berör dem.)
Först och främst definierar vi en enum kallad ScriptLanguage. Denna använder vi på ett par ställen där vi behöver veta om skriptet är C# eller Visual Basic. Detta tillvägagångssättet gör det också enkelt att lägga till fler språk senare om du skulle vilja. Inga konstigheter så långt alltså.
Sist i filen deklarerar vi tre typer av Exceptions: ScriptCompilerException, UnknownScriptLanguageException och ScriptNotImplementedException. Dessa behövs för att en användare av skriptkompilatorn lätt ska kunna hantera motsvararande fel. Mer om detta senare. Nu till själva skriptkompilatorn...
Vi har två konstruktorer: En utan parametrar och en där den instansierande koden kan bestämma vilket skriptspråk som ska förutsättas om inget annat sägs. Därefter fortsätter vi med att deklarera syntaxen för vårt egenkomponerade språkelement: #language = {CSharp | VisualBasic}. Namnet på de två språken definierar vi i två statiska strängar: ScriptLanguageName_CSharp och ScriptLanguageName_VisualBasic.
Efter detta deklarerar vi tre publika egenskaper: ThreadRoot, DefaultLanguage och Error. ThreadRoot behövs i metoden getTemporaryOutputAssemblyName() (finns i regionen “Internal stuff”) som har till uppgift att skapa ett temporärt namn på det assembly som kompilatorn bygger under arbetet. Egenskapen DefaultLanguage sätts eventuellt av konstruktorn men den kan också sättas efter instansieringen och före kompileringen av skriptet. Före och under kompileringen kan det uppstå fel och dessa lagras då av egenskapen Error.
I regionen ”Errors and error handling” hittar du en del stödmetoder som helt enkelt bara kastar rätt undantag (exception) beroende på vilka fel vi påträffar. Cirkulera, här finns inget att se...
Nu till den intressanta biten: Metoden Compile(string):
Metoden börjar med att deklarera en CodeDomProvider. Denna använder vi för att komma åt en kompilator men först måste vi ta reda på vilket skriptspråk det handlar om. Den egendefinierade metoden getScriptLanguage() kikar på skriptet innan kompileringen och letar efter direktivet #language=X (där X alltså kan vara antingen ”CSharp” eller ”VisualBasic”). Om direktivet inte finns så kollar metoden om vi har ett standardspråk varpå detta i så fall returneras. Har vi inget standardspråk så kastar metoden istället ett UnknownScriptLanguageException-undantag. Förutsatt att språkdirektivet inte innebar några problem så fortsätter vi med att instantiera rätt ”codeProvider” för antingen C# eller VB via en switch, vilken vi till sist använder för att skapa oss en kompilator (på rad 107). Beväpnad med rätt kompilator är det dags att förbereda kompileringen. Vi kommer att kompilera i minnet så någon assembly i form av en EXE eller DLL behövs alltså inte. Istället ställer vi in kompilatorn att använda en temporär assembly-fil som vi kommer att ta bort efter kompileringen. På rad 111 använder vi alltså metoden getTemporaryOutputAssemblyName(ScriptLanguage) som på ett trådsäkert sätt genererar ett lagom meningslöst namn på vårt tillfälliga assembly.
Innan vi till sist kompilerar så ger vi skriptet tillgång till de (assembly) referenser som moderapplikationen själv använder via foreach-loopen på rad 116. Det kan förstås vara så att den anropande koden vill styra detta för att inte dela ut för mycket men i denna versionen finlirar vi inte :o). På rad 119 anropar vi till sist kompilatorns metod CompileAssemblyFromSource() som alltså kompilerar skriptet med de gällande parametrarna. Observera att nästan hela metoden ScriptCompiler.Compile() omfattas av ett try-catch block som fångar allt vi vill hantera och placerar det i egenskapen Error så att den anropande koden ska kunna presentera felen efter eget godtycke.
Efter kompileringen kollar vi om kompilatorn stötte på några fel och i så fall snyggar vi till dessa och kastar slutligen ett ScriptCompilerException via metoden Error_CompilationFailed().
På rad 134 är det fortfarande ”all systems go” och vi kan nu radera den tillfälliga filen som kompilatorn har genererat. Vi måste nu leta upp den klass i det färdiga assemblyt som implementerar interfejset IScript vilket vi gör med hjälp av den egna metoden getScriptObject() som använder reflection för att lösa uppgiften och returnerar objektet eller null om den inte hittade något. Utan ett skript är förstås matchen över och i så fall kastar vi ett ScriptNotImplementedException undantag på rad 139. Förutsatt att vi nu slutligen har ett skript-objekt så returnerar vi till sist detta och arbetet är klart. Nu är det upp till den anropande koden att köra skriptet genom att ställa in lämpliga parametrar och anropa Execute().
Sammanfattning
</h3>
Som du ser är det ganska enkelt att använda scriptning i .NET. Detta exemplet motsvarar bara en enkel skriptkompilator men jag skulle tro att du har sett tillräckligt med förslag på lösningar för att kunna gå vidare på egen hand. I nästa artikel skriver vi en mycket enkel kod-editor som använder vår kompilator för att köra dina skripter.
Kända problem
</h3>
Ett potentiellt problem som jag hittils inte har djupdykt i kommer om du kompilerar VB.NET-kod två gånger efter varandra. Kompilatorn genererar ju ett assembly som sagt men i fallet med VB.NET så görs detta, av för mig okänd anledning, som en DLL-fil. Förutom att den hanteras annorlunda så får jag dessutom ett IO-fel med ”File not found” när jag ska radera assemblyt efter andra kompileringen. Jag har inte bekymrat mig om de bakomliggande orsakerna ännu men om du vet mer så dela gärna med dig.
Skriptkompilatorn (förklaring)
Sammanfattning
</h3>Som du ser är det ganska enkelt att använda scriptning i .NET. Detta exemplet motsvarar bara en enkel skriptkompilator men jag skulle tro att du har sett tillräckligt med förslag på lösningar för att kunna gå vidare på egen hand. I nästa artikel skriver vi en mycket enkel kod-editor som använder vår kompilator för att köra dina skripter.
Kända problem
</h3>
Ett potentiellt problem som jag hittils inte har djupdykt i kommer om du kompilerar VB.NET-kod två gånger efter varandra. Kompilatorn genererar ju ett assembly som sagt men i fallet med VB.NET så görs detta, av för mig okänd anledning, som en DLL-fil. Förutom att den hanteras annorlunda så får jag dessutom ett IO-fel med ”File not found” när jag ska radera assemblyt efter andra kompileringen. Jag har inte bekymrat mig om de bakomliggande orsakerna ännu men om du vet mer så dela gärna med dig.
Kända problem
Johan Segolsson
Lägg till en del radbrytningar i koden då rader inom < code > inte bryts automatiskt så blir sidan ganska bred...
Jonas Rembratt
Fixat nu
Roger Alsing
eftersom du inte kompilerar scripten i en egen appdomän så kommer du tillslut fylla minnet eftersom en assemblies inte kan laddas ur minnet.. man måste döda hela appdomänen som dom ligger i för att ladda ur assemblies.. så kompilerar du ditt script 200 ggr så har du 200 assemblies i din current appdomain //Roger
Jonas Rembratt
Det är riktigt. Den som tänker ladda skripter dynamiskt får antingen ladda dem i en egen AppDomain eller bygga in en mekanism som förhindrar att samma skript laddas flera gånger. Den ska också tilläggas att om skriptet laddas i en separat AppDomain så kommer det inte att få tillgång till objekten i "current" AppDomain utan att använda remoting (vilket, minst sagt, är ett ämne för sig ;o).