#はじめに
タイトルの通り、たまーにC#からC/C++の関数を呼ぼうとすると
アレやコレやとド忘れしていて大失敗、その都度ネットを徘徊し手順を確認するのもアレなので
備忘録としてここに記載いたします。
なお、Visual Studio 2015での実施を前提としております。
[2016/10/23追記]
コメント欄でのご指摘を受け、32bit/64bitの両環境に対応した設定方法を
「6.片手落ちだったので追加説明」に追記いたしました。
#1.事前準備
1-1.C#プロジェクトの作成
ソリューションを作ったら、関数をコールする側としてコンソールアプリを作ります。
名前は適当に"Application"としましょう。
1-2.C/C++プロジェクトの作成
次にコールされる側のDLLを作成するC/C++用のプロジェクトを作ります。とりあえず空で。
1-3.C/C++プロジェクトの設定変更
ガリガリっとコードを書く前に、プロジェクトの設定を以下の通り変更します。
ソリューションエクスプローラーのLibraryを右クリック -> プロパティ -> 全般 -> 構成の種類->
ダイナミックライブラリ(.dll)
#2.コードかきかき
2-1.C/C++関数の作成
次にlibrary.h
とlibray.cpp
を作成し、テスト用関数を定義します。
#pragma once
extern "C"
{
__declspec( dllexport ) void Test();
}
#include <iostream>
#include "library.h"
void Test()
{
std::cout << "Cの関数が呼び出されました。" << std::endl;
}
exetern "C"
と__declspec(dllexport)
についてはMSDNに説明を任せた!w
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してもよさそう。
#pragma once
#define DllExport __declspec( dllexport )
extern "C"
{
DllExport void Test();
}
ビルドはC#のアプリと一緒にやるのでおあずけです。
2-1.C#側に関数宣言を記述する
デフォルトで作成されているProgram.cs
を開き、以下の通り
C/C++関数をコールするための宣言を記述します。
using System.Runtime.InteropServices;
namespace Application
{
internal class Program
{
[DllImport( "Library.dll" )]
static extern void Test();
static void Main()
{
Test();
}
}
}
関数宣言のルール
・[DllImport]
属性をつけること。
・[DllImport]
のカッコの中は利用するDLL名とすること。
・関数の修飾子はstatic extern
とすること。
・関数名はC/C++側と同じにすること。(引数は事情が異なるのでまたの機会に)
#3.ビルドその前に
今回はC#で作成したアプリケーションと、C/C++で作成したDLLの二つをビルドする必要があります。
プロジェクト毎に一回一回ビルドするのも面倒なので、この際ドバ~っと一気にやっちゃう設定をしましょう。
メニューバーのビルド -> バッチビルドを開き以下の設定にします。
2016/10/16修正
コメント欄にて、DLLをコピーするバッチを作成しなくても、C/C++のDLLの出力先を
C#アプリの実行ファイルと同じにすれば良いのでは?とご指摘を頂きました。
その通りですので修正させて頂きます。
C/C++のDLLをC#側で利用するためには、C#アプリの実行ファイル(.exe)が格納されているフォルダに
DLLを置かなくてはいけません。ビルド時に同じパスに出力されるよう、以下の通りDLLの出力先フォルダを変更します。
(Debugモード時とReleaseモード時で異なる設定が必要なため注意)
<Debugモードの設定>
ソリューションエクスプローラーのLibraryを右クリック -> プロパティ -> 全般 -> 出力ディレクトリを
$(SolutionDir)\Application\bin\Debug\
に変更 -> 適用ボタン ぽちり。
<Releaseモードの設定>
(プロパティーページのまま)構成(C)をRelease
に切り替え。出力ディレクトリを
$(SolutionDir)\Application\bin\Release\
に変更 -> OKボタン ぽちり。
#4.ビルドぶちこんでやるぜ!!
待ちに待ったF7タイムですが、今回はバッチビルドを実行しますので以下の手順で行います。
メニューバーのビルド -> バッチビルド -> ビルド(B)
・・・で、出力ウィンドウにこのような結果が表示されればOK。
(画像は取り直しました)
これ以降C/C++のDLLのみ修正した場合は、C#側のアプリをビルドする必要無し。
(そうでないとDLLのうま味がねェ~)
#5. 動作確認
プロジェクトのターゲットをC#の"Application"に指定してCtrl+F5。
や っ た ぜ 。
(実はちゃんとできていませんでした・・・)
#6.片手落ちだったので追加説明
※この項の説明はこちらの記事を参考にさせて頂きました。
.NETにおける64ビットプロセスと32ビットプロセスについて
※あとこちらのサイト様
C#&32bitアンマネージDLL/64bitアンマネージDLLの動的な呼び出し方法
6-1.なにがダメだったのか
コメント欄にて、「その設定とC#のコードだとある条件下では動かないぞ」という旨のご指摘を
頂きました。まずはビルド設定の振り返りから。
<以下自分の理解>
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++側のDLLLibrary
はプラットフォーム -> x64
でビルドするべきだったのですが、
OSが○○ビットって話が抜けてたマウス操作をミスったらしく、
Win32
にチェックをつけてしまったようです。
##6-2.でも動いてたんだよなぁ・・・
しかしながら、上記の理解と「5.動作確認」で示した結果は矛盾しています。
結果だけ見れば動くはずのない組み合わせで動いていて・・・ん?
Application
のプロパティ
バカ除けにひっかかったオプション設定が作用していたようです。
32ビットで動作が可能な場合、32ビットで動作してくれる設定とのこと。
今回、DLL側に合わせて32ビットでアプリが動作したということか・・・。
検証のためEnvironment.ほにゃらら
で調べてみると、
// OSが何ビットで動作しているか確認
Console.WriteLine( $"OS : {( Environment.Is64BitOperatingSystem ? "64bit" : "32bit" )}" );
// プロセスが何ビットで動作しているか確認
Console.WriteLine( $"プロセス : {( Environment.Is64BitProcess ? "64bit" : "32bit" )}" );
はい。分かりました。
ちなみに先ほどの32ビットを優先(P)のチェックを外して
「4.ビルドぶちこんでやるぜ!!」までで作成したC#アプリを実行したところ、
と、64ビットのアプリと32ビットのDLLという適合しない組み合わせのため、
System.BadImageFormatException
が発生して動作しないことを確認できました(意味不明)
##6-3.64bit/32bit対応版
コメント欄にてご指摘いただいた通り、64bit/32bitに対応する設定&コーディングがベストです。
以下はそのための設定方法。
まずはバッチビルドの設定から。
メニューバーのビルド -> バッチビルドを開き、Library
のプラットフォームのx64
にチェックを入れます
次にプロジェクトの設定をします。まずは64bit版のDebugモードから。
ソリューションエクスプローラーのLibraryを右クリック -> プロパティを開き、赤枠内を変更。
出力ディレクトリをC#のアプリと同じにするのは変わりませんが、32bitのDLLファイルと名前が被らないように
ターゲット名に"64"をつけるのがポイント。
続いて32bit版のDebugモード。こちらもターゲット名に"32"をつけて64bit版と区別します。
最期にC#側のApplication
のプロパティを開き、「32ビットを優先」のチェックを外します。
64bit版のDLLを作成する設定をしたので、もう不要ですからね。
・・・でC#のソースコードは以下のようにします。
C/C++側の関数宣言を32bit/64bit両方記述し、関数名の末尾に_32
、_64
を付けるなどして区別します。
C/C++関数の本来の名前は、DllImport
属性内のEntryPoint
に記述します。
最後に、現在のプロセス(Environment.Is64BitProcess
で判断可)によって関数を呼び分ける
ラッパーメソッドTest()
を定義します。クライアントはこのラッパーメソッドを使用します。
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();
}
}
}
バッチビルドを実行し、以下の通りファイルが作成されれば成功!
おわりに
くぅ疲です。
本当はC#側からC/C++関数を呼び出す際の細かい話(引数が構造体ポインタの時はどうすんのやとか)も
したかったのですが、意外と記事が長くなってしまったので、続きはまた今度とさせて頂きたく思います。
調査出来ていないという説あり
この記事をご覧になられて、
・記載の抜け、誤り
・分かりづらかった点
・やりたいことは出来てるだろうけどその方法はアホだろ
といったご意見・ご感想があれば頂けると嬉しいです。
お声があれば、この記事は日々修正していきたいと思っております。