C#/C++相互運用時の戻り値構造体にwchar_t[]が含まれると例外が出る問題

前回記事で、戻り値の構造体にNon-Bittable型(非Blittable型)が含まれる場合例外が発生する件でchar型の固定長配列は自分でマーシャルしないと例外が出るという話の続きです。前回と同じ条件(戻り値が非ポインタ型の構造体)でwchar_t型の固定長配列が含まれる場合も例外は発生してしまうため解決を図ります。

C++側のコード

C++側は戻り値が構造体で、wchar_t型の固定長配列が含まれています。

// 構造体
struct CPP_STRUCT
{
    wchar_t path[120];
    char name[120];
    int id;
};

// 関数宣言
extern "C" CPP_STRUCT __declspec(dllexport) __stdcall getStruct(void)
{
    CPP_STRUCT ret;
    wcscpy(ret.path, L"あいうえおabcdef1234567890亜医鵜得御");
    strcpy(ret.name, "abcdef1234567890");
    ret.id = 999;
    return ret;
}

C#側コード

固定長領域はマーシャルできないので同じサイズの構造体で代用します。

// 特別な構造体(1)
[StructLayout(LayoutKind.Sequential, Size = 120)]
public struct UnbittableStructure_120 { /* Empty */ }

// 特別な構造体(2)
[StructLayout(LayoutKind.Sequential, Size = 240)] // 120の倍
public struct UnbittableStructure_240 { /* Empty */ }

// 実際に使う構造体
public struct CPP_STRUCT
{
    private UnbittableStructure_240 path;
    private UnbittableStructure_120 name;
    public int id;
}

// メソッド宣言
[DllImport("Win32Project1.dll", EntryPoint = "getStruct", CharSet = CharSet.Ansi)]
public static extern CPP_STRUCT __GetStruct();

そこから、こういったパターンの時は毎回同じような処理なので以下のようなUtilityを作成します。

/// <summary>
/// C#/C++相互運用時時に使用する汎用処理を提供します。
/// </summary>
public static class MarshalUtil
{
/// <summary>
/// 指定した構造体から文字列を取得します。
/// </summary>
public static string GetStringFromAnsi<T>(T obj, int size) where T : struct
{
    return getString(obj, size, (IntPtr ptr) => { return Marshal.PtrToStringAnsi(ptr, size); });
}

/// <summary>
/// 指定した構造体から文字列を取得します。
/// </summary>
public static string GetStringFromUni<T>(T obj, int size) where T : struct
{
    return getString(obj, size, (IntPtr ptr) => { return Marshal.PtrToStringUni(ptr, size / 2); });
}

/// <summary>
/// 指定した述語で文字列を取得します。
/// </summary>
private static string getString<T>(T obj, int size, Func<IntPtr, string> func)
{
    IntPtr ptr = IntPtr.Zero;
    try
    {
        ptr = Marshal.AllocHGlobal(size);
        Marshal.StructureToPtr(obj, ptr, false);

        string str= func(ptr);

        int len = str.IndexOf('\0');
        if (len != -1)
        {
            return str.Substring(0, len);
        }

        return str;
    }
    finally
    {
        if (ptr != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(ptr);
        }
    }
}

そして、CPP_STRUCTに以下処理を追加します。

public string GetPath()
{
    return MarshalUtil.GetStringFromUni(this.path, 240 /* physical size */);
}
public string GetName()
{
    return MarshalUtil.GetStringFromAnsi(this.name, 120);
}

利用側コード

上記準備をして以下のように使用します。

static void Main(string[] args)
{
    CPP_STRUCT ret = __GetStruct();
    Console.WriteLine("Path = " + ret.GetPath());
    Console.WriteLine("Name = " + ret.GetName());
    Console.WriteLine("ID = " + ret.id);
    
    // Path = あいうえおabcdef1234567890亜医鵜得御
    // Name = abcdef1234567890
    // ID = 999
}

全体コード

C#側のみ全体コードを以下に貼っておきます。

// C#側コード
class Program
{
    static void Main(string[] args)
    {
        CPP_STRUCT ret = __GetStruct();
        Console.WriteLine("Path = " + ret.GetPath());
        Console.WriteLine("Name = " + ret.GetName());
        Console.WriteLine("ID = " + ret.id);
        
        // Path = あいうえおabcdef1234567890亜医鵜得御
        // Name = abcdef1234567890
        // ID = 999
    }


    // 特別な構造体(1)
    [StructLayout(LayoutKind.Sequential, Size = 120)]
    public struct UnbittableStructure_120 { /* Empty */ }

    // 特別な構造体(2)
    [StructLayout(LayoutKind.Sequential, Size = 240)] // 120の倍
    public struct UnbittableStructure_240 { /* Empty */ }

    // 実際に使う構造体
    public struct CPP_STRUCT
    {
        private UnbittableStructure_240 path;
        private UnbittableStructure_120 name;
        public int id;

        public string GetPath()
        {
            return MarshalUtil.GetStringFromUni(this.path, 240 /* physical size */);
        }
        public string GetName()
        {
            return MarshalUtil.GetStringFromAnsi(this.name, 120);
        }
    }

    // メソッド宣言
    [DllImport("Win32Project1.dll", EntryPoint = "getStruct", CharSet = CharSet.Ansi)]
    public static extern CPP_STRUCT __GetStruct();
}

/// <summary>
/// C#/C++相互運用時時に使用する汎用処理を提供します。
/// </summary>
public static class MarshalUtil
{
    /// <summary>
    /// 指定した構造体から文字列を取得します。
    /// </summary>
    public static string GetStringFromAnsi<T>(T obj, int size) where T : struct
    {
        return getString(obj, size, (IntPtr ptr) => { return Marshal.PtrToStringAnsi(ptr, size); });
    }

    /// <summary>
    /// 指定した構造体から文字列を取得します。
    /// </summary>
    public static string GetStringFromUni<T>(T obj, int size) where T : struct
    {
        return getString(obj, size, (IntPtr ptr) => { return Marshal.PtrToStringUni(ptr, size / 2); });
    }

    /// <summary>
    /// 指定した述語で文字列を取得します。
    /// </summary>
    private static string getString<T>(T obj, int size, Func<IntPtr, string> func)
    {
        IntPtr ptr = IntPtr.Zero;
        try
        {
            ptr = Marshal.AllocHGlobal(size);
            Marshal.StructureToPtr(obj, ptr, false);

            string str= func(ptr);

            int len = str.IndexOf('\0');
            if (len != -1)
            {
                return str.Substring(0, len);
            }

            return str;
        }
        finally
        {
            if (ptr != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(ptr);
            }
        }
    }
}

非Bittable型というマイナーな響きがなんとも言えませんね。