20
21

[C#] CsWin32でWin32APIのプラットフォーム呼び出し(P/Invoke)コードを自動生成

Last updated at Posted at 2022-05-27

CsWin32について

前回、固定長配列を持つ構造体についての記事を書きました。

しかし、P/Invoke用のコードを手動でガリガリ書くのって正直面倒ですよね…
pinvoke.netからコピペしてもいいんですけど。 既にサイト消滅してました😱

Microsoftも、このように手動でメンテナンスしている状況が良くないと感じていたのか、CsWin32 というWin32APIのP/Invokeコードを自動生成する SourceGeneratorを現在進行形で開発しています。

まだベータ版ではあるものの、軽く触った感じでは十分使えそうな印象を受けました。

使い方

試しに、前回の記事で使用していたAPIの
GetStdHandle
GetConsoleScreenBufferInfoEx
をCsWin32で使用出来るようにしてみます。

1. NuGetでCsWin32をインストール

NuGetパッケージの管理画面から、CsWin32を検索してインストールします。

NuGet画面

2. unsafeを使用可能にする

プロジェクトのプロパティを開き、ビルド設定でunsafeコードのコンパイルを許可します。

unsafe設定

3. NativeMethods.txt という名称でテキストファイルをプロジェクトに追加

テキスト追加

4. NativeMethods.txt に使用するWin32APIを記述

NativeMethods.txt
GetStdHandle
GetConsoleScreenBufferInfoEx

このテキストを保存した時点でSourceGeneratorが実行され、P/Invokeコードが生成されます。

生成されたコード

APIに関連する構造体も自動生成されていて、便利すぎィ!
固定長配列を持つ CONSOLE_SCREEN_BUFFER_INFOEX 構造体 がどうなっているのか、生成されたコードを確認してみましょう。

// ------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------

#pragma warning disable CS1591,CS1573,CS0465,CS0649,CS8019,CS1570,CS1584,CS1658,CS0436
namespace Windows.Win32
{
	using global::System;
	using global::System.Diagnostics;
	using global::System.Runtime.CompilerServices;
	using global::System.Runtime.InteropServices;
	using global::System.Runtime.Versioning;
	using winmdroot = global::Windows.Win32;

	namespace System.Console
	{
		internal partial struct CONSOLE_SCREEN_BUFFER_INFOEX
		{
			internal uint cbSize;
			internal winmdroot.System.Console.COORD dwSize;
			internal winmdroot.System.Console.COORD dwCursorPosition;
			internal ushort wAttributes;
			internal winmdroot.System.Console.SMALL_RECT srWindow;
			internal winmdroot.System.Console.COORD dwMaximumWindowSize;
			internal ushort wPopupAttributes;
			internal winmdroot.Foundation.BOOL bFullscreenSupported;
			internal __uint_16 ColorTable;

			internal partial struct __uint_16
			{
				internal uint _0,_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,_12,_13,_14,_15;

				/// <summary>Always <c>16</c>.</summary>
				internal readonly int Length => 16;

				/// <summary>
				/// Gets a ref to an individual element of the inline array.
				/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned reference outlive the stack frame that defines it.
				/// </summary>
				internal ref uint this[int index] => ref AsSpan()[index];

				/// <summary>
				/// Gets this inline array as a span.
				/// </summary>
				/// <remarks>
				/// ⚠ Important ⚠: When this struct is on the stack, do not let the returned span outlive the stack frame that defines it.
				/// </remarks>
				internal Span<uint> AsSpan() => MemoryMarshal.CreateSpan(ref _0, 16);

				internal unsafe readonly void CopyTo(Span<uint> target, int length = 16)
				{
					if (length > 16)throw new ArgumentOutOfRangeException("length");
					fixed (uint* p0 = &_0)
for(int i = 0;
i < length;
i++)						target[i]= p0[i];
				}

				internal readonly uint[] ToArray(int length = 16)
				{
					if (length > 16)throw new ArgumentOutOfRangeException("length");
					uint[] target = new uint[length];
					CopyTo(target, length);
					return target;
				}

				internal unsafe readonly bool Equals(ReadOnlySpan<uint> value)
				{
					fixed (uint* p0 = &_0)
					{
 						int commonLength = Math.Min(value.Length, 16);
for(int i = 0;
i < commonLength;
i++)						if (p0[i] != value[i])							return false;
for(int i = commonLength;
i < 16;
i++)						if (p0[i] != default(uint))							return false;
					}
					return true;
				}
			}
		}
	}
}

固定長配列 COLORREF ColorTable[16] は、__uint_16 という構造体に置き換えられ、その中にuint型のフィールドを16個持っていますね。マーシャリングも発生しないし、ref uint thisのインデクサ、Span<T>変換のメソッドもあって使い勝手・パフォーマンスとも良さそうで、流石はMicrosoftって感じです。

前回のサンプルのP/Invokeコード部を消して、生成されたコードに合うようにさくっと書き換えてみます。

using System;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.System.Console;
using Microsoft.Win32.SafeHandles;

class Program
{
    static void Main(string[] args)
    {
        var screenBuffer = new CONSOLE_SCREEN_BUFFER_INFOEX()
        {            
            cbSize = (uint)Marshal.SizeOf(typeof(CONSOLE_SCREEN_BUFFER_INFOEX))
        };
        var result = PInvoke.GetConsoleScreenBufferInfoEx(
            PInvoke.GetStdHandle_SafeHandle(STD_HANDLE.STD_OUTPUT_HANDLE),
            ref screenBuffer
        );

        var spanColorTable = screenBuffer.ColorTable.AsSpan();
        for (var i = 0; i < spanColorTable.Length; i++)
        {
            Console.WriteLine($"ColorTable[{i}] = 0x{spanColorTable[i]:X8}");
        }

        Console.ReadKey();
    }
}

それほど変更せずに組み込めて、長いP/Invokeコードが無くなったのでスッキリしました。
とりあえず問題なく動作しております。

もしSourceGeneratorで生成されたAPIを認識しない場合、一度VisualStudioを閉じて開き直すと認識するかもしれません。(自分の時はそれで直りました)

GetConsoleScreenBufferInfoEx の引数がSafeHandleだったので、GetStdHandle の戻り値をどう渡せばいいか少し悩みましたが、Microsoft.Win32.SafeHandles 名前空間にあった SafeFileHandle を使用してみました。GetStdHandle のハンドルは解放不要なので、new SafeFileHandle の第2引数をfalseにしています。
[2024/8/23現在] GetStdHandleでSafeHandleを返すメソッド(GetStdHandle_SafeHandle)が作成されるようになっていました。

(オプション)生成コードのカスタマイズ

NativeMethods.json というファイルをプロジェクトに追加することで、生成されるコードの内容をカスタマイズすることができます。

キー 既定値 説明
comInterop COM 相互運用のコード生成に関する詳細。
allowMarshaling true 構造体の代わりに COM インターフェイスを生成し、より使いやすい API のために非Blittable構造体の生成を許可するかどうか。
friendlyOverloads フレンドリーオーバーロードの生成を制御するプロパティを持つオブジェクト。
multiTargetingFriendlyAPIs false ターゲット フレームワークを考慮して不要または冗長であると判断された API を生成するかどうか。
useSafeHandles true SafeHandleを使用するかどうか。
wideCharOnly true ANSI 関数を省略し、UTF-16 関数から W サフィックスを削除するかどうか。
emitSingleFile false 単一のソース ファイルに出力するかどうか。
className PInvoke インポートされたモジュールに関係なく、すべての p/invoke メソッドと定数が生成される単一のクラスの名前。
public false アクセス修飾子をpublicにするかどうか。(falseだとinternal)

comInterop, friendlyOverloads は更に入れ子で指定する値がありますが、自分は使う事が無さそうなので、興味がある方は https://aka.ms/CsWin32.schema.json の中身を見て自分で詳細を調べてみてください。
multiTargetingFriendlyAPIs 辺りも、説明見ても正直よく分かっていません😅

自分で使う場合には、
SafeHandleを使うのは面倒臭いと感じるので、useSafeHandles は false
生成されたコードからAPIの名前空間を検索するときに楽なので、emitSingleFile は true
にしていこうと思います。

NativeMethods.json
{
  "$schema": "https://aka.ms/CsWin32.schema.json",
  "emitSingleFile": true,
  "useSafeHandles": false
}
20
21
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
20
21