potisanのプログラミングメモ

プログラミング素人です。昔の自分を育ててくれたネット情報に少しでも貢献できるよう、情報を貯めていこうと思っています。Windows環境のC++やC#がメインです。

C# 9.0 メモリ割り当て文字列のカスタムマーシャラー

はじめに

C#では以下の手順でアンマネージ関数のP/Invoke時にマーシャリングをカスタマイズできます。

  1. ICustomMarshalerインターフェイスを実装したクラスを作成する。
  2. 適用するアンマネージ関数の引数または戻り値に属性[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(<カスタムマーシャラー>))]を指定する。

以下ではWin32APIの関数内でメモリに割り当てられた文字列に対するカスタムマーシャラーを紹介します。

カスタムマーシャラー

CoTaskMem→Unicode文字列

Win32APIのCoTaskMemFree関数あるいはC#Marshal.FreeCoTaskMem関数で解放するメモリ上の文字列のカスタムマーシャラーです。

internal sealed class CoTaskMemToStringUniMarshaler : ICustomMarshaler
{
    private static CoTaskMemToStringUniMarshaler instance;

    internal static ICustomMarshaler GetInstance(string cookie)
    {
        return instance ??= new CoTaskMemToStringUniMarshaler();
    }

    public void CleanUpManagedData(object ManagedObj) => throw new NotImplementedException();
    public IntPtr MarshalManagedToNative(object ManagedObj) => throw new NotImplementedException();
    public object MarshalNativeToManaged(IntPtr pNativeData) => Marshal.PtrToStringUni(pNativeData);
    public void CleanUpNativeData(IntPtr pNativeData) => Marshal.FreeCoTaskMem(pNativeData);
    public int GetNativeDataSize() => -1; // 値型ではない。
}

HLOCALUnicode文字列

Win32APIのLocalFree関数で解放するメモリ上の文字列のカスタムマーシャラーです。

internal sealed class LocalMemToStringUniMarshaler : ICustomMarshaler
{
    private static LocalMemToStringUniMarshaler instance;

    internal static ICustomMarshaler GetInstance(string cookie)
    {
        return instance ??= new LocalMemToStringUniMarshaler();
    }

    public void CleanUpManagedData(object ManagedObj) => throw new NotImplementedException();
    public IntPtr MarshalManagedToNative(object ManagedObj) => throw new NotImplementedException();
    public object MarshalNativeToManaged(IntPtr pNativeData) => Marshal.PtrToStringUni(pNativeData);
    public void CleanUpNativeData(IntPtr pNativeData) => NativeMethods.LocalFree(pNativeData);
    public int GetNativeDataSize() => -1; // 値型ではない。

    private static class NativeMethods
    {
        [DllImport("kernel32.dll")]
        public static extern IntPtr LocalFree(IntPtr hMem);
    }
}

FormatMessageWの例

Win32APIのFormatMessageW関数はFORMAT_MESSAGE_ALLOCATE_BUFFERフラグを指定することでメモリ上に割り当てられた文字列を取得できます。この文字列はLocalFree関数で解放するので、上記のカスタムマーシャラーが使用できます。

using System;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        const int ERROR_OUTOFMEMORY = 14;

        NativeMethods.FormatMessageW(
            FORMAT_MESSAGE_ALLOCATE_BUFFER
            | FORMAT_MESSAGE_IGNORE_INSERTS
            | FORMAT_MESSAGE_FROM_SYSTEM,
            default,
            ERROR_OUTOFMEMORY,
            0,
            out string message,
            0,
            default);

        Console.WriteLine(message);
        // -> "メモリ リソースが不足しているため、この操作を完了できません。"
    }

    private const uint FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
    private const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
    private const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;

    private static class NativeMethods
    {
        [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)]
        public static extern uint FormatMessageW(
            uint dwFlags,
            IntPtr lpSource,
            uint dwMessageId,
            uint dwLanguageId,
            [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(LocalMemToStringUniMarshaler))] out string lpBuffer,
            uint nSize,
            IntPtr Arguments);
    }
}

参考