1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PowerShell から P/Invoke を使うときに「Last Win32 Error」を C# 内で補足する

Last updated at Posted at 2025-09-16

説明

 ※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)
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?