Channels ▼
RSS

Mobile

Accessing Device Drivers from C#


In the new Microsoft vision user space applications are written with managed code in C#, VB, Managed C++, J#, or other languages using the .NET framework. This article is for those writing user space applications for Microsoft Windows in C# that need to communicate with or control device drivers.

Device Drivers still must be written largely in C or C++. No explicit support for Device Driver communication is included in the current .NET framework. The focus of this article is how to implement support with C#. We will discuss how to access Win32 APIs using the Platform Invocation Services and how to make that reusable from within the .NET framework with Overloading.

The examples for this article will be in C#. Similar code could be done in VB.NET as well as with any other .NET language. C# appears to be the preeminent .NET language. Device Driver writers familiar with C++ should find the C# examples comprehensible even with a cursory familiarity with C#. Once written in C#, the code can be freely used with Managed VB, and so on.

Note that driver writers aren’t precluded from using other languages that generate appropriate binary code. Video Drivers typically have a lot of assembler code. One could write device drivers in assembler, Delphi, or in fact anything that can generate native system code. It’s just not common.

Namespaces

Namespaces are a logical naming scheme for grouping related types into logical categories of related functionality. This allows a heirarchical structure of classes and methods. Namespaces make it easier to browse and reference code and help resolve ambiguities between symbols of the same name. Example:

namespace VibrenNameSpace {
    class SomeClass     {
         // some code
    }
    namespace OS {
        class WinCE      {
            public void MyMethod() {
                // some code    
            }
        }
    }
}

This allows us in C# to use the shorthand:

using VibrenNameSpace.OS ;
WinCE myVar = new WinCE();
myVar.MyMethod();

Further explanations of Namespaces can be found in the Visual Studio .NET help.

PInvoke

Device Driver access is platform specific and on platforms supporting the Win32 API, has a well documented interface. The Platform Invocation Services (PInvoke) allows new “managed” code in C# to interoperate seamlessly with older “unmanaged” code (written in any language) that is exported via a dll.

Note: Though there will be .NET implementations on other (non-Windows) platforms, they are not yet available. This article describes the Windows implementation only. The code presented was compiled and tested with Visual Studio .NET Beta 2 and the final release. It was tested against drivers built with the XP DDK for Windows 2000 and Windows XP (only).

For Win32, API functions are contained in system dll’s, including Kernel32.dll, User32.dll, Gdi32.dll, etc. For basic Device Driver communication we will only need Kernel32.dll.

The method of access to the functions in any system dll’s is the same. The access functions are defined in System.Runtime.InteropServices. A simple sample of using the MessageBox function is in Example 1 and will serve as an example of how to use PInvoke. It is similar to the Microsoft .NET Documentation Sample.

Example 1: The MessageBox function in C++ and in C#
// in C,C++ - Win32: 

int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);

// To make accessible and use in C#:

using System.Runtime.InteropServices;

namespace System.Runtime.InteropServices {
   using System;
   using System.Runtime.InteropServices;

   public class Win32Method   {
     [DllImport("User32.dll", CharSet=CharSet.Auto)]
     public static extern int MessageBox(int hWnd, 
        String text, String caption, uint type);
   }    
} 
public class Win32MessageBox {
   public static void Main(){
       Win32Method.MessageBox(0,"Win32 MessageBox",
        "C-Sharp Example", 0);
   }
}

Considering there is a MessageBox function already in System.Windows.Forms in the .NET framework it’s not a very useful sample, but it illustrates a couple of things:

First, this example extends System.Runtime.InteropServices. It illustrates the use of Namespaces in C#. The compiler expands “CharSet.Auto” to:

System.Runtime.InteropServices.CharSet.Auto

We could alternately have declared a new namespace. The sample also indicates that the C# String class is sufficient to handle LPCTSTR. Technically we could define an explicit unmanaged string and pass that into the Win32 API function, but that should seldom be necessary. To do so, we would define in our class:

[ MarshalAs( UnmanagedType.ByValTStr, SizeConst=256 )]
public String lpFileName = null;

In general ’String’ should be sufficient.

Device Driver Access

To access a Device Driver for C#, we’ll require at least the following namespaces:

using System;
using System.Runtime.InteropServices;

We’re also going to use two other classes for optimization.

SuppressUnmanagedCodeSecurityAttribute allows managed code to call into unmanaged code without a stack walk, and ComVisibleAttributes tells the system the methods either are visible or not visible to COM. Security optimizations are described at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguidnf/html/cpconsecurityoptimizations.asp.

We will use them explicity to show what namespace they’re in for example purposes only:

System.Runtime.
    InteropServices.ComVisible(false)
