LoginSignup
21
24

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-07-23

はじめに

タイトルの通りですが、勉強のため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側で代入した文字列が格納されている変数。
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#側のデリゲートに対応する関数ポインタの型エイリアス。
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;
    }
}
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)のマーシャリングを指示する必要があったようです。

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

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

謝辞

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

C#からC/C++の関数をコールする方法 まとめ①

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版からの主な変更点として、LPWSTRchar16_t *に変更しました。

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;
    }
}
TestDll.h
namespace TestDll {
    using CallbackFunctionPtr = int(*)(const char16_t *);

    extern "C" {
        int TestFunction(char16_t *start_message, CallbackFunctionPtr callback);
    }
}
コンパイル(Linux上)
$ g++ -std=c++17 --pedantic -Wall -Wextra -shared -fPIC -I. -o TestDll64.so TestDll.cpp

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

プログラムの実行

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

はまったところ

C++側での文字列型の扱い。精進します。

21
24
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
21
24