14
Help us understand the problem. What are the problem?

posted at

updated at

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

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を記述

Win32API書き込み

このテキストを保存した時点で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(
            new SafeFileHandle(PInvoke.GetStdHandle(STD_HANDLE.STD_OUTPUT_HANDLE), false),
            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にしています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
14
Help us understand the problem. What are the problem?