Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 21 additions & 46 deletions proposals/unsafe-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ we are trying to make it so that anything you are required to do when you are op

The `safe` modifier can be applied to all declarations which allow `unsafe` to mark them as *requires-unsafe*.
It is disallowed to apply both the `safe` and `unsafe` modifier on the same declaration.
Like `unsafe` modifier, the `safe` modifier must match on both parts of partial declarations.
Other restrictions that apply to `unsafe` and `safe` modifiers are specified in section [Unsafe modifiers and contexts](#unsafe-modifiers-and-contexts).

The compiler requires an explicit `safe` or `unsafe` modifier on [`extern` members](#extern) and [fields in explicit layout](#fields).
Allowing `safe` even on declarations where it is not required (and hence has no effect for the compiler)
is motivated by source generators, e.g., [LibraryImport](#answered-allow-safe-on-non-extern-members-libraryimport).
Expand Down Expand Up @@ -280,13 +281,13 @@ i.e., an `unsafe` constructor may call a *requires-unsafe* `base` or `this` cons
Types with parameterless *requires-unsafe* constructors do not satisfy the `new()` constraint.
Similarly and in addition, structs with parameterless *requires-unsafe* constructors do not satisfy the `struct` constraint.

`unsafe` on a member is _not_ applied to any nested anonymous or local functions inside the member.
`unsafe`/`safe` on a member is _not_ applied to any nested anonymous or local functions inside the member.
The same goes for anonymous and local functions declared inside of an `unsafe` block
(they are still in an `unsafe` context as always, but they don't become *requires-unsafe*).
To mark a local function as *requires-unsafe*, it must manually be marked as `unsafe`.
Lambdas cannot be marked *requires-unsafe* (the `unsafe` keyword is disallowed on them).

When a member is `partial`, both parts must agree on the `unsafe` modifier, unchanged from C# rules today.
When a member is `partial`, both parts must agree on the `unsafe`/`safe` modifier, unchanged from C# rules today.

```cs
partial class C1
Expand All @@ -302,7 +303,12 @@ partial class C1
}
```

For properties, `get` and `set/init` accessors can be independently declared as `unsafe`; marking the entire property as `unsafe` means that both the `get` and `set/init` accessors are unsafe.
For properties, `get` and `set`/`init` accessors can be independently declared as `unsafe`/`safe`.
If the accessors don't have the `unsafe`/`safe` modifier, they inherit it from the property.
Similar to `readonly` modifier, we introduce the following restrictions for properties and accessors:
- `unsafe`/`safe` modifiers can be applied either to the property or its accessors, not both.
- It is an error to apply the same `unsafe`/`safe` modifier to all of the property accessors (that modifier should be applied to the property instead).

It is currently not possible to place any modifiers on event accessors, and this proposal doesn't change that, i.e., `add` and `remove` event accessors cannot be independently declared as `unsafe`.
Only if the entire event is marked as `unsafe`, it means that the accessors are unsafe; otherwise they are safe.

Expand All @@ -326,9 +332,7 @@ It is an error to apply the `MemorySafetyRulesAttribute` or `RequiresUnsafeAttri

The compiler ignores `RequiresUnsafeAttribute`-marked members from assemblies that are using the legacy memory safety rules (instead, the [compat mode](#compat-mode) is used there).

When a non-type member is marked as `unsafe`, the compiler will synthesize a `RequiresUnsafeAttribute` application on the member in metadata.
When a user-facing *requires-unsafe* member generates hidden members, such as an auto-property's get/set methods,
both the user-facing member and any hidden members generated by that user-facing member are all *requires-unsafe*, and `RequiresUnsafeAttribute` is applied to all of them.
Comment on lines -330 to -331

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this detail?

@jjonescz jjonescz Jul 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem to be adding anything new. get/set methods are already members to which the kept sentence applies. And now with safe/unsafe keywords, the caller-safety can differ between the accessor and the containing member, so this detail would have to be modified somehow, so I just thought it's easier to remove it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep it. We can remove the line about auto-props, but this still impacts things such as the MoveNext of compiler-async or iterators.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this still impacts things such as the MoveNext of compiler-async or iterators.

Currently it actually does not. These members are not user-callable, and we don't emit [RequiresUnsafe] on them. (It's a good point that we should clarify that fact either way though.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users could use reflection to call them. We should make sure that synthesized members generated from an unsafe method are also marked as unsafe.

@jjonescz jjonescz Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users could use reflection to call them.

Not sure how does the fact whether [RequiresUnsafe] is on such members make a difference - unless users calling those members via reflection manually check that attribute and then do... what? add unsafe block (which IDE immediately declares as unnecessary)?

Perhaps I can just add an open question for now?

(FWIW, I don't think it was previously clear from that block of speclet that it would apply to things like MoveNext.)

@333fred 333fred Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was certainly my intention that it apply to them. That's what I meant by hidden members generated by that user-facing member.

When a non-type member is *requires-unsafe*, the compiler will synthesize a `RequiresUnsafeAttribute` application on the member in metadata.

The `MemorySafetyRulesAttribute` and `RequiresUnsafeAttribute` definition is synthesized by the compiler if necessary per standard well-known member rules.

Expand Down Expand Up @@ -651,45 +655,6 @@ We could consider not automatically making the entire lexical scope of an `unsaf

Answer: `unsafe` on a type is an error (can be revisited based on feedback) under the updated rules.

### `unsafe` on accessors

We newly allow `unsafe` on property accessors but not on event accessors, in line with other pre-existing modifiers.
This also means that `unsafe` on a property is just a shortcut for `unsafe` on its accessors.
But at the same time, `partial`s require the `unsafe` modifiers to match:

```cs
partial class C
{
unsafe partial int P { get; set; } // effectively both `get` and `set` are `unsafe` here
unsafe partial int P { unsafe get => 0; set { } } // still an error: `unsafe` on `get` doesn't match
}

// similar to this pre-existing behavior:
unsafe partial class D
{
unsafe partial void M();
}
unsafe partial class D
{
partial void M() { } // error about missing `unsafe`
}
```

Maybe it should behave similarly to `readonly`, i.e., disallow `unsafe` on both the property and its accessor at the same time:

```cs
partial struct S
{
readonly partial int P { get; set; }

// error: Both partial member declarations must be readonly or neither may be readonly
partial int P { readonly get => 0; set { } }

// error: Cannot specify 'readonly' modifiers on both property or indexer 'S.P2' and its accessor. Remove one of them.
readonly int P2 { readonly get => 0; set { } }
}
```

### (answered) Allow suppressing *requires-unsafe* errors in edge case scenarios

How should we allow suppressing *requires-unsafe* errors in the following scenarios?
Expand Down Expand Up @@ -841,6 +806,16 @@ Otherwise, the user would be forced to expand these auto-declarations into manua
What about a primary constructor parameter which gets a backing field?
Both `safe` and `unsafe` modifier is currently disallowed on a parameter declaration.

### Synthesized members

When a user-declared member is marked as *requires-unsafe*, should the associated compiler-synthesized members
that are unspeakable but could be called via reflection (like `MoveNext` for iterators)
be also marked with `[RequiresUnsafe]` attribute in metadata?

Note that this does not apply to property and event accessors
where we already exactly [specify](#unsafe-modifiers-and-contexts) when they are *requires-unsafe*
and hence they get the `[RequiresUnsafe]` attribute in metadata correspondingly.

### `[Out]` and `[SkipLocalsInit]`

Since for example VB doesn't guarantee that `[Out]` parameters are initialized, in combination with `[SkipLocalsInit]`, calling such parameters could be considered `unsafe` in C#.
Expand Down