One of the biggest benefits of the .NET Framework is
that the technology is a bridge between you and the Windows operating
system and is responsible for managing a lot of system features (such as
memory management) highly reducing the risk of bad system resources
management that could lead the system to unwanted crashes or problems.
This is the reason why .NET programming is also known as managed.
The .NET Framework base class library exposes managed wrappers for most
of the Windows API system so that you do not need to manually handle
system resources, and you can take all advantages from the CLR. By the
way, there are situations in which you still need to access the Windows
API (for example when there is not a .NET counterpart of an API
function), and thus you need to work with unmanaged code.
Basically unmanaged code is all code not controlled by the .NET
Framework and that requires you to manually handle system resources.
When you work with unmanaged code, you commonly invoke Windows API
functions; such invocations are also known as Platform Invokes or, simpler, P/Invokes. In this section I cover both situations, starting with P/Invokes.
You should always avoid
unmanaged code. The .NET Framework 4.0 offers an infinite number of
managed objects and methods for performing almost everything, and if
something from the Windows API has not been wrapped yet, you can find
lots of open-source or free third-party libraries to help you solve your
problems without P/Invokes. Using unmanaged code means working directly
against the operating system and its resources, and if your code does
not perfectly handle resources, it can lead to hard problems. Moreover,
when performing unmanaged calls you need to be certain that they work or
exist on all versions of the Windows operating system you plan to
support for your application. In a few words, always search through the
Base Class Library to ensure that a .NET counterpart for the Windows API
already exists. It probably does.
|
Understanding P/Invokes
Calls to Windows API functions
are known as Platform Invokes or P/Invokes. The Visual Basic
programming language offers two ways for performing platform invokes:
The Declare
keyword has a behavior similar to what happened in Visual Basic 6, and
it has been kept for compatibility, but you should always prefer the DllImportPathIsUrl
function, from the Shlwapi.dll system library, which checks if the
specified is an URL and returns a value according to the result. This is
with the Declare
attribute because this is the one way recognized by the Common Language
Specification. Now we can see how to declare a P/Invoke. The next
example considers the keyword:
Declare Function PathIsUrl Lib "shlwapi.dll" Alias _
"PathIsURLA" (ByVal path As String) As Integer
Keep in mind the
difference in numeric types between the Windows API system and the .NET
common types system, because generally Windows APIs return Long; however when you perform P/Invokes you must use the .NET counterpart that is Integer. The same is for Integer in the Windows API, which is mapped by Short in .NET. Similarly, remember to use the IntPtr structure for declarations that require a handle (or a pointer) of type Integer.
|
As you can see, the API
declaration looks similar to what you used to write in VB 6. The
following is instead how you declare the API function via the DllImport
attribute:
'Requires an
'Imports System.Runtime.InteropServices directive
<DllImport("shlwapi.dll", entrypoint:="PathIsURLA")>
Shared Function PathIsURL(ByVal path As String) As System.Int32
End Function
Among its number of options, the most important in DllImport are the library name and the entrypoint parameter that simply indicates the function name. It is important to remember that P/Invokes must be declared as Shared,
because they cannot be exposed as instance methods; the only exception
to this rule is when you declare a function within a module. When
declared, you can consume P/Invokes like any other method (always
remembering that you are not passing through the CLR) as demonstrated
here:
Dim testUrl As String = "http://www.visual-basic.it"
Dim result As Integer = PathIsURL(testUrl)
Both Declare and DllImport lead to the same result, but from now we use only DllImport.
Encapsulating P/Invokes
Encapsulating P/Invokes in classes
is a programming best practice and makes your code clearer and more
meaningful. Continuing the previous example, you could create a new
class and declare inside the class the PathIsUrl function, marking it as Shared so that it can be consumed by other objects. By the way, there is another consideration to make. If you
plan to wrap Windows API functions in reusable class libraries, the
best approach is to provide CLS-compliant libraries and API calls. For
this reason we now discuss how you can encapsulate P/Invokes following
the rules of the Common Language Specification. The first rule is to
create a class that stores only P/Invokes declarations. Such a class
must be visible only within the assembly, must implement a private empty
constructor, and will expose only shared members. The following is an
example related to the PathIsUri function:
Friend Class NativeMethods
<DllImport("shlwapi.dll", entrypoint:="PathIsURLA")>
Shared Function PathIsURL(ByVal path As String) As System.Int32
End Function
Private Sub New()
End Sub
End Class
The class is marked with Friend
to make it visible only within the assembly. Notice that a
CLS-compliant class for exposing P/Invokes declarations can have only
one of the following names:
NativeMethods, which is used on the development machine and indicates that the class has no particular security and permissions requirements
SafeNativeMethods,
which is used outside the development machine and indicates that the
class and methods have no particular security and permissions
requirements
UnsafeNativeMethods,
which is used to explain to other developers that the caller needs to
demand permissions to execute the code (demanding permissions for one of
the classes exposed by the System.Security.Permissions namespace)
To expose P/Invokes to the external call, you need a wrapper class. The following class demonstrates how you can expose the NativeMethods.PathIsUrl function in a programmatically correct approach:
Public Class UsefulMethods
Public Shared Function CheckIfPathIsUrl(ByVal path As String) _
As Integer
Return NativeMethods.PathIsURL(path)
End Function
End Class
Finally, you can consume the preceding code as follows (for example adding a reference to the class library):
Dim testUrl As String = "http://www.visual-basic.it"
Dim result As Integer = UsefulMethods.CheckIfPathIsUrl(testUrl)
Working with unmanaged code is
not only performing P/Invokes. There are some other important concepts
about error handling and type marshaling, as explained in next sections.
Converting Types to Unmanaged
When you work with P/Invokes, you might have the need to pass custom
types as function arguments. If such types are .NET types, the most
important thing is converting primitives into types that are acceptable
by the COM/Win32 architecture. The System.Runtime.InteropServices namespace exposes the MarshalAs
attribute that can be applied to fields and method arguments to convert
the object into the most appropriate COM counterpart. The following
sample implementation of the Person class demonstrates how to apply MarshalAs:
Imports System.Runtime.InteropServices
Public Class Person
<MarshalAs(UnmanagedType.LPStr)>
Private _firstName As String
<MarshalAs(UnmanagedType.SysInt)>
Private _age As Integer
Public Property FirstName As String
Get
Return _firstName
End Get
Set(ByVal value As String)
_firstName = value
End Set
End Property
Public Property Age As Integer
Get
Return _age
End Get
Set(ByVal value As Integer)
_age = value
End Set
End Property
Sub ConvertParameter(<MarshalAs(UnmanagedType.LPStr)> _
ByVal name As String)
End Sub
End Class
The attribute receives a value from the UnmanagedType
enumeration; IntelliSense offers great help about members in this
enumeration, showing the full members list and explaining what each
member is bound to convert. You can check this out as an exercise.
The StructLayout Attribute
An important aspect of unmanaged
programming is how you handle types, especially when such types are
passed as P/Invoke arguments. Differently from P/Invokes, types
representing counterparts from the Windows API pass through the Common
Language Runtime and, as a general rule, you should provide the CLR the
best way for handling them to keep performance high. Basically when you
write a class or a structure, you give members a particular order that
should have a meaning for you. In other words, if the Person class exposes FirstName and Age as properties, keeping this order should have a reason, which generally is dictated only by some kind of logic. With the System.Runtime.InteropServices.StructLayout
attribute, you can tell the CLR how it can handle type members; it
enables deciding if it has to respect a particular order or if it can
handle type members the best way it can according to performances. The StructLayout attribute’s constructor offers three alternatives:
StructLayout.Auto: The CLR handles type members in its preferred order.
StructLayout.Sequential: The CLR handles type members preserving the order provided by the developer in the type implementation.
StructLayout.Explicit: The CLR handles type members according to the order established by the developer, using memory offsets.
By default, if StructLayout is not specified, the CLR assumes Auto for reference types and Sequential for structures. For example, consider the COMRECT
structure from the Windows API, which represents four points. This is
how you write it in Visual Basic, making it available to unmanaged code:
<StructLayout(LayoutKind.Sequential)>
Public Structure COMRECT
Public Left As Integer
Public Top As Integer
Public Right As Integer
Public Bottom As Integer
Shared Sub New()
End Sub
Public Sub New(ByVal left As Integer,
ByVal top As Integer,
ByVal right As Integer,
ByVal bottom As Integer)
Me.Left = left
Me.Top = top
Me.Right = right
Me.Bottom = bottom
End Sub
End Structure
StructLayout must be applied explicitly if your assembly needs to be CLS-compliant. This happens because you have two choices, Sequential and Explicit. Instead, for classes this is not necessary, because they are always considered as Auto. Because of this, in this section we describe only structures.
|
This is how instead you can apply StructLayout.Explicit, providing memory offsets:
<StructLayout(LayoutKind.Explicit)>
Public Structure COMRECT
<FieldOffset(0)> Public Left As Integer
<FieldOffset(4)> Public Top As Integer
<FieldOffset(8)> Public Right As Integer
<FieldOffset(12)> Public Bottom As Integer
Shared Sub New()
End Sub
Public Sub New(ByVal left As Integer,
ByVal top As Integer,
ByVal right As Integer,
ByVal bottom As Integer)
Me.Left = left
Me.Top = top
Me.Right = right
Me.Bottom = bottom
End Sub
End Structure
The FieldOffset attribute specifies the memory offset for each field. In this case the structure provides fields of type Integer, so each offset is four bytes.
The VBFixedString attribute
The VBFixedString attribute can be applied to structure members of type String,
in order to delimit the string length, since by default string length
is variable. Such delimitation is established in bytes instead of
characters. This attribute is required in some API calls. The following
is an example:
Public Structure Contact
'Both fields are limited to 10 bytes size
<VBFixedString(10)> Public LastName As String
<VBFixedString(10)> Public Email As String
End Structure
Notice that the VBFixedString can be applied to fields but is not valid for properties.
Handling Exceptions
Functions from Windows API generally return a numeric value as their result (called HRESULT),
for communicating with the caller if the function succeeded or failed.
Prior to .NET 2.0, getting information on functions failures was a
difficult task. Starting from .NET 2.0 you can handle exceptions coming
from the P/Invokes world with a classic Try..Catch
block. The real improvement is that the .NET Framework can wrap
unmanaged errors that have a .NET counterpart into managed exceptions.
For example, if a Windows API invocation causes an out-of-memory error,
the .NET Framework maps such error as an OutOfMemoryException that you can embrace within a normal Try..Catch
block. By the way, it is reasonable that not all unmanaged errors can
have a managed counterpart, due to differences in COM and .NET
architectures. To solve this, .NET provides the System.Runtime.InteropServices.SEHException, in which SEH stands for Structured Exception Handling and that maps all unmanaged exceptions that .NET cannot map. The exception is useful because it exposes an ErrorCode property that stores the HRESULT sent from P/Invokes. You use it like this:
Try
'Add your P/Invoke here..
Catch ex As SEHException
Console.WriteLine(ex.ErrorCode.ToString)
Catch ex As Exception
End Try
Tip
The SEHException
does not provide a good number of exception details, differently from
managed exceptions, but it is the most appropriate exception for error
handling in a Try..Catch block within unmanaged code.
There is also an alternative, which requires some explanation. P/Invokes raise Win32 errors calling themselves the SetLastError
native method that is different from how exceptions are thrown in the
Common Language Runtime. In earlier days you could call the GetLastError
method to retrieve the error code, but this is not the best choice
because it can refer to managed exceptions, other than Win32 exceptions.
A better, although not the ultimate, approach can be provided by
invoking the System.Runtime.InteropServices.Marshal.GetLastWin32Error method, which can intercept the last error coming from a Win32 call. To make this work, first you need to set the SetLastError property in the DllImport attribute as True; then you can invoke the method. The following code shows an example on the Beep function, which returns a numeric value as the result:
<DllImport("kernel32.dll", entrypoint:="Beep", SetLastError:=True)>
Public Shared Function Beep(ByVal frequency As UInteger,
ByVal duration As UInteger) As Integer
End Function
Dim beepResult = NativeMethods.Beep(100, 100)
If beepResult = 0 Then
Console.WriteLine(Marshal.GetLastWin32Error())
End If
Here you need to know first what values can return a particular function. Beep returns zero if it does not succeed. So after a check on the result value, the Marshal.GetLastWin32Error method is invoked to understand the error code.