相互運用時に戻り値にNon-Bittable型が含まれる場合例外が発生する件

タイトルの通りですが、C#からC/C++のDLL関数を呼び出すとき(相互運用時)にC++側の関数の"戻り値"が構造体でかつ、非ポインタ型で、その構造体のフィールドメンバーにNon-Bittable型(非Blittable型)が含まれている場合、以下の例外が発生して処理が落ちます。このようなタイプの戻り値は、属性を用いたマーシャルの変換は不可能です。結構はまったので回避方法のメモです。

今回含まれていた方はchar[20]のような文字列の固定長配列でした。

コード例

本件、ネイティブ側のC++のコードは以下の通りです。

// C++側コード

// 構造体
struct UnBittableStructure
{
    int A;
    int B;
    char Array[10];
};

// 関数宣言
extern "C" UnBittableStructure __declspec(dllexport) __stdcall Sample(void);

C#側の呼び出しコードは以下の通りです。

// C#側コード

// 構造体
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct UnBittableStructure
{
    public int A;
    public int B;
    
    [MarshalAs(UnmanagedType.ByValTStr, Size = 10)]
    public string Array;
}

// 関数宣言
[DllImport("Sample.dll")]
public static extern UnBittableStructure Sample();

// 関数を呼び出している箇所
static void Main(string[] args)
{
    UnBittableStructure ret = Sample(); // ここで例外が発生
}

発生する例外

この場合に発生する例外ですが

System.Runtime.InteropServices.MarshalDirectiveException: ‘メソッドの型署名は PInvoke と互換していません。’

もしくは

System.Runtime.InteropServices.MarshalDirectiveException: ‘Method’s signature is not PInvoke compatible.’

どうやらchar[]型はNon-Bittable型というもので、戻り値のマーシャルの時にはサポートしていないとの事です。

解決方法

正攻法では解決不能のため魔法を使います。まず、以下の構造体をC#側に作成します。

[StructLayout(LayoutKind.Sequential, Size = 10)]
public struct NonbittableTempStructure_10
{
    // 中身が空でサイズだけ一致している構造体を用意する
}

次に、先ほど受け取りに失敗した構造体の宣言を以下のとおり変更します。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct UnBittableStructure
{
    public int A;
    public int B;
    
    // [MarshalAs(UnmanagedType.ByValTStr, CharSet = CharSet.Ansi)]
    // public string Array;
    public NonbittableTempStructure_10 Array;
}

次にUnBittableStructure構造体にNonbittableTempStructure_10の内容を文字列に変換して取得するメソッドを以下の通り追加します。

// NonbittableTempStructure_10の内容を文字列へ変換するメソッド
public string GetArrayString()
{
    IntPtr ptr = IntPtr.Zero;
    try
    {
        ptr = Marshal.AllocHGlobal(10);
        Marshal.StructureToPtr(this.Array, ptr, false);
        return Marshal.PtrToStringAnsi(ptr, 10);
    }
    finally
    {
        if (ptr != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(ptr);
        }
    }
}

上記変更後、呼び出し箇所を以下のようにメソッドに変更すれば例外が発生しなくなります。

// 関数を呼び出している箇所
static void Main(string[] args)
{
    UnBittableStructure ret = Sample();
    Console.WriteLine("ArrayString = " + ret.GetArrayString());
    // 問題なく内容が取得できる
}

コード全体

今回の問題のコード全体を以下に掲載しておきます。

// C++側コード

// 構造体
struct UnBittableStructure
{
    int A;
    int B;
    char Array[10];
};

// 関数宣言
extern "C" UnBittableStructure __declspec(dllexport) __stdcall Sample(void);
// C#側コード

// 構造体
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct UnBittableStructure
{
    public int A;
    public int B;
    
    //[MarshalAs(UnmanagedType.ByValTStr, CharSet = CharSet.Ansi)]
    //public string Array;
    public NonbittableTempStructure_10 Array;
    
    // NonbittableTempStructure_10の内容を文字列へ変換するメソッド
    public string GetArrayString()
    {
        IntPtr ptr = IntPtr.Zero;
        try
        {
            ptr = Marshal.AllocHGlobal(10);
            Marshal.StructureToPtr(this.Array, ptr, false);
            return Marshal.PtrToStringAnsi(ptr, 10);
        }
        finally
        {
            if (ptr != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(ptr);
            }
        }
    }
}

// 関数宣言
[DllImport("Sample.dll")]
public static extern UnBittableStructure Sample();

// 関数を呼び出している箇所
static void Main(string[] args)
{
    UnBittableStructure ret = Sample();
    Console.WriteLine("ArrayString = " + ret.GetArrayString());
    // 問題なく内容が取得できる
}

参考にしたサイト

以下に、答えがそのまま書いてありました。考えた人は冴えまくってますね。また、構造体を2つ切りたくない人向けにunsafeステートメントで処理する方法も書いてあるので、気になる場合参照ください。

How to: Marshal Structures Using PInvoke: MSDN、なにがNon-Bittable型なのか書いてあるのでハマったら見ると良さそうです。*1

C# calling C function that returns struct with fixed size char array: StackOverflow、そのまんまこの問題が質問されていました。

*1:MSDNはすぐリンク切れするのでリンク切れの場合左記のタイトルでググってください。