Having worked with Win32 API functions enough in
PowerShell using P/Invoke and reflection, I was constantly annoyed by the fact
that I was often unable to correctly capture the correct error code from a
function that sets its error code (by calling SetLastError) prior to returning
to the caller despite setting SetLastError to True in the DllImportAttribute.
Consider the following, simple code that calls CopyFile
within kernel32.dll:
$MethodDefinition = @'
[DllImport("kernel32.dll", CharSet = CharSet.Unicode,
SetLastError = true)]
public static extern bool CopyFile(string lpExistingFileName,
string lpNewFileName, bool bFailIfExists);
'@
$Kernel32 = Add-Type -MemberDefinition
$MethodDefinition -Name
'Kernel32' -Namespace
'Win32' -PassThru
# Perform an invalid copy
$CopyResult = $Kernel32::CopyFile('C:\foo2', 'C:\foo1', $True)
# Retrieve the last error for CopyFile. The following error is
expected:
# "The system cannot find the file specified"
$LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
# An incorrect error is retrieved:
# "The system could not find the environment option that
was entered"
# Grrrrrrrrrrrrrrrrr...
$LastError
I knew that you needed to retrieve the last error code
immediately after a call to a Win32 function so naturally, I would have
expected the correct error code. The one returned was consistently nonsensical,
however. I don’t really know how I thought to try the following but I finally
figured out how to properly capture the correct error code after an unmanaged
function call – capture the error code on the same line (i.e. immediately after a
semicolon). Apparently, the simple act of progressing to the next line in a
PowerShell console is enough for your thread to set a different error code…
The following code demonstrates how to accurately capture
the last set error code:
$MethodDefinition = @'
[DllImport("kernel32.dll", CharSet = CharSet.Unicode,
SetLastError = true)]
public static extern bool CopyFile(string lpExistingFileName,
string lpNewFileName, bool bFailIfExists);
'@
$Kernel32 = Add-Type -MemberDefinition
$MethodDefinition -Name
'Kernel32' -Namespace
'Win32' -PassThru
# Perform an invalid copy
$CopyResult = $Kernel32::CopyFile('C:\foo2', 'C:\foo1', $True);$LastError =
[ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
# The correct error is retrieved:
# "The system cannot find the file specified"
# Yayyyyyyyyyyy....
$LastError
That’s all. I felt it was necessary to share this as I’m
sure others have encountered this issue and were unable to find any solution on the Internet as
it pertained to PowerShell.
Happy New Year!