Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

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

1.事前準備

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

ソリューションを作ったら、関数をコールする側としてコンソールアプリを作ります。
名前は適当に"Application"としましょう。
001.png

1-2.C/C++プロジェクトの作成

次にコールされる側のDLLを作成するC/C++用のプロジェクトを作ります。とりあえず空で。
002.png

<確認>
こないなことになっているはずです。
003.png

1-3.C/C++プロジェクトの設定変更

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

これでDLLが作成されるわけだ:hotsprings:
004.png

2.コードかきかき

2-1.C/C++関数の作成

次にlibrary.hlibray.cppを作成し、テスト用関数を定義します。

library.h
#pragma once

extern "C"
{
    __declspec( dllexport ) void Test();
}
library.cpp
#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してもよさそう。

library.h
#pragma once

#define DllExport   __declspec( dllexport )

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

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

2-1.C#側に関数宣言を記述する

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

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

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

<Releaseモードの設定>
(プロパティーページのまま)構成(C)をReleaseに切り替え。出力ディレクトリ
$(SolutionDir)\Application\bin\Release\に変更 -> OKボタン ぽちり。
011.png

4.ビルドぶちこんでやるぜ!!

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

・・・で、出力ウィンドウにこのような結果が表示されればOK。
(画像は取り直しました)
008_new.png

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

5. 動作確認

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

009.png
無時、C#側からC/C++の関数を呼び出せているようです。:tada:

や っ た ぜ 。:sunglasses:
(実はちゃんとできていませんでした・・・)

6.片手落ちだったので追加説明

※この項の説明はこちらの記事を参考にさせて頂きました。
.NETにおける64ビットプロセスと32ビットプロセスについて

※あとこちらのサイト様
C#&32bitアンマネージDLL/64bitアンマネージDLLの動的な呼び出し方法

6-1.なにがダメだったのか

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

005.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++側のDLLLibraryはプラットフォーム -> x64でビルドするべきだったのですが、
OSが○○ビットって話が抜けてたマウス操作をミスったらしく、
Win32にチェックをつけてしまったようです。

6-2.でも動いてたんだよなぁ・・・

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

Applicationのプロパティ

012.png

013.png

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

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

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

014.png
はい。分かりました。
ちなみに先ほどの32ビットを優先(P)のチェックを外して
「4.ビルドぶちこんでやるぜ!!」までで作成したC#アプリを実行したところ、

015.png
と、64ビットのアプリと32ビットのDLLという適合しない組み合わせのため、
System.BadImageFormatExceptionが発生して動作しないことを確認できました(意味不明)

6-3.64bit/32bit対応版

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

まずはバッチビルドの設定から。
メニューバーのビルド -> バッチビルドを開き、Libraryのプラットフォームのx64にチェックを入れます
021.png

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

続いて32bit版のDebugモード。こちらもターゲット名に"32"をつけて64bit版と区別します。
017_Win32_Debug.png

64bit版のReleaseモード。
018_x64_Release.png

32bit版のReleaseモード。
019_Win32_Release.png

最期にC#側のApplicationのプロパティを開き、「32ビットを優先」のチェックを外します。
64bit版のDLLを作成する設定をしたので、もう不要ですからね。
020.png

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

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

おわりに

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

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

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away