はじめに
先日、Unity6(C#)にC++から値を渡したい記事 を公開しましたが「別に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)
と設定します。
(今回はプロジェクト名をReturnValue
としています。)
C/C++側
ReturnValue.h
とReturnValue.cpp
を作成し、以下のように記述します。
ReturnValue.h
#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
#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
が生成されていると思いますので、ReturnValue.dll
をコピーしておきます。
C#.exeプロジェクトを作成する
新しいプロジェクトの作成(N)
→ C#コンソール アプリ
→ パス&プロジェクト名設定
と進み、以下のように設定します。
(今回はプロジェクト名を ConsoleApp
としています。)
C#コンソール アプリ(.NET Framework) ではないことに注意してください。
・フレームワーク(F): .NET 8.0(長期的なサポート)
・コンテナーのサポートを有効にする: Docker/Kubernetes などで動かす予定は無いため、チェックは外しておきます。
・最上位レベルのステートメントを使用しない(T): static void Main(...)
メソッドはコード側に含まれているので、チェックは外しておきます。
・native AOT 発行を有効にする: 今回はデバッグ用なので、チェックは外しておきます。
以上設定ができましたら、作成(C) をクリックします。
プロジェクトのビルド設定
C# コンソール プロジェクトを作成すると、おそらくAny CPU
となっていると思います。
なのでここを正しい環境に合わせて設定する必要があります。
ビルド(B)
→ 構成マネージャー(O)
と進むと、
新たにウィンドウが出てくると思います。
アクティブ ソリューション プラットフォーム(P): Any CPU
をクリックして <新規作成...>
をクリックします。
するとまたウィンドウが出てくると思いますので、
新しいプラットフォームを入力または選択してください(P): x64
とします。
設定ができましたら OK を押して戻ります。
ウィンドウ上部が Debug, x64 となっていれば大丈夫です。
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#呼び出し側()
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#)向けに作成して利用するシンプルな方法
C#からC/C++の関数をコールする方法 まとめ①
(C#コンソールプロジェクト設定を追記しました。)
(確保と解放をdll側で統一しました。)