Skip to content

Conversation

@stakx
Copy link
Member

@stakx stakx commented Dec 19, 2025

Unlike my earlier draft #664, this PR will not require DynamicProxy user code to set up any value converters for byref-like parameters.

Instead, any byref-like argument will automatically get substituted in IInvocation with a "reference" type:

  • SpanArgument<T> SpanProxy<T> SpanReference<T> for Span<T> values
  • ReadOnlySpanArgument<T> ReadOnlySpanProxy<T> ReadOnlySpanReference<T> for ReadOnlySpan<T> values
  • ByRefLikeProxy ByRefLikeReference on .NET 8 for any non-span by-ref-like values
  • ByRefLikeArgument<TByRefLike> ByRefLikeProxy<TByRefLike> ByRefLikeReference<TByRefLike> on .NET 9+ for any non-span by-ref-like values of type TByRefLike

Each of these (except the non-generic ByRefLikeReference) has a ref-returning Value property for accessing the actual value.

These class types are essentially references to the actual byref-like parameters. They use unmanaged pointers (void*) under the hood. I've added a big comment in the ByRefLikeReference.cs code file explaining why I think this is safe.

public interface IFoo
{
    void A(Span<char> characters);
    void B(ref Span<char> characters);
}

public void FooAInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation);
    {
        var charactersRef = invocation.Arguments[0] as SpanReference<char>;
        Span<char> characters = charactersRef.Value;
    }
}

public void FooBInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation);
    {
        var charactersRef = invocation.Arguments[0] as SpanReference<char>;
        Span<char> characters = charactersRef.Value;
        charactersRef.Value = characters[0..1]);
    }
}

This should now be ready for merging:

  • add support for reading/writing byref-like argument values and propagating them through the interception pipeline
  • add support for byref-like return values
  • update / replace unit tests in ByRefLikeTestCase (which still expect byref-like parameters to default / get nullified)
  • write a documentation article on how to properly interact with byref-like arguments in IInvocation → preview it here
  • update ref/ contract files
  • update the changelog

Will fix #651 and close #663 once completed and merged.

@stakx stakx added this to the v6.0.0 milestone Dec 19, 2025
@stakx stakx self-assigned this Dec 19, 2025
@stakx stakx marked this pull request as draft December 19, 2025 01:30
@stakx stakx force-pushed the byref-like-arguments branch 2 times, most recently from 75d36f1 to 34dde0e Compare December 19, 2025 10:42
@stakx stakx mentioned this pull request Jan 1, 2026
@stakx stakx force-pushed the byref-like-arguments branch from 34dde0e to 9abd5df Compare January 25, 2026 23:47
{
if (checkType != type)
{
throw new AccessViolationException();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we'll probably be adding a documentation page regarding by-ref-like values during interception, perhaps we could add exception messages with a link to the documentation page for more info...?

Comment on lines 279 to 280
// TODO: perhaps we should cache these `ConstructorInfo`s?
ConstructorInfo proxyCtor = typeof(ByRefLikeProxy<>).MakeGenericType(dereferencedArgumentType).GetConstructors().Single();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should implement ConstructorInfo caching here, as the "todo" comment suggest.

Since Span<T> and ReadOnlySpan<T> are commonly encountered types in the FCL, perhaps constructed generic types deriving from these should also be in the cache.

Comment on lines +331 to +336
new MethodInvocationExpression(
ThisExpression.Instance,
InvocationMethods.GetArgumentValue,
new LiteralIntExpression(i)),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative to calling IInvocation.GetArgumentValue would be to query IInvocation.Arguments once, and then access the returned arguments array directly for every by-ref-like parameter? That might be more efficient for methods having more than one by-ref-like-typed parameter.

@stakx stakx force-pushed the byref-like-arguments branch 3 times, most recently from fc07c42 to e839375 Compare January 27, 2026 20:24
stakx added 10 commits January 31, 2026 13:09
I originally called them "proxies" instead of "references", which seemed
a little misleading given that DynamicProxy proxies provide the same
public interface as the proxied types, which isn't the case with these
new types.

Alternative names considered were "value accessors" and "arguments". The
former would lead to long type names (`ByRefLikeValueAccessor`), and the
latter would be inaccurate once we are going to start using these types
for `IInvocation.ReturnValue`, too.

Also, I originally distinguished between an internal set of class types,
and a set of public-facing interfaces. However there seemed to be little
benefit to having two parallel type hierarchies. On the contrary: users
observing (say) a `SpanReference` instance in the debugger and then
being told in the documentation to access it thru the `ISpanReference`
interface doesn't seem particularly user-friendly (nor discoverable).
Therefore I went with the simpler design: I abandoned the interfaces.
Previously, four code locations were identified where "marshalling" of
byref-like values must take place:

     +--------+    1    +--------------+    2    +---------------+
     |        | ------> |              | ------> |               |
     | caller |         | interceptors |         | target method |
     |        | <------ |              | <------ |               |
     +--------+    4    +--------------+    3    +---------------+

 1. between calling user code and the interception pipeline
    (typed byref-like arguments are put into `IInvocation`)

 2. between the interception pipeline and target methods
    (byref-like values are read from `IInvocation` into typed
    parameters)

 3. between target methods and the interception pipeline
    (typed byref-like arguments are put back into `IInvocation`)

 4. between the interception pipeline and calling user code
    (byref-like values are read from `IInvocation` back into typed
    parameters)

Currently, byref-like values simply get "nullified" (replaced with
`null` or their default value), but we want to change this behavior to
one that preserves them, so we replace the existing tests with new ones.

The general idea behind the new test fixtures is to test every one of
the above four boundaries in isolation.

We also add a test fixture for certain edge cases.

All of the added tests except those in `ProxyableTestCase` will fail
at this time.
... instead of nullifying them by making use of the `ByRefLikeReference`
family of utility types created earlier.
... when byref-likes are present, as they will be gone from the evalua-
tion stack after the intercepted method has returned for the first time.

(DynamicProxy could in theory be made to tolerate nullified `ByRefLike-
Reference` substitutes, but this doesn't seem worth the effort: after
all, `IInvocationProceedInfo` is intended mostly for async interception
scenarios, and even C# disallows `ref struct`s in `async` methods.)
... by creating defensive copies of byref-like `in` arguments and ref-
erencing those copies instead of the actual parameters.
@stakx stakx force-pushed the byref-like-arguments branch from a3126c6 to c9f7a72 Compare January 31, 2026 14:57
@stakx stakx changed the title Add support for by-ref-like (ref struct) parameter types such as Span<T> and ReadOnlySpan<T> Add support for byref-like (ref struct) parameter and return types such as Span<T> and ReadOnlySpan<T> Jan 31, 2026
@stakx stakx marked this pull request as ready for review January 31, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support by-ref-like (ref struct) parameter types such as Span<T> and ReadOnlySpan<T> InvalidProgramException when proxying MemoryStream with .NET 7

1 participant