System.Security.
    SuppressUnmanagedCodeSecurityAttribute()

We have to decide where (in what namespace) to create our methods. We could create a new namespace or chose to extend one. I chose System.IO initially because I was communicating with another piece of code, so that seemed like a reasonable place to put it. System.Runtime.InteropServices would be another good place because we need that namespace anyway. This is completely implementation specific and isn’t constrained by the .NET framework at all.

namespace System.IO {
    using System.Runtime.InteropServices;
    using System;
    using System.IO;

    public class Win32Methods {
        
    }
}

It’s important to compare what we’re trying to implement with how we’d do it in C or C++. Starting with the most basic communication between a user space application and a Device Driver, assume we have a driver that wants to send us information, perhaps from a file. For a Driver that has a name exported to DosDevices, we’d have something in the (C or C++ code) driver like Example 2.

Example 2: Exporting a driver’s name to DosDevices
RtlInitUnicodeString(&usDriverName, L"\\DosDevices\\MyDriver");
    ...
status = IoCreateDevice(DriverObject,
        sizeof(MYDEVICE_EXTENSION), 
        &usDriverName, FILE_DEVICE_UNKNOWN, 
        FILE_DEVICE_SECURE_OPEN, FALSE, &DeviceObject);

Example 3 contains simplified C code that one might use to get a few bytes of data back from a Device Driver, using a device IO control code.

Example 3: Getting data back from a Device Driver
#include <winioctl.h>
#include <iostream.h>   
#include "ioctls.h"                  // this has our IOCTL
int main(int argc, char* argv[]) {    
    HANDLE hDevice ;
    char   sOutPut[512];
    DWORD  dwDummy;
    hdevice = CreateFile("\\\\.\\MyDriver", GENERIC_READ | GENERIC_WRITE, 
        0, NULL, OPEN_EXISTING, 0, NULL);
    if (hdevice == INVALID_HANDLE_VALUE) {
        cout << "Can't open Driver" << endl;
        return -1;
    }
    if (DeviceIoControl(hDevice, IOCTL_READ_FILE, NULL, 0, 
          sOutPut, sizeof(sOutPut), &dwDummy, NULL)) {
        sOutPut[dwUnused] = 0;
        cout << sOutPut << endl;
    } else {
        cout << "Error " << GetLastError() 
            << " in call to DeviceIoControl" << endl;
    }
    CloseHandle(hdevice);
return 0;
}

The minimum Win32 APIs we’ll need to implement in C# are:

    CreateFile()
    CloseHandle()
    DeviceIoControl()

A full implementation would also contain ReadFile() and WriteFile(). For reusability we’ll need to have access to all the various constants, like GENERIC_READ, et al, and have to be able to understand an IOCTL. We will need to use DWORDS, HANDLES, NULL, and possibly other types used by the listed APIs.

In some ways, this is the most labor-intensive part of using PInvoke for Win32 APIs. Because C and C++ are not strongly typed languages, the APIs often use NULL in place of fairly complex structures to support different operations with the same API. In the case of DeviceIoControl, we could do Input, Output, or Both with a single call. If we don’t do all of them then we’ll have a lot of NULL’s where structures would have been filled in with data. C# will require overloading to support this behavior. In this way, the methods we create will be reusable. C# has a ‘Null’ keyword to reduce but not eliminate the overloading required.

Example 4 shows code from the appropriate Win32 header file. We need to understand the translation of some data types. LPSECURITY_ATTRIBUTES will provide an example of translating C/C++ structures to C#. In our example we’re not actually going to use it, but we’ll translate it anyway for future use (reusability) and for illustrative purposes.

Example 4: The CreateFile() function
HANDLE CreateFile(
  LPCTSTR lpFileName,                         // File Name
  DWORD dwDesiredAccess,                      // Access Mode
  DWORD dwShareMode,                          // Share Mode
  LPSECURITY_ATTRIBUTES lpSecurityAttributes, // Security Descriptor
  DWORD dwCreationDisposition,                // How to Create
  DWORD dwFlagsAndAttributes,                 // File Attributes
  HANDLE hTemplateFile                        // Handle to Template File
);

There is some documentation on converting structures and unions. There is a class called System.Runtime.InteropServices. StructLayoutAttribute that can be used as follows:

[ StructLayout(
  LayoutKind.Sequential,
  CharSet=CharSet.Auto )]
public struct SECURITY_ATTRIBUTES {
    public int       nLength ;         
    public IntPtr  lpSecurityDescriptor;
    public bool    bInheritHandle;
}

We can then access the data in C# as usual:

Win32Methods.SECURITY_ATTRIBUTES foo =
    new Win32Methods.SECURITY_ATTRIBUTES();
