Synthesized proxies
February 05, 2006 13:58Last time we saw a first solution to the problem of adding synchronization contracts to an arbitrary .NET class. The solution based on Context Attributes and Transparent Proxy had some limitations (performance, no access to class members), so I designed a tool based completely on SRE (System.Reflection.Emit) and System.Reflection. It can be thought of as a post-compiler that analyzes an assembly, custom attributes placed on classes and function, and write a small assembly that contains a proxy to the original one.
The proxy is written using metadata information of the real target object. We use facilities provided by System.Reflection in order to read the interface methods, the custom attributes and the fields and properties of the target object, and System.Reflection.Emit to emit code for the guarded methods and forwarders for the other public functions. The methods in the proxy validate the contract and update the state for subsequent validations, all in a automatic way.
The process to create a guarded component is the following:
- write the class as usual, then add the synchronization contract writing guards for each method using the Guard attribute and Past LTL logic;
- add a reference to the Attribute.dll assembly -the assembly containing the class implementing the Guard attribute- and compile your component. Note that the attribute can be used with every .NET language that supports custom attributes, like C#, Visual Basic, MC++, Delphi, and so on;
- the .NET compiler will store the Guard attribute and its fields and values with the metadata associated to the method.
public class Test
{
bool g;
bool f = true;
[Guard("H (g or f)")] //using constructor
public string m(int i, out int j)
{
j = i;
return (i + 2).ToString();
}
[Guard(Formula = "H (g or f)")] //using public field
public string m(int i, out int j)
{
j = i;
return (i + 2).ToString();
}
}
The component is then passed through the post-compiler, that will post-process the assembly performing the following steps:
- it walks the metadata of the given assembly, finding all the classes;
- for each class it walks its methods, searching for the Guard attribute. If the attribute is found, the class is marked as forwarded;
- it generates a new empty assembly;
- for each forwarded class, it generates a class with the same public interface as the original one:
- public, non-guarded methods are wrapped by the IL code necessary to perform the synchronization (acquiring the object-wide lock before forwarding the call to the original method and releasing it just after the method return)
- public and private guarded methods are wrapped by the IL code necessary to perform the synchronization and conditional access, like shown here:
.method public instance void m() cil managed { .maxstack 5 IL_0000: ldarg.0 IL_0001: call void [mscorlib]Monitor::Enter(object) .try { //check IL_0006: br.s IL_0008 IL_0008: ldarg.0 IL_0009: call bool [mscorlib]Monitor::Wait(object) IL_000e: pop IL_000f: ldfld bool Test::pre1 IL_0010: brfalse IL_0008 //update IL_0012: ldc.i4.0 //"false" IL_0013: ldfld bool Test::a IL_0015: ceq IL_0018: stfld bool Test::pre2 IL_001e: ldc.i4.0 //"false" IL_0021: stfld bool Test::pre1 //forward IL_0026: ldarg.0 IL_0027: ldfld class [TestLibrary]Test::GuardedTestLibrary.dll IL_0031: call instance void [TestLibrary]Test::m() IL_0036: leave IL_0048 } // end .try finally { IL_003b: ldarg.0 IL_003c: call void [mscorlib]System.Threading.Monitor::PulseAll(object) IL_0041: ldarg.0 IL_0042: call void [mscorlib]System.Threading.Monitor::Exit(object) IL_0047: endfinally } // end handler IL_0048: ret } // end of method Test::m
- for each forwarded class, it generates constructors with the same signature that calls the ones of the original class.
A schema for the usage of the generated proxy
The generated assembly and its classes are saved in a separate dll, with the same name prefixed by "Guarded". This assembly should be referenced by the client applications in place of the original one, as shown in the Figure.