-
Notifications
You must be signed in to change notification settings - Fork 6k
Update unsafe code doc #47124
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
Update unsafe code doc #47124
Conversation
There was a problem hiding this 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 |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 . |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@richlander Can you make updates here? |
Co-authored-by: Bill Wagner <[email protected]>
# 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. | |
There was a problem hiding this comment.
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`). |
There was a problem hiding this comment.
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
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. |
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 I would expect we can link to a broader doc about |
Yes. No suggestion on deleting any page. |
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. |
Updating unsafe code doc to reflect our current thinking.
Related:
@EgorBo
Internal previews