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

#はじめに
タイトルの通り、たまーにC#からC/C++の関数を呼ぼうとすると
アレやコレやとド忘れしていて大失敗、その都度ネットを徘徊し手順を確認するのもアレなので
備忘録としてここに記載いたします。
なお、Visual Studio 2015での実施を前提としております。

**[2016/10/23追記]
コメント欄でのご指摘を受け、32bit/64bitの両環境に対応した設定方法を
「6．片手落ちだったので追加説明」に追記いたしました。**

#1．事前準備

## 1-1．C#プロジェクトの作成

ソリューションを作ったら、関数をコールする側としてコンソールアプリを作ります。
名前は適当に"Application"としましょう。
![001.png](https://qiita-image-store.s3.amazonaws.com/0/143565/bfc49852-9e1e-3c81-ce0e-89b24cfb69f4.png)


## 1-2．C/C++プロジェクトの作成
次にコールされる側のDLLを作成するC/C++用のプロジェクトを作ります。とりあえず空で。
![002.png](https://qiita-image-store.s3.amazonaws.com/0/143565/d12d9f14-90f9-8952-de4e-6dece816ebf9.png)

＜確認＞
こないなことになっているはずです。
![003.png](https://qiita-image-store.s3.amazonaws.com/0/143565/ced1dcda-4ff5-e582-adee-0c251a2db66c.png)

## 1-3．C/C++プロジェクトの設定変更
ガリガリっとコードを書く前に、プロジェクトの設定を以下の通り変更します。
ソリューションエクスプローラーのLibraryを右クリック -> プロパティ -> 全般 -> 構成の種類->
**ダイナミックライブラリ(.dll)**

これでDLLが作成されるわけだ:hotsprings:
![004.png](https://qiita-image-store.s3.amazonaws.com/0/143565/e41f4128-74db-ba92-3952-4e4b5e939fb7.png)

#2．コードかきかき

## 2-1．C/C++関数の作成
次に`library.h`と`libray.cpp`を作成し、テスト用関数を定義します。

```c:library.h
#pragma once

extern "C"
{
	__declspec( dllexport ) void Test();
}
```
```c:library.cpp
#include <iostream>
#include "library.h"

void Test()
{
	std::cout << "Cの関数が呼び出されました。" << std::endl;
}
```


`exetern "C"`と`__declspec(dllexport)`についてはMSDNに説明を任せた！ｗ

`exetern "C"`
https://msdn.microsoft.com/ja-jp/library/wf2w9f6x.aspx
`__declspec(dllexport)`
https://msdn.microsoft.com/ja-jp/library/a90k134d.aspx


`__declspec(dllexport)`は、タイプするには長めなのでdefineしてもよさそう。

```c:library.h
#pragma once

#define DllExport	__declspec( dllexport )

extern "C"
{
	DllExport void Test();
}
```

ビルドはC#のアプリと一緒にやるのでおあずけです。:neutral_face:

## 2-1．C#側に関数宣言を記述する
デフォルトで作成されている`Program.cs`を開き、以下の通り
C/C++関数をコールするための宣言を記述します。

```csharp:Program.cs
using System.Runtime.InteropServices;

namespace Application
{
    internal class Program
    {
        [DllImport( "Library.dll" )]
        static extern void Test();

        static void Main()
        {
            Test();
        }
    }
}
```

:loudspeaker:関数宣言のルール
・`[DllImport]`属性をつけること。
・`[DllImport]`のカッコの中は利用するDLL名とすること。
・関数の修飾子は`static extern`とすること。
・関数名はC/C++側と同じにすること。（引数は事情が異なるのでまたの機会に）

#3．ビルドその前に

今回はC#で作成したアプリケーションと、C/C++で作成したDLLの二つをビルドする必要があります。
プロジェクト毎に一回一回ビルドするのも面倒なので、この際ドバ~っと一気にやっちゃう設定をしましょう。
メニューバーのビルド -> バッチビルドを開き以下の設定にします。

![005.png](https://qiita-image-store.s3.amazonaws.com/0/143565/19f681c7-48eb-dd80-ced3-9886f4b30bd4.png)


**2016/10/16修正**
**コメント欄にて、DLLをコピーするバッチを作成しなくても、C/C++のDLLの出力先を
C#アプリの実行ファイルと同じにすれば良いのでは？とご指摘を頂きました。
その通りですので修正させて頂きます。**

C/C++のDLLをC#側で利用するためには、C#アプリの実行ファイル(.exe)が格納されているフォルダに
DLLを置かなくてはいけません。ビルド時に同じパスに出力されるよう、以下の通りDLLの出力先フォルダを変更します。
(Debugモード時とReleaseモード時で異なる設定が必要なため注意)

＜Debugモードの設定＞
ソリューションエクスプローラーのLibraryを右クリック -> プロパティ -> 全般 -> **出力ディレクトリ**を
`$(SolutionDir)\Application\bin\Debug\`に変更 -> 適用ボタン ぽちり。

![010.png](https://qiita-image-store.s3.amazonaws.com/0/143565/230d2dc0-4007-a669-f2bf-bb47d15b2cfe.png)

＜Releaseモードの設定＞
（プロパティーページのまま）構成(C)を`Release`に切り替え。**出力ディレクトリ**を
`$(SolutionDir)\Application\bin\Release\`に変更 -> OKボタン ぽちり。
![011.png](https://qiita-image-store.s3.amazonaws.com/0/143565/98c07c37-f387-c338-8fca-046bf14d0ff0.png)

#4．ビルドぶちこんでやるぜ！！
待ちに待ったF7タイムですが、今回はバッチビルドを実行しますので以下の手順で行います。
メニューバーのビルド -> バッチビルド -> ビルド(B)

・・・で、出力ウィンドウにこのような結果が表示されればOK。
（画像は取り直しました）
![008_new.png](https://qiita-image-store.s3.amazonaws.com/0/143565/d5e4713f-6639-3412-2234-b1dfa7f676db.png)

これ以降C/C++のDLLのみ修正した場合は、C#側のアプリをビルドする必要無し。
（そうでないとDLLのうま味がねェ~）

#5． 動作確認
プロジェクトのターゲットをC#の"Application"に指定してCtrl+F5。

![009.png](https://qiita-image-store.s3.amazonaws.com/0/143565/fef94d83-7dc7-259b-8c01-6dd731ff05b2.png)
無時、C#側からC/C++の関数を呼び出せているようです。:tada:

**~~や っ た ぜ 。~~:sunglasses:**
（実はちゃんとできていませんでした・・・）

#6．片手落ちだったので追加説明
※この項の説明はこちらの記事を参考にさせて頂きました。
[.NETにおける64ビットプロセスと32ビットプロセスについて](http://qiita.com/mima_ita/items/57d7c1101543e214b1d6)

※あとこちらのサイト様
[C#＆32bitアンマネージDLL／64bitアンマネージDLLの動的な呼び出し方法](http://gurizuri0505.halfmoon.jp/develop/csharp-2/unmanagedll32_64)

## 6-1．なにがダメだったのか
コメント欄にて、「その設定とC#のコードだとある条件下では動かないぞ」という旨のご指摘を
頂きました。まずはビルド設定の振り返りから。

![005.png](https://qiita-image-store.s3.amazonaws.com/0/143565/a49f43ca-bdb4-de81-6d15-52f0af6b0fce.png)

＜以下自分の理解＞
C#側のアプリ`Application`はプラットフォーム -> `Any CPU`。
・OSが32ビットの場合 -> (EXEは)32ビットのプロセスとして動作する。
・OSが64ビットの場合 -> (EXEは)64ビットのプロセスとして動作する。

動作確認したOSは64ビットなので、後者に該当します。
64ビットのEXEは64ビットのDLLしか呼べません。今回C/C++で作成したDLLは
アンマネージDLLになりますので、C#で作ったアプリ（マネージアプリ）のように
環境によって64ビット/32ビットと動的に切り替わることはありません。
EXEに合わせてしかるべき設定でビルドする必要がありました。

したがってC/C++側のDLL`Library`はプラットフォーム -> `x64`でビルドするべきだったのですが、
~~OSが○○ビットって話が抜けてた~~マウス操作をミスったらしく、
`Win32`にチェックをつけてしまったようです。


##6-2．でも動いてたんだよなぁ・・・
しかしながら、上記の理解と「5．動作確認」で示した結果は矛盾しています。
結果だけ見れば動くはずのない組み合わせで動いていて・・・ん？

`Application`のプロパティ

![012.png](https://qiita-image-store.s3.amazonaws.com/0/143565/e112ef67-960d-f45e-1fa6-96c562ecb561.png)

![013.png](https://qiita-image-store.s3.amazonaws.com/0/143565/32312102-e2f8-df25-4383-da862b3affac.png)

**~~バカ除けにひっかかった~~オプション設定が作用していたようです。**
32ビットで動作が可能な場合、32ビットで動作してくれる設定とのこと。
今回、DLL側に合わせて32ビットでアプリが動作したということか・・・。
検証のため`Environment.ほにゃらら`で調べてみると、

```csharp:何が起こっているか.cs

// OSが何ビットで動作しているか確認
Console.WriteLine( $"OS : {( Environment.Is64BitOperatingSystem ? "64bit" : "32bit" )}" );

// プロセスが何ビットで動作しているか確認
Console.WriteLine( $"プロセス : {( Environment.Is64BitProcess ? "64bit" : "32bit" )}" );
```

![014.png](https://qiita-image-store.s3.amazonaws.com/0/143565/bda0d008-93ab-6dae-0ee2-1da82150ba14.png)
はい。分かりました。
ちなみに先ほどの32ビットを優先(P)のチェックを外して
「4．ビルドぶちこんでやるぜ！！」までで作成したC#アプリを実行したところ、

![015.png](https://qiita-image-store.s3.amazonaws.com/0/143565/e6188b42-963e-738a-85d0-827243030e0e.png)
と、64ビットのアプリと32ビットのDLLという適合しない組み合わせのため、
`System.BadImageFormatException`が発生して動作しないことを確認できました（意味不明）

##6-3．64bit/32bit対応版
コメント欄にてご指摘いただいた通り、64bit/32bitに対応する設定&コーディングがベストです。
以下はそのための設定方法。

まずはバッチビルドの設定から。
メニューバーのビルド -> バッチビルドを開き、`Library`のプラットフォームの`x64`にチェックを入れます
![021.png](https://qiita-image-store.s3.amazonaws.com/0/143565/74397631-1382-5b9c-9c63-38fb9c58779b.png)

次にプロジェクトの設定をします。まずは64bit版のDebugモードから。
ソリューションエクスプローラーのLibraryを右クリック -> プロパティを開き、赤枠内を変更。
出力ディレクトリをC#のアプリと同じにするのは変わりませんが、32bitのDLLファイルと名前が被らないように
ターゲット名に"64"をつけるのがポイント。
![016_x86_Debug.png](https://qiita-image-store.s3.amazonaws.com/0/143565/90947247-3672-79e7-bd84-f915d94c1b38.png)

続いて32bit版のDebugモード。こちらもターゲット名に"32"をつけて64bit版と区別します。
![017_Win32_Debug.png](https://qiita-image-store.s3.amazonaws.com/0/143565/3a26251e-91e8-7037-5736-b91e35d5c422.png)

64bit版のReleaseモード。
![018_x64_Release.png](https://qiita-image-store.s3.amazonaws.com/0/143565/e8efe1f6-45b5-9d98-e5f5-6c80fdc45548.png)

32bit版のReleaseモード。
![019_Win32_Release.png](https://qiita-image-store.s3.amazonaws.com/0/143565/ff159c34-b170-e40f-2262-e3866e27db01.png)

最期にC#側の`Application`のプロパティを開き、「32ビットを優先」のチェックを外します。
64bit版のDLLを作成する設定をしたので、もう不要ですからね。
![020.png](https://qiita-image-store.s3.amazonaws.com/0/143565/bd5fe733-fbe0-c277-6fbe-07989584099a.png)

・・・でC#のソースコードは以下のようにします。
C/C++側の関数宣言を32bit/64bit両方記述し、関数名の末尾に`_32`、`_64`を付けるなどして区別します。
C/C++関数の本来の名前は、`DllImport`属性内の`EntryPoint`に記述します。
最後に、現在のプロセス(`Environment.Is64BitProcess`で判断可)によって関数を呼び分ける
ラッパーメソッド`Test()`を定義します。クライアントはこのラッパーメソッドを使用します。

```csharp:64bit/32bit対応版.cs

using System;
using System.Runtime.InteropServices;

namespace Application
{
    internal class Program
    {
        // 32bit版のC/C++関数の宣言
        [DllImport( "Library32.dll", EntryPoint = "Test" )]
        static extern void Test_32();

　　　　 // 64bit版のC/C++関数の宣言
        [DllImport( "Library64.dll", EntryPoint = "Test" )]
        static extern void Test_64();

        // 現在のプロセスにより32bit/64bit版の関数を呼び分けるラッパーメソッド
        static void Test()
        {
            if ( Environment.Is64BitProcess )
                Test_64();
            else
                Test_32();
        }

        // クライアント
        static void Main()
        {
            Test();
        }
    }
}
```

バッチビルドを実行し、以下の通りファイルが作成されれば成功！
![022.png](https://qiita-image-store.s3.amazonaws.com/0/143565/0181e367-dd32-db45-0966-58519332a3b1.png)

# おわりに
くぅ疲です。
本当はC#側からC/C++関数を呼び出す際の細かい話（引数が構造体ポインタの時はどうすんのやとか）も
したかったのですが、意外と記事が長くなってしまったので、続きはまた今度とさせて頂きたく思います。
~~調査出来ていないという説あり~~

この記事をご覧になられて、
・記載の抜け、誤り
・分かりづらかった点
・やりたいことは出来てるだろうけどその方法はアホだろ
といったご意見・ご感想があれば頂けると嬉しいです。

お声があれば、この記事は日々修正していきたいと思っております。