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:
http://www.100procentjan.nl/tweakers/postsharp.png