LoginSignup
8
11

More than 5 years have passed since last update.

既存のC++ネイティブプロジェクトでC#マネージドコードを使う

Last updated at Posted at 2019-03-29

はじめに

こんにちは。Daddy's Officeの市川です。

私が10年以上開発を続けているWindowsPCを監視カメラシステムにする「LiveCapture3」
先日、今後の拡張性や生産性を考慮して、内部処理を見直しました。

基本的には、C++で構築していた各機能を、C#で置き換える、という作業です。

個人的には今でもC++は好きな言語なのですが、Windowsアプリ開発を行う場合、APIや各種SDKの提供状況を考えると、C#に移行していかないと難しい状況です。

とはいっても、すべてをC#に移行するのは現実的ではありません。

そこで「C++からC#をコールする」方法を調べました。

C++からC#をコールする方法

色々な方法がありますが、C#の生産性の高さを既存のC++ネイティブプロジェクトで利用したい、というのが大きな目的なので、C++/CLIは極力使いたくない。
C#の処理をCOM参照可能にする、というのもコード量が増えるしめんどくさい。

そこで、

「C#処理をDLLで作成し、C++/CLIラッパープロジェクト経由で、C++ネイティブプロジェクトからコールする」

という方法で対応しました。

これであれば、若干スマートさには欠けますが、既存のC++ネイティブプロジェクトを変更せず、生産性の高いC#で処理を記述できます。

構成としてはこんな感じです。
image.png

実装(AES複号)

今回、AESの複号処理を追加する必要があったのですが、C++でAES複号処理を記述するのは非常に面倒。
そこで、この方法を使用して、AES複号処理をC#で記述し、それをネイティブC++からコールする形にしました。

DLLプロジェクト(C#)

まず、AESの複号を行うC#のクラスを作成します。
プロジェクトをC#のクラスライブラリとして作成し、下記のようなクラスを定義します。

AES複号クラス(C#)
namespace AesCrypto
{
    public class AesDecoder
    {
        public void Decode(byte[] Key, byte[] IV, byte[] src, out byte[] dst)
        {
            AesCryptoServiceProvider aes = new AesCryptoServiceProvider();
            aes.BlockSize = 256;
            aes.KeySize = 256;
            aes.IV = IV;
            aes.Key = Key;
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            using (ICryptoTransform decrypt = aes.CreateDecryptor())
            {
                dst = decrypt.TransformFinalBlock(src, 0, src.Length);
            }
        }
    }
}

うーん、簡単です!

メソッドはDecodeメソッドのみで、引数に、鍵と初期ベクタ、AESエンコードされたデータを渡すと、out引数にデコードされたデータが格納される感じです。

DLLラッパープロジェクト(C++/CLI)

作成したDLLプロジェクトを参照に追加して、C++/CLIのDLLプロジェクトを作成します。

このプロジェクトの目的は、マネージドとアンマネージドの引数型変換と、参照で追加したAES複号クラスの生成とメソッドコールになります。

アンマネージド-マネージド引数型変換とC#クラスのコール(C++/CLI)
__declspec(dllimport) HRESULT DoAesDecode(
        LPBYTE pKey, DWORD dwKeyLength,
        LPBYTE pIV, DWORD dwIVLen,
        LPBYTE pSrcData, DWORD dwSrcDataLen,
        LPBYTE pDstData, LPDWORD pdwDstDataLen)
{
    HRESULT hr = S_OK;

    try {

        # C#クラスに渡すマネージド配列を作成
        array< Byte >^ key = gcnew array< Byte >(dwKeyLength);
        array< Byte >^ iv = gcnew array< Byte >(dwIVLen);
        array< Byte >^ src = gcnew array< Byte >(dwSrcDataLen);
        array< Byte >^ dst;

        # 引数のメモリ(アンマネージド)を、マネージド配列にコピー      
        Marshal::Copy((IntPtr)pKey, key, 0, dwKeyLength);
        Marshal::Copy((IntPtr)pIV, iv, 0, dwIVLen);
        Marshal::Copy((IntPtr)pSrcData, src, 0, dwSrcDataLen);

        # C#クラスを作成して、メソッドをコール
        AesDecoder^ decoder = gcnew AesDecoder();
        decoder->Decode(key, iv, src, dst);

        # 戻り値をアンマネージドメモリにコピー        
        pin_ptr<Byte> pinnedBuf = &dst[0];
        memcpy_s(pDstData, *pdwDstDataLen, pinnedBuf, dst->Length);
        *pdwDstDataLen = dst->Length;
    }
    catch (Exception^ e) {
        hr = E_FAIL;
    }

    return hr;
}

AES複号を行うので、扱うデータはメモリ上のバイナリ値になります。

当然、C++上で確保したメモリ領域のポインタをC#クラスに渡すことはできませんので、変換処理が必要になります。

まず、C#クラスに渡す為のByte配列を定義し、そこに、引数で渡されてきたメモリの中身をMarshal::Copyでコピーします。

引数の内容がコピーされたByte配列を引数にC#の複号クラスのメソッドをコールします。

結果はout引数のByte配列に確保されますので、一旦pin_ptrでピン止めしてからmemcpyで内容をコピーします。

C++/CLIは、(個人的には)あまり書きたくないので、極力、型変換処理のみに抑えるようにしています。

メインプロジェクト(ネイティブC++)

あとは、作成したDLLラッパープロジェクト(C++/CLI)でExportされた関数をC、++ネイティブプロジェクトで呼び出すだけです。

下記のようなExport宣言を行います。

Export関数宣言(C++/CLI)
__declspec(dllexport) HRESULT DoAesDecode(  
                    LPBYTE pKey, DWORD dwKeyLength, 
                    LPBYTE pIV, DWORD dwIVLen,
                    LPBYTE pSrcData, DWORD dwSrcDataLen, 
                    LPBYTE pDstData, LPDWORD pdwDstDataLen);

この関数をC++ネイティブプロジェクト内でコールすると、AESの複号処理が実行されます。

参考

以下の情報を参考にさせていただきました。

Calling C# .NET methods from unmanaged C/C++ code
Visusal C++ネイティブプロジェクトからC#マネージドコードを使用する方法を不要な文章抜きで説明する
C#プログラミング解説 マーシャリング (Marshaling)

8
11
1

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
8
11