Tuesday, May 31, 2011

CodeDOM and poor man's lambdas -- Part 2

Unfortunately there is one major sticking point in getting the generation to work for F# -- local variables are declared as let mutable rather than as a ref type which means that the necessary closure cannot be made; this is rather stronger than the lack of support for CodeDefaultValueExpression, which can be fudged around, or for nested types (which just become mutually recursive), though with up-front decision as to the language to generate (rather than making the choice following the expression tree), we could replace problematic elements with snippets.

That aside, the main operation looks like this, similar to the previous examples

let GenerateWrapper (target:Type) =
// Create a compile unit with a namespace
let compileUnit = new CodeCompileUnit()
let scope = new CodeNamespace("Derived")
compileUnit.Namespaces.Add( scope ) |> ignore
// Create a GeneratedCodeAttribute expression
let generated = typeof<GeneratedCodeAttribute>
let ga = new CodeAttributeDeclaration(TypeRef generated)
ga.Arguments.Add(new CodeAttributeArgument( new CodePrimitiveExpression("Generator"))) |> ignore
ga.Arguments.Add(new CodeAttributeArgument( new CodePrimitiveExpression("0.0.0.0"))) |> ignore
// Create the wrapper type and attribute it
let wrapper = new CodeTypeDeclaration(target.Name + "Wrapper")
wrapper.TypeAttributes <- TypeAttributes.Public ||| TypeAttributes.Sealed
wrapper.CustomAttributes.Add(ga) |> ignore
scope.Types.Add(wrapper) |> ignore
// Add a field for the wrapped object
let this = new CodeMemberField(TypeRef target, "this")
wrapper.Members.Add(this) |> ignore
// Create a simple constructor to set the wrapped object
let construct = new CodeConstructor()
construct.Parameters.Add(new CodeParameterDeclarationExpression(TypeRef target, target.Name)) |> ignore
let self = new CodeThisReferenceExpression()
let atThis = new CodeFieldReferenceExpression(self, "this")
let assign = new CodeAssignStatement(atThis, new CodeArgumentReferenceExpression(target.Name))
construct.Statements.Add(assign) |> ignore
wrapper.Members.Add(construct) |> ignore
// Create a set to hold namespaces
let namespaces = new HashSet<string>()
// Get all the methods of interest (properties would be similar)
let methods = target.GetMethods(BindingFlags.Instance ||| BindingFlags.Public ||| BindingFlags.DeclaredOnly)
|> Seq.filter (fun x -> not x.IsSpecialName )
|> Seq.sortBy (fun p -> p.Name)
// Accumulate all referenced namespaces (aside from generic types)
methods
|> Seq.iter (fun x -> namespaces.Add(x.ReturnType.Namespace) |> ignore)
methods
|> Seq.collect (fun m -> m.GetParameters())
|> Seq.iter (fun x -> namespaces.Add(x.ParameterType.Namespace) |> ignore)
// Generate a proxy class for each call
methods
|> Seq.map (GenerateClass target ga)
|> Seq.map wrapper.Members.Add
|> Seq.iter ignore
// Generate a delegating method for each call
methods
|> Seq.map (GenerateProxy target)
|> Seq.map wrapper.Members.Add
|> Seq.iter ignore
// Add in all the namespaces
namespaces.Add(target.Namespace) |> ignore
namespaces.Add(generated.Namespace) |> ignore
namespaces
|> Seq.sort
|> Seq.iter (fun n-> scope.Imports.Add(new CodeNamespaceImport(n)))
compileUnit
view raw gistfile1.fs hosted with ❤ by GitHub

If we eschewed F# support entirely, and used a partial class, then we could skip the constructor and field declarations and provide the input by any other mechanism of our choice in a hand-written part.

Generating the closure classes is a simple matter -- especially as the names of the parameters can be used as fields directly without any sigils, provided that they never contain the @this or proxy name by convention. This makes the closure class approach slightly simpler than a snippet driven use of direct lambda syntax, where local variable names to hold out parameters would in general have to be generated so as not to clash with any of the arguments:

