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

C# needs some way to transition between generic type restrictions without boxing or reflection #7909

Closed
Enichan opened this issue Dec 13, 2023 · 6 comments

Comments

@Enichan
Copy link

Enichan commented Dec 13, 2023

Background: I'm working on a library for unmanaged memory arenas in C#. These arenas take two generic types:

  1. Types where T : unmanaged
  2. Types where T : unmanaged, IArenaContents

I want methods to allocate both types to the arena. Not all types are under control by the user after all (stuff like primitive types like int for example) so I can't simply restrict everything to IArenaContents. Under regular .NET this leaves me with two choices:

  1. Have separate methods for both
  2. Have one method for unmanaged, and dynamically detect IArenaContents instances and use reflection to dispatch to the relevant interface methods. Doing this without boxing takes a bit of work, but is possible.

Option number 1 is a bad option. Methods can't be overloaded by generic type restrictions, so you'd wind up with two differently named methods for the two different types. Not only that, you could pass structs which implement IArenaContents into the method that isn't meant for it. You can't limit generic types by exclusion, so the best you can do is check this at runtime and then throw an exception. This means creating a more brittle implementation because errors won't be caught at compile-time, and a small mistake is enough to take down an entire application.

That leaves option 2, which while annoying does actually work. You have one method, and do reflection shenanigans to call the interface, even without boxing if you put your back into it. This is the clearly superior option.

Problem: NativeAOT is unhappy about option 2. Especially once you add in the code for unboxed interface method calls there's really no guarantee that you can safely ignore the reflection warnings it rightfully throws at you. Unfortunately there is, to my knowledge, no way to transition from a generic method call T : unmanaged to a generic, unboxed, method call T : unmanaged, IWhatever. I've been code golfing for days without any success at making this work.

In the end I managed to solve this problem specifically for the unmanaged case by having IArenaContents implementors return an object with methods that take an IntPtr, cast it to their type T*, and then invoke the method. I can cache this method object for each type, and indirectly call the methods (sans boxing) through IntPtr.

This solution is also not ideal. It works for unmanaged, but it will not work for struct as there is no generic unboxed type to cast them to and back like IntPtr. In this case, there would simply be no fix for this issue, and it would result in worse, less robust code and more crashing bugs from runtime type checks.

The ideal solution for this would be non-NativeAOT specific and would involve C# in general obtaining some kind of "trust me bro" syntax that lets you forcibly call struct, ISomeInterface generic functions (without boxing, obviously.) I've been informed that the following pattern can do that, but that it's reliant on the JIT, brittle, and I couldn't actually find any confirmation that it even works, works in NativeAOT (which does not have a JIT), or let alone works reliably enough to be an actual solution in any circumstance.

if (myStruct is ISomeInterface)
    ((ISomeInterface)myStruct).MyMethod();

Of course none of this has ever been a big issue because reflection could be used to (jankily) solve the problem, but now with NativeAOT taking a more prominent role in the .NET ecosystem this patchwork solution is no longer sufficient for the problem.

@ghost ghost added the untriaged label Dec 13, 2023
@huoyaoyuan
Copy link
Member

#6308

@MichalStrehovsky
Copy link
Member

Yep. this is a common problem. It also showed up:

Probably elsewhere that I don't remember.

@MichalPetryka
Copy link

dotnet/runtime#89439 (comment) could possibly solve this too.

@ghost
Copy link

ghost commented Dec 15, 2023

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

Issue Details

Background: I'm working on a library for unmanaged memory arenas in C#. These arenas take two generic types:

  1. Types where T : unmanaged
  2. Types where T : unmanaged, IArenaContents

I want methods to allocate both types to the arena. Not all types are under control by the user after all (stuff like primitive types like int for example) so I can't simply restrict everything to IArenaContents. Under regular .NET this leaves me with two choices:

  1. Have separate methods for both
  2. Have one method for unmanaged, and dynamically detect IArenaContents instances and use reflection to dispatch to the relevant interface methods. Doing this without boxing takes a bit of work, but is possible.

Option number 1 is a bad option. Methods can't be overloaded by generic type restrictions, so you'd wind up with two differently named methods for the two different types. Not only that, you could pass structs which implement IArenaContents into the method that isn't meant for it. You can't limit generic types by exclusion, so the best you can do is check this at runtime and then throw an exception. This means creating a more brittle implementation because errors won't be caught at compile-time, and a small mistake is enough to take down an entire application.

That leaves option 2, which while annoying does actually work. You have one method, and do reflection shenanigans to call the interface, even without boxing if you put your back into it. This is the clearly superior option.

Problem: NativeAOT is unhappy about option 2. Especially once you add in the code for unboxed interface method calls there's really no guarantee that you can safely ignore the reflection warnings it rightfully throws at you. Unfortunately there is, to my knowledge, no way to transition from a generic method call T : unmanaged to a generic, unboxed, method call T : unmanaged, IWhatever. I've been code golfing for days without any success at making this work.

In the end I managed to solve this problem specifically for the unmanaged case by having IArenaContents implementors return an object with methods that take an IntPtr, cast it to their type T*, and then invoke the method. I can cache this method object for each type, and indirectly call the methods (sans boxing) through IntPtr.

This solution is also not ideal. It works for unmanaged, but it will not work for struct as there is no generic unboxed type to cast them to and back like IntPtr. In this case, there would simply be no fix for this issue, and it would result in worse, less robust code and more crashing bugs from runtime type checks.

The ideal solution for this would be non-NativeAOT specific and would involve C# in general obtaining some kind of "trust me bro" syntax that lets you forcibly call struct, ISomeInterface generic functions (without boxing, obviously.) I've been informed that the following pattern can do that, but that it's reliant on the JIT, brittle, and I couldn't actually find any confirmation that it even works, works in NativeAOT (which does not have a JIT), or let alone works reliably enough to be an actual solution in any circumstance.

if (myStruct is ISomeInterface)
    ((ISomeInterface)myStruct).MyMethod();

Of course none of this has ever been a big issue because reflection could be used to (jankily) solve the problem, but now with NativeAOT taking a more prominent role in the .NET ecosystem this patchwork solution is no longer sufficient for the problem.

Author: Enichan
Assignees: -
Labels:

untriaged, area-NativeAOT-coreclr, needs-area-label

Milestone: -

@agocke agocke changed the title Native AOT (or C#) needs some way to transition between generic type restrictions without boxing or reflection C# needs some way to transition between generic type restrictions without boxing or reflection Feb 4, 2024
@agocke
Copy link
Member

agocke commented Feb 4, 2024

Agreed that this is a fundamental C# restriction. I'm moving to csharplang.

@agocke agocke transferred this issue from dotnet/runtime Feb 4, 2024
@333fred
Copy link
Member

333fred commented Feb 4, 2024

Duplicate of #6308.

@333fred 333fred closed this as not planned Won't fix, can't repro, duplicate, stale Feb 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

No branches or pull requests

6 participants