Windows or Linux上でC#/C++間のP/Invokeを試してみる（引数=デリゲート、ワイド文字列）

#はじめに

タイトルの通りですが、勉強のため**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側で代入した文字列が格納されている変数。|

```csharp:Program.cs
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#側のデリゲートに対応する関数ポインタの型エイリアス。|

```cpp:TestDll.cpp
#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;
	}
}
```

```cpp:TestDll.h
#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`）のマーシャリングを指示する必要があったようです。

```csharp:Program.cs（抜粋）
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int CallbackDelegate([MarshalAs(UnmanagedType.LPWStr)]string message);
```

正直、マーシャリングの原理があまり理解できていないので、おいおい調べていきたいと思います。

##謝辞
DLLの作成方法と、32ビット／64ビットの両プロセスへ対応するためのラッパー関数の実装は、こちらの記事とコメントを参考にさせていただきました。ありがとうございます。

「[C#からC/C++の関数をコールする方法　まとめ①](http://qiita.com/TackKaiware/items/27ebcf10bb2624db197a)」

# 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 *`に変更しました。

```cpp:TestDll.cpp
#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;
    }
}
```

```cpp:TestDll.h
namespace TestDll {
    using CallbackFunctionPtr = int(*)(const char16_t *);

    extern "C" {
        int TestFunction(char16_t *start_message, CallbackFunctionPtr callback);
    }
}
```

```bash:コンパイル（Linux上）
$ g++ -std=c++17 --pedantic -Wall -Wextra -shared -fPIC -I. -o TestDll64.so TestDll.cpp
```

コンパイルしたdllは、.NET Coreのアセンブリ一式を配置したディレクトリにコピーしておきます。

## プログラムの実行

```bash:プログラムの実行と実行結果（Linux上）
$ cd （.NET Coreのアセンブリ一式を配置したディレクトリ）
$ chmod 744 PInvokeTest
$ ./PInvokeTest
---- C#側からC++側に渡した文字列をC++側で出力 ----
処理を開始します（C#側で定義した文字列）
-------------------------------------------
---- C#側からC++側にコールバックを渡して、C++側でコールバック関数の引数に文字列を渡してC#側で出力 ----
Progress :処理1が完了しました.
Progress :処理2が完了しました.
Progress :処理3が完了しました.
-------------------------------------------
```
##はまったところ
C++側での文字列型の扱い。精進します。