let GenerateClass (target:Type) (ga:CodeAttributeDeclaration)(m:MethodInfo) =
// Generate the type and mark as generated
let newtype = new CodeTypeDeclaration(m.Name + "__Proxy")
newtype.TypeAttributes <- TypeAttributes.NestedPrivate /// .NotPublic ||| TypeAttributes.Sealed
newtype.CustomAttributes.Add(ga) |> ignore
// add a field to point to the wrapped object
let this = new CodeMemberField(TypeRef target, "this")
this.Attributes <- MemberAttributes.Public
newtype.Members.Add(this) |> ignore
// Fields to allow us to close over all the parameters
m.GetParameters()
|> Seq.map GenerateField
|> Seq.map newtype.Members.Add
|> Seq.iter ignore
// The lambda method
let proxy = new CodeMemberMethod()
proxy.Name <- m.Name
proxy.Attributes <- MemberAttributes.Public ||| MemberAttributes.Final
// Fudge the case of a void method
proxy.ReturnType <- TypeRef(m.ReturnType)
if m.ReturnType = typeof<System.Void> then proxy.ReturnType <- TypeRef(typeof<int>)
// Build the call to the wrapped object
let self = new CodeThisReferenceExpression()
let atThis = new CodeFieldReferenceExpression(self, "this")
let call = new CodeMethodReferenceExpression(atThis, m.Name)
let parameters = m.GetParameters()
|> Seq.map (fun x -> let ex = new CodeFieldReferenceExpression(self, x.Name)
let dir = if x.IsIn && x.IsOut then FieldDirection.Ref
else if x.IsOut then FieldDirection.Out
else FieldDirection.In
new CodeDirectionExpression(dir, ex) :> CodeExpression)
|> Seq.toArray
let invoke = new CodeMethodInvokeExpression(call, parameters)
// return a dummy zero instead of void, or the result
if m.ReturnType = typeof<System.Void> then
proxy.Statements.Add invoke |> ignore
let result = new CodeMethodReturnStatement(new CodePrimitiveExpression(0))
proxy.Statements.Add result |> ignore
else
proxy.Statements.Add (new CodeMethodReturnStatement(invoke)) |> ignore
// add this method to the class
newtype.Members.Add(proxy) |> ignore
newtype
view raw gistfile1.fs hosted with ❤ by GitHub

where the individual field declarations have to work around the decorations for out or ref, thus:

let GenerateField (p:ParameterInfo) =
let t = if p.ParameterType.IsByRef then p.ParameterType.GetElementType() else p.ParameterType
let field = new CodeMemberField(TypeRef t, p.Name)
field.Attributes <- MemberAttributes.Public
field
view raw gistfile1.fs hosted with ❤ by GitHub

The generation of the proxy methods is equally mechanical

let GenerateProxy (target:Type) (m:MethodInfo) =
// Generate the method and its parameter list
let proxy = new CodeMemberMethod()
proxy.Name <- m.Name
proxy.Attributes <- MemberAttributes.Public ||| MemberAttributes.Final
proxy.ReturnType <- TypeRef(m.ReturnType)
m.GetParameters()
|> Seq.iter ( fun (t:ParameterInfo) ->
let px = new CodeParameterDeclarationExpression()
px.Direction <- if t.IsIn && t.IsOut then FieldDirection.Ref
else if t.IsOut then FieldDirection.Out
else FieldDirection.In
px.Name <- t.Name
let tt = if t.ParameterType.IsByRef then t.ParameterType.GetElementType() else t.ParameterType
px.Type <- TypeRef(tt)
proxy.Parameters.Add(px) |> ignore
)
// construct the closure object -- the value is "let mutable" in F#
let proxyType = new CodeTypeReference(m.Name + "__Proxy")
let construct = new CodeVariableDeclarationStatement(proxyType, "proxy",
CodeObjectCreateExpression((m.Name + "__Proxy"), [||]))
proxy.Statements.Add construct |> ignore
// create a reference to the closure
let self = new CodeThisReferenceExpression()
let atThis = new CodeFieldReferenceExpression(self, "this")
let proxyRef = CodeVariableReferenceExpression("proxy")
// initialize its reference to the wrapped object
let assign = new CodeAssignStatement( new CodeFieldReferenceExpression(proxyRef, "this"), atThis)
proxy.Statements.Add assign |> ignore
// initialize all the other fields (to default if out only)
m.GetParameters()
|> Seq.map (fun x -> let left = new CodeFieldReferenceExpression(proxyRef, x.Name)
let right = if x.IsOut && (not x.IsIn) then
// Doesn't work in the F# CodeDOM
new CodeDefaultValueExpression(TypeRef (x.ParameterType.GetElementType())) :> CodeExpression
else
new CodeArgumentReferenceExpression(x.Name) :> CodeExpression
new CodeAssignStatement(left, right)
)
|> Seq.map proxy.Statements.Add
|> Seq.iter ignore
// Expect the lambda to return int for a void method
let typeRef = if m.ReturnType = typeof<System.Void> then TypeRef(typeof<int>)
else TypeRef(m.ReturnType)
// call the Wrapping method which takes the lambda
let funcType = CodeTypeReference("Func")
funcType.TypeArguments.Add typeRef |> ignore
// because proxyRef is "let mutable" in F#, this attempt to close over it here is a compiler error in F#, but not in C#
let func = CodeObjectCreateExpression(funcType, new CodeMethodReferenceExpression(proxyRef, m.Name))
let computing = new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression("Wrapper"),
"LogCall",
new CodePrimitiveExpression(m.Name),
func)
// and store the result
let returnValue = new CodeVariableDeclarationStatement(typeRef, "return", computing)
proxy.Statements.Add returnValue |> ignore
// copy back all the out or ref parameters
m.GetParameters()
|> Seq.filter (fun x -> x.IsOut)
|> Seq.map (fun x -> let left = new CodeArgumentReferenceExpression(x.Name)
let right = new CodeFieldReferenceExpression(proxyRef, x.Name)
new CodeAssignStatement(left, right)
)
|> Seq.map proxy.Statements.Add
|> Seq.iter ignore
// and for a non-void method, return the value from the lambda
if m.ReturnType <> typeof<System.Void> then
let r = new CodeMethodReturnStatement(CodeVariableReferenceExpression("return"))
proxy.Statements.Add r |> ignore
proxy
view raw gistfile1.fs hosted with ❤ by GitHub

