【.dll】C++からC#に値を渡したり、関数を実行したい。

# はじめに
先日、[Unity6(C#)にC++から値を渡したい記事](https://qiita.com/0xE4F90A/items/efc656872038d4ae2427) を公開しましたが「別にUnityに限定しなくてもいいんじゃない？」と思い、書き直すことにしました。
内容は前回よりもパワーアップしております。

# やりたいこと
C#からC++の関数を呼び出し、戻り値として値や文字列、配列を受け取りたいです。

前回は、C++側からの戻り値を単にコンソールに表示するだけでしたが、
今回はC#側から引数を渡し、それを基にC++側で一度処理を行ったうえで結果を返すようにします。

具体的には以下のような型の関数をC#から呼び出します。

・int
・double
・bool
・文字列 (const char*)
・int配列 (int[])
・void (戻り値なし)

## 環境
> ・Visual Studio 2022 Community
> ・C++ 17
> ・C# 12.0
> ・.NET 8.0.16


# .dllプロジェクトを作成する
今回はVisual Studio 2022 の Community版を使用します。

**`新しいプロジェクトの作成(N)`** → **`Windowsデスクトップウィザード`**
→ **`パス&プロジェクト名設定`** → **`Windowsデスクトッププロジェクト`**
と進み、

**アプリケーションの種類(T)**: `ダイナミックライブラリ(.dll)`
**追加のオプション**: `空のプロジェクト(E)`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920837/b58c7cf7-fb91-4a89-b60a-2d5b88e8fd24.png)
と設定します。
(今回はプロジェクト名を`ReturnValue`としています。)

## C/C++側
`ReturnValue.h`と`ReturnValue.cpp`を作成し、以下のように記述します。

ReturnValue.h
-
```cpp
#pragma once

#ifdef RETURNVALUE_EXPORTS
	#define EXPORT __declspec(dllexport)
#else
	#define EXPORT __declspec(dllimport)
#endif

extern "C" EXPORT int ReturnInt(int i);
extern "C" EXPORT double ReturnDouble(double d);
extern "C" EXPORT bool ReturnBool(bool b);
extern "C" EXPORT const char* ReturnString(const char* c);
extern "C" EXPORT const int* ReturnIntAry(int add, std::size_t* size);
extern "C" EXPORT void ReturnPtrFree(void* p);
extern "C" EXPORT void ShowFuncSig();
```

ReturnValue.cpp
-
```cpp
#include <string>
#include <iostream>
#include "ReturnValue.h"

// 123 に int型 の引数を足して返す
__declspec(dllexport) int ReturnInt(int i)
{
	return 123 + i;
}

// 1.23 に double型 の引数を足して返す
__declspec(dllexport) double ReturnDouble(double d)
{
	return 1.23 + d;
}

// bool型 の引数を受け取って返す
__declspec(dllexport) bool ReturnBool(bool b)
{
	return b;
}

// Hello, C++!\n に const char*型 の引数を足して返す
__declspec(dllexport) char* ReturnString(const char* c)
{
	std::string s = std::string(c) + "Hello, C++!\n";

	std::size_t byte = s.size() + 1;
	char* p = static_cast<char*>(::CoTaskMemAlloc(byte));
	if (!p) return nullptr;

	std::memcpy(p, s.c_str(), byte);
	return p;
}

// int型配列 の各要素に int型引数 を足して返す
__declspec(dllexport) int* ReturnIntAry(int add, std::size_t* size)
{
	static const int array[] = { 1, 3, 5, 7, 9, 11, 13 };
	constexpr std::size_t N = std::size(array);

	int* p = static_cast<int*>(::CoTaskMemAlloc(sizeof(int) * N));
	if (!p) return nullptr;

	for (std::size_t i = 0; i < N; ++i)
		p[i] = array[i] + add;

	if (size) *size = N;

	return p;
}

// 共通解放関数
__declspec(dllexport)
void ReturnPtrFree(void* p)
{
	::CoTaskMemFree(p);
}

// 現在のソース行番号 と 関数シグネチャ を表示する 
__declspec(dllexport) void ShowFuncSig()
{
	std::cout << __LINE__ << ": " << __FUNCSIG__ << '\n';
}
```

## ReturnValue.dll を生成する
リリースビルドをするとソリューションエクスプローラーの`\x64\Release\`の中に

> ・ReturnValue.dll
> ・ReturnValue.exp
> ・ReturnValue.lib
> ・ReturnValue.pdb

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920837/1da66c7b-f4d8-427c-97e1-2c08c1e1d5ed.png)

が生成されていると思いますので、`ReturnValue.dll` をコピーしておきます。


# C#.exeプロジェクトを作成する
**`新しいプロジェクトの作成(N)`** → **`C#コンソール アプリ`**
→ **`パス&プロジェクト名設定`**
と進み、以下のように設定します。
(今回はプロジェクト名を `ConsoleApp` としています。)

:::note warn
**C#コンソール アプリ(.NET Framework)** ではないことに注意してください。
:::


**・フレームワーク(F):** **`.NET 8.0(長期的なサポート)`**

**・コンテナーのサポートを有効にする:** Docker／Kubernetes などで動かす予定は無いため、チェックは外しておきます。

**・最上位レベルのステートメントを使用しない(T):** `static void Main(...)` メソッドはコード側に含まれているので、チェックは外しておきます。

**・native AOT 発行を有効にする:** 今回はデバッグ用なので、チェックは外しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920837/7291e04d-1ec3-44cf-b967-d34de90b47e1.png)

