Visual C# .NET (C#)
allows you to step outside of the safe environment of managed code
and write code that is considered
“unsafe” by the Common Language
Runtime (CLR). Running code that is considered unsafe by the CLR
presents a certain set of restrictions in exchange for opening up
possibilities like accessing memory-mapped data or implementing
time-critical algorithms that use pointers directly. These
restrictions are mainly based in the
Code Access Security (CAS)
system of the CLR and are in place to draw a distinct line between
code the CLR knows to be playing by the rules (or
“safe”), and code that needs to do
a bit outside of the traditional sandbox of the CLR (and is thus
“unsafe” code). In order to run
code that is marked as unsafe by the CLR, you need the CAS
SkipVerification
privilege granted to the assembly that the unsafe code is implemented
in. This tells the CLR to not bother verifying the code and to allow
it to run, whereas normally unverified code would not run. This is a
highly privileged operation and is not to be done lightly, as you
increase the permissions your application will require in order to
operate correctly on a user’s system. If you use
unsafe types in a method signature, you also make the code
non-CLS-compliant. This means that interoperability with other .NET
based languages, like VB.NET or Managed C++, for this assembly is
compromised.
Even though unsafe code allows you to easily write potentially
unstable code, it does have several safeguards.
Only value types or pointers to value types
inside of reference types can be used with unsafe code; reference
types cannot. This allows pointer types to be created solely on the
stack, so you do not have to use the new
and
delete
operations to allocate and release memory
to which the variable points. You only have to wait for the method
that declared the pointer type to return, forcing the pointer to go
out of scope and clearing any stack space devoted to this method. You
can get into a bit of trouble if you are doing exotic things with
unsafe code (such as pointing to a value type inside of a reference
type) since this behavior allows access to heap-based memory and
opens up the possibility for pointer pitfalls such as those seen in
C++.
You must pass a pointer variable in to a method; however, you do not want to allow the called method to change the address that the pointer passed in is pointing to. For example, a developer wants to assume that after passing in a pointer parameter to a method that that parameter is still pointing to the same address when this method returns. If the called method were to change what the pointer pointed to, bugs could be introduced into the code.
In other cases, the converse may be true: the developer wants to allow the address to be changed in the method she passes the pointer to. Consider a developer who might create a method that accepts two pointers and switches those pointers by switching the memory locations to which each pointer points to, rather than swapping the values each pointer points to.
You must decide whether to pass this pointer by value, by reference,
or as an out
parameter. There are several methods
of passing arrays to methods. These methods include using or not
using the ref
or out
keywords
to define how the parameters are to be handled.
To make sure that a method does not modify the pointer itself, you would pass the pointer by value, as shown here:
unsafe { int num = 1; int* numPtr = # ModifyValue(numPtr); // Continue using numPtr... }
The method ModifyValue
can still change the value
in the memory location to which the NumPtr
pointer
is pointing to, but it cannot force NumPtr
to
point to a different memory location after the method
ModifyValue
returns.
To allow the method to modify the pointer, pass it in by reference:
public unsafe void TestSwitchXY( ) { int x = 100; int y = 20; int* ptrx = &x; int* ptry = &y; Console.WriteLine(*ptrx + " " + (int)ptrx); Console.WriteLine(*ptry + " " + (int)ptry); SwitchXY(ref ptrx, ref ptry); Console.WriteLine(*ptrx + " " + (int)ptrx); Console.WriteLine(*ptry + " " + (int)ptry); } public unsafe void SwitchXY(ref int* x, ref int* y) { int* temp = x; x = y; y = temp; }
The SwitchXY
method switches the values of the
x
and y
pointers so that they
point to the memory location originally pointed to by the other
parameter. In this case, you must pass the pointers in to the
SwitchXY
method by reference
(ref
). This action allows the
SwitchXY
method to actually modify where a pointer
points to and to return this modified pointer.
In safe code, passing a value type to a method by value means that the value is passed in, not the reference to that value. Therefore, the called method cannot modify the value that the calling method’s reference points to; it can modify only the copy that it received.
It works the same way with unsafe code. When an unsafe pointer is passed in to a method by value, the value of the pointer (which is a memory location) cannot be modified; however, the value that this pointer points to can be modified.
To examine the difference between passing a pointer by reference and by value, we first need to set up a pointer to an integer:
int x = 5; int* ptrx = &x;
Next, we write the method that attempts to modify the pointer parameter:
private unsafe void CallByValue(int* x) { int newNum = 7; x = &newNum; }
Finally, we call the method and pass in ptrx
to
this method:
CallByValue(ptrx);
If we examine the pointer variable ptrx
before the
call to CallByValue
, we see that it points to the
value 5
. The called method
CallByValue
changes the passed in parameter to
point to a different memory location. However, when the
CallByValue
returns, the ptrx
pointer still points to the original memory location that contains
the value 5
. The reason for this is that the
CallByValue
method accepts the pointer
ptrx
by value. This means that whatever value that
ptrx
holds, a memory location in this case, it
cannot be modified, which is similar to when a reference type is
passed.
There are other times when we need
to allow a called method to modify the memory location that a pointer
points to. Passing a pointer by reference
into a
method does this. This means that the called method may, in fact,
modify the memory location to which a pointer parameter points. To
see this, we again set up a pointer:
int x = 5; int* ptrx = &x;
Next, we write the method that attempts to modify the parameter:
private unsafe void CallByRef(ref int* x) { int newNum = 7; x = &newNum; }
Finally, we call the method and pass the pointer by reference:
CallByRef(ref ptrx);
Now if we examine the value that the pointer ptrx
points to, before and after the call is made to
CallByRef
, we see that it has indeed changed from
5
to 7
. Not only this, but the
ptrx
pointer is actually pointing to a different
memory location. Essentially, the ref
keyword
allows the method CallByRef
to modify the value
contained in the ptrx
variable.
Let’s consider the
use of the out
or ref
keywords
with pointers. A method that accepts a pointer as an
out
or ref
parameter is called
like this:
public unsafe void TestOut( ) { int* ptrx; CallUsingOut(out ptrx); Console.WriteLine(*ptrx + " " + (int)ptrx); }
The CallUsingOut
method is written as follows:
public unsafe void CallUsingOut(out int* ptrx) { int x = 7; ptrx = &x; }
The ptrx
variable is initially a
null
pointer. After the call is made to the
CallUsingOut
method, the ptrx
variable points to the value 7
.
The code in this section of this recipe is meant to be as simple as
possible in order to explain the difference between passing a pointer
by value, by reference, and as an out
parameter.
However, there is a serious flaw in the design of this example code
(the code in the Solution section does not contain this flaw). Take
the following code, for example:
public unsafe void TestOut( ) { int* ptrx; CallUsingOut(out ptrx); Console.WriteLine(*ptrx); SomeOtherMethod("Some Text"); Console.WriteLine(*ptrx); }
The called method is written as follows:
public unsafe void CallUsingOut(out int* ptrx) { int temp = 7; ptrx = &temp; }
The problem is that the temp
variable, pointed to
by the out
parameter ptrx
in
the CallUsingOut
method, is in the stack frame of
the CallUsingOut
method. The first call to
WriteLine
displays the correct value
(7
) for the pointer variable
ptrx
since the CallUsingOut
method’s stack frame is still intact. However, the
stack frame to the CallUsingOut
method is promptly
overwritten when the call to SomeOtherMethod
is
made, thereby causing the second call to WriteLine
to display garbage.
This mistake is easy to make, especially as the code gets more and more complex. This error can also occur when returning a pointer from a method as a return value. To solve this, you need to not assign local variables in the scope of the method that are created on the stack to the pointer since the value being pointed to can “go away” once the scope is exited, creating a dangling pointer.
Be
very careful that you do not create dangling pointers (a pointer that
doesn’t point at anything valid, such as by
assignging a pointer to memory that is collected before leaving the
function) when passing pointer parameters as ref
or out
. This warning also applies to pointers used
as return values.