dotnet / dotnet/runtime

[API Proposal]: Add support for `ValueTypes` when using `UnsafeAccessorTypeAttribute`

Open
#121,655 9 comments 4 reactions 0 assignees View on GitHub
api-suggestion area-System.Runtime.CompilerServices
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.