Probleemstelling
Tijdens een recentelijk pilot-project is een nieuwe service-interface ontworpen om informatie uit te wisselen tussen twee bedrijven. Een webservice buiten de firewall (in de demilitarized zone, DMZ) moest als cliënt een applicatieserver binnen de firewall aanroepen. De oplossing was in eerste instantie in Windows Communication Foundation (WCF) ontwikkeld. Hierin is een interfacedefinitie namelijk snel geschreven en dan moeten alleen nog de WCF attributen ServiceContract en OperationContract op de service-interface en de bijbehorende methodes geplaatst worden (kijk voor meer informatie over WCF op www.netfx3.com). Helaas bleek in een later, dat op de webserver maximaal het .Net 2.0 framework beschikbaar was en dus moesten we voor de communicatie uitwijken naar .Net Remoting.
Met .Net Remoting was eenzelfde oplossing met dezelfde methodes op een andere interfacedefinitie te bouwen. Maar één duidelijk verschil met WCF viel direct op: bij WCF maken de cliënt-proxy en de server van dezelfde service-interface gebruik, waardoor het in de WCF attributen nodig is om een referentie te leggen naar System.ServiceModel op de cliënt, de aanroepende partij. Nu lijkt dit op zich triviaal, deze assembly is namelijk gewoon geïnstalleerd met het .Net framework en dus altijd overal aanwezig. Maar opvallender is dat de gebruikte manier van communiceren zichtbaar wordt voor de aanroepende cliënt en dat het zelfs afdwingt, dat bepaalde gerefereerde assemblies geladen moeten worden. De WCF interfacedefinitie is dus een beetje opdringerig!
De WCF interfacedefinitie is dus een beetje opdringerig
Dit zou opgelost kunnen worden door gebruik te maken van een factory design pattern voor de communicatie. Design patterns zijn standaard oplossingen voor standaard problemen (zoals hier de abstractie van de communicatie) en binnen Atos Origin proberen wij altijd eerst terug te grijpen op deze bewezen oplossingen. (Meer weten over design patterns? www.dofactory.com)

Fig. 1: Class diagram van factory pattern: de twee interfacedefinities zijn helaas niet gelijk
Bij een factory design pattern wordt een instantie van de interface opgevraagd om bepaalde werkzaamheden uit te voeren, zonder kennis te nemen van de gemaakte keuze. De beslissing welke klasse wordt geïnstantieerd zal door de factory gemaakt worden. De factory kan bijvoorbeeld afhankelijk van het aanwezige .Net platform de keuze tussen .Net Remoting of WCF maken (zie figuur 1). Maar omdat de twee genoemde technieken ieder een eigen interfacedefinitie vereisen, met of zonder WCF attributen, moet hier een tweede design pattern toegepast worden.
Wrapper design pattern
In eerste instantie lijkt het niet mogelijk om WCF zonder de benodigde WCF attributen te laten communiceren. De WCF Proxy moet de benodigde kennis over de interface bezitten. Daarom is onderzocht of het mogelijk is om de cliënt WCF-proxy wel te blijven aanroepen, maar deze proxy ‘in te pakken’ met een andere interface. De WCF-proxy aanroep moet tenslotte ergens uitgevoerd worden. Omdat hiermee alle communicatietechnieken met een uniforme, abstracte interface zijn aan te roepen, kan de factory de meest optimale techniek toepassen zonder dat de aanroepende cliënt zelf kennis over de communicatie krijgt. De oplossing voor dit probleem kan uitgewerkt worden in het wrapper design pattern (ook wel adapter genoemd).
Dit wrapper design pattern wordt vaak toegepast, het is een fraaie manier om klassen samen te laten werken, die anders niet goed op elkaar aansluiten. De nieuwe interface wordt letterlijk als inpakpapier rond de aan te roepen logica gelegd (zie figuur 2).

