Artykuł pokazuje jak można wykorzystać przestrzeń nazw System.Codedom do stworzenia programu, który modyfikuje dynamicznie własną ścieżkę wykonania.
Ostatnio zastanawiałem się, czy można w C# napisać program, który sam modyfikuje się w trakcie działania. Okazało się (jak to zwykle w takich sytuacjach bywa), że można osiągnąć efekt co najmniej zbliżony ;).
Zakładam, że potencjalny czytelnik posiada dość biegłą znajomość koncepcji programowania obiektowego, oraz oczywiście języka C# ;).
Przestrzeń nazw System.Codedom
Jak sama nazwa wskazuje, jest to document object model dla kodu wykonywalnego, czyli obiektowa struktura reprezentująca tenże kod w postaci drzewka. Drzewko to jest oczywiście tylko abstrakcją programu, a zatem możemy na jego podstawie wygenerować kod w dowolnym zarządzanym języku, o ile tylko posiadamy odpowiedni provider (C# i VB są w standardzie ;)).
Przestrzeń ta stanowi bardziej wysokopoziomową alternatywę dla generacji kodu CIL (przestrzeń System.Reflection.Emit).
Stworzymy teraz w pamięci przykładową prostą klasę:
var compileUnit = newCodeCompileUnit(); //compile unit odpowiada logicznie assembly
var codeNamespace = newCodeNamespace("Some.Namespace"); //tworzymy przestrzeń nazwa
codeNamespace.Imports.Add(newCodeNamespaceImport("System")); //dołączamy sobie “using System;”
compileUnit.Namespaces.Add(codeNamespace); // włączamy przestrzeń nazw do naszego assembly
var typeDeclaration = newCodeTypeDeclaration("FooClass"); //tworzymy deklaracje typu
codeNamespace.Types.Add(typeDeclaration); //i włączamy ją do przestrzeni nazw
Dodajmy jeszcze do naszej klasy jedną metodę:
var method = newCodeMemberMethod
{
Attributes = MemberAttributes.Public | MemberAttributes.Static,
Name = “ZugZug”,
ReturnType = newCodeTypeReference(typeof(string))
};
Mamy publiczną statyczną metodę o nazwie ZugZug zwracającą string. Dodajmy jej parametr typu int o nazwie blabla.
method.Parameters.Add(newCodeParameterDeclarationExpression(newCodeTypeReference(typeof (int)), "blabla"));
Samą metodę dodamy jeszcze do klasy:
typeDeclaration.Members.Add(method);
Warto byłoby także uzupełnić wnętrze metody kodem (potrzebujemy przynajmniej return’a żeby kod w ogóle się skompilował). Zwróćmy zatem przekazany parametr zamieniony na string:
var returnStatement = newCodeMethodReturnStatement(
new CodeMethodInvokeExpression(newCodeVariableReferenceExpression("blabla"), "ToString"));
method.Statements.Add(returnStatement);
Voila!
Następująca metoda pozwoli nam zamienić nasze wypociny na „żywy” kod C#:
private static string GenerateCode(CodeNamespace sourceCode)
{
var codeGeneratorOptions = newCodeGeneratorOptions
{
BlankLinesBetweenMembers = true,
BracingStyle = "C",
IndentString = " "
};
var codeGenerator = newCSharpCodeProvider().CreateGenerator();
var code = newStringBuilder();
var stringWriter = newStringWriter(code);
codeGenerator.GenerateCodeFromNamespace(sourceCode, stringWriter, codeGeneratorOptions);
return code.ToString();
}
Wypisany na konsolę efekt wygląda całkiem zadowalająco:
namespace Some.Namespace
{
using System;
public class FooClass
{
public static string ZugZug(int blabla)
{
return blabla.ToString();
}
}
}
Co więcej, kod ten możemy uruchomić w trakcie działania programu .
var csc = newCSharpCodeProvider(newDictionary<string, string>() {{"CompilerVersion", "v3.5"}}); //tworzymy provider
var parameters = newCompilerParameters(new[] {"System.dll", "System.Core.dll"}, "Test.dll", true)
{
GenerateExecutable = false,
GenerateInMemory = true,
};
//oraz parametry kompilacji (ustawienie GenerateInMemory = false spowoduje zapisanie wyniku na dysk – póki co nie chcemy tego)
var results = csc.CompileAssemblyFromDom(parameters, new[] {compileUnit});
results.Errors.Cast<CompilerError>().ToList().ForEach(error => Console.WriteLine(error.ErrorText));
//ewentualne błędy wypisujemy na konsolę
var type = results.CompiledAssembly.GetType(codeNamespace.Name + "." + typeDeclaration.Name); //ze zbudowanego assembly pobieramy po nazwie nasz typ
var result = (string)type.InvokeMember("ZugZug", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new object[] {3});
// na koniec wywołujemy naszą metodę
Wypisując na konsolę zawartość zmiennej result dowiemy się, żę 3 to 3 ;).
W drugiej cześci pokażę, przykład modyfikacji kodu w trakcie działania.
Załączniki: