potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C# P/Invoke時の文字列型マーシャリングの考察

概要

文字列型の引数を伴うプラットフォーム呼び出し(P/Invoke)を実行するとき、引数の型によりマーシャリング動作が変わります。具体的にはC#側がstring/StringBuilder/byte型、プラットフォーム側(C/C++側)がLPWSTR/LPSTR(UnmanagedType.LPWStr/LPStr)の場合について次表の動作が得られます。LPTSTR(UnmanagedType.LPTStr)の場合はDllImport属性のCharSetで指定された値によりLPWSTRまたはLPSTRが選択されます。

この動作により、文字列型を渡すだけの場合はstring型、呼び出し側で処理された結果が必要な場合はstring型(LPWSTR変換限定)、StringBuilder型が適することが分かります。処理された結果が必要な場合はbyte型も使用できますが、自分でエンコーディングする必要があります。なお、各動作はIn、Out属性の追加により呼び出し前後の動作を無効化することができます。また、string型はBSTR変換(UnmanagedType.BStr)でもLPWSTR変換と同様に動作します。

C#側の型 プラットフォーム側の型 呼び出し時動作 C#側の変数内容
string LPWSTR 文字列部分のポインタを渡す。 一部書き換わる。
string LPSTR 新しいANSI文字列を作成して渡す。 書き換わらない。
StringBuilder LPWSTR 文字列部分のポインタを渡す。 一部書き換わる。
StringBuilder LPSTR 新しいANSI文字列を作成して渡し、呼び出し後のANSI文字列を元の文字列に上書きする。 全部書き換わる。
byte LPWSTR そのまま渡す。 一部書き換わる
byte LPSTR そのまま渡す。 一部書き換わる

実行結果からの考察なので、仕様の誤解などがあればご指摘いただけると嬉しいです。

サンプルコード

上記の動作は次のサンプルコードで確認することができます。なお、システムディレクトリのパス長として適当な数値256を使用しているため、パスがこれ以上の場合は正常なパスが取得されません。

using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        private static class NativeMethods
        {
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern uint GetSystemDirectoryW(
                string lpBuffer, uint uSize);
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
            public static extern uint GetSystemDirectoryA(
                [MarshalAs(UnmanagedType.LPWStr)]string lpBuffer, uint uSize);
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
            public static extern uint GetSystemDirectoryA(
                StringBuilder lpBuffer, uint uSize);
            // CharSetは無意味
            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern uint GetSystemDirectoryA(
                byte[] lpBuffer, uint uSize);
        }

        static void Main(string[] args)
        {
            var buffer1 = new string('0', 256);
            var ret1 = NativeMethods.GetSystemDirectoryW(buffer1, (uint)buffer1.Length);
            // buffer1は「システムディレクトリのパス+'\0'+(0の繰り返し)」。

            var buffer2 = new string('0', 256);
            var ret2 = NativeMethods.GetSystemDirectoryA(buffer2, (uint)buffer2.Length);
            // buffer2は「('0'の繰り返し)」。

            var buffer3 = new StringBuilder(new string('0', 256));
            var ret3 = NativeMethods.GetSystemDirectoryA(buffer3, (uint)buffer3.Capacity);
            // buffer3は「システムディレクトリのパス+'\0'+(0の繰り返し)」。

            var buffer4 = new byte[256];
            var ret4 = NativeMethods.GetSystemDirectoryA(buffer4, (uint)buffer4.Length);
            var ansiCodePage = CultureInfo.CurrentCulture.TextInfo.ANSICodePage;
            var s4 = Encoding.GetEncoding(ansiCodePage).GetString(buffer4);
            // buffer4は「システムディレクトリのパス+'\0'+(0の繰り返し)」(ANSI文字列)
            // s4は「システムディレクトリのパス」
        }
    }
}

2021/3/10:この記事は別のブログで投稿した記事を移動したものです。