Skip to content

Commit 0a4d51c

Browse files
Dangercoderdmiller
authored andcommitted
Fix GenDelegate to produce clean delegates without hidden Closure parameter
GenDelegate.Create previously used Expression.Lambda + Compile() which injected a hidden System.Runtime.CompilerServices.Closure parameter into the delegate's Invoke signature. Frameworks like ASP.NET Minimal API that reflect on delegate parameters would fail with "parameter without a name". Fix: generate a proper wrapper class via TypeBuilder with the IFn stored as a private field. The Invoke method exactly matches the delegate signature with no hidden parameters. Includes 7 NUnit tests covering: no hidden parameters, parameter signature matching, IFn invocation, value type boxing/unboxing, void delegates.
1 parent eb62161 commit 0a4d51c

File tree

2 files changed

+208
-37
lines changed

2 files changed

+208
-37
lines changed

Clojure/Clojure/CljCompiler/GenDelegate.cs

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/**
22
* Copyright (c) Rich Hickey. All rights reserved.
33
* The use and distribution terms for this software are covered by the
44
* Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
@@ -10,9 +10,9 @@
1010

1111
using clojure.lang.CljCompiler.Context;
1212
using System;
13-
using System.Collections.Generic;
14-
using System.Linq.Expressions;
1513
using System.Reflection;
14+
using System.Reflection.Emit;
15+
using System.Threading;
1616

1717

1818
namespace clojure.lang
@@ -22,6 +22,7 @@ public static class GenDelegate
2222
#region Data
2323

2424
static GenContext _context = GenContext.CreateWithInternalAssembly("delegates", false);
25+
static int _wrapperCount = 0;
2526

2627
#endregion
2728

@@ -42,48 +43,86 @@ public static Delegate Create(Type delegateType, IFn fn)
4243
{
4344
MethodInfo invokeMI = delegateType.GetMethod("Invoke");
4445
Type returnType = invokeMI.ReturnType;
45-
4646
ParameterInfo[] delParams = invokeMI.GetParameters();
4747

48-
List<ParameterExpression> parms = new List<ParameterExpression>();
49-
List<Expression> callArgs = new List<Expression>();
50-
51-
foreach (ParameterInfo pi in delParams)
48+
// Generate a wrapper class with the IFn as a field and an Invoke
49+
// method that exactly matches the delegate signature. This produces
50+
// delegates whose Method.GetParameters() contains only the declared
51+
// parameters — no hidden Closure or IFn parameter — making them
52+
// compatible with frameworks like ASP.NET that reflect on delegates.
53+
54+
int id = Interlocked.Increment(ref _wrapperCount);
55+
TypeBuilder tb = _context.ModuleBuilder.DefineType(
56+
"clojure.delegate.Wrapper_" + id,
57+
TypeAttributes.Public | TypeAttributes.Sealed,
58+
typeof(object));
59+
60+
FieldBuilder fnField = tb.DefineField("_fn", typeof(IFn), FieldAttributes.Private);
61+
62+
// Constructor: takes IFn
63+
ConstructorBuilder ctor = tb.DefineConstructor(
64+
MethodAttributes.Public,
65+
CallingConventions.Standard,
66+
[typeof(IFn)]);
67+
ILGenerator ctorIL = ctor.GetILGenerator();
68+
ctorIL.Emit(OpCodes.Ldarg_0);
69+
ctorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
70+
ctorIL.Emit(OpCodes.Ldarg_0);
71+
ctorIL.Emit(OpCodes.Ldarg_1);
72+
ctorIL.Emit(OpCodes.Stfld, fnField);
73+
ctorIL.Emit(OpCodes.Ret);
74+
75+
// Invoke method: matches delegate signature exactly
76+
Type[] paramTypes = new Type[delParams.Length];
77+
for (int i = 0; i < delParams.Length; i++)
78+
paramTypes[i] = delParams[i].ParameterType;
79+
80+
MethodBuilder mb = tb.DefineMethod(
81+
"Invoke",
82+
MethodAttributes.Public,
83+
returnType,
84+
paramTypes);
85+
86+
// Name parameters to match the delegate
87+
for (int i = 0; i < delParams.Length; i++)
88+
mb.DefineParameter(i + 1, delParams[i].Attributes, delParams[i].Name ?? ("p" + i));
89+
90+
ILGenerator ilg = mb.GetILGenerator();
91+
92+
// Load this._fn
93+
ilg.Emit(OpCodes.Ldarg_0);
94+
ilg.Emit(OpCodes.Ldfld, fnField);
95+
96+
// Load and box each parameter for IFn.invoke(object, object, ...)
97+
for (int i = 0; i < delParams.Length; i++)
5298
{
53-
ParameterExpression pe = Expression.Parameter(pi.ParameterType, pi.Name);
54-
parms.Add(pe);
55-
callArgs.Add(MaybeBox(pe));
99+
ilg.Emit(OpCodes.Ldarg, i + 1);
100+
if (delParams[i].ParameterType.IsValueType)
101+
ilg.Emit(OpCodes.Box, delParams[i].ParameterType);
56102
}
57103

58-
Expression call =
59-
Expression.Call(
60-
Expression.Constant(fn),
61-
Compiler.Methods_IFn_invoke[parms.Count],
62-
callArgs);
63-
64-
Expression body = returnType == typeof(void)
65-
? (Expression)Expression.Block(call,Expression.Default(typeof(void)))
66-
: (Expression)Expression.Convert(call, returnType);
67-
68-
LambdaExpression lambda = Expression.Lambda(delegateType, body, true, parms);
69-
70-
return lambda.Compile();
71-
}
72-
73-
74-
#endregion
104+
// Call IFn.invoke(...)
105+
ilg.Emit(OpCodes.Callvirt, Compiler.Methods_IFn_invoke[delParams.Length]);
75106

76-
#region Boxing arguments
107+
if (returnType == typeof(void))
108+
{
109+
ilg.Emit(OpCodes.Pop);
110+
}
111+
else if (returnType.IsValueType)
112+
{
113+
ilg.Emit(OpCodes.Unbox_Any, returnType);
114+
}
115+
else if (returnType != typeof(object))
116+
{
117+
ilg.Emit(OpCodes.Castclass, returnType);
118+
}
77119

78-
internal static Expression MaybeBox(Expression expr)
79-
{
80-
if (expr.Type == typeof(void))
81-
// I guess we'll pass a void. This happens when we have a throw, for example.
82-
return Expression.Block(expr, Expression.Default(typeof(object)));
120+
ilg.Emit(OpCodes.Ret);
83121

84-
return expr.Type.IsValueType
85-
? Expression.Convert(expr, typeof(object))
86-
: expr;
122+
// Create instance and delegate
123+
Type wrapperType = tb.CreateType();
124+
object wrapper = Activator.CreateInstance(wrapperType, fn);
125+
return Delegate.CreateDelegate(delegateType, wrapper, "Invoke");
87126
}
88127

89128
#endregion
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using System;
2+
using System.Reflection;
3+
using clojure.lang;
4+
using NUnit.Framework;
5+
6+
namespace Clojure.Tests.LibTests
7+
{
8+
[TestFixture]
9+
public class GenDelegateTests
10+
{
11+
[OneTimeSetUp]
12+
public void Setup()
13+
{
14+
RT.Init();
15+
}
16+
17+
// Helper IFn that records what it was called with
18+
class RecordingFn : AFn
19+
{
20+
public object[] LastArgs;
21+
public object ReturnValue;
22+
23+
public RecordingFn(object returnValue = null)
24+
{
25+
ReturnValue = returnValue;
26+
}
27+
28+
public override object invoke()
29+
{
30+
LastArgs = Array.Empty<object>();
31+
return ReturnValue;
32+
}
33+
34+
public override object invoke(object arg1)
35+
{
36+
LastArgs = new[] { arg1 };
37+
return ReturnValue;
38+
}
39+
40+
public override object invoke(object arg1, object arg2)
41+
{
42+
LastArgs = new[] { arg1, arg2 };
43+
return ReturnValue;
44+
}
45+
}
46+
47+
[Test]
48+
public void DelegateInvokeHasNoHiddenParameters()
49+
{
50+
var fn = new RecordingFn("hello");
51+
var del = GenDelegate.Create(typeof(Func<string>), fn);
52+
53+
// The delegate's Method.GetParameters() must have exactly 0 parameters.
54+
// The old Expression.Lambda approach injected a hidden Closure parameter.
55+
var parameters = del.Method.GetParameters();
56+
Assert.That(parameters, Has.Length.EqualTo(0),
57+
"Func<string> delegate should have no parameters (no hidden Closure)");
58+
}
59+
60+
[Test]
61+
public void DelegateWithParametersMatchesSignature()
62+
{
63+
var fn = new RecordingFn("result");
64+
var del = GenDelegate.Create(typeof(Func<int, string, string>), fn);
65+
66+
var parameters = del.Method.GetParameters();
67+
Assert.That(parameters, Has.Length.EqualTo(2));
68+
Assert.That(parameters[0].ParameterType, Is.EqualTo(typeof(int)));
69+
Assert.That(parameters[1].ParameterType, Is.EqualTo(typeof(string)));
70+
}
71+
72+
[Test]
73+
public void DelegateInvokesIFnCorrectly()
74+
{
75+
var fn = new RecordingFn("hello from IFn");
76+
var del = (Func<string>)GenDelegate.Create(typeof(Func<string>), fn);
77+
78+
var result = del();
79+
80+
Assert.That(result, Is.EqualTo("hello from IFn"));
81+
Assert.That(fn.LastArgs, Has.Length.EqualTo(0));
82+
}
83+
84+
[Test]
85+
public void DelegateBoxesValueTypeArgs()
86+
{
87+
var fn = new RecordingFn("ok");
88+
var del = (Func<int, string>)GenDelegate.Create(typeof(Func<int, string>), fn);
89+
90+
del(42);
91+
92+
Assert.That(fn.LastArgs, Has.Length.EqualTo(1));
93+
Assert.That(fn.LastArgs[0], Is.EqualTo(42));
94+
Assert.That(fn.LastArgs[0], Is.InstanceOf<object>(),
95+
"int argument should be boxed to object for IFn.invoke");
96+
}
97+
98+
[Test]
99+
public void DelegateUnboxesValueTypeReturn()
100+
{
101+
var fn = new RecordingFn(99);
102+
var del = (Func<int>)GenDelegate.Create(typeof(Func<int>), fn);
103+
104+
var result = del();
105+
106+
Assert.That(result, Is.EqualTo(99));
107+
}
108+
109+
[Test]
110+
public void VoidDelegateWorks()
111+
{
112+
var fn = new RecordingFn(null);
113+
var del = (Action)GenDelegate.Create(typeof(Action), fn);
114+
115+
Assert.DoesNotThrow(() => del());
116+
Assert.That(fn.LastArgs, Has.Length.EqualTo(0));
117+
}
118+
119+
[Test]
120+
public void DelegateWithTwoArgsPassesBothCorrectly()
121+
{
122+
var fn = new RecordingFn("combined");
123+
var del = (Func<string, int, string>)GenDelegate.Create(typeof(Func<string, int, string>), fn);
124+
125+
var result = del("hello", 7);
126+
127+
Assert.That(result, Is.EqualTo("combined"));
128+
Assert.That(fn.LastArgs[0], Is.EqualTo("hello"));
129+
Assert.That(fn.LastArgs[1], Is.EqualTo(7));
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)