We can combine the ideas and code snippets discussed to create Example 8. In the downloadable file, DeviceDriver.cs, I have a few more constants than I strictly need for this sample. They have been cut from Example 8 for brevity. Note that in this listing, Ive extended the namespace System.IO rather than System.Runtime.InteropServices shown previously simply to show that any namespace could be created or extended. As shown, when declaring constants int for 0x80000000 (or larger) we used unchecked.
Example 8: Combining the code snippets
namespace System.IO { using System; using System.Runtime.InteropServices; using System.IO; [ System.Runtime.InteropServices.ComVisible(false), System.Security.SuppressUnmanagedCodeSecurityAttribute() ] public class Win32Methods { public const int INVALID_HANDLE_VALUE = (-1), NULL = 0, ... ERROR_SUCCESS = 0, FILE_READ_DATA = (0x0001), ... FILE_SHARE_READ = 0x00000001, ... OPEN_EXISTING = 3, GENERIC_READ = unchecked((int)0x80000000), ... METHOD_BUFFERED = 0, ... METHOD_NEITHER = 3, FILE_ANY_ACCESS = 0, ... FILE_DEVICE_VIRTUAL_DISK = 0x00000024; [DllImport("Kernel32.dll", ExactSpelling=true, CharSet=CharSet.Auto, SetLastError=true)]public static extern bool CloseHandle(int hHandle); // CreateFile is is Overloaded for having SecurityAttributes or not [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, SECURITY_ATTRIBUTES lpSecurityAttributes, int dwCreationDisposition,int dwFlagsAndAttributes,int hTemplateFile); // DeviceIoControl is Overloaded for byte or int data [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); [DllImport("Kernel32.dll", CharSet=CharSet.Auto, SetLastError = true)]public static extern bool DeviceIoControl( int hDevice, int dwIoControlCode, int[] InBuffer, int nInBufferSize, int[] OutBuffer,int nOutBufferSize,ref int pBytesReturned, int pOverlapped); // These replace Macros in winioctl.h 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) ; } [ StructLayout( LayoutKind.Sequential, CharSet=CharSet.Auto )] public struct SECURITY_ATTRIBUTES { public int nLength ; // DWORD public IntPtr lpSecurityDescriptor; // LPVOID public int bInheritHandle; // BOOL } } // End win32methods } // end
Now we need a simple test program to use this code. With Visual Studio .NET you can build a project and include two files based on Example 8 and Example 9. Alternately, you can include both files (combined into DeviceDriver.cs, available online) in one file and build them in the IDE or on the command line.
Example 9: CallDriver class
public class CallDriver { public static void Main() { // Here's how we'd declare the sa, though we // won't be using it here Win32Methods.SECURITY_ATTRIBUTES sa = new Win32Methods.SECURITY_ATTRIBUTES(); int hFileHandle = new int(); hFileHandle = Win32Methods.INVALID_HANDLE_VALUE; hFileHandle = Win32Methods.CreateFile("\\\\.\\MyDriver", Win32Methods.GENERIC_READ | Win32Methods.GENERIC_WRITE, 0, (IntPtr) 0, Win32Methods.OPEN_EXISTING,0,Win32Methods.NULL); if (hFileHandle == Win32Methods.INVALID_HANDLE_VALUE) { MessageBox.Show("Cannot Open theDriver!", "LAME"); // This is usually a place to throw an exception, perhaps by: // throw new FileNotFoundException(Res.GetString(Res.IOError)); return ; } try { int IOCTL_READ_FILE = new int(); // Note you get to define whatever code you want for the IOCTL IOCTL_READ_FILE = Win32Methods.CTL_CODE ( Win32Methods.FILE_DEVICE_UNKNOWN, (int) 0x969, Win32Methods.METHOD_BUFFERED,Win32Methods.FILE_ANY_ACCESS); byte[] sOutput = new byte[512]; byte[] Input = new byte[8] ; int bytesReturned = new int(); if (Win32Methods.DeviceIoControl(hFileHandle, IOCTL_READ_FILE,Input,0,sOutput,512,ref bytesReturned,0 )) MessageBox.Show("Success", "IOCTL = ?" ); else MessageBox.Show("Failure", "IOCTL = ?" ); // show what's in sOutput } // try - note normally we'd have an except to handle errors finally { // cleanup Win32Methods.CloseHandle(hFileHandle); } } }
If youre a driver writer then you already have a driver you can try this on. If not, you can try any driver sample with virtually any IOCTL. Changing this to write data to a Driver should be clear, and I hope the overloading of types shows how to change the format of the data that will get sent to the driver.
One simple example you can try this on is the FILEIO driver sample from the Walter Oney book Programming the Windows Driver Model, from Microsoft Press. There are also many simple driver examples in the various Windows DDKs (NT, W2K, XP, and so on).
SetupApi
To open a driver more typically one uses the SetupDiXXX functions. They are in Setupapi.dll. For Example:
SetupDiEnumDeviceInfo() SetupDiGetClassDevs() etc.
We can convert these APIs in the same manner as before, using Overloading as needed (see Example 10).
Example 10: Converting APIs
[DllImport("Setupapi.dll", CharSet=CharSet.Auto, SetLastError = true)] public static extern bool SetupDiEnumDeviceInfo( int DeviceInfoSet,int MemberIndex,ref SP_DEVINFO_DATA DeviceInfoData); [DllImport("Setupapi.dll", CharSet=CharSet.Auto, SetLastError = true)] public static extern int SetupDiGetClassDevs( ref Guid ClassGuid, ref String Enumerator,int hwndParent,int Flags ); [DllImport("Setupapi.dll", CharSet=CharSet.Auto, SetLastError = true)] public static extern int SetupDiGetClassDevs( IntPtr ClassGuid, ref String Enumerator,int hwndParent,int Flags ); ...
Some of the APIs need to return data unmanaged as shown in Example 11.
Example 11: Some of the APIs need to return data unmanaged
[DllImport("Setupapi.dll", CharSet=CharSet.Auto, SetLastError = true)] public static extern bool SetupDiGetDeviceInstanceId( int DeviceInfoSet, ref SP_DEVINFO_DATA DeviceInfoData, [MarshalAs(UnmanagedType.LPWStr)] String DeviceInstanceId, int DeviceInstanceIdSize, ref int RequiredSize);
GUIDS are supported by the .NET Framework:
Guid GUID_DEVINTERFACE_TOASTER = new Guid(781EF630-72B2-11d2-B852-00C04FAD5171);
An example showing the use of many of these functions is TalkToToaster.cs (available online), which uses the Toaster sample driver from the Windows DDK and can be downloaded as part of this months source code.
Loose Ends
There are several loose ends that we have not covered here. We didnt present an example of using SecurityAttributes, though this should be straightforward with the material presented here. We also didnt present an example of using Overlapped IO.
There are a couple ways to do this. There is support for C# Overlapped structures in System.Threading. There are classes System.Threading.Overlapped and System.Threading.NativeOverlapped. There are methods Overlapped.Pack and Overlapped.Unpack to transfer data from a managed Overlapped class to an unmanaged NativeOverlapped structure. In managed code, these should be used if at all possible.
The RTM (final released version) documentation has the following information on the Overlapped Class:
The Overlapped type supports the .NET Framework infrastructure and is not intended to be used directly from your code.
Despite this comment, there is a good sample called HandleRef using Overlapped IO that uses PInvoke as well as the keyword null to avoid having to Overload the definitions of some classes if the need was solely to deal with a NULL passed in place of some data type.
When we get a buffer back from ReadFile or DeviceIoControl, we might have a block of data that we need to decode. We can use a Structure, or choose to parsing data directly. One way is to use System.Runtime.InteropServices.Marshal.ReadInt32 (or ReadInt64). We can extract what we want using, based on knowing where the data actually is. For example:
public const int Offset0 = 0; public const int Offset4 = 4; byte[] pDataBuf = new byte[256]; // ... code to fill this from // DeviceIoControl or // etc. goes here... int Value1 = Marshal.ReadInt32( (int)pDataBuf,(int)Offset0); int Value2 = Marshal.ReadInt32( (IntPtr)((((long)pDataBuf)+ (((long)Offset4))))); //... etc;
Finally, it is common to use an int or IntPtr to store the results of a native handle. In most cases this is sufficient. There is a class, System.Runtime.Interopservices.HandleRef, which can be used to wrap the handle returned via PInvoke. This will keep the managed object from being garbage collected and ensure the handle is valid for further use with PInvoke.
In summary, PInvoke, the Platform Invocation Services, combined with Overloading allows for reusable access to all the Win32 system calls from inside a managed language like C#. In particular, it easily allows for all the ones wed ever need to communicate with a Device Driver.
David Union is a Senior Software Engineer at Vibren Technologies Inc. He can be reached at [email protected].