Introduction
We will see that C# allows suspending the verification of code by the CLR to allow developers to directly access memory using pointers. Hence with C#, you can complete, in a standard way, certain optimizations which were only possible within unmanaged development environments such as C++. These optimizations concern, for example, the processing of large amounts of data in memory such as bitmaps.
Pointers and unsafe code
C++ does not know the notion of code management. This is one of the advantages of C++ as it allows the use of pointers and thus allows developers to write optimized code which is closer to the target machine.
This is also a disadvantage of C++ since the use of pointers is cumbersome and potentially dangerous, significantly increasing the development effort and maintenance required.
Before the .NET platform, 100% of the code executed on the Windows operating system was unmanaged. This means the executable contains the code directly in machine instructions which are compatible with the type of processor (i.e. machine language code). The introduction of the managed execution mode with the .NET platform is revolutionary. The main sources of hard to track bugs are detected and resolved by the CLR. Amongst these:
- Array access overflows (Now dynamically managed by the CLR).
- Memory leaks (Now mostly managed by the garbage collector).
- The use of an invalid pointer. This problem is solved in a radical way as the manipulation of pointers if forbidden in managed mode.
The CLR knows how to manipulate three kinds of pointers:
- Managed pointers. These pointers can point to data contained in the object heap managed by the garbage collector. These pointers are not used explicitly by the C# code. They are thus used implicitly by the C# compiler when it compiles methods with out and ref arguments.
- Unmanaged function pointers. The pointers are conceptually close to the notion of delegate. We will discuss them at the end of this article.
- Unmanaged pointers. These pointers can point to any data contained in the user addressing space of the process. The C# language allows to use this type of pointers in zones of code considered unsafe. The IL code emitted by the C# compiler corresponding to the zones of code which use these unmanaged pointers make use of specialized IL instructions. Their effect on the memory of the process cannot be verified by the JIT compiler of the CLR. Consequently, a malicious user can take advantage of unsafe code regions to accomplish malicious actions. To counter this weakness, the CLR will only allow the execution of this code at run-time if the code has the SkipVerification CAS meta-permission.
Since it allows to directly manipulating the memory of a process through the use of an unmanaged pointer, unsafe code is particularly useful to optimize certain processes on large amounts of data stored in structures.
Compilation options to allow unsafe code
Unsafe code must be used on purpose and you must also provide the /unsafe option to the csc.exe compiler to tell it that you are aware that the code you wish to compile contains zones which will be seen as unverifiable by the JIT compiler. Visual Studio offers the Build Allow unsafe code project property to indicate that you wish to use this compiler option.
Declaring unsafe code in C#
In C#, the unsafe keyword lets the compiler know when you will use unsafe code. It can be used in three situations:
- Before the declaration of a class or structure. In this case, all the methods of the type can use pointers.
- Before the declaration of a method. In this case, the pointers can be used within the body of this method and in its signature.
- Within the body of a method (static or not). In this case, pointers are only allowed within the marked block of code. For example:
unsafe
{
...
}
Let us mention that if a method accepts at least one pointer as an argument or as a return value, the method (or its class) must be marked as unsafe, but also all regions of code calling this method must also be marked as unsafe.
Using pointers in C#
Each object, whether it is a value or reference type instance, has a memory address at which it is physically located in the process. This address is not necessarily constant during the lifetime of the object as the garbage collector can physically move objects store in the heap.
.NET types that support pointers
For certain types, there is a dual type, the unmanaged pointer type which corresponds to the managed type. A pointer variable is in fact the address of an instance of the concerned type. The set of types which authorizes the use of pointers limits itself to all value types, with the exception of structures with at least one reference type field. Consequently, only instances of the following types can be used through pointers: primitive types; enumerations; structures with no reference type fields; pointers.
Declaring pointers
A pointer might point to nothing. In this case, it is extremely important that its value should be set to null (0). In fact, the majority of bugs due to pointers come from pointers which are not null but which point to invalid data. The declaration of a pointer on the FooType is done as follows:
FooType * pointeur;
For example:
long * pAnInteger = 0;
Note that the declaration...
int * p1,p2;
... makes it so that p1 is a pointer on an integer and p2 is a pointer.
Indirection and dereferencing operators
In C#, we can obtain a pointer on a variable by using the address of operator &. For example:
long anInteger = 98;
long * pAnInteger = &anInteger;
We can access to the object through the indirection operator *. For example:
long anInteger = 98;
long * pAnInteger = &anInteger;
long anAnotherInteger = *pAnInteger;
// Here, the value of 'anAnotherInteger' is 98.
The sizeof operator
The sizeof operator allows obtaining the size in bytes of instances of a value type. This operator can only be used in unsafe mode. For example:
int i = sizeof(int) // i is equal to 4
int j = sizeof(double) // j is equal to 8
Pointer arithmetic
A pointer on a type T can be modified through the use of the '++' and '--' unary operator. The '-' operator can also be used with pointers.
- The '++' operator increments the pointer by sizeof(T) bytes.
- The '--' operator decrements the pointer by sizeof(T) bytes.
- The '-' operator used between two pointers of same type T, returns a value of type long. This value is equal to the byte offset between the two pointers divided by sizeof(T).
The comparison can also be used on two pointers of a same or different type. The supported comparison operators are:
== != < > <= >=
Pointer casting
Pointers in C# do not derive from the Object class and thus the boxing and unboxing does not exist on pointers. However, pointers support both implicit and explicit casting.
Implicit casts are done from any type of pointer to a pointer of type void*.
Explicit casts are done from:
- Any pointer type to any other pointer type.
- Any pointer type to the sbyte, byte, short, ushort, int, uint, long, ulong types (caution, we are not talking about the sbyte*, byte*, short*... types).
- One of sbyte, byte, short, ushort, int, uint, long, ulong types to any pointer type.
Join the conversation! Your thoughts help the community grow.