何をしたいのか
C++で書いていたツールを訳あってC#のWPFアプリケーションに移植することになりました。
既存資産を生かすため、ロジックはC++のDLLにして、C#からC++のDLLを読みだして、双方向にやり取りすることを目的とします。
動作イメージ

①WPFの画面にテキストを入力すると、C++側でテキストに"Made by C++DLL"を付与して返す。
②C++側の処理は非同期処理を想定するため、コールバック関数でテキストをC#側に返します。
③返ってきたテキストをC#側 WPFのラベルに表示する
プロジェクト構成
変更するファイルは以下の通り
- C++ DLL側
- dllmain.cpp (DLL側の記述はすべてここに書いています)
- WPFアプリ側
- MainWindow.xaml(WPF側のxaml)
- MainWindow.xaml.cs(WPF側のロジックすべて)
ソースコード
dllmain.cpp
#include "pch.h"
#include <string>
#include <thread>
BOOL APIENTRY DllMain(
HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}
// DLLとC#間でやり取りするメッセージを受け取るコールバック関数
typedef bool(__stdcall* MessageCallback)(const char* message);
// DLL内でメッセージの送受信を管理するクラス
class MessageService {
private:
// C#へメッセージを通知するコールバック関数
MessageCallback callback = nullptr;
// 送信したメッセージの回数
int messageCount = 0;
public:
// C#から受け取ったコールバック関数を登録する
void SetCallback(MessageCallback newCallback) {
callback = newCallback;
}
// 登録済みのコールバック関数を解除する
void ClearCallback() {
callback = nullptr;
}
// メッセージを送信した回数を取得する
int GetMessageCount() const {
return messageCount;
}
// コールバックが登録されているか調べる
bool HasCallback() const {
return callback != nullptr;
}
// C#へメッセージを送信する
// 処理に1秒かかるので、UIスレッド以外から呼び出すことを想定している
bool SendMessageToCallback(const std::string& message) {
Sleep(1000);
// メッセージ送信回数を1増やす
messageCount++;
// C#へ返すメッセージを作成する
std::string responseMessage = "Made by C++DLL " + message;
// コールバックが登録されていれば呼び出す
if (callback != nullptr) {
return callback(responseMessage.c_str());
}
// コールバック未登録の場合は何もしない
return true;
}
};
// DLL全体で共有するMessageServiceオブジェクト
MessageService messageService;
// C#からコールバック関数を登録する
extern "C" __declspec(dllexport)
void __stdcall SetCallback(MessageCallback callback)
{
messageService.SetCallback(callback);
}
// C#からコールバック関数を解除する
extern "C" __declspec(dllexport)
void __stdcall ClearCallback()
{
messageService.ClearCallback();
}
// C#から受け取ったメッセージを加工し、コールバックでC#へ通知する
extern "C" __declspec(dllexport)
bool __stdcall OnGetMessage(const char* message)
{
if (message == nullptr) {
return false;
}
std::thread(
&MessageService::SendMessageToCallback,
&messageService,
std::string(message)
).detach();
return true;
}
// メッセージ送信回数を取得する
extern "C" __declspec(dllexport)
int __stdcall GetCount()
{
return messageService.GetMessageCount();
}
MainWindow.xaml
<Window x:Class="WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF"
mc:Ignorable="d"
Title="C++ DLL Callback Sample"
Height="450"
Width="800">
<StackPanel Margin="40"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Width="300">
<TextBlock Text="C++ DLLへ送るメッセージ"
Margin="0,0,0,8"/>
<TextBox Text="{Binding TextBoxText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Height="30"
Margin="0,0,0,16"/>
<!--<Button Content="DLLへ送信"
Height="40"
Click="Button_Click"
Margin="0,0,0,16"/>-->
<TextBlock Text="C++ DLLからの返信"
Margin="0,0,0,8"/>
<TextBlock Text="{Binding ResponseMessage}"
TextWrapping="Wrap"
MinHeight="30"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
namespace WPF
{
// C++ DLLとのやり取りを担当するクラス
// WPFの画面やViewModelには依存しない
static class Native
{
// C++ DLLからC#へ通知するときに使うコールバック関数の型
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool MessageCallback(
[MarshalAs(UnmanagedType.LPStr)] string message);
// C#のコールバック関数をC++ DLLへ登録する
[DllImport("C++DLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void SetCallback(MessageCallback callback);
// C#からC++ DLLへメッセージを渡す
[DllImport("C++DLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern bool OnGetMessage(
[MarshalAs(UnmanagedType.LPStr)] string message);
// C++ DLL側で処理したメッセージ数を取得する
[DllImport("C++DLL.dll", CallingConvention = CallingConvention.StdCall)]
public static extern int GetCount();
// コールバックdelegateを保持するための変数
// これを保持しないとGCで回収される可能性がある
private static MessageCallback? _callback;
// C++ DLLとの接続を初期化する
public static void Initialize(MessageCallback callback)
{
_callback = callback;
SetCallback(_callback);
}
// C++ DLLへメッセージを送信する
public static void SendMessageToCppDll(string message)
{
OnGetMessage(message);
}
}
// 画面に表示するデータを管理するViewModel
public sealed class MainViewModel : INotifyPropertyChanged
{
// アプリ全体で共有するViewModelインスタンス
public static readonly MainViewModel Instance = new();
private MainViewModel()
{
// C++ DLLから呼ばれるコールバックを登録する
Native.Initialize(ReceiveMessageFromNative);
}
// C++ DLLからメッセージを受け取る処理
private bool ReceiveMessageFromNative(string message)
{
// DLLから受け取った文字列を画面表示用プロパティへ反映する
ResponseMessage = message;
// DLL側で処理された回数をデバッグ出力する
Debug.WriteLine($"MessageCount: {Native.GetCount()}");
return true;
}
// TextBoxの内容をC++ DLLへ送信する
public void SendTextBoxMessage()
{
Native.SendMessageToCppDll(TextBoxText);
}
private string _textBoxText = "";
// TextBoxに入力された文字列
public string TextBoxText
{
get => _textBoxText;
set
{
_textBoxText = value;
PropertyChanged?.Invoke(this, new(nameof(TextBoxText)));
// TextBoxの内容が変わるたびにC++ DLLへ送信する
SendTextBoxMessage();
}
}
private string _responseMessage = "";
// C++ DLLから返ってきたメッセージ
public string ResponseMessage
{
get => _responseMessage;
set
{
_responseMessage = value;
PropertyChanged?.Invoke(this, new(nameof(ResponseMessage)));
}
}
// WPFのBindingへプロパティ変更を通知するイベント
public event PropertyChangedEventHandler? PropertyChanged;
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = MainViewModel.Instance;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// C++ DLLへ現在のTextBox文字列を送信する
Native.SendMessageToCppDll(MainViewModel.Instance.TextBoxText);
}
}
}
実装で心がけた部分
① C++ DLL側
C++ DLL側では、次の点を意識して実装しています。
- C#から呼び出しやすい関数を公開する
- C#へ結果を返すためにコールバック関数を登録する
- DLL内で状態を管理する
- 重い処理を想定して別スレッドで処理する
コールバック関数を登録してC#へ結果を返す
C++ DLL側では、C#から渡されたコールバック関数を MessageCallback として保持しています。
typedef bool(__stdcall* MessageCallback)(const char* message);
MessageCallback callback = nullptr;
これで、C++側の処理が終わったタイミングで、C#側の関数を呼び出すことができる。
callback(responseMessage.c_str());
非同期処理を想定して std::thread を使う
今回はC++側の処理に時間がかかる想定として、Sleep(1000) を入れています。
Sleep(1000);
この処理をC#のUIスレッドで待ってしまうと、画面が固まる原因になります。
そのため、DLL側では std::thread を使って別スレッドで処理しています。
std::thread(
&MessageService::SendMessageToCallback,
&messageService,
std::string(message)
).detach();
messageをstd::string(message) に変換しているのは、const char* のまま別スレッドへ渡すと、元の文字列の寿命が切れる可能性があるため。
messageService はDLL全体で共有する
MessageService messageService;
このオブジェクトをDLL全体で共有することで、C#から何度呼び出しても共通の状態を扱えます。
messageCount の値が呼び出しごとに増えていきます。
② WPF側:Nativeクラスの役割
Nativeは画面やViewModelを知らないようにする
Native クラスは「DLLとの通信だけ」に集中させるため、MainWindow や MainViewModel を直接参照しないようにしています。
もし Native が MainViewModel.Instance.ResponseMessage を直接更新してしまうと、DLL通信と画面更新の責務が混ざってしまいます。
そのため、C++ DLLから返ってきたメッセージをどう扱うかは、MainViewModel 側で決めるようにしています。
DllImportはNativeクラスにまとめる
C++ DLLの関数は、DllImport を使ってC#側に定義します。
[DllImport("C++DLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern bool OnGetMessage(
[MarshalAs(UnmanagedType.LPStr)] string message);
このようなDLL呼び出しの定義を Native に集めることで、C++ DLLとの接続部分を一か所にまとめられます。
コールバックdelegateを保持する
C++ DLLへ渡すコールバックは delegate として定義します。
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool MessageCallback(
[MarshalAs(UnmanagedType.LPStr)] string message);
そして、登録されたコールバックは Native クラス内でstatic変数として保持しています。
private static MessageCallback? _callback;
これは、delegate がGCで回収されないようにするためです。
実行時に起こりうる例外、その対処について
DllNotFoundException
DLLが見つからない場合に発生します。
System.DllNotFoundException
よくある原因
-
C++DLL.dllがWPFアプリの実行フォルダにない - DLL名が間違っている
BadImageFormatException
DLLのビット数が一致していない場合に発生します。
EntryPointNotFoundException
C#側で指定した関数名がDLL内に見つからない場合に発生します。
extern "C" を付け忘れると、C++の名前修飾のせいで、C#側から OnGetMessage という名前で見つけられないことがあります。
DLLで公開している関数名の確認方法
x64 Native Tools for visual studioを使って確認できます。
dumpbin /exports C++DLL.dll

このように、OnGetMessageの後に@@YA_NPEBD@Zのような謎の文字列が付いている場合、
C++の名前修飾が付いています。
AccessViolationException
不正なメモリアクセスが起きた場合に発生します。
System.AccessViolationException
主な原因
- delegateをC#側で保持していない
- C++側に渡した文字列の寿命が切れている
- C++側で不正なポインタを呼び出している
-
CallingConventionが一致していない
const char* のまま別スレッドに渡すと、元の文字列が無効になった後に参照してしまう可能性があるので、std::stringに変換してます
std::thread(
&MessageService::SendMessageToCallback,
&messageService,
std::string(message)
).detach();
例外にならないけど、文字列の文字コードについて
今回のコードではC++側が const char*、C#側が LPStr です。
[MarshalAs(UnmanagedType.LPStr)] string message
日本語を扱う場合はこのように C++側をwchat_tを使うのが良いです。
wchar_t配列=LPWStr(Word型 16bit文字の文字列ポインタ)
C++側:
typedef bool(__stdcall* MessageCallback)(const wchar_t* message);
extern "C" __declspec(dllexport)
bool __stdcall OnGetMessage(const wchar_t* message)
C#側:
[MarshalAs(UnmanagedType.LPWStr)] string message

