- One of C#’s great features is the fact that it is strongly typed and supports type checking throughout the runtime execution.
- What makes this feature especially great is that it is possible to circumvent this support and manipulate memory and addresses directly.
- You would do this when working with things such as memory-mapped devices, or if you wanted to implement time-critical algorithms.
- C# supports this in three ways:
- Through unsafe code, which enables access to memory pointers and addresses.
- Through Platform Invoke (P/Invoke) and calls into APIs exposed by unmanaged DLLs. Frequently, code uses these features in combination.
- Through COM interoperability.
Unsafe Code & Pointers
- The key is to designate a portion of the code as unsafe.
- Unsafe code is an explicit code block and compilation option.
- The unsafe modifier has no effect on the generated CIL code itself.
- It is only a directive to the compiler to permit pointer and address manipulation within the unsafe block.
- You can use unsafe as a modifier to the type or to specific members within the type.
- C# allows unsafe as a statement that flags code blocks to allow unsafe code:
- Code within the unsafe block can include unsafe constructs such as pointers.
- It is important to note that it is necessary to explicitly indicate to the compiler that unsafe code is supported.
- With Visual Studio this may be activated by checking the Allow Unsafe Code checkbox from the Build tab of the Project Properties window.
- You need to use the
/unsafe
switch because unsafe code opens up the possibility of buffer overflows and similar possibilities that expose the potential for security holes.
- The
/unsafe
switch includes the ability to directly manipulate memory and execute instructions that are unmanaged.
Unsafe { /* ... */ }
Pointer Declaration
- Unsafe code allows the declaration of a pointer.
- Assuming a pointer variable is not null, its value points to a location that contains one or more sequential bytes. The value of a pointer variable represents the memory address of the bytes.
- The type specified before the * is the referent type, or the type located where the value of the pointer refers.
- Because pointers are simply integers that happen to refer to a memory address, they are not subject to GC. C# does not allow referent types other than unmanaged types, which are: Types that are not reference types, are not generics, and do not contain reference types.
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/64266519-b4bc-40c8-ac52-def83d8571c3/Untitled
// Compile error for both of these declarations.
string* pMessage;
ServiceStatus* pStatus;
// ServiceStatus is defined as shown in the code below.
// The problem is that ServiceStatus includes a string field:
struct ServiceStatus
{
int State;
// Description is a reference type.
string Description;
}
- In addition to custom structs that contain only unmanaged types, valid referent types include enums, predefined value types (sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, and bool), and pointer types (such as byte**). Lastly, valid syntax includes void* pointers, which represent pointers to an unknown type.
Language Contrast: C/C++ Pointer Declaration
- In C/C++, multiple pointers within the same declaration are declared as follows:
int *p1, *p2;
- Notice the
*
on p2; this makes p2 an int* rather than an int. In contrast, C# always places the * with the data type: int* p1, p2;
- The result is two variables of type int*. Unlike structs, enums, and classes, pointers don’t ultimately derive from System.Object and are not even convertible to System.Object. Instead, they are convertible to System.IntPtr which does convert to System.Object.
Assigning a Pointer
- Once code defines a pointer, it needs to assign a value before accessing it. Just like reference types, pointers can hold the value null; this is their default value. The value stored by the pointer is the address of a location. Therefore, in order to assign it, you must first retrieve the data address.