説明
※PowerShell 側からでもエラーの取得はできたようです。PowerShell 側で取得
背景
私の環境では PowerShell に戻ってきてから GetLastWin32Error()(C#) または GetLastError()(C++) を呼び出すと関数の成否にかかわらず、「203 エラー」を取得するため、 Win32 API 実行直後に C# 側で LastError を取得しなければなりませんでした。そのために以下のコード群を作成しています。
できること
[class]::Method() 形式で呼び出される Win32 API の LastError を捕捉します。単純な式でしか使えませんが、関数一つ一つにラッパークラスを作成しなくて済みます。
コード
P/Invoke Signature
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using System.Runtime.ExceptionServices;
namespace Win32
{
public static class ErrorInfoWrapper
{
const uint FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
public class Win32Result<T>
{
public T Value;
public int ErrorCode;
public string ErrorMessage;
public string FunctionName;
public string GetFullMessage()
{
string baseMsg = string.IsNullOrEmpty(this.ErrorMessage)
? ("Unknown error " + this.ErrorCode.ToString())
: this.ErrorMessage;
string hex = "0x" + this.ErrorCode.ToString("X8");
return baseMsg + " (" + hex + ")";
}
public void ThrowIfError()
{
if (this.ErrorCode != 0)
{
throw new Exception(this.GetFullMessage(), new System.ComponentModel.Win32Exception(this.ErrorCode));
}
}
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint FormatMessage(
uint dwFlags,
IntPtr lpSource,
uint dwMessageId,
uint dwLanguageId,
out IntPtr lpBuffer,
uint nSize,
IntPtr Arguments
);
[DllImport("kernel32.dll")]
private static extern IntPtr LocalFree(
IntPtr hMem
);
private static string GetErrorMessage(int errorCode)
{
if (errorCode == 0)
{
return null;
}
IntPtr buffer = IntPtr.Zero;
try
{
const uint flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
uint lang = 0; // default language
uint length = FormatMessage(flags, IntPtr.Zero, (uint)errorCode, lang, out buffer, 0, IntPtr.Zero);
if (length == 0 || buffer == IntPtr.Zero)
{
return "Unknown error " + errorCode.ToString();
}
string message = Marshal.PtrToStringUni(buffer, (int)length);
if (!string.IsNullOrEmpty(message))
{
message = message.TrimEnd('\r', '\n', '\t', ' ');
}
return message;
}
finally
{
if (buffer != IntPtr.Zero)
{
LocalFree(buffer);
}
}
}
public static Win32Result<T> Call<T>(Func<T> apiCall, string functionName = null)
{
T value = default(T);
try
{
value = apiCall();
}
catch (System.Reflection.TargetInvocationException tie)
{
if (tie.InnerException != null)
{
ExceptionDispatchInfo.Capture(tie.InnerException).Throw();
}
throw;
}
int lastError = Marshal.GetLastWin32Error();
string message = GetErrorMessage(lastError);
return new Win32Result<T>
{
Value = value,
ErrorCode = lastError,
ErrorMessage = message,
FunctionName = string.IsNullOrEmpty(functionName) ? null : functionName
};
}
}
}
"@
Helper function
function WithError
{
[CmdletBinding(
DefaultParameterSetName="FunctionOnly",
PositionalBinding=$true
)]
param(
# FunctionOnly モード:
[Parameter(
ParameterSetName="FunctionOnly",
Mandatory=$true,
Position=0
)]
[ScriptBlock] $A_Win32API,
# SpecificReturnType モード:
[Parameter(
ParameterSetName="SpecificReturnType",
Mandatory=$true,
Position=0
)]
[type] $ReturnType,
[Parameter(
ParameterSetName="SpecificReturnType",
Mandatory=$true,
Position=1
)]
[ScriptBlock] $B_Win32API
)
Set-Alias Error Write-Error -Scope:Local
try
{
if ($PSBoundParameters.ContainsKey("A_Win32API"))
{
$Win32API = $A_Win32API
}
else
{
$Win32API = $B_Win32API
}
$nameFunc = $Win32API.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.Member.Extent.Text
if (!$nameFunc) { Throw }
}
catch
{
$message = @(
$_.Exception.GetType().FullName +
"`r`n " +
$_.Exception.Message +
"`r`n " +
"引数: Win32API の与え方がこの関数に合っていません。"
)
.{Error @message}
return
}
try
{
if ($PSCmdlet.ParameterSetName -eq "FunctionOnly")
{
$function = $Win32API -as [Func[object]]
return [Win32.ErrorInfoWrapper]::Call($function, $nameFunc)
}
else
{
$type = ([type]$ReturnType).FullName
$cast = [type]"Func[$type]"
$function = $Win32API -as $cast
return [Win32.ErrorInfoWrapper]::Call($function, $nameFunc)
}
}
catch
{
$message = @(
$_.Exception.GetType().FullName +
"`r`n " +
$_.Exception.Message
)
.{Error @message}
return
}
}
使い方
PowerShell
# 使う Win32 API を定義
Add-Type -Namespace Win32 -Name API -MemberDefinition @"
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(
int nStdHandle
);
"@
# そのまま使う
# 1. 戻り値の型指定
[Win32.ErrorInfoWrapper]::Call([Func[IntPtr]]{ [Win32.API]::GetStdHandle(-11) }, "GetStdHandle")
# 2. いろいろ省略
[Win32.ErrorInfoWrapper]::Call([Func[object]]{ [Win32.API]::GetStdHandle(-11) }, $null)
# 関数から呼び出してみる
# 1. 戻り値の型指定
WithError ([IntPtr]) {[Win32.API]::GetStdHandle(-11)}
# 2. いろいろ省略
WithError {[Win32.API]::GetStdHandle(-11)}
# 表示を絞る
# 1. インスタンスを作成する
$result = WithError {[Win32.API]::GetStdHandle(-1)}
# 2. Win32 API の戻り値だけ取得
$result.Value
# 3. エラーメッセージの取得
$result.GetFullMessage()
# 4. 例外を発生させてみる(Last Error の値が 0 でなければ全て例外扱い)
$result.ThrowIfError()
<#
・「203 入力された環境オプションが見つかりませんでした」は PowerShell 特有のエラーです。
・コールバック関数等、API 呼び出しの後に PowerShell コードが含まれるとこのエラーが発生します。
(つまりこの場合はデバッグできません)
#>
PowerShell 側で取得
関数の戻り値を CLI に書き出さなければ、直後に GetLastWin32Error() を実行することでエラーコードを取得できるみたいです。ソース:Powershell, PInvoke and GetLastError
PowerShell
# Win32 API
Add-Type -Namespace Win32 -Name API -MemberDefinition @"
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(
int nStdHandle
);
"@
# 戻り値をなくす
[void][Win32.API]::GetStdHandle(-1); [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
# 戻り値を変数に入れる
$Re = [Win32.API]::GetStdHandle(-1); ($LE = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error())
# エラーメッセージを表示する
[System.ComponentModel.Win32Exception]::new($LE)