sa.nLength = ...

In this instance, Win32Methods is the class name we’ve chosen to implement this in.

This is documented in the .NET documentation under “Structures” and the “StructLayout Class,” but as shown, the usage doesn’t quite follow the name listed as the method. This is certainly a point to check after the final version of Visual Studio .NET ships. If you need to use other Win32 APIs, many of them have structures defined in the header files that will need converting in a similar fashion.

The purpose for the overloading in Example 5 should be clear. If I want to actually use lpSecurityAttributes, we have to have that type explicitly in the method definition. In most cases, we would pass a NULL to this method. The easiest way to do that is via an IntPtr to 0, so by overloading this function with another copy that has lpSecurityAttributes typed to IntPtr we can use this either way.

Example 5: Overloading
[DllImport("Kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CreateFile(String lpFileName, 
   int dwDesiredAccess, int dwShareMode,
   IntPtr lpSecurityAttributes, int dwCreationDisposition,
   int dwFlagsAndAttributes, int hTemplateFile);

[DllImport("Kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CreateFile(String lpFileName, 
   int dwDesiredAccess, int dwShareMode,
   ref SECURITY_ATTRIBUTES lpSecurityAttributes, int dwCreationDisposition,
   int dwFlagsAndAttributes, int hTemplateFile);

This way we can implement any of the APIs that can take NULL in place of a structure or variable. In some cases where we translate the variable in C# to an Int or IntPtr, we might not have to do this. Further Overloading will allow us to use all the possible arrangements of data input to the Win32 API functions.

CloseHandle is simple. In C & C++, from the Win32 header is:

BOOL CloseHandle(
  HANDLE hObject
);

This can be converted to C# as:

[DllImport(“Kernel32.dll”, ExactSpelling=true,
  CharSet=CharSet.Auto, SetLastError=true)]
  public static extern
  bool CloseHandle(int hHandle); 

We are really using unmanaged code within the context of managed C#, so we still have to call CloseHandle after the CreateFile, just like with the Win32 API.

We could have made CloseHandle an int or a bool. That’s more or less dependent on how you plan to use it in the user application. In C++ a bool is an int, in C# it’s a distinct type.

Finally from the Win32 Headers, we have:

BOOL DeviceIoControl(
  HANDLE hDevice,    
  DWORD dwIoControlCode,     // operation
  LPVOID lpInBuffer,        // input data 
                           // buffer
  DWORD nInBufferSize,       // size of input 
                           // data buffer
  LPVOID lpOutBuffer,        // output data 
                           // buffer
  DWORD nOutBufferSize,      // size of output 
                           // data buffer
  LPDWORD lpBytesReturned,   // byte count
  LPOVERLAPPED lpOverlapped  // overlapped 
                           // information
);

LPOVERLAPPED is not straightforward. If we don’t need it, we’re almost done. First, let’s finish a working sample then get back to what to do about LPOVERLAPPED. In many cases, we really don’t need LPOVERLAPPED.

[DllImport(“Kernel32.dll”,
  CharSet=CharSet.Auto, SetLastError = true)]
public static extern bool DeviceIoControl(
  int hDevice,
  int dwIoControlCode,  
  byte[] InBuffer,
  int nInBufferSize,
  byte[] OutBuffer,
  int nOutBufferSize,
  ref int pBytesReturned,
  int pOverlapped);

I’ve made a few simplifications, including getting the data back as an array of bytes. We could (and in the real world would) overload this with functions to return data in a series of required forms such as Strings, arrays of Strings, etc. To do that, we might have to do some marshalling of unprotected data, which we touched on briefly above.

The last things we need to actually talk to a Driver are some constants and to have the code understand what an IOCTL is. From the Win32 C/C++ header files the macro declaration for CTL_CODE is shown in Example 6.

Example 6: The macro declaration for CTL_CODE
#define CTL_CODE( DeviceType, Function, Method, Access ) (  \
 ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)

Some macros are hard to turn into nice functions. This one is easy! Along with CTL_CODE we’ll build DEVICE_TYPE_FROM_CTL_CODE for future use, though it won’t be used in this sample (See Example 7).

Example 7: CTL_CODE and DEVICE_TYPE_FROM_CTL_CODE
public static int CTL_CODE(int DeviceType, int Function, int Method, 
    int Access) {
   return 
     (((DeviceType) << 16)|((Access) << 14)|((Function) << 2)|(Method));
} 
    
public int DEVICE_TYPE_FROM_CTL_CODE(int ctrlCode)     { 
    return (int)(( ctrlCode & 0xffff0000) >> 16) ;
}


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video