Skip to content

Conversation

richlander
Copy link
Member

@richlander richlander commented Jul 7, 2025

Updating unsafe code doc to reflect our current thinking.

Related:

@EgorBo


Internal previews

📄 File 🔗 Preview link
docs/csharp/language-reference/unsafe-code.md Unsafe code

@dotnetrepoman dotnetrepoman bot added this to the July 2025 milestone Jul 7, 2025
@richlander richlander changed the title Update intro sections Update unsafe code doc Jul 7, 2025
@richlander richlander marked this pull request as ready for review July 10, 2025 17:03
@richlander richlander requested review from a team and BillWagner as code owners July 10, 2025 17:03
Copy link
Member

@BillWagner BillWagner left a comment

Choose a reason for hiding this comment

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

Overall, this is a good improvement. I had a few comments we should address before merging.

``` csharp
```csharp
type* identifier;
void* identifier; //allowed but not recommended
Copy link
Member

Choose a reason for hiding this comment

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

This is legal, and this is the reference, so we should leave this here.

### Pointer operations

You can't apply the indirection operator to a pointer of type `void*`. However, you can use a cast to convert a void pointer to any other pointer type, and vice versa.
Pointers don't inherit from [`object`](builtin-types/reference-types.md). You can't box or unbox pointers, and there's no conversion between pointers and `object`. However, you can cast between pointer types and between pointers and integral types (with an explicit cast).
Copy link
Member

Choose a reason for hiding this comment

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

And, we do need to add void* conversions (reference), with appropriate "danger, not recommended" language.

Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
Pointers don't inherit from [`object`](builtin-types/reference-types.md). You can't box or unbox pointers, and there's no conversion between pointers and `object`. However, you can cast between pointer types and between pointers and integral types (with an explicit cast).
Pointers don't inherit from [`object`](builtin-types/reference-types.md). You can't box or unbox pointers, and there's no conversion between pointers and `object`. However, you can cast between pointer types, between a pointer and integral types(with an explicit cast), and between a void pointer and another pointer type .

Copy link
Member

Choose a reason for hiding this comment

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

