Skip to content

Commit def518f

Browse files
committed
Preserve by-ref-like values of ref params & add tests for by-ref params
1 parent 65054c1 commit def518f

File tree

2 files changed

+242
-1
lines changed

2 files changed

+242
-1
lines changed

src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
#if FEATURE_BYREFLIKE
1616

17+
#nullable enable
18+
1719
namespace Castle.DynamicProxy.Tests
1820
{
1921
using System;
@@ -38,6 +40,8 @@ public void Can_proxy_type(Type interfaceType)
3840
_ = generator.CreateInterfaceProxyWithoutTarget(interfaceType);
3941
}
4042

43+
#region Can methods with by-ref-like parameters be intercepted without crashing?
44+
4145
[Test]
4246
public void Can_invoke_method_with_by_ref_like_parameter()
4347
{
@@ -76,6 +80,10 @@ public void Can_invoke_method_with_by_ref_like_return_type()
7680
_ = proxy.Method();
7781
}
7882

83+
#endregion
84+
85+
#region Can methods with by-ref-like parameters be proceeded to without crashing?
86+
7987
[Test]
8088
public void Can_proceed_to_target_method_with_by_ref_like_parameter()
8189
{
@@ -119,6 +127,8 @@ public void Can_proceed_to_target_method_with_by_ref_like_return_type()
119127
_ = proxy.Method();
120128
}
121129