以上設定ができましたら、**作成(C)** をクリックします。

## プロジェクトのビルド設定
C# コンソール プロジェクトを作成すると、おそらく`Any CPU`となっていると思います。
なのでここを正しい環境に合わせて設定する必要があります。

**`ビルド(B)`** → **`構成マネージャー(O)`** と進むと、
新たにウィンドウが出てくると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920837/33189dc8-59ed-4c9b-b594-7edbc0879d3d.png)


**アクティブ ソリューション プラットフォーム(P):** `Any CPU`
をクリックして **`<新規作成...>`** をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920837/77300204-61f8-4db3-9846-08402c41eddf.png)

するとまたウィンドウが出てくると思いますので、

**新しいプラットフォームを入力または選択してください(P):** **`x64`**

とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920837/5e500ea0-eed9-458a-b1dc-7d81adc1ff53.png)

設定ができましたら **OK** を押して戻ります。
ウィンドウ上部が **Debug**, **x64** となっていれば大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3920837/7e3ea70c-57d3-4ba4-a295-c85b523a8c4e.png)

## ReturnValue.dll の配置
先ほど .dll プロジェクトで生成した `ReturnValue.dll` を C#コンソールプロジェクト側に正しく配置をする必要があります。
**`ConsoleApp\bin\x64\Debug\net8.0`** の中に `ReturnValue.dll` をペーストします。
フォルダが見当たらない場合、一度Visual Studio上でビルドを実行すると生成されるかと思います。

適切に`.dll`を配置できていない場合、以下手順を進めても
```
System.DllNotFoundException: 'Unable to load DLL 'ReturnValue.dll' or one of its dependencies: 指定されたモジュールが見つかりません。 (0x8007007E)'
```
DLLが見つからないというエラーが表示され、正常に動作しません。

## C#呼び出し側()
```cpp
using System;
using System.Runtime.InteropServices;

namespace ConsoleApp
{
    internal class Program
    {
        [DllImport("ReturnValue.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern int ReturnInt(int i);

        [DllImport("ReturnValue.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern double ReturnDouble(double d);

        [DllImport("ReturnValue.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern bool ReturnBool(bool b);

        [DllImport("ReturnValue.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern IntPtr ReturnString(string s);

        [DllImport("ReturnValue.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern IntPtr ReturnIntAry(int add, out UIntPtr size);

        [DllImport("ReturnValue.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern void ReturnPtrFree(IntPtr p);

        [DllImport("ReturnValue.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern void ShowFuncSig();


        static void Main(string[] args)
        {
            Console.WriteLine(RuntimeInformation.FrameworkDescription);

            // 123 に int型 の引数を足して返す
            Console.Write($"int: {ReturnInt(123333)}\n");


            // 1.23 に double型 の引数を足して返す
            Console.Write($"double: {ReturnDouble(0.00456):F5}\n");


            // bool型 の引数を受け取って返す
            Console.Write($"bool: {ReturnBool(true)}\n");


            // Hello, C++!\n に const char*型 の引数を足して返す
            IntPtr pStr = ReturnString("From C#: ");
            string? str = Marshal.PtrToStringAnsi(pStr);
            ReturnPtrFree(pStr);
            Console.Write(str);

            // int型配列 の各要素に int型引数 を足して返してから
            IntPtr pArry = ReturnIntAry(2, out var size);
            int len = checked((int)size);
            var result = new int[len];
            Marshal.Copy(pArry, result, 0, len);
            ReturnPtrFree(pArry);
            Console.Write($"int[]: {string.Join(", ", result)}");


            // .dll側のソース行番号 と 関数シグネチャ を表示する 
            Console.WriteLine("");
            ShowFuncSig();
        }
    }
}
```

# 結果出力
```
int: 123456
double: 1.23456
bool: True
From C#: Hello, C++!
int[]: 2, 4, 6, 8, 10, 12, 14
45: void __cdecl ShowFuncSig(void)
```
なにか間違っている点があれば教えてください。

**参考文献**
[C++ライブラリ（DLL）をUnity（C#）向けに作成して利用するシンプルな方法](https://zenn.dev/meson_tech_blog/articles/make-dll-for-unity?redirected=1)
[C#からC/C++の関数をコールする方法　まとめ①](https://qiita.com/TackKaiware/items/27ebcf10bb2624db197a)

(C#コンソールプロジェクト設定を追記しました。)
(確保と解放をdll側で統一しました。)