Fig. 2: Class diagram van wrapper design pattern
De wrapper ondersteunt geheel of gedeeltelijk de logica van de ingepakte instantie, maar laat zich aanroepen met zijn eigen interface. De IWrapperInterface heeft hierbij geen enkele relatie met IWrappedInterface. IWrapperInterface hoeft dus niet eens alle members van IWrappedInterface te ondersteunen (zie listing 1). Het is nu ook mogelijk om bepaalde complexiteit van het ingepakte object bij het aanroepen te vereenvoudigen.
public class WrapperClass : IWrapperInterface
{
private IWrappedInterface _WrappedClass;
public IWrappedInterface WrappedClass
{
get
{
return _WrappedClass;
}
}
public WrapperClass(IWrappedInterface wrappedClass)
{
_WrappedClass = wrappedClass;
}
public string MethodOne(string parameter)
{
return _WrappedClass.MethodOne(parameter);
}
public int MethodTwo(int parameter)
{
return _WrappedClass.MethodTwo(parameter);
}
}
Listing 1: Een wrapper-implementatie met statische code
Bij het voorbeeld in listing 1 zijn de twee interfaces wel gelijkvormig en daardoor is de wrapper zo dun mogelijk te houden. Toch kan met een wrapper nog meer gedaan worden dan alleen maar het doorlussen van de methodes. Zo kan bijvoorbeeld aan iedere aanroep logging, timing of extreme foutafhandeling toegevoegd worden.
Het gebruik van reflection samen met dynamische code generatie kan uitkomst bieden
Maar inpakken zoals in het voorbeeld wordt al snel monnikenwerk vanwege het uittypen, vooral als het interface tijdens de ontwikkeling aan veel wijzigingen onderhevig is.
Uiteindelijk bleek het gebruik van reflection samen met dynamische code generatie uitkomst te bieden om deze wrappers runtime te genereren zonder steeds extra te moeten coderen.
Reflection en MSIL generatie
Met reflection wordt het mogelijk nieuwe types, compleet met methodes en logica, runtime in het geheugen te brengen als Microsofts Intermediate Language (MSIL). Dit is de ultieme just-in-time (JIT) codegeneratie, maar in de praktijk zie je het maar weinig toegepast worden. Enerzijds omdat de veilige wereld van de code-editor en de design-time compiler verlaten moet worden. Anderzijds is de leercurve hoog want het komt dicht bij het zelf op de stack zetten van attributen, het aanroepen van methodes om de stack uit te lezen en weer verder te vullen. (Kijk voor meer details op http://msdn2.microsoft.com/en-US/library/8ffc3x75(VS.80).aspx)
Wie ervaring heeft met assembler, beleeft hier het feest der herkenning. Toch is er een fundamenteel verschil met assembler. Ten eerste is er tegenwoordig IntelliSense en dat scheelt heel wat zoekwerk waar voorheen vaak een teksteditor het enige gereedschap was. Maar belangrijker is, dat deze MSIL wel degelijk typesafe is en blijft! Het is praktisch niet mogelijk om geheugenfouten te veroorzaken. De MSIL generator zal invalide opdrachten bij het samenstellen van het type gewoon afkeuren.
Het gebruik van reflection betekent wel, dat ingeleverd moet worden op performance. Runtime MSIL generatie is echt enkele factoren trager dan het gebruik maken van voorgecompileerde code. Toch kan de runtime gegenereerde code opgeslagen worden, zodat later de assembly hergebruikt kan worden. Deze code zal dan geen snelheidsverlies veroorzaken. Dit opslaan van de assembly zal verderop gedemonstreerd worden.
In System.Runtime.Remoting.Proxies wordt overigens de abstracte base class RealProxy aangeboden en deze geeft de mogelijkheid om twee interfaces op elkaar te mappen. Alle logica rond het inpakken wordt in één Invoke methode runtime uitgevoerd. Hierdoor is het geen vereiste dat de twee interfaces gelijkvormig zijn en is dus een andere mogelijke oplossing van de in dit artikel uitgewerkte oplossing. Wel zal de RealProxy altijd via reflection de mapping moeten uitvoeren en dus relatief trager blijven.
Runtime wrapper generatie
Onze wrapper gaat dus runtime twee willekeurige maar ‘gelijkvormige’ interfaces op elkaar laten aansluiten. Hiervoor is een static helper class geschreven om het gewenste type te genereren (zie listing 2). Om tot een dergelijke dynamische wrapper te komen moeten vier stappen doorlopen worden, waarna het gewenste type klaar voor gebruik is. Dus voor iedere gewenste wrapper tussen twee interfaces moet deze helper apart aangeroepen worden.
// Step 1: Generate the wrapper class type
TypeBuilder typeBuilder =
GenerateWrapperType(typeOfWrapperInterface);
// Remember the getter and setter
MethodBuilder methodBuilderGet;
MethodBuilder methodBuilderSet;
// Step 2: Generate the wrapped object property
GenerateWrappedObjectProperty(
typeBuilder, typeOfWrappedInterface,
out methodBuilderGet, out methodBuilderSet);
// Step3: Generate the constructor
GenerateConstructor(
typeBuilder, typeOfWrappedInterface, methodBuilderSet);
// Step 4: Generate all methods
GenerateWrappedMethodes(
typeBuilder, typeOfWrappedInterface, methodBuilderGet);
// Finally, build this wrapper type and return
return typeBuilder.CreateType();
Listing 2: De wrapper helper class aanroep met de vier stappen
De eerste stap is van administratieve aard. Types kunnen niet gegenereerd worden zonder er een assembly voor te definiëren. Ook moet een module verplicht aanwezig zijn; dat is niet gelijk aan een namespace maar geeft wel de mogelijkheid om types te groeperen. Pas daarna is het mogelijk om met de bouw van het eigenlijke type te starten. Geef hierbij aan de typebuilder door dat een class aangemaakt moet worden en geef ook de overerving op (zie listing 3). Hier zal een overerving van de class Object de IWrapperInterface definitie gaan implementeren.
// Define an assembly and a module
AssemblyBuilder assemblyBuilder =
AppDomain.CurrentDomain.DefineDynamicAssembly(
new AssemblyName(
typeOfWrapperInterface.Name + "ClassDll"),
AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder =
assemblyBuilder.DefineDynamicModule(
"Module" + typeOfWrapperInterface.Name);
// Finally, define the type of the wrapper
TypeBuilder typeBuilder =
moduleBuilder.DefineType(
typeOfWrapperInterface.Name + "Class",
TypeAttributes.Class
| TypeAttributes.Public,
typeof(object),
new Type[] { typeOfWrapperInterface });
return typeBuilder;
Listing 3: Beginnen met het definiëren van het wrapper type
De tweede stap bestaat uit het mogelijk maken van het intern onthouden van het object dat de IWrappedInterface implementeert (zie listing 4).
// The private field
FieldBuilder fieldBuilder =
typeBuilder.DefineField(
"_WrappedObject", typeOfWrappedInterface,
FieldAttributes.Private);
// The public property
PropertyBuilder propertyBuilder =
typeBuilder.DefineProperty(
"WrappedObject", PropertyAttributes.HasDefault,
typeOfWrappedInterface, null);
// Define the public "get" accessor.
methodBuilderGet =
typeBuilder.DefineMethod("getWrappedObject",
MethodAttributes.Public
| MethodAttributes.SpecialName
| MethodAttributes.HideBySig,
typeOfWrappedInterface, Type.EmptyTypes);
ILGenerator iLGeneratorGet =
methodBuilderGet.GetILGenerator();
// Put 'this' on the stack
iLGeneratorGet.Emit(OpCodes.Ldarg_0);
// Load the field on the stack
iLGeneratorGet.Emit(OpCodes.Ldfld, fieldBuilder);
// Ready and return
iLGeneratorGet.Emit(OpCodes.Ret);
// Define the private "set" accessor.
methodBuilderSet =
typeBuilder.DefineMethod("setWrappedObject",
MethodAttributes.Private
| MethodAttributes.SpecialName
| MethodAttributes.HideBySig,
null, new Type[] { typeOfWrappedInterface });
ILGenerator iLGeneratorSet =
methodBuilderSet.GetILGenerator();
// Put 'this' on the stack
iLGeneratorSet.Emit(OpCodes.Ldarg_0);
// Put 'value' passed on the stack
iLGeneratorSet.Emit(OpCodes.Ldarg_1);
// Put the 'value' passed in the private field
iLGeneratorSet.Emit(OpCodes.Stfld, fieldBuilder);
// Ready and return
iLGeneratorSet.Emit(OpCodes.Ret);
// Now inform the property to use this setter
// and getter methods
propertyBuilder.SetGetMethod(methodBuilderGet);
propertyBuilder.SetSetMethod(methodBuilderSet);
Listing 4: De private field en de read-only property
Eerst wordt een private field aangemaakt. Vervolgens wordt de publieke property gedefinieerd. Daarna worden de publieke getter en de private setter van de property gedefinieerd. Deze twee gedragen zich namelijk als methodes. In het algemeen voeren methodes code uit, dus die code moet ook gedefinieerd worden. Dit gebeurt met een ILGenerator en opcodes. Opcodes zijn de MSIL instructies, waarmee bijvoorbeeld doorgegeven parameters op de stack worden geplaatst en methodes worden uitgevoerd, die dan uiteraard van de stack lezen en er wellicht ook weer op terugschrijven (zie http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes_members.aspx voor details over de hier gebruikte opcodes).
Zoals wellicht bekend heeft een setter van een property een nogal magische ‘value’. Hier zie je dan ook hoe bij de setter methode gewoon al een waarde op de stack beschikbaar blijkt te zijn: Ldarg_1. De setter is dus inderdaad gewoon een methode. De getter en de setter worden teruggegeven aan de helper class, omdat we deze methodes later nog nodig hebben.
De derde stap is het aanmaken van de constructor. De property wordt namelijk afgeschermd voor overschrijven, omdat de wrapper toch als IWrapperInterface wordt aangeroepen en die interface definitie kent geen property van het type IWrappedInterface (zie listing 5).
De constructor ontvangt hierbij het ingepakte object als parameter en schrijft die simpelweg weg in het private field. Dit kan omdat een MethodBuilder een overerving is van een MethodInfo.
// Define the constructor parameters
Type[] constructorParameters =
new Type[] { wrappedInterfaceType };
// Define the constructor
ConstructorBuilder constructorBuilder =
builder.DefineConstructor(
MethodAttributes.Public
| MethodAttributes.RTSpecialName,
CallingConventions.Standard,
constructorParameters);
ILGenerator iLGenerator =
constructorBuilder.GetILGenerator();
// Put 'this' on the stack
iLGenerator.Emit(OpCodes.Ldarg_0);
// Put the wrapped object passed on the stack
iLGenerator.Emit(OpCodes.Ldarg_1);
// Call the setter of the property
iLGenerator.EmitCall(
OpCodes.Call, methodBuilderSet, null);
// Ready and return
iLGenerator.Emit(OpCodes.Ret);
Listing 5: De constructor vult de property
Als laatste stap moeten alle methodes van IWrappedInterface ondersteund gaan worden. Voor iedere te ondersteunen methode moeten alle door te geven parameters op de stack geplaatst gaan worden en daarna moet de methode van het ingepakte object aangeroepen worden om de logica uit te voeren (zie listing 6). Hiervoor maken we gebruik van de getter van de property. Een eventueel geretourneerde waarde uit de aanroep naar de methode van het ingepakte object wordt uiteindelijk gewoon weer terug op de stack gezet.
MethodInfo[] methodInfosWrappedInterface =
typeOfWrappedInterface.GetMethods();
foreach (MethodInfo methodInfoInterfaceType in
methodInfosWrappedInterface)
{
//Put the parameter types in an array
Type[] parameterTypes =
new Type[
methodInfoInterfaceType.GetParameters().Length];
ParameterInfo[] pia =
methodInfoInterfaceType.GetParameters();
for (int i = 0; i < pia.Length; i++)
{
parameterTypes[i] = pia[i].ParameterType;
}
// Define the wrapper method for the wrapped method
MethodBuilder methodBuilder =
typeBuilder.DefineMethod(
methodInfoInterfaceType.Name,
MethodAttributes.Public
| MethodAttributes.Virtual
| MethodAttributes.NewSlot,
methodInfoInterfaceType.ReturnType,
parameterTypes);
ILGenerator iLGenerator =
methodBuilder.GetILGenerator();
// Put 'this' on the stack
iLGenerator.Emit(OpCodes.Ldarg_0);
// Load the field on the stack
iLGenerator.EmitCall(
OpCodes.Call, methodBuilderGet, null);
// Put every passed parameter on the stack
for (int j = 0; j < pia.Length; j++)
{
iLGenerator.Emit(OpCodes.Ldarg, j + 1);
}
// Call the method of the wrapped object
iLGenerator.Emit(OpCodes.Callvirt,
methodInfoInterfaceType);
// Ready and return
iLGenerator.Emit(OpCodes.Ret);
}
Listing 6: Alle methodes overnemen en doorlussen
De MSIL generatie is afgerond. Kijk nu nog eens naar listing 2. Helemaal onderaan wordt vanuit de typebuilder het type gecreëerd. Mocht er zich nu ergens een probleem voor doen in de typebuilder (bijvoorbeeld een methode zonder voldoende of verkeerde parameters op de stack), dan zal hier een exception optreden. Als hier niet wordt geklaagd, dan hebben we nu eindelijk het gewenste type van de wrapper class.
Om dit gegenereerde type te kunnen gebruiken zal dus de wrapper class geïnstantieerd worden, waarbij het ingepakte object aan de constructor meegegeven wordt (zie listing 7). Het gegenereerde type wordt dus via een activator in een runtime object omgezet, welke de IWrappedInterface ondersteunt.
// Make an instance of the wrapped class
IWrappedInterface wrappedclass = new WrappedClass();
// Create the type of the wrapper
Type wrapperType =
WrapperHelper.CreateWrapperType(
typeof(IWrapperInterface),
typeof(IWrappedInterface));
// Make an instance of the type of the wrapper
IWrapperInterface wrapper =
(IWrapperInterface)Activator.CreateInstance(
wrapperType, new object[] { wrappedclass });
// Call the wrapper instance
string returnValue = wrapper. MethodOne("It is a wrap!");
Console.WriteLine(returnValue);
Listing 7: Wrapper creëren en aanroepen
Zelfreflectie voor de wrapper
Met het gebruik van de wrapper is aangetoond, dat de wrapper-helper werkelijk iedere willekeurige interfacedefinitie met een andere interface definitie kan inpakken.
Het is mogelijk om te controleren wat nu runtime gegenereerd wordt door de gegeneerde assembly op te slaan. Verander hiervoor de code in listing 3. De aanroep van DefineDynamicAssembly moet AssemblyBuilderAccess.RunAndSave meekrijgen zodat de assembly op schijf vastgelegd kan worden en de aanroep DefineDynamicModule moet een bestandsnaam meekrijgen (“bestandsnaam.dll”). Tevens moet de assemblyBuilder als out parameter doorgegeven worden, want die wordt na het uiteindelijke samenstellen van het type (via TypeBuilder.CreateType) aangeroepen met het veelzeggende assemblyBuilder.Save(“bestandsnaam.dll”).
De runtime opgeslagen assembly is later gewoon te hergebruiken
De runtime opgeslagen assembly is later gewoon te hergebruiken. Hierdoor is enigszins de grootste tekortkoming van het runtime genereren van code teniet gedaan, namelijk het relatief trage genereren. Open met bijvoorbeeld ILDasm de gegenereerde code en herken hier de code die zojuist met de ILGenerator samengesteld is (zie listing 8).
.method public newslot virtual instance
string MethodOne(string A_1) cil managed
{
// Code size 18 (0x12)
.maxstack 2
IL_0000: ldarg.0
IL_0001: call instance class
[ReflectionDocumentClassLibrary]
ReflectionDocumentClassLibrary.
IWrappedInterface
IWrapperInterfaceClass::
getWrappedObject()
IL_0006: ldarg A_1
IL_000a: nop
IL_000b: nop
IL_000c: callvirt instance string
[ReflectionDocumentClassLibrary]
ReflectionDocumentClassLibrary.
IWrappedInterface::MethodOne(string)
IL_0011: ret
}
Listing 8: MethodOne gezien door de ogen van IlDasm
Het is zeker de moeite waard om dezelfde assembly ook eens met Reflector van Lutz Roeder te openen (http://www.lutzroeder.com/) Met deze tool is het mogelijk om MSIL code naar andere .Net talen (C#, VB.Net, Chrome, Delphi, etc) te ‘reflecteren’. Via de plug-in techniek van .Net Reflector wordt deze lijst van talen regelmatig uitgebreid en zo is het inmiddels ook mogelijk om de MSIL code om te zetten naar broncode zoals in listing 2 t/m 6 (research.microsoft.com/~jhalleux/ - ReflectionEmitLanguage).
Deze wrapper-helper is getest met het doorgeven van verschillende types. Het heeft geen moeite met value types, reference types, generic types, nullable types, etc. Ook kan voldoende omgegaan worden met exceptions vanuit de ingepakte klasse. Het feit dat de twee interface-definities ook echt op elkaar moeten aansluiten, wordt nog niet afgedwongen door deze te vergelijken. Ook wordt niet op een Null referentie getest voor het ingepakte object. Dit voorbeeld kan naar eigen inzicht aangepast gaan worden en het uitbreiden met bijvoorbeeld logging van de aanroep met alle parameters erbij spreekt dan ook tot de verbeelding.
Conclusie
Voor mij was het bouwen van een dynamische wrapper een aangename kennismaking met MSIL. Ik heb gemerkt dat de leercurve eerst redelijk steil is, maar als je eenmaal bezig bent, valt alles redelijk te begrijpen. Een goede kennis van het .Net framework is wel een vereiste. MSIL generatie is ontzettend krachtig. Het wordt mogelijk om zaken die normaal private gedefinieerd zijn uit te lezen en hierdoor zijn heel krachtige oplossingen te bouwen die de compiler normaal gesproken niet toelaat. Met de getoonde tools kan ook goed bekeken worden, hoe bepaalde code-constructies in MSIL gerepresenteerd worden. Dit is handig, want goede documentatie is schaars. Veel succes!
Links:
De sources die bij dit artikel horen kun je downloaden via Velde_ReflectionInpakpapier_SRC.zip.