Unsafe Code

C#’s support for unsafe code is the language’s most significant divergence from Java. Unsafe code provides access to pointer data types and allows the programmer to work directly with the contents of memory. Unsafe features can be dangerous, are rarely needed, and should be avoided if possible. However, unsafe code is required when calling pre-.NET dynamic-link libraries (DLLs) that take pointers as arguments.

The unsafe Modifier

The unsafe features described in this section are available only within an unsafe context, declared using the unsafe modifier. The unsafe modifier is applicable to the declaration of classes, structs, interfaces, delegates, fields, methods, properties, events, indexers, operators, constructors, destructors, and statement blocks. The presence of the unsafe modifier in the declaration of these program elements signals to the compiler that the element contains unsafe code. The unsafe modifier is not applicable to local variables; they must be used within the unsafe context of one of the program elements previously mentioned.

The following code demonstrates an unsafe context applied to a statement block, a method declaration, and a member field; we’ll discuss the pointer syntax later in this section.

// Unsafe statement block containing pointer-type local variable
unsafe {
    float* f;
}

// Unsafe method declaration with pointer arguments
protected unsafe int SomeMethod (int* p1, byte* p2, short* p3) { /*...*/}

// Unsafe member field
class SomeClass {
    private unsafe char* SomeField;
}

With functional members, the unsafe context includes the function parameters allowing pointers as arguments.

The /unsafe Compiler Flag

The /unsafe flag must be used when compiling an application that contains unsafe code, or a compiler error will occur. Compiler options are discussed in Chapter 3.

Managed and Unmanaged Types

The CLR distinguishes between managed and unmanaged data types. A compiler error will occur if pointers or unsafe operators are used to reference managed types. Managed types include all reference types and any struct that contains or inherits a reference-type field. Unmanaged types include the following:

  • All of the inbuilt simple types (sbyte, ushort, float, decimal, and so on)

  • Any enum type

  • Any pointer type

  • Any user-defined struct that contains only fields of unmanaged types

The sizeof Operator

The sizeof operator returns the memory size (in bytes) occupied by a variable of a specified unmanaged type. The size of the inbuilt simple types is fixed, so sizeof provides negligible value. However, sizeof also returns the size of user-defined structs, which may be useful when working with unsafe code. The following example demonstrates use of the sizeof operator to get the size of the struct SomeStruct:

public struct SomeStruct {
    int x;        // 4 bytes
    byte y;       // 1 byte
    float z;      // 4 bytes

    unsafe public static void Main() {
        System.Console.WriteLine("Size = " + sizeof(SomeStruct));
    }
}

The output of the above example is

Size = 12

As can be seen from the output, the size of a struct is not necessarily the sum of the size of the individual member types. The compiler determines how to allocate space within a struct and may pad the member values to align the data correctly for the underlying hardware platform.

Pointers

A pointer variable holds a memory address that references another data item. Pointers are similar to Java and C# reference types except that pointers give the developer access to both the memory address referenced by the pointer and the data at that address. Pointers are unique in that they do not derive from System.Object.

Declaration

A pointer declaration takes the following form:

referent-type* identifier;

The referent-type specifies the type of value the pointer is referencing and can be any unmanaged type. The referent-type can be void, declaring a pointer to an unknown type. The * token identifies this as a pointer-type declaration. The following example demonstrates four pointer declarations:

int* a, b, c;    // Declare 3 integer pointers
char** d;        // Declare a pointer to a character pointer
byte*[] e;       // An single dimension array of pointers to bytes
void* f;         // A pointer to an unknown type

Address-Of Operator and Pointer Indirection

The address-of operator (&) gets the address of an existing unmanaged variable for assignment to a pointer. Pointer indirection (*) enables the programmer to get or set the data value that the pointer references. The following code demonstrates the address-of and pointer indirection operators:

unsafe {
    // Declare and initialize an int variable
    int SomeInt = 500;

    // Declare a pointer to an int
    int* SomePtr;

    // Use address-of (&) operator to get the address of SomeInt
    // and assign to SomePtr
    SomePtr = &SomeInt;

    // Use indirection (*) to get value of the int referenced by SomePtr
    System.Console.WriteLine("Value of a = " + *SomePtr);

    // Use indirection (*) to set value of int pointed to by SomePtr
    *SomePtr = 300;
}

The output of the above example is

Value of a = 500

Pointer Member Access

When a pointer references a struct, the -> token is used to access members of the struct. For example:

public struct SomeStruct {
    public int SomeField;
    public byte SomeOtherField;
}
public class SomeClass {
    public unsafe static void Main() {
        SomeStruct s = new SomeStruct();
        SomeStruct* p = &s;
        p->SomeField = 234;
        int x = p->SomeField;
        p->SomeOtherField = 5;
    }
}

