MSIL injection met PostSharp
This post was originally published on Coding Glamour.
Wanneer je manager een nieuwe techniek ten strengste verbied met als argument
'we snappen je code normaal al niet, dit maakt het alleen maar erger'
weet je dat je goud in handen hebt. Voor een eigen projectje om vals te spelen met Football Manager
(waar ik later absoluut nog eens terug kom) had ik globaal de volgende
situatie:
public byte CurrentAbility
{
get {
if (_mode == DbMode.Cached) {
// in cached mode, hebben we een byte-array met alle waardes
return _bytes[Offsets.CurrentAbility];
} else {
// anders lezen we via een helper-method
return ProcessManager.ReadByte(_address + Offsets.CurrentAbility);
}
}
set {
// zelfde soort code voor de setter
}
}
Leuk, neat, en vrij goed te lezen; probleem alleen dat ik een paar honderd
properties heb, met een stuk of zeven verschillende types. Te veel werk.
En aangezien je in eigen projecten toch helemaal los mocht gaan, leek een
oplossing op basis van AOP me veel leuker. Nieuwe situatie:
[FMEntity(Offsets.CurrentAbility)]
public byte CurrentAbility { get; set; }
Bovenstaande is best eenvoudig werkend te krijgen met PostSharp, een framework voor Aspect Oriented Programming in .NET. Een eenvoudige implementatie
van bovenstaande is iets als:
public class FMEntityAttribute : LocationInterceptionAspect
{
public FMEntityAttribute (int offset) {
// doe wat
}
public override void OnGetValue( LocationInterceptionArgs args ) {
if (args.Instance is byte) {
// doe byte lezen enzo
}
}
public override void OnSetValue( LocationInterceptionArgs args ) {
// ongeveer hetzelfde
}
}
Je kunt nu alle logica die toch steeds hetzelfde is, eenvoudig webabstraheren
in een aparte file. Maar... té traag. In mijn geval werd het bepalen
van de rating voor spelers ruim tien keer zo traag; door alle overhead.
Oplossing? Zelf MSIL injecten!
MSIL?
MSIL, de immediate language van Microsoft (vergelijkbaar met Java's
bytecode) is een stack-based taal die uiteindelijk wordt uitgepoept als
de compiler je C# code compileert. Shameless kopie van Wikipedia:
int r = Foo.Add(2, 3);
wordt:
ldc.i4.2 // laadt een Int32 met waarde 2 op de stack
ldc.i4.3 // laadt een Int32 met waarde 3 op de stack
call int32 Foo::Add(int32, int32) // roep Int32.Add aan, met de waardes op de stack als params
// de functie schrijft zelf de retval op de stack
stloc.0 // lees return value van de stack, en sla op in local var op positie 0
Voor meer info hierover, zie dit artikel op CodeGuru.
MSIL injection?
Eén van de leukste functies van PostSharp is, is dat het op basis
van de attributes die je set, en de implementatie die je daarna schrijft
direct ná compilatie extra code aan je assembly toevoegd. De AOP code
zit dus in je DLL geweven. Het mooie hieraan is, is dat je ook zelf extra
code kan toevoegen via PostSharp. Hiermee ben je dus niet gebonden aan
de (trage) versie die PostSharp je aanbiedt.
Uh dus?
Als basis heb ik de volgende helper-functie gemaakt: PropertyInvoker.cs. Deze moet vanuit elke property worden
aangeroepen:
[FMEntity(Offset.CurrentAbility)]
public byte CurrentAbility { get;set;}
// wordt:
public byte CurrentAbility {
// Get<T>(int offset, byte[] bytes, int baseAddress, DatabaseMode mode)
get { return PropertyInvoker.Get<byte>(Offset.CurrentAbility, _bytes, address, _mode); }
// Set<T>(int offset, int baseAddress, T newValue, DatabaseMode mode)
set { PropertyInvoker.Set<byte>(Offset.CurrentAbility, address, value, _mode; }
}
Ergo: we moeten de implementatie van onze property on the fly gaan veranderen.
Yay!
Get it started
We beginnen met het maken van een Task, waarin we aangeven op welk 'attribute'
we werken: in dit geval 'FMEntityAttribute'. Hierna kunnen we
een Advice schrijven, waarin we de daadwerkelijke implementatie
doen.
Weave
De 'weave' method is het hart van het 'Advice'. Deze
wordt aangeroepen voor elke property waarop we ons attribute hebben gezet.
public void Weave(WeavingContext context, InstructionBlock block)
{
// parent.Project.Module is de class-instance
// we gaan eerst zoeken naar de velden die we nodig hebben
// dat zijn
// 'OriginalBytes' (byte[])
// 'MemoryAddress' (int32)
// 'DatabaseMode' (DatabaseMode)
bytesFieldDef = parent.Project.Module.FindField(typeof(Player)
.GetField("OriginalBytes"), BindingOptions.Default).GetFieldDefinition();
memAddressFieldDef = parent.Project.Module.FindField(typeof(Player)
.GetField("MemoryAddress"), BindingOptions.Default).GetFieldDefinition();
databaseModeFieldDef = parent.Project.Module.FindField(typeof(Player)
.GetField("DatabaseMode"), BindingOptions.Default).GetFieldDefinition();
// nu gaan we op basis van de return-type, bepalen of we een getter of een setter hebben
if (context.Method.ReturnParameter.ParameterType.GetSystemType(null, null) != typeof(void))
{
this.WeaveGetter(context, block);
}
else
{
this.WeaveSetter(context, block);
}
}
WeaveGetter
In de 'WeaveGetter' kunnen we nu de MSIL gaan schrijven om de
implementatie van de 'Get' te vervangen:
private void WeaveGetter(WeavingContext context, InstructionBlock block)
{
// we gaan 'voor' de huidige implementatie schrijven
InstructionSequence innerBodySequence = context.Method.MethodBody.CreateInstructionSequence();
block.AddInstructionSequence(innerBodySequence, NodePosition.Before, null);
// instructionWriter is waar je je MSIL op kan kloppen
context.InstructionWriter.AttachInstructionSequence(innerBodySequence);
// zoek PropertyInvoker.Get op, met als <T> het huidige type
// in dit geval dus Get<byte>
MethodBase m = typeof(PropertyInvoker).GetMethod("Get").MakeGenericMethod(context.Method.ReturnParameter.ParameterType.GetSystemType(null, null));
IMethod propertyInvokerGet = parent.Project.Module.FindMethod(m, BindingOptions.RequireGenericInstance);
// zet 'Offset', de waarde die je meegeeft in de attribute op de stack
context.InstructionWriter.EmitInstructionInt32(OpCodeNumber.Ldc_I4, this.attribute.Offset);
// zet nu ldarg.0 op de stack (je huidige instance)
context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
// en lees hier het veld 'bytes' vanaf; zet deze ook op de stack
context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, bytesFieldDef);
// zelfde als hierboven
context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
// en zet het veld 'memAddress' op de stack
context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, memAddressFieldDef);
context.InstructionWriter.EmitInstruction(OpCodeNumber.Ldarg_0);
// zet 'databaseMode' op de stack
context.InstructionWriter.EmitInstructionField(OpCodeNumber.Ldfld, databaseModeFieldDef);
// stack bevat nu:
// 1. de waarde die je meegeeft aan attribute
// 2. field 'OriginalBytes'
// 3. field 'MemoryAddress'
// 4. field 'DatabaseMode'
// we roepen nu de .Get<T> aan, met de bovenstaande waardes als argumenten
context.InstructionWriter.EmitInstructionMethod(OpCodeNumber.Call, propertyInvokerGet);
// in MSIL wordt de geretouneerde waarde nu op de stack gezet
// als we nu dus 'return' doen, wordt deze waarde ge-'popt' van de stack
// en teruggegeven
context.InstructionWriter.EmitInstruction(OpCodeNumber.Ret);
context.InstructionWriter.DetachInstructionSequence();
}
Registreren in PostSharp
Om ervoor te zorgen dat PostSharp deze code uitvoert, moet je een 'psplugin'
schrijven. Deze hoeft niet ingewikkeld te zijn. Zie als voorbeeld hier.
Et voila
Na het builden van je DLL kan je deze openen met Reflector, en zien dat de inner-method is veranderd:
There are 4 comments on this article, read them on Coding Glamour.