C#とC++DLL間の構造体、配列、コールバックなどの受け渡し方法

C/C++

前回の記事でC#からC++のDLLのロード方法を紹介しました。

今回はその続きで、ロードしたDLLと様々なデータ形式をやり取りしてみようと思います。

構造体、配列を受け渡ししてみる

以下のデータをC#から渡し、そしてC++から受け取ってみます。

public class MainClass
{
    public List<SubClass> subList;
};

public class SubClass
{
    public string str;
};

まずC++のDLLの用意

C#側からデータを受け取り、C#へそのまま返す関数を作成します。
データ受け渡しの可否を証明するためだけのシンプルな実装です。

struct SubStruct {
public:
	const char* str;
};

struct MainStruct {
public:
	SubStruct* subStructArray;
	int subStructCount;
};

extern "C" __declspec(dllexport) MainStruct Test(MainStruct args) {
	return args;
}

C#側にC++にデータを渡すための構造体を用意

クラスのままではC++に渡せないので、C#側にも以下の構造体を用意します。
配列を渡すにはIntPtr型(C#のポインタ)を使います。
今回の配列は固定長ではないため長さも一緒に渡して上げることでポインタから配列を取得します。

[StructLayout(LayoutKind.Sequential)]
public struct MainStruct
{
    public IntPtr subStructArray;
    public int subStructCount;
};


[StructLayout(LayoutKind.Sequential)]
public struct SubStruct
{
    public string str;
};

BaseDllClassを拡張

前回の記事で紹介したBaseDllClassを少し修正します。

abstruct class BaseDllClass : IDisposable
{
    [DllImport("kernel32")]
    private static extern IntPtr LoadLibrary(string dllFilePath);

    [DllImport("kernel32")]
    private static extern IntPtr GetProcAddress(IntPtr module, string functionName);

    [DllImport("kernel32")]
    private static extern bool FreeLibrary(IntPtr module);

    private IntPtr _dllPtr;

    public BaseDllClass(string dllFilePath)
    {
        if (!File.Exists(dllFilePath))
            throw new Exception("Not found dll.");
        // DLLのロード
        _dllPtr = LoadLibrary(dllFilePath);
        if (_dllPtr == IntPtr.Zero)
            throw new Exception("Failed load dll.");
    }

    protected T LoadFunction<T>(string functionName)
    {
        IntPtr functionPtr = GetProcAddress(_dllPtr, functionName);
        if (functionPtr == IntPtr.Zero)
            throw new Exception("function not found.");
        return (T)(object)Marshal.GetDelegateForFunctionPointer(functionPtr, typeof(T));
    }

    // この関数を追加
    protected T[] PtrToArray<T>(IntPtr ptr, int count)
    {
        var parameters = new T[count];
        for (int i = 0; i < count; i++)
        {
            parameters[i] = Marshal.PtrToStructure<T>(ptr + i * Marshal.SizeOf<T>())!;
        }
        return parameters;
    }

    // この関数を追加
    protected IntPtr ArrayToPtr<T>(T[] array, int count)
    {
        IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf<T>() * count);

        for (int i = 0; i < count; i++)
        {
            Marshal.StructureToPtr(array[i]!, ptr + i * Marshal.SizeOf<T>(), false);
        }
        return ptr;
    }

    public void Dispose()
    {
        FreeLibrary(_dllPtr);
    }
}

C#側にDLLのラッパークラスを追加

public class DllTest : BaseDllClass
{
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    delegate MainStruct Test_Internal(MainStruct testData);

    private Test_Internal _testFunction;

    public DllTest(string dllFilePath) : base(dllFilePath) 
    {
        _testFunction = LoadFunction<Test_Internal>("Test");
    }

    // MainクラスをC++のDLLに投げて、MainクラスをC++のDLLから受け取るテスト関数
    public MainClass Test(MainClass mainData)
    {
        // 配列をポインタに変換
        IntPtr ptr = ArrayToPtr(mainData.subList.ToArray(), mainData.subList.Count);
        
        MainStruct args = new MainStruct()
        {
            subStructArray = ptr,
            subStructCount = mainData.subList.Count,
        };
        MainStruct result = _testFunction(args);

        // ポインタを配列に変換
        SubStruct[] subStructArray = PtrToArray<SubStruct>(result.subStructArray, result.subStructCount);

        return new MainClass()
        {
            subList = subStructArray
                .Select(x => new SubClass() { str = x.str})
                .ToList()
        };
    }
}

ここまでの手順で以下のような構成になったかと思います。

いざ呼び出し

using var dllTest = new DllTest("dllTest.dll");

var testData = new MainClass()
{
    subList = new List<SubClass>()
    {
        new SubClass()
        {
            str = "test1"
        },
         new SubClass()
        {
            str = "test2"
        }
    }
};
var result = dllTest.Test(testData);
Console.WriteLine(result.subList[0].str);
Console.WriteLine(result.subList[1].str);

無事応答が返ってくるのが確認できました。

コールバックを渡してみる

これまでの手順で、構造体や配列がやり取りできれば、ほとんどの要求はクリアできると思います。
ただコールバック関数に関しても、C#からC++に渡したい要求があると思うので引き続き検証してきます。

C++のDLLの用意

コールバック関数を受け取り、引数を作って呼び出すだけの関数です。

extern "C" __declspec(dllexport) void Test(void (*callback)(SubStruct)){
	auto sub = SubStruct{
		"test1"
	};
	return callback(sub);
}

C#側に呼び出すラッパークラスを追加

public class CallbackTest : BaseDllClass
{
    delegate void ManageCallBack(MainStruct mainStruct);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    delegate void Test_Internal(ManageCallBack callback);

    private Test_Internal _testFunction;

    public CallbackTest(string dllFilePath) : base(dllFilePath)
    {
        _testFunction = LoadFunction<Test_Internal>("Test");
    }

    public void Test(ManageCallBack callBack)
    {
        _testFunction(callBack);
    }
}

いざ呼び出し

using var testDll = new CallbackTest("callbackTest.dll");
testDll.Test(subStruct =>
{
    Console.WriteLine(subStruct.str);
});

コールバック関数が呼ばれているのが確認できました。

以上がC#とC++のDLL間での構造体、配列、コールバックなどの受け渡し方法でした。

タイトルとURLをコピーしました