diff --git a/README.md b/README.md index 52b0b5006..ad0c30cd5 100644 --- a/README.md +++ b/README.md @@ -50,16 +50,18 @@ build.cmd The following conditional compilation symbols (vertical) are currently defined for each of the build configurations (horizontal): -Symbol | .NET 4.6.2 | .NET Standard 2.0 | .NET 8 ------------------------------------ | ------------------ | ----------------- | ------------------ -`FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: -`FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: -`FEATURE_BYREFLIKE` | :no_entry_sign: | :no_entry_sign: | :white_check_mark: -`FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: -`FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: +Symbol | .NET 4.6.2 | .NET Standard 2.0 | .NET 8 | .NET 9 +------------------------------------------- | ------------------ | ----------------- | ------------------ | ------------ +`FEATURE_APPDOMAIN` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: +`FEATURE_ASSEMBLYBUILDER_SAVE` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: +`FEATURE_BYREFLIKE` | :no_entry_sign: | :no_entry_sign: | :white_check_mark: | :white_check_mark: +`FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT` | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :white_check_mark: +`FEATURE_SERIALIZATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: +`FEATURE_SYSTEM_CONFIGURATION` | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: * `FEATURE_APPDOMAIN` - enables support for features that make use of an AppDomain in the host. * `FEATURE_ASSEMBLYBUILDER_SAVE` - enabled support for saving the dynamically generated proxy assembly. +* `FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT` - enables support for by-ref-like (`ref struct`) types being used as generic type arguments. * `FEATURE_BYREFLIKE` - enables support for by-ref-like (`ref struct`) types such as `Span` and `ReadOnlySpan`. * `FEATURE_SERIALIZATION` - enables support for serialization of dynamic proxies and other types. * `FEATURE_SYSTEM_CONFIGURATION` - enables features that use System.Configuration and the ConfigurationManager. diff --git a/buildscripts/common.props b/buildscripts/common.props index b2989c095..4dd0647b4 100644 --- a/buildscripts/common.props +++ b/buildscripts/common.props @@ -4,6 +4,7 @@ 14.0 $(NoWarn);CS1591;CS3014;CS3003;CS3001;CS3021 $(NoWarn);CS0612;CS0618 + true git https://github.com/castleproject/Core 0.0.0 @@ -70,6 +71,10 @@ $(DefineConstants);FEATURE_BYREFLIKE + + $(DefineConstants);FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + + diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs index 0c1a1195e..ecb9ad096 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ByRefLikeTestCase.cs @@ -15,6 +15,7 @@ #if FEATURE_BYREFLIKE #nullable enable +#pragma warning disable CS8500 namespace Castle.DynamicProxy.Tests { @@ -198,47 +199,179 @@ public virtual ByRefLike Method() #region What values do interceptors see for by-ref-like arguments? [Test] - public void By_ref_like_arguments_are_replaced_with_null_in_invocation() + public void By_ref_like_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation() { var interceptor = new ObservingInterceptor(); var proxy = generator.CreateClassProxy(interceptor); var arg = "original".AsSpan(); proxy.Method(arg); - Assert.IsNull(interceptor.ObservedArg); + Assert.IsInstanceOf(interceptor.ObservedArg); + Assert.IsInstanceOf>(interceptor.ObservedArg); +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + Assert.IsInstanceOf>>(interceptor.ObservedArg); +#endif + } + + [Test] + public unsafe void By_ref_like_arguments_can_be_restored_from_ByRefLikeArgument_GetPointer_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(arg); + Assume.That(interceptor.ObservedArg is ByRefLikeArgument); + var wrappedArg = (ByRefLikeArgument)interceptor.ObservedArg!; + var unwrappedArg = *(ReadOnlySpan*)wrappedArg.GetPointer(); + Assert.AreEqual("original", unwrappedArg.ToString()); + } + +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + [Test] + public void By_ref_like_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(arg); + Assume.That(interceptor.ObservedArg is ByRefLikeArgument>); + var wrappedArg = (ByRefLikeArgument>)interceptor.ObservedArg!; + var unwrappedArg = wrappedArg.Get(); + Assert.AreEqual("original", unwrappedArg.ToString()); + } +#endif + + [Test] + public void By_ref_like_in_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(in arg); + Assert.IsInstanceOf(interceptor.ObservedArg); + Assert.IsInstanceOf>(interceptor.ObservedArg); +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + Assert.IsInstanceOf>>(interceptor.ObservedArg); +#endif + } + + [Test] + public unsafe void By_ref_like_in_arguments_can_be_restored_from_ByRefLikeArgument_GetPointer_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(in arg); + Assume.That(interceptor.ObservedArg is ByRefLikeArgument); + var wrappedArg = (ByRefLikeArgument)interceptor.ObservedArg!; + var unwrappedArg = *(ReadOnlySpan*)wrappedArg.GetPointer(); + Assert.AreEqual("original", unwrappedArg.ToString()); } +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT [Test] - public void By_ref_like_in_arguments_are_replaced_with_null_in_invocation() + public void By_ref_like_in_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation() { var interceptor = new ObservingInterceptor(); var proxy = generator.CreateClassProxy(interceptor); var arg = "original".AsSpan(); proxy.Method(in arg); - Assert.IsNull(interceptor.ObservedArg); + Assume.That(interceptor.ObservedArg is ByRefLikeArgument>); + var wrappedArg = (ByRefLikeArgument>)interceptor.ObservedArg!; + var unwrappedArg = wrappedArg.Get(); + Assert.AreEqual("original", unwrappedArg.ToString()); + } +#endif + + [Test] + public void By_ref_like_ref_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(ref arg); + Assert.IsInstanceOf(interceptor.ObservedArg); + Assert.IsInstanceOf>(interceptor.ObservedArg); +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + Assert.IsInstanceOf>>(interceptor.ObservedArg); +#endif } [Test] - public void By_ref_like_ref_arguments_are_replaced_with_null_in_invocation() + public unsafe void By_ref_like_ref_arguments_can_be_restored_from_ByRefLikeArgument_GetPointer_in_invocation() { var interceptor = new ObservingInterceptor(); var proxy = generator.CreateClassProxy(interceptor); var arg = "original".AsSpan(); proxy.Method(ref arg); - Assert.IsNull(interceptor.ObservedArg); + Assume.That(interceptor.ObservedArg is ByRefLikeArgument); + var wrappedArg = (ByRefLikeArgument)interceptor.ObservedArg!; + var unwrappedArg = *(ReadOnlySpan*)wrappedArg.GetPointer(); + Assert.AreEqual("original", unwrappedArg.ToString()); } +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + [Test] + public void By_ref_like_ref_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(ref arg); + Assume.That(interceptor.ObservedArg is ByRefLikeArgument>); + var wrappedArg = (ByRefLikeArgument>)interceptor.ObservedArg!; + var unwrappedArg = wrappedArg.Get(); + Assert.AreEqual("original", unwrappedArg.ToString()); + } +#endif + // Note the somewhat weird semantics of this test: DynamicProxy allows you to read the incoming values // of `out` arguments, which would be illegal in plain C# ("use of unassigned out parameter"). // DynamicProxy does not distinguish between `ref` and `out` in this regard. [Test] - public void By_ref_like_out_arguments_are_replaced_with_null_in_invocation() + public void By_ref_like_out_arguments_are_wrapped_as_ByRefLikeArgument_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(out arg); + Assert.IsInstanceOf(interceptor.ObservedArg); + Assert.IsInstanceOf>(interceptor.ObservedArg); +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + Assert.IsInstanceOf>>(interceptor.ObservedArg); +#endif + } + + // Should theoretically be as above (read comment there), but isn't. To be revisited later! + [Test] + [Ignore("Have not yet found out why byref-like `out` arguments are initially set to their default values unlike `ref` ones.")] + public unsafe void By_ref_like_out_arguments_can_be_restored_from_ByRefLikeArgument_GetPointer_in_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + var arg = "original".AsSpan(); + proxy.Method(out arg); + Assume.That(interceptor.ObservedArg is ByRefLikeArgument); + var wrappedArg = (ByRefLikeArgument)interceptor.ObservedArg!; + var unwrappedArg = *(ReadOnlySpan*)wrappedArg.GetPointer(); + Assert.AreEqual("original", unwrappedArg.ToString()); + } + +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + // Should theoretically be as above (read comment there), but isn't. To be revisited later! + [Test] + [Ignore("Have not yet found out why byref-like `out` arguments are initially set to their default values unlike `ref` ones.")] + public void By_ref_like_out_arguments_can_be_restored_from_ByRefLikeArgument_Get_in_invocation() { var interceptor = new ObservingInterceptor(); var proxy = generator.CreateClassProxy(interceptor); var arg = "original".AsSpan(); proxy.Method(out arg); - Assert.IsNull(interceptor.ObservedArg); + Assume.That(interceptor.ObservedArg is ByRefLikeArgument>); + var wrappedArg = (ByRefLikeArgument>)interceptor.ObservedArg!; + var unwrappedArg = wrappedArg.Get(); + Assert.AreEqual("original", unwrappedArg.ToString()); } +#endif #endregion @@ -367,6 +500,43 @@ public void By_ref_like_out_arguments_cannot_be_set_by_target() #endregion + #region Memory safety + + // Byref-like arguments live exclusively on the evaluation stack. + // We need to make sure that references to them (such as those in `IInvocation.Arguments`) + // do not have a longer lifetime than the arguments themselves. + + [Test] + public unsafe void Cannot_use_ByRefLikeArgument_GetPointer_after_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + proxy.Method(default); + var byRefLikeArg = (ByRefLikeArgument)interceptor.ObservedArg!; + Assert.Throws(() => _ = byRefLikeArg.GetPointer()); + } + + [Test] + public void Cannot_use_ByRefLikeArgument_Get_after_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + proxy.Method(default); + var byRefLikeArg = (ReadOnlySpanArgument)interceptor.ObservedArg!; + Assert.Throws(() => _ = byRefLikeArg.Get()); + } + + [Test] + public void ByRefLikeArguments_are_erased_from_invocation_Arguments_after_invocation() + { + var interceptor = new ObservingInterceptor(); + var proxy = generator.CreateClassProxy(interceptor); + proxy.Method(default); + Assert.IsNull(interceptor.AllArguments![0]); + } + + #endregion + public class HasMethodWithSpanParameter { public string? RecordedArg; @@ -408,14 +578,18 @@ public virtual void Method(out ReadOnlySpan arg) public class ObservingInterceptor : IInterceptor { + public object?[]? AllArguments; public object? ObservedArg; public void Intercept(IInvocation invocation) { + AllArguments = invocation.Arguments; ObservedArg = invocation.Arguments[0]; } } } } +#pragma warning restore CS8500 + #endif diff --git a/src/Castle.Core/Castle.Core.csproj b/src/Castle.Core/Castle.Core.csproj index fa705deaf..80765867c 100644 --- a/src/Castle.Core/Castle.Core.csproj +++ b/src/Castle.Core/Castle.Core.csproj @@ -3,7 +3,7 @@ - net8.0;net462;netstandard2.0 + net9.0;net8.0;net462;netstandard2.0 @@ -37,7 +37,7 @@ - + diff --git a/src/Castle.Core/DynamicProxy/AbstractInvocation.cs b/src/Castle.Core/DynamicProxy/AbstractInvocation.cs index 6c0490640..ef8fe1f4c 100644 --- a/src/Castle.Core/DynamicProxy/AbstractInvocation.cs +++ b/src/Castle.Core/DynamicProxy/AbstractInvocation.cs @@ -129,6 +129,26 @@ public void Proceed() finally { currentInterceptorIndex--; + +#if FEATURE_BYREFLIKE + if (currentInterceptorIndex < 0) + { + // TODO: This will require further optimization; + // we should not iterate through the arguments array on a hot code path! + for (int i = 0; i < arguments.Length; ++i) + { + if (arguments[i] is ByRefLikeArgument byRefLikeArg) + { + // Invalidate the `ByRefLikeArgument` in case someone copied it away + // (i. e. let it escape the lifetime of the invocation and its arguments on the stack): + byRefLikeArg.Dispose(); + + // Reset the argument so the `ByRefLikeArgument` becomes eligible for GC sooner: + arguments[i] = null; + } + } + } +#endif } } diff --git a/src/Castle.Core/DynamicProxy/ByRefLikeArgument.cs b/src/Castle.Core/DynamicProxy/ByRefLikeArgument.cs new file mode 100644 index 000000000..2fa011b23 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/ByRefLikeArgument.cs @@ -0,0 +1,240 @@ +// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if FEATURE_BYREFLIKE + +#nullable enable + +namespace Castle.DynamicProxy +{ + using System; + using System.Collections.Concurrent; + using System.ComponentModel; + using System.Reflection; + using System.Runtime.CompilerServices; + + /// + /// Wraps a byref-like (ref struct) method argument + /// such that it can be placed in the array during interception. + /// + public unsafe class ByRefLikeArgument : IDisposable + { + private static readonly ConcurrentDictionary constructorMap = new(); + + internal static ConstructorInfo GetConstructorFor(Type byRefLikeType) + { + return constructorMap.GetOrAdd(byRefLikeType, static byRefLikeType => + { + Type? type = null; + + if (byRefLikeType.IsConstructedGenericType) + { + var typeDef = byRefLikeType.GetGenericTypeDefinition(); + if (typeDef == typeof(Span<>)) + { + var typeArg = byRefLikeType.GetGenericArguments()[0]; + type = typeof(SpanArgument<>).MakeGenericType(typeArg); + } + else if (typeDef == typeof(ReadOnlySpan<>)) + { + var typeArg = byRefLikeType.GetGenericArguments()[0]; + type = typeof(ReadOnlySpanArgument<>).MakeGenericType(typeArg); + } + } + +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + type ??= typeof(ByRefLikeArgument<>).MakeGenericType(byRefLikeType); +#else + type ??= typeof(ByRefLikeArgument); +#endif + + return type.GetConstructor([ typeof(void*) ])!; + }); + } + + protected void* ptr; + + /// + /// Do not use this! Only generated proxies should construct instances this type. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public ByRefLikeArgument(void* ptr) + { + this.ptr = ptr; + } + + /// + /// Gets an unmanaged pointer to the byref-like (ref struct) argument. + /// + /// + /// + /// You may only use the returned pointer during the first run through the interception pipeline. + /// After that, it will be invalid, because the argument that it referred to will be gone + /// from the evaluation stack. + /// + /// + /// In particular, if you intercept an method and make use of + /// to proceed through the pipeline again + /// after an , you may no longer access any byref-like arguments. + /// (.NET compilers would forbid any such attempts, too.) + /// + /// + /// Using the returned pointer beyond the lifetime of the byref-like argument + /// will cause undefined behavior, or an at best. + /// + /// + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Advanced)] + public void* GetPointer() + { + EnsureNotDisposed(); + + return ptr; + } + + public void Dispose() + { + ptr = null; + } + + protected void EnsureNotDisposed() + { + if (ptr == null) + { + throw new ObjectDisposedException( + message: "Byref-like method arguments are only available during the method call. " + + "This reference has been invalidated to prevent potentially unsafe access.", + objectName: null); + } + } + } + +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + + /// + /// Wraps a byref-like (ref struct) method argument + /// such that it can be placed in the array during interception. + /// + public unsafe class ByRefLikeArgument : ByRefLikeArgument where TByRefLike : allows ref struct + { + /// + /// Do not use this! Only generated proxies should construct instances this type. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public ByRefLikeArgument(void* ptr) + : base(ptr) + { + } + + /// + /// Gets the byref-like (ref struct) argument. + /// + /// + public ref TByRefLike Get() + { + EnsureNotDisposed(); + + return ref Unsafe.AsRef(ptr); + } + } + +#endif + + // The following two specializations for `Span` and `ReadOnlySpan` are provided + // because those two types have become so common in the Framework Class Library, and + // dealing with them through unmanaged pointers all the time would be cumbersome. + // We can provide a type-safe wrapper for them even on .NET 8. And we keep the types + // for .NET 9 (even though they're redundant) so downstream code can expect to always + // encounter a `[ReadOnly]SpanArgument<>` for `[ReadOnly]Span<>` regardless of + // whether they target .NET 8 or 9. + + /// + /// Wraps a method argument + /// such that it can be placed in the array during interception. + /// + public unsafe class ReadOnlySpanArgument +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + : ByRefLikeArgument> +#else + : ByRefLikeArgument +#endif + { + /// + /// Do not use this! Only generated proxies should construct instances this type. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public ReadOnlySpanArgument(void* ptr) + : base(ptr) + { + } + +#if !FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + /// + /// Gets the byref-like (ref struct) argument. + /// + /// + public ref ReadOnlySpan Get() + { + EnsureNotDisposed(); + +#pragma warning disable CS8500 + return ref *(ReadOnlySpan*)ptr; +#pragma warning restore CS8500 + } +#endif + } + + /// + /// Wraps a method argument + /// such that it can be placed in the array during interception. + /// + public unsafe class SpanArgument +#if FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + : ByRefLikeArgument> +#else + : ByRefLikeArgument +#endif + { + /// + /// Do not use this! Only generated proxies should construct instances this type. + /// + [CLSCompliant(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public SpanArgument(void* ptr) + : base(ptr) + { + } + +#if !FEATURE_ALLOWS_REF_STRUCT_ANTI_CONSTRAINT + /// + /// Gets the byref-like (ref struct) argument. + /// + /// + public ref Span Get() + { + EnsureNotDisposed(); + +#pragma warning disable CS8500 + return ref *(Span*)ptr; +#pragma warning restore CS8500 + } +#endif + } +} + +#endif diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs index e4ab715a3..e6aea8c5f 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ArgumentReference.cs @@ -39,7 +39,12 @@ public ArgumentReference(Type argumentType, int position) public override void EmitAddress(ILGenerator gen) { - throw new NotSupportedException(); + if (Position == -1) + { + throw new InvalidOperationException("ArgumentReference uninitialized"); + } + + gen.Emit(OpCodes.Ldarga_S, Position); } public override void Emit(ILGenerator gen) diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ReferencesToObjectArrayExpression.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ReferencesToObjectArrayExpression.cs index 67cb7b714..547c7b6db 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ReferencesToObjectArrayExpression.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/SimpleAST/ReferencesToObjectArrayExpression.cs @@ -21,6 +21,7 @@ namespace Castle.DynamicProxy.Generators.Emitters.SimpleAST using System.Reflection.Emit; using Castle.DynamicProxy.Internal; + using Castle.DynamicProxy.Tokens; internal class ReferencesToObjectArrayExpression : IExpression { @@ -49,13 +50,11 @@ public void Emit(ILGenerator gen) #if FEATURE_BYREFLIKE if (reference.Type.IsByRefLikeSafe()) { - // The by-ref-like argument value cannot be put into the `object[]` array, - // because it cannot be boxed. We need to replace it with some other value. - - // For now, we just erase it by substituting `null`: - gen.Emit(OpCodes.Ldnull); + // We cannot box a byref-like argument directly, so we wrap it as + // `BoxedByRefLikeArgument` instead (which references it using a pointer). + reference.EmitAddress(gen); + gen.Emit(OpCodes.Newobj, ByRefLikeArgument.GetConstructorFor(reference.Type)); gen.Emit(OpCodes.Stelem_Ref); - continue; } #endif