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

[API Proposal]: Methods to support ByRefLike types in "is-type" IL sequences #105435

Open
AaronRobinsonMSFT opened this issue Jul 24, 2024 · 17 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.CompilerServices
Milestone

Comments

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Jul 24, 2024

Background and motivation

Due to the non-boxing requirment of ByRefLike types, it is complicated to express generic IL instructions for methods where the allows ref struct constraint is applied, given existing semantics of certain IL instructions. This is particularly limiting when attempting an "is-type" check (that is, x is Y y). These APIs will be provided and their semantics defined in coordination with the Roslyn team to help us avoid augmenting the definitions of certain IL instruction, in specific sequences - see option two in the below design notes. ECMA-335 doesn't dictate details of .NET APIs, so adding APIs avoids any ECMA-335 augmentation with respect to IL instruction semantics.

See further design notes in byreflike-generics.md.

API Proposal

namespace System.Runtime.CompilerServices
{
    public static class RuntimeHelpers
    {
        // Replacement for the [box; isinst; brfalse/true] sequence.
        public static bool IsInstanceOf<TFrom, TTo>(TFrom source)
            where TFrom: allows ref struct
            where TTo: allows ref struct;

        // Replacement for the [box; isinst; unbox.any] sequence.
        // Would throw InvalidCastException for invalid use at run-time.
        // For example:
        //  TFrom: RS, TTo: object      => always throws
        //  TFrom: RS, TTo: <interface> => always throws
        public static TTo CastTo<TFrom, TTo>(TFrom source)
            where TFrom: allows ref struct
            where TTo: allows ref struct;
    }
}

API Usage

This API is not meant to be used directly by user code. It called by Roslyn generated code to perform "is-type" checks involving ByRefLike types.

Instead of the following IL sequences for "is-type":

// Type check
ldarg.0
    box <Source>
    isinst <Target>
    brfalse.s NOT_INST

// Unbox and store unboxed instance
ldarg.0
    box <Source>
    isinst <Target>
    unbox.any <Target>
stloc.X

NOT_INST:

The following C# will now be possible:

TTo result;
if (RuntimeHelpers.IsInstanceOf<TFrom, TTo>(source))
{
    result = RuntimeHelpers.CastTo<TFrom, TTo>(source);
}

Alternative Designs

The troublesome IL sequences can be identified to support ByRefLike types when detected by the JIT or Interpreter. This requires changing the semantic meaning of some IL instructions, but only in certain sequences and then updating ECMA-335. This was the approach attempted in .NET 9 and has proven to be very difficult to reconcile with the broader ecosystem with the needed language.

Risks

No response

@AaronRobinsonMSFT AaronRobinsonMSFT added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.CompilerServices labels Jul 24, 2024
@AaronRobinsonMSFT AaronRobinsonMSFT added this to the 10.0.0 milestone Jul 24, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices
See info in area-owners.md if you want to be subscribed.

@AaronRobinsonMSFT
Copy link
Member Author

@lambdageek
Copy link
Member

lambdageek commented Jul 25, 2024

  public static TTo CastTo<TFrom, TTo>(TFrom source)
            where TFrom: allows ref struct
            where TTo: allows ref struct;

Would it be more helpful to have the same signature as ref TTo Unsafe.As<TFrom,TTo>(ref TFrom source) where TFrom: allows ref struct where TTo: allows ref struct ?

  1. Do we need both IsInstanceOf and CastTo? would it be ok to just have one intrinsic that either returns ref source or a null reference?

@AaronRobinsonMSFT
Copy link
Member Author

Would it be more helpful to have the same signature as ref TTo Unsafe.As<TFrom,TTo>(ref TFrom source) where TFrom: allows ref struct where TTo: allows ref struct ?

Agree, the refs might make more sense.

Do we need both IsInstanceOf and CastTo? would it be ok to just have one intrinsic that either returns ref source or a null reference?

Yes, we need both since we can't return null if TTo can be ByRefLike. That is basically the fundamental issue with the isinst insrtuction.

@jkotas
Copy link
Member

jkotas commented Jul 25, 2024

Agree, the refs might make more sense.

I do not think that it works well with Nullable<>.

@lambdageek
Copy link
Member

