Introduction
Although the .NET framework wraps up the majority of Windows API in an easy to use and safe framework, there are occasions when a direct call to the Windows API cannot be avoided. In such cases it is useful to be able to provide meaningful exceptions should any error occur in the API call.
Telling the system that you want to be able to get the last API error
When you declare an API function in VB.NET, you set the attribute SetsLastError to indicate that if an error occurs while calling that function it should set the last error for you. For example, the declaration of the API call MessageBeep would be declared as:
<DllImport("user32", EntryPoint:="MessageBeep", _
SetLastError:=True, _
CallingConvention:=CallingConvention.StdCall)> _
Public Shared Function MessageBeep _
ByVal uType As Int32 ) As Boolean
End Function
According to the documentation for this function on MSDN this function returns non-zero on success and sets the last error on failure. The parameter uType only accepts certain values so passing an incorrect value will set this error value to 87 meaning that the parameter is incorrect.
Getting the last error value
In Visual Basic 5 or 6 the error used to be held in Err.LastDllError. In Visual Basic .NET this has been replaced by the GetLastWin32Error.
Imports System.Runtime.InteropServices
Debug.Write(Marshal.GetLastWin32Error)
Getting the text description of this error
The Windows API has a function, FormatMessage that can be used to turn the number of the last error to a meaningful description. This is declared as:
<DllImport("kernel32.dll", EntryPoint:="FormatMessageA", _
CharSet:=CharSet.Ansi, _
ExactSpelling:=True, _
CallingConvention:=CallingConvention.StdCall)> _
Public Shared Function FormatMessage( _
ByVal dwFlags As Format_Message_Flags, ByVal lpSource As Int32, _
ByVal dwMessageId As Int32, ByVal dwLanguageId As Int32, _
ByVal lpBuffer As StringBuilder, _
ByVal nSize As Int32, ByVal Arguments As Int32) As Int32
End Function
and used as:
Imports System.Text
Private Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
Private Const MAX_MESSAGE_LENGTH = 512
Private Function GetAPIErrorMessageDescription( _
ByVal ApiErrNumber As Int32) As String
Dim sError As New StringBuilder(MAX_MESSAGE_LENGTH)
Dim lErrorMessageLength AsInt32
lErrorMessageLength = _
FormatMessage(Format_Message_Flags.FORMAT_MESSAGE_FROM_SYSTEM, _
0, ApiErrNumber, 0, sError, sError.Capacity, 0)
If lErrorMessageLength > 0 Then
Return sError.ToString
End If
End Function
Wrapping this up in a class derived from Exception
To be of use to the VB.NET Try..Catch..Finally syntax for error handling, this code must be wrapped up in a class that is derived from Exception.
Public Class ApiException
Inherits ApplicationException
And the New constructor is overridden to put the last system error in place of the base class' Message member:
Private msMessage As String
Public Sub New()
msMessage = GetAPIErrorMessageDescription(Marshal.GetLastWin32Error)
End Sub
Public Overrides ReadOnly Property Message() As String
Get
Return msMessage
End Get
End Property
Using the ApiException class
You can now write wrappers for your API calls that raise ApiException. E.g.:
Public Sub ApiMessageBeep(ByVal uType As Int32)
If Not Messagebeep(uType) Then
Throw New ApiException
End If
End Sub
Acknowledgements
This article was originally published on the Merrion Computing website.
Appendix: Full class code
Imports System.Text
Imports System.Runtime.InteropServices
Public Class ApiException
Inherits ApplicationException
#Region "API Declarations"
<DllImport("kernel32.dll", EntryPoint:="FormatMessageA", _
CharSet:=CharSet.Ansi, _
ExactSpelling:=True, _
CallingConvention:=CallingConvention.StdCall)> _
Public Shared Function FormatMessage( _
ByVal dwFlags As Format_Message_Flags, _
ByVal lpSource As Int32, _
ByVal dwMessageId As Int32, _
ByVal dwLanguageId As Int32, _
ByVal lpBuffer As StringBuilder, _
ByVal nSize As Int32, _
ByVal Arguments As Int32) As Int32
End Function
#End Region
#Region "Private members"
Private Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
Private Const MAX_MESSAGE_LENGTH = 512
#End Region
#Region "Private member variables "
Private msFunctionName As String
Private msMessage As String
#End Region
Private Function GetAPIErrorMessageDescription(_
ByVal ApiErrNumber As Int32) As String
Dim sError As New StringBuilder(MAX_MESSAGE_LENGTH)
Dim lErrorMessageLength As Int32
lErrorMessageLength = _
FormatMessage(Format_Message_Flags.FORMAT_MESSAGE_FROM_SYSTEM,
0, ApiErrNumber, 0, sError, sError.Capacity, 0)
If lErrorMessageLength > 0 Then
Return sError.ToString
End If
End Function
Public Sub New(ByVal ErrNumber As Int32, ByVal FunctionName As String)
msMessage = GetAPIErrorMessageDescription(ErrNumber)
msFunctionName = FunctionName
End Sub
Public Sub New(ByVal ErrNumber As Int32)
msMessage = GetAPIErrorMessageDescription(ErrNumber)
End Sub
Public Sub New()
msMessage = GetAPIErrorMessageDescription(Marshal.GetLastWin32Error)
End Sub
Public Sub New(ByVal FunctionName As String)
msMessage = GetAPIErrorMessageDescription(Marshal.GetLastWin32Error)
msFunctionName = FunctionName
End Sub
Public ReadOnly Property FunctionName() As String
Get
Return msFunctionName
End Get
End Property
Public Overrides ReadOnly Property Message() As String
Get
Return msMessage
End Get
End Property
End Class