C++ で他の言語からでも利用可能な汎用 DLL を作成するには、次のことを守る必要があります。
- クラスではなく関数をエクスポートするようにする
クラスをエクスポートした場合、DLL 側のコンストラクタ/デストラクタを C# 側から直接呼び出せないため、DLL 側になんらかのヘルパが必要となるため。
また、クラスのメンバ関数はマングリングによって関数名が自動的に変更され、C# 側は常にその関数名に追従するようにメンテナンスすることが必要となるため。 - エクスポートする関数の呼び出し規約は __stdcall とする
Windows API のデファクトスタンダードであるため。 - __declspec(dllexport) は使用せず、モジュール定義ファイル (*.def) でエクスポートする関数を定義する
C++ では異なる名前空間上に同じ関数名が定義されたり、関数のオーバーロード機能によって同じ関数名でも機能が異なる関数が実装されたりします。このため、コンパイル後は定義した関数の名前が自動的に変更されるマングリングという処理がおこなわれてしまいます。したがって、DLL 化するときにも関数の名前が自動的に変更されるため、DLL を使う側が変更された名前を把握しなければならなくなります。しかし、マングリングによる命名規則はコンパイラに依存するため、完全に対応付けることは現実的ではありません。
そこで、関数宣言時に extern “C” を付けることで C++ の関数を C として扱うようにすることができます。つまりオーバーロード機能などがない関数となるため、マングリングによる処理がおこなわれなくなります。結果として、extern “C” を付けて DLL 化することで外部から同じ名前でアクセスできるようになる、というわけです。この場合、関数宣言時に __declspec(dllexport) も同時に付加する必要があります。
一方、extern “C” および __declspec(dllexport) を付けて宣言する代わりに、モジュール定義ファイル「SampleDll.def」に公開する関数名を並べることでも対応できます。どちらの場合でも、関数オーバーロードは使えないようです。
モジュール定義ファイルを使用した場合のコード例を以下に示します。
1 2 3 4 5 6 |
#pragma once namespace Tips_Win32DLL { double __stdcall Sample01(int a); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> #include "SampleDll.h" #define PI 3.1415926536 namespace Tips_Win32DLL { double __stdcall Sample01(int a) { printf("--<SampleDll:Sample01>---------------\r\n"); printf("a = %d\r\n", a); printf("-------------------------------------\r\n"); return PI; } } |
1 2 3 4 5 |
LIBRARY Tips_Win32DLL EXPORTS ; 公開する関数名をリストアップ Sample01 |
モジュール定義ファイルには名前空間を除いた関数名のみを記述します。コメントを書きたい場合は “//” ではなく、”;” を記述します。
ヘッダ、ソースファイルともに、エクスポートしたい関数は __stdcall を付けて宣言します。__stdcall は呼び出し規約と呼ばれるキーワードで、その他に次のような呼び出し規約があります。
キーワード | スタック維持の責任 | パラメータ渡し |
---|---|---|
__cdecl | 呼び出し元 | パラメータをスタックに逆の順序で右から左にプッシュする |
__clrcall | 適用なし | CLR 式スタックの順に左から右にパラメータを読み込む |
__stdcall | 呼出先 | パラメータをスタックに逆の順序で右から左にプッシュする |
__fastcall | 呼出先 | レジスタに格納されてからスタックにプッシュする |
__thiscall | 呼出先 | スタックにプッシュされる |
__vectorcall | 呼出先 | レジスタに格納されてからスタックに逆の順序で右から左にプッシュされる |
__cdecl | 呼出先 | パラメータをスタックに逆の順序で右から左にプッシュする |
呼び出し規約 __stdcall は Windows API で使用されているデファクトスタンダードであり、特別な理由がない限りこれを使用したほうが良いようです。
また、上記のコード例ではクラスを使用していません。というのは、C++ のクラスのメンバ関数の呼び出し規約が __thiscall であり、関数名が自動的に変更されるマングリング処理が働いてしまいます。
以上の作成方法からだいたい察しが付くと思いますが、DLL としてエクスポートできる関数には次のような制約条件があります。
- 公開する関数に関数オーバーロードは使えない
- 既にグローバルや別の名前空間で定義されている名前の関数はどちらも公開できない
- 公開する関数はクラスのメンバ関数にできない
これらの制約があることから、モジュール定義ファイルで公開する関数を管理したほうがメンテナンスしやすいのではないかと思います。また、公開する関数自体はクラスにすることはできませんが、内部実装に関してはクラスを使用することができるので、既にあるクラスが公開している関数を公開するためのラッパーを作ることで既存の資産を流用できます。
次回は作成した DLL を C# から呼び出す方法を紹介します。