Yes, we need both since we can't return null if TTo can be ByRefLike. That is basically the fundamental issue with the isinst insrtuction.

I meant if we have a single TryCastTo operation that takes a ref TFrom and returns ref TTo:

  ref TTo TryCastTo<TFrom,TTo>(ref TFrom source);

Can't we return a null reference on failure? We have ref T Unasfe.NullRef<T>() where T : allows ref struct. So a null reference of a byreflike seems like something we support elsewhere.

But I see this won't work because...

I do not think that it works well with Nullable<>

I see. Because if I have int? x then IsInstanceOf<int?, int>(x) should be true and CastTo<int?, int>(x) should do the unboxing. It's not always an identity like Unsafe.As

In that case I think these operations need better names.

@AaronRobinsonMSFT
Copy link
Member Author

I see. Because if I have int? x then IsInstanceOf<int?, int>(x) should be true and CastTo<int?, int>(x) should do the unboxing. It's not always an identity like Unsafe.As

Can't we just define the semantics as the Rolsyn team prefers? I don't see why the Nullable<> doesn't work in this case. Or was that specifically for the TryCastTo example?

@lambdageek
Copy link
Member

Or was that specifically for the TryCastTo example?

yes, I meant my TryCastTo suggestion won't work with nullables - as Jan pointed out - since in that case we want to actually do an unboxing, not just return the input but with a different type.

@jkotas
Copy link
Member

jkotas commented Jul 25, 2024

Can't we just define the semantics as the Roslyn team prefers?

Right. These APIs are specifically designed to support Roslyn pattern matching. The APIs are not meant to be callable by the ordinary user code (nothing prevents that though). The API proposal should mention that.

This:

U M<T, U>(T t) where T: allows ref struct where U: allows ref struct
{
    if (t is U u)
        return u;
    return default;
}

is going to be lower by Roslyn into something like this:

U M<T, U>(T t) where T: allows ref struct where U: allows ref struct
{
    if (RuntimeHelpers.IsInstanceOf<T,U>(t))
    {
        U u = RuntimeHelpers.CastTo<T,U>(t);
        return u;
    }
    return default;
}

The APIs need to maintain the behavior of the existing C# pattern matching that dictates how they need to behave for non-byref like types. It is why it is not feasible to change the arguments to byrefs (consider RuntimeHelpers.CastTo<int,int?> with byref arguments).

@jkotas
Copy link
Member

jkotas commented Jul 25, 2024

In that case I think these operations need better names.

I agree we may need better names. Are there names we can borrow from C# spec?

@hamarb123
Copy link
Contributor

What happened to the "Special IL sequences"? Will they still be supported? Will they be emitted on .NET 9 only? Are they going to be added to the ECMA-335 addendum? Thanks!

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Sep 27, 2024

What happened to the "Special IL sequences"? Will they still be supported? Will they be emitted on .NET 9 only? Are they going to be added to the ECMA-335 addendum? Thanks!

The special IL sequences were not implemented and their specification removed. These APIs are designed to avoid the complexity that we faced in trying to special case these IL sequences and reconcile them with higher level language semantics.

@hamarb123
Copy link
Contributor

hamarb123 commented Sep 27, 2024

So, presumably you will not be able to write code that would require such constructs in 9, but will be able to in .NET 10 (using these APIs)?

@AaronRobinsonMSFT
Copy link
Member Author

Yes, that is the current plan.

@hez2010
Copy link
Contributor

hez2010 commented Nov 16, 2024

What happened to the "Special IL sequences"? Will they still be supported?

It's already working if you write IL manually: https://godbolt.org/z/rqKP71d6f

@jkotas
Copy link
Member

jkotas commented Nov 16, 2024

The special IL sequences are invalid IL today. Invalid IL has undefined behavior. It may appear to work, but it is not guaranteed to work.

@Enderlook
Copy link

Why not include?

public static bool TryCastTo<TFrom, TTo>(TFrom source, out TTo destination)
    where TFrom: allows ref struct
    where TTo: allows ref struct;

Doing so would save one check, otherwise IsInstanceOf does a check, and CastTo checks again.
And unlike returning ref TTo this would probably have issues no with Nullable<T>, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime.CompilerServices
Projects
None yet
Development

No branches or pull requests

6 participants