So, given a simple type

public class Subject
{
public int DoIt(int argument, out string result) {...}
public void DoItSilently(int argument, out string result) {...}
}
view raw gistfile1.cs hosted with ❤ by GitHub

we generate

namespace Derived {
using Base;
using System;
using System.CodeDom.Compiler;
[GeneratedCodeAttribute("Generator", "0.0.0.0")]
public sealed class SubjectWrapper {
private Subject @this;
private SubjectWrapper(Subject Subject) {
this.@this = Subject;
}
public int DoIt(int argument, out string result) {
DoIt__Proxy proxy = new DoIt__Proxy();
proxy.@this = this.@this;
proxy.argument = argument;
proxy.result = default(string);
int @return = Wrapper.LogCall("DoIt", new Func<int>(proxy.DoIt));
result = proxy.result;
return @return;
}
public void DoItSilently(int argument, out string result) {
DoItSilently__Proxy proxy = new DoItSilently__Proxy();
proxy.@this = this.@this;
proxy.argument = argument;
proxy.result = default(string);
int @return = Wrapper.LogCall("DoItSilently", new Func<int>(proxy.DoItSilently));
result = proxy.result;
}
[GeneratedCodeAttribute("Generator", "0.0.0.0")]
private class DoIt__Proxy {
public Subject @this;
public int argument;
public string result;
public int DoIt() {
return this.@this.DoIt(this.argument, out this.result);
}
}
[GeneratedCodeAttribute("Generator", "0.0.0.0")]
private class DoItSilently__Proxy {
public Subject @this;
public int argument;
public string result;
public int DoItSilently() {
this.@this.DoItSilently(this.argument, out this.result);
return 0;
}
}
}
}
view raw gistfile1.fs hosted with ❤ by GitHub

The F# code contains

let mutable (proxy:SubjectWrapper_DoIt__Proxy) = new SubjectWrapper_DoIt__Proxy()
...
proxy.result <- (* Unknown expression type 'CodeDefaultValueExpression' please report this to the F# team. *)
view raw gistfile1.fs hosted with ❤ by GitHub

Manually replacing this with

let mutable (proxy:SubjectWrapper_DoIt__Proxy) = new SubjectWrapper_DoIt__Proxy()
...
proxy.result <- Unchecked.defaultof<string>
view raw gistfile1.fs hosted with ❤ by GitHub

then shows up on the next line

let mutable (_return:int) = Wrapper.LogCall("DoIt", new Func(proxy.DoIt))
view raw gistfile1.fs hosted with ❤ by GitHub

as an error The mutable variable 'proxy' is used in an invalid way. Mutable variables cannot be captured by closures. Consider eliminating this use of mutation or using a heap-allocated mutable reference cell via 'ref' and '!'.; and as there are no readonly locals in the CLR -- it's all F# compiler magic that gives the illusion of same -- we're stuck with no control to tweak to make proxy immutable in the F# output.

No comments :