Unsafe Code


As you have just seen, C# is very good at hiding much of the basic memory management from the developer, thanks to the garbage collector and the use of references. However, cases exist in which you will want direct access to memory. For example, you might want to access a function in an external (non-.NET) DLL that requires a pointer to be passed as a parameter (as many Windows API functions do), or possibly for performance reasons. This section examines C#'s facilities that provide direct access to the contents of memory.

Pointers

Although we are introducing pointers as if they are a new topic, in reality pointers are not new at all. You have been using references freely in your code, and a reference is simply a type-safe pointer. You have already seen how variables that represent objects and arrays actually store the memory address of where the corresponding data (the referent) is stored. A pointer is simply a variable that stores the address of something else in the same way as a reference. The difference is that C# does not allow you direct access to the address contained in a reference variable. With a reference, the variable is treated syntactically as if it stores the actual contents of the referent.

C# references are designed to make the language simpler to use and to prevent you from inadvertently doing something that corrupts the contents of memory. With a pointer, on the other hand, the actual memory address is available to you. This gives you a lot of power to perform new kinds of operations. For example, you can add 4 bytes to the address, so that you can examine or even modify whatever data happens to be stored 4 bytes further on in memory.

The two main reasons for using pointers are as follows:

  • Backwards compatibility — Despite all of the facilities provided by the .NET runtime, it is still possible to call native Windows API functions, and for some operations, this may be the only way to accomplish your task. These API functions are generally written in C and often require pointers as parameters. However, in many cases it is possible to write the DllImport declaration in a way that avoids use of pointers; for example, by using the System.IntPtr class.

  • Performance — On those occasions where speed is of the utmost importance, pointers can provide a route to optimized performance. Provided you know what you are doing, you can ensure that data is accessed or manipulated in the most efficient way. However, be aware that more often than not, there are other areas of your code where you can make the necessary performance improvements without resorting to using pointers. Try using a code profiler to look for the bottlenecks in your code — one comes with Visual Studio 2005.

Low-level memory access comes at a price. The syntax for using pointers is more complex than that for reference types and pointers are unquestionably more difficult to use correctly. You need good programming skills and an excellent ability to think carefully and logically about what your code is doing in order to use pointers successfully. If you are not careful, it is very easy to introduce subtle, difficult-to- find bugs into your program using pointers. For example, it is easy to overwrite other variables, cause stack overflows, access areas of memory that don't store any variables, or even overwrite information about your code that is needed by the .NET runtime, thereby crashing your program.

In addition, if you use pointers your code must be granted a high level of trust by the runtime's code access security mechanism or it will not be allowed to execute. Under the default code access security policy, this is only possible if your code is running on the local machine. If your code must be run from a remote location, such as the Internet, users must grant your code additional permissions for it to work. Unless the users trust you and your code, they are unlikely to grant these permissions. Code access security is discussed more in Chapter 16, ".NET Security."

Despite these issues, pointers remain a very powerful and flexible tool in the writing of efficient code and are worth learning about.

Note

We strongly advise against using pointers unnecessarily because your code will not only be harder to write and debug, but it will also fail the memory type-safety checks imposed by the CLR, which is discussed in Chapter 1, ".NET Architecture."

Writing unsafe code

As a result of the risks associated with pointers, C# allows the use of pointers only in blocks of code that you have specifically marked for this purpose. The keyword to do this is unsafe. You can mark an individual method as being unsafe like this:

 unsafe int GetSomeNumber() { // code that can use pointers } 

Any method can be marked as unsafe, irrespective of what other modifiers have been applied to it (for example, static methods or virtual methods). In the case of methods, the unsafe modifier applies to the method's parameters, allowing you to use pointers as parameters. You can also mark an entire class or struct as unsafe, which means that all of its members are assumed to be unsafe:

 unsafe class MyClass { // any method in this class can now use pointers } 

Similarly, you can mark a member as unsafe:

 class MyClass { unsafe int *pX;   // declaration of a pointer field in a class } 

Or you can mark a block of code within a method as unsafe:

 void MyMethod() { // code that doesn't use  unsafe { // unsafe code that uses pointers here } // more 'safe' code that  } 

Note, however, that you cannot mark a local variable by itself as unsafe:

 int MyMethod() {  unsafe int *pX;   // WRONG } 

If you want to use an unsafe local variable, you will need to declare and use it inside a method or block that is unsafe. There is one more step before you can use pointers. The C# compiler rejects unsafe code unless you tell it that your code includes unsafe blocks. The flag to do this is unsafe. Hence, to compile a file named MySource.cs that contains unsafe blocks (assuming no other compiler options), the command is:

csc /unsafe MySource.cs

or:

csc –unsafe MySource.cs
Note

If you are using Visual Studio 2005, you will find the option to compile unsafe code in the Build tab of the project properties window.

Pointer syntax

Once you have marked a block of code as unsafe, you can declare a pointer using this syntax:

 int* pWidth, pHeight; double* pResult; byte*[] pFlags; 

This code declares four variables: pWidth and pHeight are pointers to integers, pResult is a pointer to a double, and pFlags is an array of pointers to bytes. It is common practice to use the prefix p in front of names of pointer variables to indicate that they are pointers. When used in a variable declaration, the symbol * indicates that you are declaring a pointer (that is, something that stores the address of a variable of the specified type).

Note

C++ developers should be aware of the syntax difference between C++ and C#. The C# statement int* pX, pY; corresponds to the C++ statement int *pX, *pY;. In C#, the * symbol is associated with the type rather than the variable name.

Once you have declared variables of pointer types, you can use them in the same way as normal variables, but first you need to learn two more operators:

  • & means take the address of, and converts a value data type to a pointer, for example int to *int.

    This operator is known as the address operator.

  • * means get the contents of this address, and converts a pointer to a value data type (for example, *float to float). This operator is known as the indirection operator (or sometimes as the dereference operator).

You will see from these definitions that & and * have the opposite effect to one another.

Note

You might be wondering how it is possible to use the symbols & and * in this manner, because these symbols also refer to the operators of bitwise AND (&) and multiplication (*). Actually, it is always possible for both you and the compiler to know what is meant in each case, because with the new pointer meanings, these symbols always appear as unary operators — they only act on one variable and appear in front of that variable in your code. On the other hand, bitwise AND and multiplication are binary operators — they require two operands.

The following code shows examples of how to use these operators:

 int x = 10; int* pX, pY; pX = &x; pY = pX; *pY = 20; 

You start off by declaring an integer, x, with the value 10 followed by two pointers to integers, pX and pY. You then set pX to point to x (that is, you set the contents of pX to be the address of x). Then you assign the value of pX to pY, so that pY also points to x. Finally, in the statement *pY = 20, you assign the value 20 as the contents of the location pointed to by pY — in effect changing x to 20 because pY happens to point to x. Note that there is no particular connection between the variables pY and x. It's just that at the present time, pY happens to point to the memory location at which x is held.

To get a better understanding of what is going on, consider that the integer x is stored at memory locations 0x12F8C4 through 0x12F8C7 (1243332 to 1243335 in decimal) on the stack (there are 4 locations because an int occupies 4 bytes). Because the stack allocates memory downward, this means that the variables pX will be stored at locations 0x12F8C0 to 0x12F8C3, and pY will end up at locations 0x12F8BC to 0x12F8BF. Note that pX and pY also occupy 4 bytes each. That is not because an int occupies 4 bytes. It's because on a 32-bit processor you need 4 bytes to store an address. With these addresses, after executing the previous code, the stack will look like Figure 7-5.

image from book
Figure 7-5



Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net