Friday, January 1, 2016

Properly Retrieving Win32 API Error Codes in PowerShell

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!