[API Proposal]: Add support for `ValueTypes` when using `UnsafeAccessorTypeAttribute`
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
*(Consolidated from [#129459](https://github.com/dotnet/runtime/issues/129459) and related discussion)*
### Current State
`UnsafeAccessorType` intentionally disallows value types in **all** positions. This was a deliberate design choice to avoid piecemeal support and maintain a clean, consistent rule surface.
Minimal repo is at https://github.com/almightyju/dotnet-unsafeaccessortype
### Design Goal
Establish a simple, memorable rule:
> When using `UnsafeAccessorType`, the placeholder for unspeakable **reference types** is `object`; the placeholder for unspeakable **value types** is `TypedReference`.
Using `object` for reference types imposes no safety or performance cost. Using `object` for value types would introduce an implicit **boxing cost** that is unacceptable for a performance-oriented API.
### Why `TypedReference`?
[`TypedReference`](https://learn.microsoft.com/dotnet/api/system.typedreference) can carry both a pointer to the data and the runtime type, avoiding boxing. However, `TypedReference` itself needs modernization (see [#26186](https://github.com/dotnet/runtime/issues/26186)) before it can serve this role cleanly.
### Key Problems to Solve
#### 1. Return values ("out" problem)
When an unspeakable value type is *returned*, the caller needs a block of memory — ideally on the stack — sized and typed correctly to receive the value. The caller doesn't know the concrete type at compile time, so the JIT or a runtime helper would need to:
- Determine the concrete type at JIT time
- Allocate appropriately-sized stack space before the call
- Return the result wrapped in a `TypedReference`
#### 2. Parameter values ("in" problem)
When passing an unspeakable value type *into* a method, the caller must already hold an instance. That instance most likely came from a prior "out/ret" call, so solving the return path largely solves the input path as well.
#### 3. `ref struct` support
For non-`ref struct` value types, a less optimal fallback exists: allocate a `T[1]` on the heap and point the `TypedReference` there. This **does not work** for `ref struct` types, which cannot live on the managed heap. A general solution must handle `ref struct` too.
#### 4. JIT involvement
A JIT helper could be taught to:
- Know `T` at JIT time
- Allocate stack space sized for `T` prior to the call
- Pass/receive via `TypedReference`
This raises questions about how deep the JIT changes need to go and whether modern C# mechanics (e.g., the `scoped` keyword) can be leveraged to enforce safety around the lifetime of the stack-allocated memory.
#### 5. NativeAOT compatibility
Any solution must work under NativeAOT, where JIT-time tricks aren't available in the traditional sense. The AOT compiler would need equivalent support.
#### 6. The "recreated struct" pattern
[Comment](https://github.com/dotnet/runtime/issues/121655#issuecomment-4063677450) suggests recreation of the private struct layout in their own code and use that as the signature type. While functional, this is fragile and any layout change in the target struct silently breaks the caller at runtime, not compile time.
### Why Not Piecemeal Support (for example, Static Methods Only)?
Static methods on value types don't actually involve value type ABI, so they *could* be enabled independently. However, the runtime team prefers a unified approach to avoid:
- Multiple special-case rules that are hard to remember
- Encouraging patterns that work for statics but fail confusingly for instance members
- Creating sharp edges in an already-unsafe API
The "perfect is the enemy of the good" counter argument is acknowledged. A compelling blocked-business-scenario or demonstrated performance need could justify enabling static methods earlier.
### Open Questions
1. Should the `scoped` keyword or other lifetime annotations be used to constrain `TypedReference` usage and prevent dangling references?
2. What is the right level of JIT support vs. runtime helper support?
3. Can the solution be designed so that future `TypedReference` improvements ([#26186](https://github.com/dotnet/runtime/issues/26186)) compose naturally?
4. How do we handle the case where users need to pass/return unspeakable value types through multiple layers of `UnsafeAccessor` calls?
5. Should we replace `TypedReference` with a new type?
Contributor guide
Assessment
This issue has not been assessed yet.