[ Team LiB ] Previous Section Next Section

22.5 Pointers

Until now you've seen no code using C/C++-style pointers. Only here, in the final paragraphs of the final pages of the book, does this topic arise, even though pointers are central to the C family of languages. In C#, pointers are relegated to unusual and advanced programming; typically they are used only when interoperating with COM.

C# supports the usual C pointer operators, listed in Table 22-1.

Table 22-1. C# pointer operators

Operator

Meaning

&

The address-of operator returns a pointer to the address of a value.

*

The dereference operator returns the value at the address of a pointer.

->

The member access operator is used to access the members of a type.

The use of pointers is almost never required, and is nearly always discouraged. When you do use pointers, you must mark your code with the C# unsafe modifier. The code is marked unsafe because you can manipulate memory locations directly with pointers. This is a feat that is otherwise impossible within a C# program. In unsafe code you can directly access memory, perform conversions between pointers and integral types, take the address of variables, and so forth. In exchange, you give up garbage collection and protection against uninitialized variables, dangling pointers, and accessing memory beyond the bounds of an array. In essence, unsafe code creates an island of C++ code within your otherwise safe C# application.

As an example of when this might be useful, read a file to the console by invoking two Win32 API calls: CreateFile and ReadFile. ReadFile takes, as its second parameter, a pointer to a buffer. The declaration of the two imported methods is not unlike those shown in Example 22-11.

Example 22-11. Declaring Win32 API methods for import into a C# program
[DllImport("kernel32", SetLastError=true)]
static extern unsafe int CreateFile(
   string filename,
   uint desiredAccess,
   uint shareMode,
   uint attributes,   
   uint creationDisposition,
   uint flagsAndAttributes,
   uint templateFile);

[DllImport("kernel32", SetLastError=true)]
static extern unsafe bool ReadFile(
   int hFile,
   void* lpBuffer, 
   int nBytesToRead,
   int* nBytesRead, 
   int overlapped);

You will create a new class, APIFileReader, whose constructor will invoke the CreateFile( ) method. The constructor takes a filename as a parameter, and passes that filename to the CreateFile( ) method:

public APIFileReader(string filename)
{
   fileHandle = CreateFile(
      filename,      // filename
      GenericRead,   // desiredAccess 
      UseDefault,    // shareMode
      UseDefault,    // attributes
      OpenExisting,  // creationDisposition 
      UseDefault,    // flagsAndAttributes
      UseDefault);   // templateFile
}

The APIFileReader class implements only one other method, Read( ), which invokes ReadFile( ). It passes in the file handle created in the class constructor, along with a pointer into a buffer, a count of bytes to retrieve, and a reference to a variable that will hold the number of bytes read. It is the pointer to the buffer that is of interest to us here. To invoke this API call, you must use a pointer.

Because you will access it with a pointer, the buffer needs to be pinned in memory; the .NET Framework cannot be allowed to move the buffer during garbage collection. To accomplish this, use the C# fixed keyword. fixed allows you to get a pointer to the memory used by the buffer, and also to mark that instance so that the garbage collector won't move it.

The block of statements following the fixed keyword creates a scope, within which the memory will be pinned. At the end of the fixed block, the instance will be marked so that it can be moved. This is known as declarative pinning:

public unsafe int Read(byte[] buffer, int index, int count) 
{
   int bytesRead = 0;
   fixed (byte* bytePointer = buffer) 
   {
      ReadFile(
        fileHandle, 
        bytePointer +     index, 
        count, 
        &bytesRead, 0);
   }
   return bytesRead;
}

Notice that the method must be marked with the unsafe keyword. This creates an unsafe context and allows you to create pointers. To compile this you must use the /unsafe compiler option.

The test program instantiates the APIFileReader and an ASCIIEncoding object. It passes the filename to the constructor of the APIFileReader and then creates a loop to repeatedly fill its buffer by calling the Read( ) method, which invokes the ReadFile API call. An array of bytes is returned, which is converted to a string using the ASCIIEncoding Object's GetString( ) method. That string is passed to the Console.Write( ) method, to be displayed on the console. The complete source is shown in Example 22-12.

Example 22-12. Using pointers in a C# program
using System;
using System.Runtime.InteropServices;
using System.Text;

class APIFileReader
{
   const uint GenericRead = 0x80000000;
   const uint OpenExisting = 3;
   const uint UseDefault = 0;
   int fileHandle;

   [DllImport("kernel32", SetLastError=true)]
   static extern unsafe int CreateFile(
      string filename,
      uint desiredAccess,
      uint shareMode,
      uint attributes,   
      uint creationDisposition,
      uint flagsAndAttributes,
      uint templateFile);

   [DllImport("kernel32", SetLastError=true)]
   static extern unsafe bool ReadFile(
      int hFile,
      void* lpBuffer, 
      int nBytesToRead,
      int* nBytesRead, 
      int overlapped);

   // constructor opens an existing file
   // and sets the file handle member 
   public APIFileReader(string filename)
   {
      fileHandle = CreateFile(
         filename,      // filename
         GenericRead,   // desiredAccess 
         UseDefault,    // shareMode
         UseDefault,    // attributes
         OpenExisting,  // creationDisposition 
         UseDefault,    // flagsAndAttributes
         UseDefault);   // templateFile
   }

   
   public unsafe int Read(byte[] buffer, int index, int count) 
   {
      int bytesRead = 0;
      fixed (byte* bytePointer = buffer) 
      {
         ReadFile(
            fileHandle,             // hfile
            bytePointer + index,    // lpBuffer
            count,                  // nBytesToRead
            &bytesRead,             // nBytesRead
            0);                     // overlapped
      }
      return bytesRead;
   }
}

class Test
{
   public static void Main( )
   {
      // create an instance of the APIFileReader, 
      // pass in the name of an existing file
      APIFileReader fileReader = 
        new APIFileReader("myTestFile.txt");

      // create a buffer and an ASCII coder      
      const int BuffSize = 128;
      byte[] buffer = new byte[BuffSize];
      ASCIIEncoding asciiEncoder = new ASCIIEncoding( );

      // read the file into the buffer and display to console
      while (fileReader.Read(buffer, 0, BuffSize) != 0)
      {
         Console.Write("{0}", asciiEncoder.GetString(buffer));
      }
   }
}

The key section of code where you create a pointer to the buffer and fix that buffer in memory using the fixed keyword is shown in bold. You need to use a pointer here because the API call demands it, though you've seen in Chapter 21 that all this can be done without using the API call at all.

    [ Team LiB ] Previous Section Next Section