W poprzednim poście wspomniałem o mojej walce w Mono Cecil, dzisiaj chciałbym się podzielić moimi wrażeniami i doświadczeniem. Dodam tylko, że o Mono usłyszałem na spotkaniach wrocławskiej grupy .net, wykład prowadził Paweł Łukasik, slajdy z wykładu dostępne są na jego blogu: http://pawlos.blogspot.com. Jak zwykle zapraszam na spotkania i wykłady.
Źródła które pokaże, są tylko prostym przykładem, zamysłem tego co chciałem zrobić w docelowym rozwiązaniu. Powinny jednak wystarczyć by ukazać jak działa Mono Cecil.
Sztuczka miała polegać na dodaniu funkcjonalności do istniejącej już aplikacji; jestem leniwy i nie chce klikać myszą w guziczki góra, dół, prawo i lewo, chciałbym mieć możliwość nawigowania za pomocą strzałek na klawiaturze. Rozwiązania są dwa, można napisać taką implementacji od zera w IL i doklejeniu jej do docelowej aplikacji. Druga możliwość, to skopiowanie gotowej implementacji z innej aplikacji. Wszyscy jesteśmy leniwi, tak więc rozwiązanie drugie było bardziej kuszące.
Cały projekt jest dostępy na git hubie dostępy dla każdego.
Zabrałem się więc do roboty i oto do czego doszedłem:
Kod wykonujący brudną robotę:
- using System.Linq;
- namespace mcWorker
- {
- using Mono.Cecil;
- using Mono.Cecil.Cil;
- class Program
- {
- static void Main(string[] args)
- {
- string sourceExe = @”contentSourceForm.exe”;
- string targetExe = @”contentTargetForm.exe”;
- string srcMethod1 = “Form1_KeyDown”;
- string srcMethod2 = “Form1_KeyUp”;
- // Source of code
- AssemblyDefinition adSrc = AssemblyDefinition.ReadAssembly(sourceExe);
- TypeDefinition typeSrc = adSrc.MainModule.Types
- .Where(t => t.Name == “SourceForm”).First();
- // Place where code will be added
- AssemblyDefinition adDst = AssemblyDefinition.ReadAssembly(targetExe);
- TypeDefinition typeDst = adDst.MainModule.Types
- .Where(t => t.Name == “TargetForm”).First();
- // Copy both methods from src to dst
- MethodDefinition m1 = CopyMethod(adSrc, typeSrc, srcMethod1, adDst, typeDst);
- MethodDefinition m2 = CopyMethod(adSrc, typeSrc, srcMethod2, adDst, typeDst);
- // Now they should be marked as event handlers
- AddEventHandlers(adDst, typeDst, “KeyDown”, m1);
- AddEventHandlers(adDst, typeDst, “KeyUp”, m2);
- adDst.Write(@”newTarget.exe”);
- }
- private static void AddEventHandlers(AssemblyDefinition adDestination,
- TypeDefinition typeDestination,
- string aEventName,
- MethodDefinition aEventHandler)
- {
- // For the simpliciyty of code the event handler will be connected
- // to the events in default constructors
- // Just before leaving it
- // I know that there is just one constructor, but this is only an example!
- var ctor = typeDestination.Methods.Where(m => m.IsConstructor).First();
- // Find last return code
- // We will put our code just before that opcode
- var lastRet = ctor.Body.Instructions.Reverse()
- .Where(i => i.OpCode == OpCodes.Ret).First();
- // Now we need an IL generator
- var ilg = ctor.Body.GetILProcessor();
- // and now the magic
- ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldarg_0));
- ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldarg_0));
- ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldftn, aEventHandler));
- // I did check here also that there is only one construcor
- ilg.InsertBefore(
- lastRet,
- Instruction.Create(
- OpCodes.Newobj,
- adDestination.MainModule
- .Import(typeof(System.Windows.Forms.KeyEventHandler)
- .GetConstructors().First())));
- ilg.InsertBefore(
- lastRet,
- Instruction.Create(
- OpCodes.Callvirt,
- adDestination.MainModule
- .Import(typeof(System.Windows.Forms.Control)
- .GetEvent(aEventName).GetAddMethod())));
- }
- private static MethodDefinition CopyMethod(AssemblyDefinition adSource,
- TypeDefinition typeSource,
- string mthdName,
- AssemblyDefinition adDestination,
- TypeDefinition typeDestination)
- {
- // source
- MethodDefinition srcMethod = typeSource.Methods
- .Where(m => m.Name == mthdName).First();
- // now create a new place holder for copy
- MethodDefinition target = new MethodDefinition(srcMethod.Name,
- srcMethod.Attributes,
- adDestination.MainModule
- .Import(srcMethod.ReturnType));
- // Copy all method parameters
- // I could use var, but I did this on purpose to show the type used.
- foreach (ParameterDefinition pd in srcMethod.Parameters)
- {
- target.Parameters.Add(
- new ParameterDefinition(pd.Name, pd.Attributes, adDestination.MainModule
- .Import(pd.ParameterType)));
- }
- // Now copy all local variables that are defined withing method body
- // I could use var, but I did this on purpose to show the type used.
- foreach (VariableDefinition vd in srcMethod.Body.Variables)
- {
- target.Body.Variables
- .Add(new VariableDefinition(adDestination.MainModule
- .Import(vd.VariableType)));
- }
- // copy the state
- target.Body.InitLocals = srcMethod.Body.InitLocals;
- /* copy all instructions from SRC to DST */
- foreach (Instruction instruction in srcMethod.Body.Instructions)
- {
- // Case when method call another method defined withing SRC type/assembly
- MethodReference mr = instruction.Operand as MethodReference;
- // Case when method load field from type/assembly
- FieldReference fr = instruction.Operand as FieldReference;
- TypeReference tr = instruction.Operand as TypeReference;
- if (mr != null)
- {
- if (mr.DeclaringType == typeSource)
- {
- // That would mean that here we have a
- // method call to method within source type
- // And this need to be redirected to source type
- // or handled in some other way
- // But in this example is not used
- // If you want some examples please contace me
- }
- else
- {
- target.Body.Instructions.Add(
- Instruction.Create(instruction.OpCode,
- adDestination.MainModule.Import(mr)));
- }
- }
- else
- {
- if (fr != null)
- {
- // So we migth found our selfs in position that we need
- // to redirect this load to some other field or remove it.
- // For now lets redirect for different field
- // Please try to remove the code between TRY ME
- // and check what peverify.exe will tell
- /*TRY ME*/
- if (fr.Name == “sourceStatus”)
- {
- target.Body.Instructions.Add(
- Instruction.Create(
- instruction.OpCode,
- adDestination.MainModule.Import(typeDestination.Fields
- .Where(f => f.Name == “targetStatus”).First())));
- }
- else/*TRY ME*/
- {
- target.Body.Instructions.Add(Instruction
- .Create(instruction.OpCode, adDestination.MainModule.Import(fr)));
- }
- }
- else if (tr != null)
- {
- target.Body.Instructions.Add(Instruction
- .Create(instruction.OpCode, adDestination.MainModule.Import(tr)));
- }
- else
- {
- target.Body.Instructions.Add(instruction);
- }
- } // else
- } // foreach
- typeDestination.Methods.Add(target);
- return target;
- }
- }
- }
Źródło z którego chce wziąć kod (zależy mi na obsłudze klawiszy).
- using System.Windows.Forms;
- namespace SourceForm
- {
- public partial class SourceForm : Form
- {
- public SourceForm()
- {
- InitializeComponent();
- }
- private void Form1_KeyDown(object sender, KeyEventArgs e)
- {
- if (e.KeyCode == Keys.Escape)
- {
- this.sourceStatus.Text = string.Format(“I did forget to mention that {0} ends the game.”, e.KeyCode.ToString());
- }
- else
- {
- this.sourceStatus.Text = string.Format(“Key {0} (down).”, e.KeyCode.ToString());
- }
- }
- private void Form1_KeyUp(object sender, KeyEventArgs e)
- {
- if (e.KeyCode == Keys.Escape)
- {
- this.Close();
- }
- else
- {
- this.sourceStatus.Text = string.Format(“Key {0} (up).”, e.KeyCode.ToString());
- }
- }
- private void Form1_MouseDown(object sender, MouseEventArgs e)
- {
- this.sourceStatus.Text = string.Format(“Mouse: {0} down”, e.Button.ToString());
- }
- private void Form1_MouseUp(object sender, MouseEventArgs e)
- {
- this.sourceStatus.Text = string.Format(“Mouse: {0} up”, e.Button.ToString());
- }
- }
- }
Tak wygląda implementacja, którą chce rozszerzyć:
- using System.Windows.Forms;
- namespace TargetForm
- {
- public partial class TargetForm : Form
- {
- public TargetForm()
- {
- InitializeComponent();
- }
- private void TargetForm_MouseDown(object sender, MouseEventArgs e)
- {
- this.targetStatus.Text = string.Format(“down: {0}”, e.Button.ToString());
- }
- private void TargetForm_MouseUp(object sender, MouseEventArgs e)
- {
- this.targetStatus.Text = string.Format(“up: {0}”, e.Button.ToString());
- }
- }
- }
Komentarze w kodzie 🙂 Mam nadzieję że są w miarę zrozumiałe, jeżeli będzie coś niejasnego, zawsze służę pomocą.
Co ciekawe, warto spojrzeć (w źródła), że pola sourceStatus oraz targetStatus nie są tego samego typu, ale oba posiadają te same pola (Text) i oba dziedziczą po Control, dzięki temu pięknie zadziałał polimorfizm.
Oczywiste oczywistości:
- Kod który podłącza event handlery do eventów nie wymyśliłem sam, z pomocą przyszedł reflektor. Zobaczyłem (czytaj skopiowałem) kod do obsługi myszy i wstawiłem analogiczny do obsługi klawiatury
- Za pierwszym razem też wydawało mi się to strasznie zakręcone i okrutnie trudne, ale po trzecim podejściu do problemu, wszystko nabiera sensu. W sumie nawet fajnie się przegląda IL 😉 żarcik taki.
- Nie wszystko się od razu udaje, przykład z TRY ME, za pierwszym razem (w trzecim podejściu) zapomniałem o tym i coś nie zadziałało. Na szczęście narzędzie peverify.exe potrafi o tym przypomnieć. Także możliwość debugowania dużo ułatwia, gdy można podejrzeć dokładnie wartości zmiennych i w razie potrzeby dla testów zmieniać jest w trakcie działania programu.
- Google – szukajcie rozwiązań, jest spora szansa, że ktoś już miał problem podobny do waszego i został on rozwiązany. Jeżeli nie, to być może naprowadzi was na rozwiązanie waszego problemu. Nie warto odpuszczać, bo na pewno jakoś się da 🙂
Pokazany przykład jest prostym rozwiązaniem, a kod który przenosi funkcjonalność z aplikacji do aplikacji nie jest najbardziej rozbudowany i przemyślany. W przypadku, gdy pojawiają się dodatkowe wywołania metod w źródłowym assembly, zaczyna komplikować wszystko, trzeba sprawdzać nazwy metod, przypisywać referencję na docelowe assembly, pilnować typów, etc. Trzeba się bardziej nagimnastykować. Tutaj akurat nie chciałem się na tym skupiać.
Jeżeli coś nie działa sprawdzajcie reflektorem czy innym programem do podglądania kodu, czy nie zapomnieliście za importować gdzieś typu, lub czy wywołania się zgadzają. Czy wszystkie zmienne zostały za deklarowane w ciele (body) metody. Jeszcze raz przypominam o narzędziu peverify, które wskazuje co i gdzie jest nie tak.
Powodzenia i niech moc będzie z wami.
Jarek
//EDIT
Klient nasz pannn. Poprawiłem główny kod, teraz powinno być łatwiej go czytać. Ale nie chciało mi się tego robić dla form. Tam zresztą nie ma wiele ciekawego do oglądania.
A dałoby się połamać linie w kodzie? tak aby nie trzeba było używać scroll'a poziomego ? 🙂
Troszku wygodniej by się analizowało kod, oczywiście ja nie narzekam 😉
Zrobiłem co mogłem 🙂