Skip to content

Commit 615ba2d

Browse files
committed
Introduce type-safe [ReadOnly]SpanArgument<T> for .NET 8
This yields the following `ByRefLikeArgument` type hierarchy on .NET 8: ByRefLikeArgument ^ | +-- SpanArgument<T> | | +-- ReadOnlySpanArgument<T> While on .NET 9, there is an intermediate generic type (which wouldn't be allowed on earlier runtimes): ByRefLikeArgument ^ | +-- ByRefLikeArgument<TByRefLike> ^ | +-- SpanArgument<T> | (with TByRefLike = Span<T>) | | +-- ReadOnlySpanArgument<T> (with TByRefLike = ReadOnlySpan<T>) The intention behind those different hiearchies is to give .NET 8 users a type-safe argument wrapper for spans, too (because those have become commonly encountered types), while allowing .NET 9 code to ignore those specialized span variants and testing only for the more generic inter- mediate type. Hopefully this won't bite us in the future.
1 parent fac7242 commit 615ba2d

File tree

2 files changed

+114
-11
lines changed

2 files changed

+114
-11
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ public void By_ref_like_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation
206206
var arg = "original".AsSpan();
207207
proxy.Method(arg);
208208
Assert.IsInstanceOf<ByRefLikeArgument>(interceptor.ObservedArg);
209+
Assert.IsInstanceOf<ReadOnlySpanArgument<char>>(interceptor.ObservedArg);
210+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
211+
Assert.IsInstanceOf<ByRefLikeArgument<ReadOnlySpan<char>>>(interceptor.ObservedArg);
212+
#endif
209213
}
210214

211215
[Test]
@@ -244,6 +248,10 @@ public void By_ref_like_in_arguments_are_wrapped_as_ByRefLikeArgument_in_invocat
244248
var arg = "original".AsSpan();
245249
proxy.Method(in arg);
246250
Assert.IsInstanceOf<ByRefLikeArgument>(interceptor.ObservedArg);
251+
Assert.IsInstanceOf<ReadOnlySpanArgument<char>>(interceptor.ObservedArg);
252+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
253+
Assert.IsInstanceOf<ByRefLikeArgument<ReadOnlySpan<char>>>(interceptor.ObservedArg);
254+
#endif
247255
}
248256

249257
[Test]
@@ -282,6 +290,10 @@ public void By_ref_like_ref_arguments_are_wrapped_as_ByRefLikeArgument_in_invoca
282290
var arg = "original".AsSpan();
283291
proxy.Method(ref arg);
284292
Assert.IsInstanceOf<ByRefLikeArgument>(interceptor.ObservedArg);
293+
Assert.IsInstanceOf<ReadOnlySpanArgument<char>>(interceptor.ObservedArg);
294+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
295+
Assert.IsInstanceOf<ByRefLikeArgument<ReadOnlySpan<char>>>(interceptor.ObservedArg);
296+
#endif
285297
}
286298

287299
[Test]
@@ -323,6 +335,10 @@ public void By_ref_like_out_arguments_are_wrapped_as_ByRefLikeArgument_in_invoca
323335
var arg = "original".AsSpan();
324336
proxy.Method(out arg);
325337
Assert.IsInstanceOf<ByRefLikeArgument>(interceptor.ObservedArg);
338+
Assert.IsInstanceOf<ReadOnlySpanArgument<char>>(interceptor.ObservedArg);
339+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
340+
Assert.IsInstanceOf<ByRefLikeArgument<ReadOnlySpan<char>>>(interceptor.ObservedArg);
341+
#endif
326342
}
327343

328344
// Should theoretically be as above (read comment there), but isn't. To be revisited later!