130+
#endregion
131+
122132
public ref struct ByRefLike
123133
{
124134
}
@@ -184,6 +194,227 @@ public virtual ByRefLike Method()
184194
return default;
185195
}
186196
}
197+
198+
#region What values do interceptors see for by-ref-like arguments?
199+
200+
[Test]
201+
public void By_ref_like_arguments_are_replaced_with_null_in_invocation()
202+
{
203+
var interceptor = new ObservingInterceptor();
204+
var proxy = generator.CreateClassProxy<HasMethodWithSpanParameter>(interceptor);
205+
var arg = "original".AsSpan();
206+
proxy.Method(arg);
207+
Assert.IsNull(interceptor.ObservedArg);
208+
}
209+
210+
[Test]
211+
public void By_ref_like_in_arguments_are_replaced_with_null_in_invocation()
212+
{
213+
var interceptor = new ObservingInterceptor();
214+
var proxy = generator.CreateClassProxy<HasMethodWithSpanInParameter>(interceptor);
215+
var arg = "original".AsSpan();
216+
proxy.Method(in arg);
217+
Assert.IsNull(interceptor.ObservedArg);
218+
}
219+
220+
[Test]
221+
public void By_ref_like_ref_arguments_are_replaced_with_null_in_invocation()
222+
{
223+
var interceptor = new ObservingInterceptor();
224+
var proxy = generator.CreateClassProxy<HasMethodWithSpanRefParameter>(interceptor);
225+
var arg = "original".AsSpan();
226+
proxy.Method(ref arg);
227+
Assert.IsNull(interceptor.ObservedArg);
228+
}
229+
230+
// Note the somewhat weird semantics of this test: DynamicProxy allows you to read the incoming values
231+
// of `out` arguments, which would be illegal in plain C# ("use of unassigned out parameter").
232+
// DynamicProxy does not distinguish between `ref` and `out` in this regard.
233+
[Test]
234+
public void By_ref_like_out_arguments_are_replaced_with_null_in_invocation()
235+
{
236+
var interceptor = new ObservingInterceptor();
237+
var proxy = generator.CreateClassProxy<HasMethodWithSpanOutParameter>(interceptor);
238+
var arg = "original".AsSpan();
239+
proxy.Method(out arg);
240+
Assert.IsNull(interceptor.ObservedArg);
241+
}
242+
243+
#endregion
244+
245+
#region What values do proceeded-to targets see for by-ref-like arguments?
246+
247+
// This test merely describes the status quo, and not the behavior we'd ideally want.
248+
[Test]
249+
public void By_ref_like_arguments_arrive_reset_to_default_value_at_target()
250+
{
251+
var target = new HasMethodWithSpanParameter();
252+
var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor());
253+
var arg = "original".AsSpan();
254+
proxy.Method(arg);
255+
Assert.AreEqual("", target.RecordedArg);
256+
}
257+
258+
// This test merely describes the status quo, and not the behavior we'd ideally want.
259+
[Test]
260+
public void By_ref_like_in_arguments_arrive_reset_to_default_value_at_target()
261+
{
262+
var target = new HasMethodWithSpanInParameter();
263+
var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor());
264+
var arg = "original".AsSpan();
265+
proxy.Method(in arg);
266+
Assert.AreEqual("", target.RecordedArg);
267+
}
268+
269+
// This test merely describes the status quo, and not the behavior we'd ideally want.
270+
[Test]
271+
public void By_ref_like_ref_arguments_arrive_reset_to_default_value_at_target()
272+
{
273+
var target = new HasMethodWithSpanRefParameter();
274+
var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor());
275+
var arg = "original".AsSpan();
276+
proxy.Method(ref arg);
277+
Assert.AreEqual("", target.RecordedArg);
278+
}
279+
280+
#endregion
281+
282+
#region How are by-ref-like by-ref arguments changed by interception?
283+
284+
[Test]
285+
public void By_ref_like_in_arguments_are_left_unchanged()
286+
{
287+
var proxy = generator.CreateClassProxy<HasMethodWithSpanInParameter>(new DoNothingInterceptor());
288+
var arg = "original".AsSpan();
289+
proxy.Method(in arg);
290+
Assert.AreEqual("original", arg.ToString());
291+
}
292+
293+
[Test]
294+
public void By_ref_like_in_arguments_are_left_unchanged_if_interception_includes_proceed_to_target()
295+
{
296+
var target = new HasMethodWithSpanInParameter();
297+
var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor());
298+
var arg = "original".AsSpan();
299+
proxy.Method(in arg);
300+
Assert.AreEqual("original", arg.ToString());
301+
}
302+
303+
[Test]
304+
public void By_ref_like_ref_arguments_are_left_unchanged()
305+
{
306+
var proxy = generator.CreateClassProxy<HasMethodWithSpanRefParameter>(new DoNothingInterceptor());
307+
var arg = "original".AsSpan();
308+
proxy.Method(ref arg);
309+
Assert.AreEqual("original", arg.ToString());
310+
}
311+
312+
[Test]
313+
public void By_ref_like_ref_arguments_are_left_unchanged_if_interception_includes_proceed_to_target()
314+
{
315+
var target = new HasMethodWithSpanRefParameter();
316+
var proxy = generator.CreateClassProxyWithTarget(target, new DoNothingInterceptor());
317+
var arg = "original".AsSpan();
318+
proxy.Method(ref arg);
319+
Assert.AreEqual("original", arg.ToString());
320+
}
321+
322+
// This test merely describes the status quo, and not the behavior we'd ideally want:
323+
// DynamicProxy records the initial values of all by-ref arguments in `IInvocation.Arguments` and,
324+
// unless changed during interception, writes out the final value for both `ref` and `out` parameters
325+
// from there... meaning all non-by-ref-like by-ref arguments are by default left unchanged.
326+
// This cannot work for by-ref-likes, since their initial value cannot be preserved in `Arguments`.
327+
// To honor the semantics of `out` parameters DynamicProxy *does* write a value (unlike with `ref`,
328+
// above, where it is free to choose not to).
329+
[Test]
330+
public void By_ref_like_out_arguments_are_reset_to_default_value()
331+
{
332+
var proxy = generator.CreateClassProxy<HasMethodWithSpanOutParameter>(new DoNothingInterceptor());
333+
var arg = "original".AsSpan();
334+
proxy.Method(out arg);
335+
Assert.AreEqual("", arg.ToString());
336+
}
337+
338+
// Once we manage to change the implementation so that `out` arguments aren't reset,
339+
// and the above test is replaced with an `are_left_unchanged` version, then we should also add
340+
// an additional `are_left_unchanged_if_interception_includes_proceed_to_target` test variant.
341+
342+
#endregion
343+
344+
#region Can interception targets set by-ref-like by-ref arguments?
345+
346+
// This test merely describes the status quo, and not the behavior we'd ideally want.
347+
[Test]
348+
public void By_ref_like_ref_arguments_cannot_be_set_by_target()
349+
{
350+
var target = new HasMethodWithSpanRefParameter();
351+
var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor());
352+
var arg = "original".AsSpan();
353+
proxy.Method(ref arg);
354+
Assert.AreEqual("original", arg.ToString()); // ideally, would be equal to "set"
355+
}
356+
357+
// This test merely describes the status quo, and not the behavior we'd ideally want.
358+
[Test]
359+
public void By_ref_like_out_arguments_cannot_be_set_by_target()
360+
{
361+
var target = new HasMethodWithSpanOutParameter();
362+
var proxy = generator.CreateClassProxyWithTarget(target, new StandardInterceptor());
363+
var arg = "original".AsSpan();
364+
proxy.Method(out arg);
365+
Assert.AreEqual("", arg.ToString()); // ideally, would be equal to "set"
366+
}
367+
368+
#endregion
369+
370+
public class HasMethodWithSpanParameter
371+
{
372+
public string? RecordedArg;
373+
374+
public virtual void Method(ReadOnlySpan<char> arg)
375+
{
376+
RecordedArg = arg.ToString();
377+
}
378+
}
379+
380+
public class HasMethodWithSpanInParameter
381+
{
382+
public string? RecordedArg;
383+
384+
public virtual void Method(in ReadOnlySpan<char> arg)
385+
{
386+
RecordedArg = arg.ToString();
387+
}
388+
}
389+
390+
public class HasMethodWithSpanRefParameter
391+
{
392+
public string? RecordedArg;
393+
394+
public virtual void Method(ref ReadOnlySpan<char> arg)
395+
{
396+
RecordedArg = arg.ToString();
397+
arg = "set".AsSpan();
398+
}
399+
}
400+
401+
public class HasMethodWithSpanOutParameter
402+
{
403+
public virtual void Method(out ReadOnlySpan<char> arg)
404+
{
405+
arg = "set".AsSpan();
406+
}
407+
}
408+
409+
public class ObservingInterceptor : IInterceptor
410+
{
411+
public object? ObservedArg;
412+
413+
public void Intercept(IInvocation invocation)
414+
{
415+
ObservedArg = invocation.Arguments[0];
416+
}
417+
}
187418
}
188419
}
189420

src/Castle.Core/DynamicProxy/Generators/GeneratorUtil.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,17 @@ public static void CopyOutAndRefParameters(TypeReference[] dereferencedArguments
5151
// We need to replace it with some other value.
5252

5353
// For now, we just substitute the by-ref-like type's default value:
54-
emitter.CodeBuilder.AddStatement(new AssignStatement(dereferencedArguments[i], new DefaultValueExpression(dereferencedParameterType)));
54+
if (parameters[i].IsOut)
55+
{
56+
emitter.CodeBuilder.AddStatement(new AssignStatement(dereferencedArguments[i], new DefaultValueExpression(dereferencedParameterType)));
57+
}
58+
else
59+
{
60+
// ... except when we're dealing with a `ref` parameter. Unlike with `out`,
61+
// where we would be expected to definitely assign to it, we are free to leave
62+
// the original incoming value untouched. For now, that's likely the better
63+
// interim solution than unconditionally resetting.
64+
}
5565
}
5666
else
5767
#endif

0 commit comments

Comments
 (0)