1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++のDLLをC# WPFから使う(双方向のやり取りあり)

1
Posted at

何をしたいのか

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

動作イメージ

image.png
①WPFの画面にテキストを入力すると、C++側でテキストに"Made by C++DLL"を付与して返す。
②C++側の処理は非同期処理を想定するため、コールバック関数でテキストをC#側に返します。
③返ってきたテキストをC#側 WPFのラベルに表示する

プロジェクト構成

image.png

変更するファイルは以下の通り

  • 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との通信だけ」に集中させるため、MainWindowMainViewModel を直接参照しないようにしています。

もし NativeMainViewModel.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

image.png

C#側で指定した関数名がDLL内に見つからない場合に発生します。

extern "C" を付け忘れると、C++の名前修飾のせいで、C#側から OnGetMessage という名前で見つけられないことがあります。

DLLで公開している関数名の確認方法
x64 Native Tools for visual studioを使って確認できます。

dumpbin /exports C++DLL.dll

image.png
このように、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
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?