src/Castle.Core/DynamicProxy/ByRefLikeArgument.cs

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,38 @@ namespace Castle.DynamicProxy
3030
/// </summary>
3131
public unsafe class ByRefLikeArgument
3232
{
33-
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
3433
private static readonly ConcurrentDictionary<Type, ConstructorInfo> constructorMap = new();
3534

3635
internal static ConstructorInfo GetConstructorFor(Type byRefLikeType)
3736
{
3837
return constructorMap.GetOrAdd(byRefLikeType, static byRefLikeType =>
3938
{
40-
var type = typeof(ByRefLikeArgument<>).MakeGenericType(byRefLikeType);
41-
return type.GetConstructor([ typeof(void*) ])!;
42-
});
43-
}
39+
Type? type = null;
40+
41+
if (byRefLikeType.IsConstructedGenericType)
42+
{
43+
var typeDef = byRefLikeType.GetGenericTypeDefinition();
44+
if (typeDef == typeof(Span<>))
45+
{
46+
var typeArg = byRefLikeType.GetGenericArguments()[0];
47+
type = typeof(SpanArgument<>).MakeGenericType(typeArg);
48+
}
49+
else if (typeDef == typeof(ReadOnlySpan<>))
50+
{
51+
var typeArg = byRefLikeType.GetGenericArguments()[0];
52+
type = typeof(ReadOnlySpanArgument<>).MakeGenericType(typeArg);
53+
}
54+
}
55+
56+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
57+
type ??= typeof(ByRefLikeArgument<>).MakeGenericType(byRefLikeType);
4458
#else
45-
private static readonly ConstructorInfo constructor =
46-
typeof(ByRefLikeArgument).GetConstructor([ typeof(void*) ])!;
59+
type ??= typeof(ByRefLikeArgument);
60+
#endif
4761

48-
internal static ConstructorInfo GetConstructorFor(Type byRefLikeType)
49-
{
50-
return constructor;
62+
return type.GetConstructor([ typeof(void*) ])!;
63+
});
5164
}
52-
#endif
5365

5466
protected void* ptr;
5567

@@ -103,6 +115,81 @@ public ref TByRefLike Get()
103115

104116
#endif
105117

118+
// The following two specializations for `Span<T>` and `ReadOnlySpan<T>` are provided
119+
// because those two types have become so common in the Framework Class Library, and
120+
// dealing with them through unmanaged pointers all the time would be cumbersome.
121+
// We can provide a type-safe wrapper for them even on .NET 8. And we keep the types
122+
// for .NET 9 (even though they're redundant) so downstream code can expect to always
123+
// encounter a `[ReadOnly]SpanArgument<>` for `[ReadOnly]Span<>` regardless of
124+
// whether they target .NET 8 or 9.
125+
126+
/// <summary>
127+
/// Wraps a <see cref="ReadOnlySpan{T}"/> method argument
128+
/// such that it can be placed in the <see cref="IInvocation.Arguments"/> array during interception.
129+
/// </summary>
130+
public unsafe class ReadOnlySpanArgument<T>
131+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
132+
: ByRefLikeArgument<ReadOnlySpan<T>>
133+
#else
134+
: ByRefLikeArgument
135+
#endif
136+
{
137+
/// <summary>
138+
/// Do not use this! Only generated proxies should construct instances this type.
139+
/// </summary>
140+
[CLSCompliant(false)]
141+
[EditorBrowsable(EditorBrowsableState.Never)]
142+
public ReadOnlySpanArgument(void* ptr)
143+
: base(ptr)
144+
{
145+
}
146+
147+
#if !FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
148+
/// <summary>
149+
/// Gets the byref-like (<c>ref struct</c>) argument.
150+
/// </summary>
151+
public ref ReadOnlySpan<T> Get()
152+
{
153+
#pragma warning disable CS8500
154+
return ref *(ReadOnlySpan<T>*)ptr;
155+
#pragma warning restore CS8500
156+
}
157+
#endif
158+
}
159+
160+
/// <summary>
161+
/// Wraps a <see cref="Span{T}"/> method argument
162+
/// such that it can be placed in the <see cref="IInvocation.Arguments"/> array during interception.
163+
/// </summary>
164+
public unsafe class SpanArgument<T>
165+
#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
166+
: ByRefLikeArgument<Span<T>>
167+
#else
168+
: ByRefLikeArgument
169+
#endif
170+
{
171+
/// <summary>
172+
/// Do not use this! Only generated proxies should construct instances this type.
173+
/// </summary>
174+
[CLSCompliant(false)]
175+
[EditorBrowsable(EditorBrowsableState.Never)]
176+
public SpanArgument(void* ptr)
177+
: base(ptr)
178+
{
179+
}
180+
181+
#if !FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT
182+
/// <summary>
183+
/// Gets the byref-like (<c>ref struct</c>) argument.
184+
/// </summary>
185+
public ref Span<T> Get()
186+
{
187+
#pragma warning disable CS8500
188+
return ref *(Span<T>*)ptr;
189+
#pragma warning restore CS8500
190+
}
191+
#endif
192+
}
106193
}
107194

108195
#endif

0 commit comments

Comments
 (0)