in the suggested change - a whitespace got removed? types(with

Copy link
Member

Choose a reason for hiding this comment

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

You can't box or unbox pointers, and there's no conversion between pointers and object

Not sure it's worth mentioning, but there is Pointer.Box/Unbox API for that

Copy link
Member

Choose a reason for hiding this comment

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

That's different kind of boxing than what the language calls boxing. I do not think it belongs into language reference.


### Pointer safety reminders

- Dereferencing a null pointer is implementation-defined and may crash your program.
Copy link
Member

Choose a reason for hiding this comment

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

Because this is the language reference for our implementation, we should say what we do on all platforms here.

I'll defer to others, but I think it's consistent, but platform dependent. The null value is defined as all 0's. IIRC, all platforms we support have an OS level exception for that operation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't that undefined behavior (even if consistent)?

Copy link
Member

@EgorBo EgorBo Aug 7, 2025

Choose a reason for hiding this comment

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

I think it is defined, but only for managed code, if you dereference 0 inside a pinvoke it will be an AccessViolationException. But given this talks about C#, I guess the proposed change needs to be reworded.

What is actually an implementation-defined is what the exact range of values for an address will be handled as an NRE, e.g. _ = *(int*)50000; - will this throw NRE? likely yes, but it's impl-defined.

Copy link
Member

Choose a reason for hiding this comment

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

Isn't that undefined behavior (even if consistent)?

In general, if behavior is "undefined", anything can happen. If a language behavior is "implementation defined", from the standpoint of the standard, an implementation must state what happens. I think we want "undefined" here.

@BillWagner BillWagner modified the milestones: July 2025, August 2025 Aug 6, 2025
@BillWagner
Copy link
Member

@richlander Can you make updates here?

# Unsafe code

C# supports an [`unsafe`](keywords/unsafe.md) context, in which you can write *unverifiable* code. In an `unsafe` context, code can use pointers, allocate and free blocks of memory, and call methods using function pointers. Unsafe code in C# isn't necessarily dangerous; it's just code whose safety can't be verified.
C#'s unsafe code feature enables direct memory manipulation using pointers and other low-level constructs. These capabilities are essential for interop with native libraries and high-performance scenarios. However, unsafe code bypasses C#'s safety guarantees, so it's up to you, the author, to ensure correctness. Bugs like reading uninitialized/incorrect memory, buffer overruns, and use-after-free become possible. Unsafe code must appear within an [`unsafe`](keywords/unsafe.md) context and requires the [`AllowUnsafeBlocks`](compiler-options/language.md#allowunsafeblocks) compiler option.
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
C#'s unsafe code feature enables direct memory manipulation using pointers and other low-level constructs. These capabilities are essential for interop with native libraries and high-performance scenarios. However, unsafe code bypasses C#'s safety guarantees, so it's up to you, the author, to ensure correctness. Bugs like reading uninitialized/incorrect memory, buffer overruns, and use-after-free become possible. Unsafe code must appear within an [`unsafe`](keywords/unsafe.md) context and requires the [`AllowUnsafeBlocks`](compiler-options/language.md#allowunsafeblocks) compiler option.
C#'s unsafe code context enables direct memory manipulation using pointers and other low-level constructs. These capabilities are essential for interop with native libraries and high-performance scenarios. However, unsafe code bypasses C#'s safety guarantees, so it's up to you, the author, to ensure correctness. Bugs like reading uninitialized/incorrect memory, buffer overruns, and use-after-free become possible. Unsafe code must appear within an [`unsafe`](keywords/unsafe.md) context and requires the [`AllowUnsafeBlocks`](compiler-options/language.md#allowunsafeblocks) compiler option.


``` csharp
```csharp
type* identifier;
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
type* identifier;
type* identifier;
void* identifier; //allowed but not recommended

for (int i = 0; i < a.Length; i++)
{
// p3 will reference past the end of the loop on the last iteration
// a good reason to keep it private to the loop
Copy link
Member

Choose a reason for hiding this comment

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

only last? it's already bumped by +4 above. Not sure what exactly this loop is supposed to show - a bad/unreliable code example?


- Dereferencing a null pointer is implementation-defined and may crash your program.
- Passing pointers to or from methods, especially if they refer to stack or pinned data, can cause undefined behavior if the referent is no longer valid.
- Never store a pointer to stack memory outside the current method.
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps it should be generalized like don't expose pointer to resources outside of their scopes? because it's definitely not stack-limited.

The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
The 4 bytes of the integer: 01 04 00 00
The value of the integer: 1025
Copy link
Member

Choose a reason for hiding this comment

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

Why the change to 1025?

Console.WriteLine(*p);
*p += 1;
Console.WriteLine(*p);
Console.WriteLine("Increment each element by 5 using interior pointers");
Copy link
Member

Choose a reason for hiding this comment

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

There's not really any "interior pointers" used here. That term is typically reserved for ref T which are GC tracked.

More specifically, interior pointers are what allow Span<T> on modern .NET ("fast span") to function correctly and safely.

What's being used here is just regular pointers with no special tracking support. The pin is the only thing keeping a alive.

# Unsafe code

C# supports an [`unsafe`](keywords/unsafe.md) context, in which you can write *unverifiable* code. In an `unsafe` context, code can use pointers, allocate and free blocks of memory, and call methods using function pointers. Unsafe code in C# isn't necessarily dangerous; it's just code whose safety can't be verified.
C#'s unsafe code feature enables direct memory manipulation using pointers and other low-level constructs. These capabilities are essential for interop with native libraries and high-performance scenarios. However, unsafe code bypasses C#'s safety guarantees, so it's up to you, the author, to ensure correctness. Bugs like reading uninitialized/incorrect memory, buffer overruns, and use-after-free become possible. Unsafe code must appear within an [`unsafe`](keywords/unsafe.md) context and requires the [`AllowUnsafeBlocks`](compiler-options/language.md#allowunsafeblocks) compiler option.
Copy link
Member

@tannergooding tannergooding Aug 7, 2025

Choose a reason for hiding this comment

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

I don't think we want to have the verbiage imply that unsafe is required for high-performance scenarios (particularly since that isn't strictly true).

Rather, unsafe is used in some scenarios to achieve better performance; but in many scenarios there exists an idiomatic pattern that will do the same thing and which should be preferred. There are some scenarios which benefit more from unsafe usage, but it should generally not be "required", especially over time.

Copy link
Member

Choose a reason for hiding this comment

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

The tone I've been adopting is that unsafe should be the final tool you use for performance. Span, ref and friends should be used first.

Pointers don't inherit from [`object`](builtin-types/reference-types.md). You can't box or unbox pointers, and there's no conversion between pointers and `object`. However, you can cast between pointer types and between pointers and integral types (with an explicit cast).

A pointer can be `null`. Applying the indirection operator to a null pointer causes an implementation-defined behavior.
The garbage collector doesn't track references from pointers. If you're pointing to a managed object, you must [pin](./statements/fixed.md) it for as long as the pointer is used.
Copy link
Member

Choose a reason for hiding this comment

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

Notably fixed isn't the only way to pin. You also have options like GC.AllocateArray and GCHandle which can allow pinning for longer scopes. They just come with their own nuance and considerations.

| [`stackalloc`](operators/stackalloc.md) | Allocates memory on the stack. |
| [`fixed` statement](statements/fixed.md) | Temporarily fixes a variable so that its address can be found. |
| `*` | Dereference (pointer indirection). |
| `->` | Access struct member through a pointer. |
Copy link
Member

Choose a reason for hiding this comment

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

This isn't specific to structs. You can have say a string* p and do p->Member() as well.

Any pointer type can be implicitly converted to a `void*` type. Any pointer type can be assigned the value `null`. Any pointer type can be explicitly converted to any other pointer type using a cast expression. You can also convert any integral type to a pointer type, or any pointer type to an integral type. These conversions require an explicit cast.
- Any pointer type can be implicitly converted to `void*`.
- Any pointer type can be set to `null`.
- You can explicitly cast between pointer types and between pointers and integral types (integral types must be at least the size of a pointer: `nint`, `nuint`, `IntPtr`, `UIntPtr`, or — on 64-bit — `long`/`ulong`).
Copy link
Member

Choose a reason for hiding this comment

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

nint and IntPtr are the same type, do they need to be distinguished?

We're not specifying long and Int64 here, for example.

If we wanted to specify both, it might be better as

nint (System.IntPtr), nuint (System.UIntPtr)

Just to make it clearer that they are the same thing, just aliases of eachother

@richlander
Copy link
Member Author

As I stated initially, I was inspired by the Rust unsafe doc. It is good. The feedback (particularly from Tanner and Bill) is making me wonder if our primary unsafe doc should be in the language reference. I think we need a conceptual doc with examples tht doesn't feel the need to cover every single case, particularly syntax that is legal but not best practice.

Particularly as we move forward, unsafe code is not held solely within the language.

@tannergooding
Copy link
Member

I think the language doc still needs to exist. Not all languages support working with unsafe features and the language page is meant to be explicitly about C# specific support for unsafe (specifically the unsafe { } keyword).

I would expect we can link to a broader doc about unsafe in .NET, as it applies across languages. It would then potentially be desirable for F# and VB to have their own unsafe pages as well, where VB covers it doesn't support things like pointers and F# covers the things it exposes for unsafe support.

@richlander
Copy link
Member Author

Yes. No suggestion on deleting any page.

@richlander
Copy link
Member Author

I'm going to close this PR and re-group with @EgorBo on how to proceed. There are likely some good edits here, but also some less-than-good ones.

@richlander richlander closed this Aug 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants