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 |
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 |
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 |
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 |
So, given a simple type
public class Subject | |
{ | |
public int DoIt(int argument, out string result) {...} | |
public void DoItSilently(int argument, out string result) {...} | |
} |
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; | |
} | |
} | |
} | |
} |
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. *) |
Manually replacing this with
let mutable (proxy:SubjectWrapper_DoIt__Proxy) = new SubjectWrapper_DoIt__Proxy() | |
... | |
proxy.result <- Unchecked.defaultof<string> |
then shows up on the next line
let mutable (_return:int) = Wrapper.LogCall("DoIt", new Func(proxy.DoIt)) |
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.