はじめに
タイトルの通りですが、勉強のためC#からC++のDLLへデリゲートを渡すサンプルコードを作成してみました。仕様のイメージは以下の通りです。
- C#側のEXEからC++のDLLの関数を呼び出す。
- その際、C#で実装したコールバックメソッドのデリゲートをC++側へ引数で渡す。
- DLL内部の処理の各チェックポイントにおいて、上で渡されたメソッドをコールバックする。
ようは、DLL内部の処理のプログレスを、都度、C#側にコールバック関数経由で通知するイメージです。
実装を見れば分かるように、プログレス情報は文字列で渡されています。ここは整数で受け渡しするのが自然だと思いますが、おそらくハマるであろう「C#/C++間でワイド文字列を受け渡しする」勉強のために、不自然ながらもそのように実装しました。
なお、DLLファイルは、32ビット/64ビットの両プロセスに対応させるため、それぞれに対応した「TestDll32.dll」「TestDll64.dll」の2種類をコンパイルして生成しています。これらはファイル名が異なるだけで、ソースファイルは共通です(詳細は「謝辞」を参照下さい)。
C#とC++間のP/InvokeをWindows上で試してみる
実行例
> PInvokeTest.exe
処理を開始します(C#側で定義した文字列)
Progress :処理1が完了しました.
Progress :処理2が完了しました.
Progress :処理3が完了しました.
DLLを使用する側の実装(C#)
登場人物を紹介します。
識別子 | 型 | 用途 |
---|---|---|
CallbackDelegate | デリゲート | コールバック関数を登録するデリゲート。 |
TestFunction | メソッド | DLL関数のラッパーメソッド。 |
ProgressNotifier | メソッド | DLL側からコールバックしてほしいメソッド。 |
(仮引数)progressMessage | 文字列 | DLL側からコールバックされる際、DLL側で代入した文字列が格納されている変数。 |
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace PInvokeTest
{
class Program
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int CallbackDelegate([MarshalAs(UnmanagedType.LPWStr)]string message);
[DllImport("TestDll32.dll", EntryPoint = "TestFunction", CharSet = CharSet.Unicode)]
private extern static int TestFunction_32(string message, [MarshalAs(UnmanagedType.FunctionPtr)] CallbackDelegate callback);
[DllImport("TestDll64.dll", EntryPoint = "TestFunction", CharSet = CharSet.Unicode)]
private extern static int TestFunction_64(string message, [MarshalAs(UnmanagedType.FunctionPtr)] CallbackDelegate callback);
/// <summary>
/// DLL関数のラッパー
/// </summary>
private static int TestFunction(string startMessage, CallbackDelegate callback)
{
return System.Environment.Is64BitProcess ? TestFunction_64(startMessage, callback) : TestFunction_32(startMessage, callback);
}
static void Main(string[] args)
{
CallbackDelegate pn = ProgressNotifier;
TestFunction("処理を開始します(C#側で定義した文字列)", pn);
}
/// <summary>
/// DLL側の処理内の各チェックポイントでコールバックされる。
/// <param name="progressMessage">処理状況を示すメッセージ</param>
/// </summary>
private static int ProgressNotifier(string progressMessage)
{
Console.WriteLine($"Progress :{progressMessage}.");
return 0;
}
}
}
DLLの実装(C++)
登場人物を紹介します。
識別子 | 型 | 用途 |
---|---|---|
TestFunction | 関数 | C#側から呼出しされる関数。この内部で、C#側から引数で渡されたコールバック関数を呼び出す。 |
(仮引数)start_message | LPWSTR | C#側から渡される文字列のポインタ。 |
(仮引数)callback | 関数ポインタ | C#側から渡されるコールバック関数。TestFunctionの内部で呼び出す。 |
CallbackFunctionPtr | 関数ポインタの型エイリアス | C#側のデリゲートに対応する関数ポインタの型エイリアス。 |
#include "stdafx.h"
#include "TestDll.h"
#include <iostream>
using namespace std;
namespace TestDll {
int TestFunction(LPWSTR start_message, CallbackFunctionPtr callback) {
_wsetlocale(LC_ALL, L"");
wprintf(L"%s\n", start_message);
// ToDo : ここで処理1を行う
callback(L"処理1が完了しました");
// ToDo : ここで処理2を行う
callback(L"処理2が完了しました");
// ToDo : ここで処理3を行う
callback(L"処理3が完了しました");
return 0;
}
}
#pragma once
#ifdef TESTDLL_EXPORTS
#define TESTDLL_API __declspec(dllexport)
#else
#define TESTDLL_API __declspec(dllimport)
#endif // TESTDLL_EXPORTS
namespace TestDll {
using CallbackFunctionPtr = int(*)(LPWSTR);
extern "C" {
TESTDLL_API int TestFunction(LPWSTR start_message, CallbackFunctionPtr callback);
}
}
はまったところ
C#側からC++側へワイド文字列を渡すのは簡単にできたのですが、C++側からC#側へ(コールバック関数の引数経由で)文字列を渡す所でハマりました。
デリゲートを定義するさい、以下のように引数(string message
)のマーシャリングを指示する必要があったようです。
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int CallbackDelegate([MarshalAs(UnmanagedType.LPWStr)]string message);
正直、マーシャリングの原理があまり理解できていないので、おいおい調べていきたいと思います。
謝辞
DLLの作成方法と、32ビット/64ビットの両プロセスへ対応するためのラッパー関数の実装は、こちらの記事とコメントを参考にさせていただきました。ありがとうございます。
C#とC++間のP/InvokeをLinux上で試してみる(2020/07/11追記)
上記では、C#もC++もVisual Studio上で実装し、Windows上で動かしてみた実験ですが、今回は
- .NET Core3.1で実験プログラムを実装(ターゲットはLinux(x64))。
- dllはLinux上で実装し、g++でコンパイル。
- Linux上で、実験プログラムが動作するか検証してみる。
というのもやってみました。
環境(Linux)
$ uname -a
Linux ip-10-1-2-203 5.3.0-1023-aws #25~18.04.1-Ubuntu SMP Fri Jun 5 15:18:30 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
ubuntu@ip-10-1-2-203:~/src/cpp/testdll$ lsb_release
No LSB modules are available.
ubuntu@ip-10-1-2-203:~/src/cpp/testdll$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.4 LTS
Release: 18.04
Codename: bionic
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.4 LTS
Release: 18.04
Codename: bionic
$ locale
LANG=C.UTF-8
LANGUAGE=
LC_CTYPE="C.UTF-8"
LC_NUMERIC="C.UTF-8"
LC_TIME="C.UTF-8"
LC_COLLATE="C.UTF-8"
LC_MONETARY="C.UTF-8"
LC_MESSAGES="C.UTF-8"
LC_PAPER="C.UTF-8"
LC_NAME="C.UTF-8"
LC_ADDRESS="C.UTF-8"
LC_TELEPHONE="C.UTF-8"
LC_MEASUREMENT="C.UTF-8"
LC_IDENTIFICATION="C.UTF-8"
LC_ALL=
$ g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
DLLを使用する側の実装(C#)
使用するソースコードは前述のもの(Program.cs
)と一字一句同じです。
ビルド方法は以下の通り。
>dotnet --version
3.1.301
>cd /d (.slnのあるディレクトリ)
>dotnet publish -c Release --self-contained true -r linux-x64
.NET Core 向け Microsoft (R) Build Engine バージョン 16.6.0+5ff7b0c9e
Copyright (C) Microsoft Corporation.All rights reserved.
復元対象のプロジェクトを決定しています...
復元対象のすべてのプロジェクトは最新です。
PInvokeTest -> G:\local\sandbox\PInvokeTest\PInvokeTest\bin\Release\netcoreapp3.1\linux-x64\PInvokeTest.dll
PInvokeTest -> G:\local\sandbox\PInvokeTest\PInvokeTest\bin\Release\netcoreapp3.1\linux-x64\publish\
bin\Release\netcoreapp3.1\linux-x64\publish\
配下のアセンブリ一式を、Linux上の任意の場所に配置します。
DLLの実装(C++)
Windows版からの主な変更点として、LPWSTR
をchar16_t *
に変更しました。
#include <iostream>
#include <codecvt>
#include <locale>
#include <string>
#include "TestDll.h"
namespace TestDll {
int TestFunction(char16_t *start_message, CallbackFunctionPtr callback) {
setlocale(LC_ALL, "ja_JP.UTF-8");
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> cv;
std::string u8str = cv.to_bytes(start_message);
std::cout << "---- C#側からC++側に渡した文字列をC++側で出力 ----" << std::endl;
std::cout << u8str << std::endl;
std::cout << "-------------------------------------------" << std::endl;
// ToDo : ここで処理1を行う
std::cout << "---- C#側からC++側にコールバックを渡して、C++側でコールバック関数の引数に文字列を渡してC#側で出力 ----" << std::endl;
callback(u"処理1が完了しました");
// ToDo : ここで処理2を行う
callback(u"処理2が完了しました");
// ToDo : ここで処理3を行う
callback(u"処理3が完了しました");
std::cout << "-------------------------------------------" << std::endl;
return 0;
}
}
namespace TestDll {
using CallbackFunctionPtr = int(*)(const char16_t *);
extern "C" {
int TestFunction(char16_t *start_message, CallbackFunctionPtr callback);
}
}
$ g++ -std=c++17 --pedantic -Wall -Wextra -shared -fPIC -I. -o TestDll64.so TestDll.cpp
コンパイルしたdllは、.NET Coreのアセンブリ一式を配置したディレクトリにコピーしておきます。
プログラムの実行
$ cd (.NET Coreのアセンブリ一式を配置したディレクトリ)
$ chmod 744 PInvokeTest
$ ./PInvokeTest
---- C#側からC++側に渡した文字列をC++側で出力 ----
処理を開始します(C#側で定義した文字列)
-------------------------------------------
---- C#側からC++側にコールバックを渡して、C++側でコールバック関数の引数に文字列を渡してC#側で出力 ----
Progress :処理1が完了しました.
Progress :処理2が完了しました.
Progress :処理3が完了しました.
-------------------------------------------
はまったところ
C++側での文字列型の扱い。精進します。