Pointer Element Access

Pointer element access enables the developer to access the data referenced by a pointer as if it were an array of values. For example:

public unsafe char* CharCopy (char* chars, int size) {
    char* r = stackalloc char[size];
    for (int x = 0 ; x < size ; x++ ) {
        r[x] = chars[x];
    }
    return r;
}

Pointer Arithmetic

Pointer types support the increment (++) and decrement (--) operators as well as the addition (+) and subtraction (-) operators. These operators affect the memory address contained in the pointer, not the value referenced. However, the value being added to or subtracted from the pointer is implicitly multiplied by the size (in bytes) of the underlying type the pointer references. For example:

unsafe {
    byte a = 5;
    long b = 1000;

    byte* x = &a;  // Declare a pointer to a byte
    x++;           // Increments by 1, the size of a byte = 1
    x += 4;        // Adds 4 x 1 = 4,  because the size of a byte = 1

    long* y = &b;  // Declare a pointer to a long
    y++;           // Increments by 8, the size of a long = 8
    y += 4;        // Adds 4 * 8 = 32, because the size of a long = 8
}

Pointer Comparison

Pointers can be compared using the following operators: ==, !=, >, <, <=, and >=. These operators compare the address contained in the pointer operands.

Pointer Conversion

Pointers of one type can be explicitly converted to pointers of any other type. Pointers can also be explicitly converted to and from any integer value type. Conversion of a pointer to an integer type results in unpredictable behavior if the integer type isn’t large enough to hold the full memory address referenced by the pointer. Overflow checking isn’t performed on pointer conversions, and the checked keyword isn’t applicable. See the section The checked and unchecked Keywords in Chapter 4 for details of the checked keyword.

A common use of pointer conversion to integer types is to display the referenced memory address. The String class doesn’t support concatenation with pointer types, and the Console.Write and WriteLine methods don’t have overloads that take pointer arguments. The following code demonstrates pointer conversion:

unsafe {
    int a = 30;          //Declare an int
    int* x  = &a;        //Pointer to int
    byte* y = (byte*)x;  //Cast int* to byte*

    // Display the referenced address by casting to uint
    System.Console.WriteLine("Address of a = " + (uint)y);
}

All pointers are implicitly convertible to the void pointer type (void*).

The fixed Statement

Although unsafe code cannot be used with managed data types, it can be used with accessible unmanaged fields contained in managed types. However, the garbage collector doesn’t track pointer references and can feasibly move or collect an object containing a field referenced by a pointer.

The fixed statement forces the CLR to pin an object in memory so that the garbage collector won’t move the referenced field. The fixed statement both declares and initializes a pointer. It has the following form:

fixed (type* identifier = expression) statement

The expression used to initialize the pointer must be implicitly convertible to a pointer of the type specified. If multiple pointers of the same type are required within the statement, use a comma-separated list of declarations in a single fixed statement. Alternatively, use multiple fixed statements.

The following code demonstrates the use of multiple fixed statements to access the members of a SomeClass instance using pointers:

using System;

public class SomeClass {
    public int FirstField = 4;
    public int SecondField = 5;
    public byte ThirdField = 22;

    public unsafe static void Main() {

        //Instantiate SomeClass
        SomeClass x = new SomeClass();

        //Fix the fields and use them
        fixed (int* a = &x.FirstField, b = &x.SecondField)
        fixed (byte* c = &x.ThirdField) {
            Console.WriteLine("First + Second Field = " + (*a + *b));
            Console.WriteLine("Third Field = " + *c);
        }
    }
}

The pointer variables are read-only and exist only for the scope of the fixed statement block. As soon as execution leaves the fixed statement block, all objects become unfixed and are subject to normal garbage collection.

The stackalloc Command

The stackalloc command allocates memory from the stack and returns the address of the allocated memory. The returned address must be assigned to a local pointer variable as part of the variable’s declaration.

The stackalloc command takes an unmanaged data type and a quantity as arguments, calculates the size of the memory block to accommodate the specified number of instances, allocates memory from the stack, and returns a pointer to the newly allocated memory block. The initial content of the memory is undefined.

The following code demonstrates a number of stackalloc operations:

unsafe {
    //Allocate stack memory for 50 int values
    int* a = stackalloc int [50];

    //Allocate stack memory for 30 char values
    char* b = stackalloc char [30];
}

No mechanism exists to explicitly free memory allocated using the stackalloc command. Stack-allocated memory is freed when the function member returns.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset