Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support by-ref-like (ref struct) parameter types such as Span<T> and ReadOnlySpan<T> #663

Open
stakx opened this issue Sep 1, 2023 · 10 comments

Comments

@stakx
Copy link
Member

stakx commented Sep 1, 2023

DynamicProxy does not currently support those because by-ref-like values cannot be boxed to be put in the object[] IInvocation.Arguments array.

I haven't yet been able to think of a general way how all by-ref-like types (including user-defined ones) could be supported, ecause of their various limitations. If anyone has ideas, I'd be interested in hearing them!

However, it might be fairly easy to at least add support for Span<T> and ReadOnlySpan<T> specifically. Those types are becoming more and more common in the .NET FCL, so while not an ideal solution, it might still be sufficient for most use cases:

We could introduce a new type in DynamicProxy's namespace:

public interface ISpanMarshaller  // NOTE: outdated API proposal!
{
    object BoxSpan<T>(Span<T> span);
    object BoxReadOnlySpan<T>(ReadOnlySpan<T> readOnlySpan);
    ReadOnlySpan<T> UnboxReadOnlySpan<T>(object boxedReadOnlySpan);
    Span<T> UnboxSpan<T>(object boxedSpan);
}

Then we could introduce a new property to ProxyGenerationOptions, ISpanMarshaller SpanMarshaller { get; set; }, which would get injected into generated proxies so they could use it to transfer spans into and out of the object[] IInvocation.Arguments array.

I don't really like an addition that deals with two very specific types, but they are becoming more and more common, and I haven't yet been able to come up with a more general mechanism.

Update

See the PR linked further down for an updated proposal that supports arbitrary by-ref-like types, including user-defined ones.

Opinions, or alternate ideas, anyone?

@stakx

This comment was marked as outdated.

@stakx
Copy link
Member Author

stakx commented Sep 1, 2023

Scratch the above proposal, I think I can actually come up with one that works for arbitrary by-ref-like types. I'll post an updated feature proposal shortly.

@stakx
Copy link
Member Author

stakx commented Sep 2, 2023

I've put something together that appears to work... see the draft PR linked above (#664) for two short code examples how this would be used in practice. I'd be glad for some feedback (and whether the suggested API would actually be usable e. g. in downstream testing libraries).

/cc @thomaslevesque & @blairconrad (FakeItEasy), @dtchepak & @zvirja (NSubstitute), @jonorossi

@stakx stakx self-assigned this Sep 2, 2023
@thomaslevesque
Copy link
Contributor

Hey @stakx, nice job!
Using converters is an interesting approach. I think it would work just fine for mocking scenarios, where performance isn't typically a critical concern. In other scenarios, the performance impact of converting spans to arrays is something to keep in mind. I don't see a better way, though...
Something else to consider: there might not always be a sensible way of converting a ref struct to an object and still be able to convert back (I'm thinking of Utf8JsonReader, for instance).
From FakeItEasy's perspective, I think the converters for Span<T>/ReadOnlySpan<T> would be provided directly by the library, and we could expose extension points to provide additional converters.
BTW, is there a particular reason why converters don't implement an interface in your proposal?

@stakx
Copy link
Member Author

stakx commented Sep 4, 2023

Thanks for the feedback @thomaslevesque.

is there a particular reason why converters don't implement an interface in your proposal?

Yes, there is. ref struct types cannot be used as generic type arguments. So a proper interface type such as IByRefLikeConverter<TByRefLike> could never be instantiated.

@stakx
Copy link
Member Author

stakx commented Sep 4, 2023

there might not always be a sensible way of converting a ref struct to an object and still be able to convert back

True. I don't see what we could do about that, though... do you? If I understand your concern correctly, this would make it only impossible to do round-tripping of ref struct values during interception. "Half-trips" should still work since the Box / Unbox methods are only looked up & called when needed; in theory, if you don't need the conversion back from object (i.e. your ref struct type does not figure as a method return type or in a ref/out parameter) you wouldn't even need to provide the Unbox conversion method.

@stakx
Copy link
Member Author

stakx commented Sep 4, 2023

the performance impact of converting spans to arrays is something to keep in mind.

Indeed. Which is why the copying behavior shouldn't be the default behavior (for spans), and why I thought it would be a good idea for the converter selectors to pinpoint single method parameters, so you're free to choose to nullify most spans except where retaining the value somehow – e.g. as an array copy – is actually important. Being only able to set up a single converter globally that gets used for all by-ref-like parameters equally might be too coarse-grained.

I've also briefly considered doing weird unsafe pointer-based stuff (similar to what System.Runtime.CompilerServices.Unsafe does) instead of converters that simply convert to/from object, but apart from being unsure if it would've even worked, I don't think it would've resulted in a very nice, easy to understand API).

@blairconrad
Copy link
Contributor

blairconrad commented Sep 4, 2023

Thanks, @stakx. Slightly over my head, but in general, I think I understand the approach and the tradeoffs. Appreciate your work. Thanks for thinking of us to invite for comments.

The possible performance optimization by specifying the position of the parameter is interesting, and not something I would've thought of.

Just to make sure I understand everything, if I combine that power with a comment @thomaslevesque made earlier ("From FakeItEasy's perspective, I think the converters for Span<T>/ReadOnlySpan<T> would be provided directly by the library"), then I expect FakeItEasy would end up providing a selector not unlike the sample one you had:

// this will get invoked during proxy type generation:
class ConverterSelector : IByRefLikeConverterSelector
{
    public Type SelectConverterType(MethodInfo method, int parameterPosition, Type parameterType)
    {
        return parameterType == typeof(Span<byte>) ? typeof(CopyByteSpanConverter) : null;
    }
}

where we'd end up ignoring the parameterPosition.

@stakx
Copy link
Member Author

stakx commented Sep 4, 2023

@blairconrad, I suppose that you'd typically configure a converter out-of-the-box that checks for both Span<*> and ReadOnlySpan<*>, and optionally lets user code opt into the conversion process for all other types. And yes, it should typically suffice to only check the type and ignore the MethodInfo and the parameter position.

For mocking libraries, the selector's method body could look like this:

if (parameterType.IsGenericType)
{
    var def = parameterType.GetGenericTypeDefinition();
    var args = paramrterType.GetGenericArguments();
    if (def == typeof(Span<>))
    {
        return typeof(CopySpanConverter<>).MakeGenericType(args);
    }
    else if (def == typeof(ReadOnlySpan<>)
    {
        // as above but with a CopyReadOnlySpanConverter<> instead
    }
}

// for all other by-ref-like types, either
// * delegate to user-provided selector function; or
// * return null to let DynamicProxy nullify everything.

(Btw., I'm not 100% sure yet whether the Box method's parameter list is going to stay that way; it might also be possible to pass in a ParameterInfo which would tie those three parameters nicely together.)

@thomaslevesque
Copy link
Contributor

Yes, there is. ref struct types cannot be used as generic type arguments. So a proper interface type such as IByRefLikeConverter<TByRefLike> could never be instantiated.

Ah, yes, of course. Thanks for the reply!

True. I don't see what we could do about that, though... do you?

No... I'm not even sure there is a solution. I'm just pointing out a limitation.

If I understand your concern correctly, this would make it only impossible to do round-tripping of ref struct values during interception. "Half-trips" should still work since the Box / Unbox methods are only looked up & called when needed; in theory, if you don't need the conversion back from object (i.e. your ref struct type does not figure as a method return type or in a ref/out parameter) you wouldn't even need to provide the Unbox conversion method.

Yeah, half-trips would probably work in most cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants