Posts Adventures in Win32/Interop - Episode 1

Adventures in Win32/Interop - Episode 1

By Cory Smith

<p>Introduction </p> <p>OK, we've run into a problem where the task at hand doesn't have an answer with the .NET Framework.  Yes, the framework is huge, but it's not all inclusive.  This means that we need to step outside of the safety zone that is the framework and delve into the realm of Win32/Interop.  So the question is, just exactly what is this Interop thing, how do I find the necessary functions, and more importantly how do I use them once I've found them?</p> <p>What is Interop?</p> <p>Interop is the term used to describe interacting with the native api's of the underlying platform.  This could mean Win32 style api's or COM/ActiveX.  Basically, there is a layer within the framework that allows the various .NET languages to interoperate with the underlying platform.  The most important namespace for this interoperability is the appropriately named, System.Runtime.InteropServices.  This article will focus on Win32 style api's and is meant to be an introduction to the subject.  Further episodes will venture further into this subject.</p> <p>Why is Interop so important?</p> <p>There are times when some task at hand is just not available within the framework.  Here are a few examples:</p> <ul> <li>How do I disable the close button of a windows form while still allowing the minimize and maximize buttons? </li><li>How do I determine what drives I have available on this machine? </li><li>How do I determine the available hard drive space of a drive?</li></ul> <p>Another reason to use interop is when you are working with performance related tasks.  For example, calculating how much hard drive space is being used per folder.  This can be accomplished using the System.IO namespace, however, the Win32/Interop method is much faster.  So depending on how many iterations you are going to be doing, it might be appropriate to use the underlying api's to improve your overall performance.</p> <p>What are the negatives?</p> <p>If you stick to just using the namespaces available within the framework, your application can achieve 100% CLS compliance.  Meaning, that your application is following all the rules set forth for CLS compliance in that all of the code executed is managed and can be verified as being safe.  Whenever you use the Win32/Interop functionality, you are stepping outside of this safety net and therefore your application can't be validated as being 'safe'.  You also run into risks associated with memory leaks, obscure crashes, non-descriptive error messages, etc.  This is meant more as a simple warning, I'm not trying to scare you off ;-)</p> <p>Finding the information.</p> <p>Once you've determined that you are going to use something at the platform level, where do you start in order to find the necessary information?  There is a ton of information scattered in various locations.  </p> <ul> <li>Take a look is at Dan Appleman's Visual Basic Guide to the Win32 API.  Although it's written for a VB6 target audience, most of the common api's are described and there is a couple of chapters that will be invaluable on how to convert from C/C++ api documentation to VB6.  The leap from VB6 to VB.NET/C# is not to difficult.  If you don't have this book (or the api in question isn't in the book), skip to the next step. </li><li>Of course, Google. Using Google, search for various keywords that describes the problem that you are trying to solve.  At some point, you should come across a reference to some function(s) that are used to deal with the problem at hand.  You might even get lucky and find the exact solution in your language of choice.  At this stage, you could also use another invaluable source... friends and co-workers ;-)  I guess you could also use the search within VS.NET; however, I still find the results returned to be a bit cumbersome; but nonetheless it's still a valuable resource. </li><li>You can also look through the list sorted alphabetically or by category on MSDN using the Windows API Reference. </li><li>Once you have found a function that is in the ballpark of what you are trying to accomplish, time to start using this name as a starting point to actually get it working within your code.  Fire up Visual Studio .NET and using the Index tab (usually near the solution explorer), type in the function name you've found.  You will then be able to view the Platform SDK documentation for the function in question.  At the bottom of the document, you should find other pointers to associated functions.  Remember these.</li></ul> <p>From this point on, I'm going to start using an example to better illustrate the steps involved.  In order to keep things simple, I'll use the example of finding out how much hard drive space we have left on a drive.</p> <ul> <li>After searching the internet, I found that using GetDiskFreeSpace was the function that I needed to use.  So I typed it into the Index tool in VS.NET. </li><li>Once I read the documentation for this method, I found that there was another version that used 64 bit numbers instead of having to do math to calculate the values with high and low 32 bit values.  So, now time to read more about GetDiskFreeSpaceEx. </li><li>OK, looking at the documentation we find that it's possible that this newer function isn't available on all versions of Windows.  But, we are in luck! It's available on all versions of Windows that support .NET.  So no need to call upon the GetModuleHandle and GetProcAddress to determine if the function is available.</li></ul> <p>Now let's look at what we need to do in order to get this function working within .NET (I'll be using VB.NET for the example, however, the process is the same for C#).</p> <p>Converting the information to VB.NET/C#</p> <p>Using the documentation, we have the function defined as follows:</p> <p>BOOL GetDiskFreeSpaceEx(
  LPCTSTR lpDirectoryName,
PULARGE_INTEGER lpFreeBytesAvailable,
PULARGE_INTEGER lpTotalNumberOfBytes,
PULARGE_INTEGER lpTotalNumberOfFreeBytes ); </p> <p>Here's where it's starts to get tricky.  Your probably not going to know off the top of your head what a LPCTSTR and PULARGE_INTEGER is.  Also, you might think that BOOL would be the same as Boolean.  That would be an incorrect assumption.  So how do we find out what these types are?  More digging. (NOTE: The interop marshaller will automatically convert int values to Boolean.)</p> <p>Before continuing, I'd like to point out that based on the letters used for the variable types, it is possible to determine the type of variable being used.  Dan Appleman's book goes into great detail on this subject.  For now, let's do this in a manner that would be as if we didn't know this naming convention.</p> <p>At this point, open up Windows Explorer.  Browse to the C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include\ folder (adjust accordingly for your installation).  In here you will find many .h files.  Hidden within these files is some of the necessary information we need to complete the translation from C++ to VB.NET/C#.  Initiate a find dialog (CTRL+F).  Select 'All Files and Folders'.  We are going to look for a word or phrase.  In this entry, type PULARGE_INTEGER.  You will see that three items show up in the files list.  Two of these are probably going to be the biggest resourse to what you looking for.  The WinNT.h and WinBase.h files.  By using this method, you can eventually determine that PULARGE_INTEGER is a pointer to an unsigned large integer (System.UInt64).  You would do the same for the other variable types.</p> <p>Again, I highly recommend reading Dan Appleman's book on this subject if you are unfamiliar with C/C++ syntax.  It's probably the best resource on this subject for both C# and VB.NET developers wanting to translate api definitions.</p> <p>At this point we know the following:</p> <ul> <li>BOOL is an Integer. </li><li>LPCTSTR is a long pointer to a character string (ByRef String). </li><li>PULARGE_INTEGER is a pointer to a 64 bit unsigned number (ByRef System.UInt64).</li></ul> <p>Going back to the documentation, we find that this function is defined within the Kernel32.lib file used with C++.  This let's us know that the function is inside of the Kernel32.dll.  It also supports both ANSI and Unicode versions.  We need this information to flush out our VB.NET declaration.</p> <p>Declare the Function</p> <p>So let's translate this into something we can use from VB.NET.</p> <p>  Private Declare Auto Function GetDiskFreeSpaceEx Lib "kernel32.dll" ( _
    ByVal Drive As String, _
    ByRef FreeBytesAvailableToCaller As System.UInt64, _
    ByRef TotalNumberOfBytes As System.UInt64, _
    ByRef TotalNumberOfFreeBytes As System.UInt64) As Integer </p> <p>For further information on the various pieces of this declaration, refer to the help within Visual Studio .NET.  Look for the keyword 'Declare'.</p> <p>At this point, we can call upon the GetDiskFreeSpaceEx function from within our code.</p>
<p>Implemented the Code</p> <p>We will wrap this code into a function that will return the available freespace for a specified path.  This function will do some minimal error checking on the path passed in (making sure it's not equal to nothing) and do some type conversion on the unsigned value returning a signed version.  This is probably safe since the values we are returning shouldn't be (at least not yet) greater than a that of a signed 64 bit integer.  The following code also assumes that the System.Runtime.InteropServices namespace has been imported.</p> <p></p>Private Function AvailableSpace(ByVal rootPath As String) As Int64
'  receives the total number of free bytes on the disk that are available to the user associated with the calling thread. 
  '  If per-user quotas are in use, this value may be less than the total number of free bytes on the disk.
  If Not rootPath Is Nothing Then
    Dim uavailable As System.UInt64
Dim utotal As System.UInt64
Dim ufree As System.UInt64
If GetDiskFreeSpaceEx(path, uavailable, utotal, ufree) <> 0 Then
Return System.Convert.ToInt64(uavailable)
Dim e As Integer = Marshal.GetLastWin32Error
If e = 21 Then ' device not ready.
Return 0     
Throw New System.IO.IOException("Win32 error # " & e & " occured.")
      End If
End If
Throw New System.IO.DirectoryNotFoundException("Invalid path specified.")
End If
End Function <p>There you have it.  A method that will return the available hard drive space for the user associated with the calling thread.  You can use the same method to get the total drive size and actual free space.</p> <p>This concludes our first trip into the Win32/Interop world.  I hope you've learned something along the way and find this information useful.  If you have any comments, critisism, or suggestions for this and future articles, please do so below.</p> <p>Additional Resources</p> <p>MSDN - An Overview of Managed/Unmanaged Code Interoperability
Provides basic facts about interoperability between managed and unmanaged code, and guidelines and common practices for accessing and wrapping unmanaged API from managed code, and for exposing managed APIs to unmanaged callers.  Security and reliability considerations, performance data, and general practices for development processes are also highlighted.

This post is licensed under CC BY 4.0 by the author.