[解決済み] オブジェクト、特にSTLオブジェクトをDLLと安全に受け渡しするには?
質問
C++のDLLとクラスオブジェクト、特にSTLオブジェクトをやり取りするにはどうすればよいですか?
私のアプリケーションは DLL ファイルの形でサードパーティのプラグインと対話する必要があり、これらのプラグインがどのコンパイラーでビルドされるかを制御することはできません。STL オブジェクトの保証された ABI がないことは承知しており、私のアプリケーションで不安定さを引き起こすことを懸念しています。
どのように解決するのですか?
この質問に対する簡単な回答は ではありません。 . なぜなら、標準的なC++は存在しないからです。 ABI (アプリケーションバイナリインターフェース、呼び出し規約、データパッキング/アライメント、型サイズなどの標準) がないため、プログラム内でクラスオブジェクトを扱う標準的な方法を強制するには、多くの輪をくぐり抜けなければなりません。また、あるコンパイラー リリースで動作するソリューションが、次のリリースでも動作するという保証もありません。
ただ、プレーンなCのインターフェイスを
extern "C"
というように、C ABI の
は
はよく定義され、安定しているからです。
もしあなたが本当に 本当に C++ オブジェクトを DLL の境界を越えて渡したいのであれば、技術的には可能です。ここでは、考慮しなければならない要因をいくつか紹介します。
データのパッキング/アライメント
与えられたクラスの中で、個々のデータメンバは通常、そのアドレスが型のサイズの倍数に対応するように、メモリ内に特別に配置されます。例えば
int
は 4 バイトの境界線にアラインされるかもしれません。
DLL が EXE と異なるコンパイラーでコンパイルされている場合、あるクラスの DLL のバージョンは EXE のバージョンと異なるパッキングを持つ可能性があり、EXE がクラス オブジェクトを DLL に渡すと、DLL はそのクラス内のあるデータ メンバーに適切にアクセスできない可能性があります。DLL は、EXE の定義ではなく、独自のクラスの定義によって指定されたアドレスから読み取ろうとし、目的のデータ メンバが実際にはそこに格納されていないため、ガベージ値が発生します。
この問題を回避するには
#pragma pack
プリプロセッサーディレクティブを使うと、コンパイラーが特定のパッキングを適用するように強制します。
よりも大きな値を選択した場合でも、コンパイラはデフォルトのパッキングを適用します。
そのため、大きなパッキング値を選択した場合、クラスはコンパイラによって異なるパッキングを持つことがあります。この解決策として
#pragma pack(1)
これはコンパイラに1バイトの境界でデータメンバを揃えるように強制します(基本的にパッキングは適用されません)。
これは、パフォーマンスの問題や、特定のシステムでクラッシュする可能性があるため、あまり良いアイデアではありません。
しかし、それは
は
は、クラスのデータメンバがメモリ上で整列される方法の一貫性を保証します。
メンバの再順序付け
もしあなたのクラスが スタンダードレイアウト である場合、コンパイラは はメモリ上でデータメンバを並べ替えることができます。 . これがどのように行われるかについての標準はないので、データの並べ替えはコンパイラ間の非互換性を引き起こす可能性があります。したがって、DLLとの間でデータをやり取りするには、標準的なレイアウトのクラスが必要になります。
呼び出しの規則
複数の の呼び出し規約があります。 があります。これらの呼び出し規約は、関数にどのようにデータを渡すかを指定します:パラメータはレジスタに格納されるのか、スタックに格納されるのか?引数はどのような順番でスタックに格納されるのでしょうか。関数が終了した後、スタックに残っている引数を誰が片付けるのか?
標準的な呼び出し方法を維持することは重要です。
_cdecl
と宣言し、それを
_stdcall
悪いことが起こる
.
_cdecl
は C++ 関数のデフォルトの呼び出し規則です。
_stdcall
を1つずつ、そして
_cdecl
が別の場所に
データ型の大きさ
によると
このドキュメント
によると、Windows では、アプリケーションが 32 ビットか 64 ビットかに関係なく、ほとんどの基本的なデータ型は同じサイズを持っています。しかし、与えられたデータ型のサイズは標準ではなくコンパイラーによって強制されるため (標準が保証するのはすべて
1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
であることです)。
固定サイズのデータ型
を使用して、可能な限りデータ型のサイズの互換性を確保することをお勧めします。
ヒープ問題
DLL が EXE と異なるバージョンの C ランタイムにリンクしている場合。 の場合、2 つのモジュールは異なるヒープを使用します。 . これは、モジュールが異なるコンパイラでコンパイルされている場合、特に起こりやすい問題です。
これを軽減するために、すべてのメモリは共有ヒープに割り当てられ、同じヒープから解放される必要があります。幸いなことに、Windows はこれを支援する API を提供しています。
GetProcessHeap
により、ホスト EXE のヒープにアクセスできます。
ヒープアロック
/
ヒープフリー
は、このヒープ内でメモリを割り当てたり解放したりすることができます。
を使用しないことが重要です。
malloc
/
free
のように、期待通りに動作する保証はありません。
STL問題
C++の標準ライブラリには、独自のABIの問題があります。それは 保証なし また、ある STL クラスが実装ごとに同じサイズであるという保証もありません (特に、デバッグビルドでは、与えられた STL 型に余分なデバッグ情報が含まれる可能性があります)。したがって、DLL の境界を越えて渡され、反対側で再パックされる前に、任意の STL コンテナは基本型に解凍される必要があります。
名前のマングリング
DLL は、おそらく EXE が呼び出したいと思うような関数をエクスポートします。しかし、C++ コンパイラは
は関数名をマングリングする標準的な方法を持っていません。
. これはつまり
GetCCDLL
という名前の関数が
_Z8GetCCDLLv
に変換されるかもしれませんし、GCCの
?GetCCDLL@@YAPAUCCDLL_v1@@XZ
をMSVCで使用します。
GCC で生成された DLL は .lib ファイルを生成せず、MSVC で DLL を静的にリンクすると .lib ファイルが必要になるため、すでに DLL への静的リンクを保証することができません。動的リンクはよりクリーンなオプションのように見えますが、名前のマングリングが邪魔になります。
GetProcAddress
を試すと、呼び出しに失敗し、DLL を使用することができなくなります。これは、回避するために少しハックする必要があり、DLL の境界を越えて C++ クラスを渡すことが悪い考えであるかなり大きな理由になっています。
DLL をビルドし、生成された .def ファイル (生成された場合。これはプロジェクトのオプションによって異なります) を調べるか、Dependency Walker のようなツールを使用して、マングル化された名前を見つける必要があります。それから
自身の
.def ファイルを書き、マングルドされた関数のマングルドされていないエイリアスを定義する必要があります。例として
GetCCDLL
関数を使ってみましょう。私のシステムでは、次の .def ファイルが GCC と MSVC でそれぞれ動作します。
GCCです。
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVCです。
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
DLLを再構築し、エクスポートする関数を再確認してください。マングルされていない関数名がその中にあるはずです。 この方法ではオーバーロードされた関数を使用できないことに注意してください。 : マングルされていない関数名は の別名であり、特定の関数のオーバーロード のエイリアスです。また、マングルされた名前が変わるので、関数宣言を変更するたびに DLL 用の新しい .def ファイルを作成する必要があることに注意してください。最も重要なことは、名前のマングリングをバイパスすることで、非互換性の問題に関してリンカーが提供しようとする保護を上書きすることです。
この全体の処理は、もし インターフェイスを作成する を作成すれば、DLL 内のすべての関数にエイリアスを作成する必要がなく、エイリアスを定義する関数はひとつですむからです。しかし、同じ注意点がまだ適用されます。
クラスオブジェクトを関数に渡す
これはおそらく、クロスコンパイラのデータ受け渡しを悩ませる問題の中で、最も微妙で最も危険なものでしょう。たとえ、他のすべてを処理したとしても 関数に引数がどのように渡されるかについての標準はありません。 . このため 明らかな理由のない微妙なクラッシュを引き起こし、それをデバッグする簡単な方法がありません。 . そのため すべて 引数をポインタ経由で渡す必要があります。これは不器用で不便なものであり、また、うまくいくかもしれないし、いかないかもしれない、別の厄介な回避策です。
これらの回避策をすべてまとめ、さらに
テンプレートと演算子を使ったいくつかの創造的な作業
を組み合わせることで、DLL の境界を越えてオブジェクトを安全に渡すことを試みることができます。C++11 のサポートが必須であることに注意してください。
#pragma pack
とその変種のサポートと同様に、C++11 のサポートが必須であることに注意してください。MSVC 2013 はこのサポートを提供しており、最近のバージョンの GCC や clang もそうです。
//POD_base.h: defines a template base class that wraps and unwraps data types for safe passing across compiler boundaries
//define malloc/free replacements to make use of Windows heap APIs
namespace pod_helpers
{
void* pod_malloc(size_t size)
{
HANDLE heapHandle = GetProcessHeap();
HANDLE storageHandle = nullptr;
if (heapHandle == nullptr)
{
return nullptr;
}
storageHandle = HeapAlloc(heapHandle, 0, size);
return storageHandle;
}
void pod_free(void* ptr)
{
HANDLE heapHandle = GetProcessHeap();
if (heapHandle == nullptr)
{
return;
}
if (ptr == nullptr)
{
return;
}
HeapFree(heapHandle, 0, ptr);
}
}
//define a template base class. We'll specialize this class for each datatype we want to pass across compiler boundaries.
#pragma pack(push, 1)
// All members are protected, because the class *must* be specialized
// for each type
template<typename T>
class pod
{
protected:
pod();
pod(const T& value);
pod(const pod& copy);
~pod();
pod<T>& operator=(pod<T> value);
operator T() const;
T get() const;
void swap(pod<T>& first, pod<T>& second);
};
#pragma pack(pop)
//POD_basic_types.h: holds pod specializations for basic datatypes.
#pragma pack(push, 1)
template<>
class pod<unsigned int>
{
//these are a couple of convenience typedefs that make the class easier to specialize and understand, since the behind-the-scenes logic is almost entirely the same except for the underlying datatypes in each specialization.
typedef int original_type;
typedef std::int32_t safe_type;
public:
pod() : data(nullptr) {}
pod(const original_type& value)
{
set_from(value);
}
pod(const pod<original_type>& copyVal)
{
original_type copyData = copyVal.get();
set_from(copyData);
}
~pod()
{
release();
}
pod<original_type>& operator=(pod<original_type> value)
{
swap(*this, value);
return *this;
}
operator original_type() const
{
return get();
}
protected:
safe_type* data;
original_type get() const
{
original_type result;
result = static_cast<original_type>(*data);
return result;
}
void set_from(const original_type& value)
{
data = reinterpret_cast<safe_type*>(pod_helpers::pod_malloc(sizeof(safe_type))); //note the pod_malloc call here - we want our memory buffer to go in the process heap, not the possibly-isolated DLL heap.
if (data == nullptr)
{
return;
}
new(data) safe_type (value);
}
void release()
{
if (data)
{
pod_helpers::pod_free(data); //pod_free to go with the pod_malloc.
data = nullptr;
}
}
void swap(pod<original_type>& first, pod<original_type>& second)
{
using std::swap;
swap(first.data, second.data);
}
};
#pragma pack(pop)
は
pod
クラスは基本的なデータ型ごとに特化されており、そのため
int
は自動的に
int32_t
,
uint
はラップされて
uint32_t
などとなります。これはすべて舞台裏で行われることで、オーバーロードされた
=
と
()
演算子を使用することができます。残りの基本的な型の特殊化については、基礎となるデータ型以外はほとんど同じなので省略しました(
bool
に変換されるため、特殊化には少し余分なロジックがあります。
int8_t
に変換され、その後
int8_t
は 0 と比較され
bool
に戻すために0と比較されますが、これはかなり些細なことです)。
少し余分な作業が必要ですが、この方法でSTL型をラップすることもできます。
#pragma pack(push, 1)
template<typename charT>
class pod<std::basic_string<charT>> //double template ftw. We're specializing pod for std::basic_string, but we're making this specialization able to be specialized for different types; this way we can support all the basic_string types without needing to create four specializations of pod.
{
//more comfort typedefs
typedef std::basic_string<charT> original_type;
typedef charT safe_type;
public:
pod() : data(nullptr) {}
pod(const original_type& value)
{
set_from(value);
}
pod(const charT* charValue)
{
original_type temp(charValue);
set_from(temp);
}
pod(const pod<original_type>& copyVal)
{
original_type copyData = copyVal.get();
set_from(copyData);
}
~pod()
{
release();
}
pod<original_type>& operator=(pod<original_type> value)
{
swap(*this, value);
return *this;
}
operator original_type() const
{
return get();
}
protected:
//this is almost the same as a basic type specialization, but we have to keep track of the number of elements being stored within the basic_string as well as the elements themselves.
safe_type* data;
typename original_type::size_type dataSize;
original_type get() const
{
original_type result;
result.reserve(dataSize);
std::copy(data, data + dataSize, std::back_inserter(result));
return result;
}
void set_from(const original_type& value)
{
dataSize = value.size();
data = reinterpret_cast<safe_type*>(pod_helpers::pod_malloc(sizeof(safe_type) * dataSize));
if (data == nullptr)
{
return;
}
//figure out where the data to copy starts and stops, then loop through the basic_string and copy each element to our buffer.
safe_type* dataIterPtr = data;
safe_type* dataEndPtr = data + dataSize;
typename original_type::const_iterator iter = value.begin();
for (; dataIterPtr != dataEndPtr;)
{
new(dataIterPtr++) safe_type(*iter++);
}
}
void release()
{
if (data)
{
pod_helpers::pod_free(data);
data = nullptr;
dataSize = 0;
}
}
void swap(pod<original_type>& first, pod<original_type>& second)
{
using std::swap;
swap(first.data, second.data);
swap(first.dataSize, second.dataSize);
}
};
#pragma pack(pop)
これで、これらのポッドタイプを利用するDLLを作成することができます。最初にインターフェースが必要なので、マングリングが必要なメソッドは1つだけです。
//CCDLL.h: defines a DLL interface for a pod-based DLL
struct CCDLL_v1
{
virtual void ShowMessage(const pod<std::wstring>* message) = 0;
};
CCDLL_v1* GetCCDLL();
これは、DLLと呼び出し元の両方が使用できる基本的なインターフェイスを作成するだけです。へのポインタを渡していることに注意してください。
pod
へのポインタを渡しているのであって
pod
そのものではありません。あとは、DLL側でそれを実装する必要があります。
struct CCDLL_v1_implementation: CCDLL_v1
{
virtual void ShowMessage(const pod<std::wstring>* message) override;
};
CCDLL_v1* GetCCDLL()
{
static CCDLL_v1_implementation* CCDLL = nullptr;
if (!CCDLL)
{
CCDLL = new CCDLL_v1_implementation;
}
return CCDLL;
}
そして、今度は
ShowMessage
関数を実装してみましょう。
#include "CCDLL_implementation.h"
void CCDLL_v1_implementation::ShowMessage(const pod<std::wstring>* message)
{
std::wstring workingMessage = *message;
MessageBox(NULL, workingMessage.c_str(), TEXT("This is a cross-compiler message"), MB_OK);
}
あまり派手なことはしていません: これは単に渡された
pod
を通常の
wstring
に変換し、メッセージボックスに表示します。結局のところ、これは単なる
POC
であり、完全なユーティリティ ライブラリではありません。
これでDLLをビルドすることができます。リンカの名前間違いを回避するための特別な.defファイルを忘れないように。(注意: 私が実際にビルドして実行したCCDLL構造体は、ここで紹介するものよりも多くの機能を持っていました。.defファイルは期待通りに動作しないかもしれません)。
さて、DLLを呼び出すEXEです。
//main.cpp
#include "../CCDLL/CCDLL.h"
typedef CCDLL_v1*(__cdecl* fnGetCCDLL)();
static fnGetCCDLL Ptr_GetCCDLL = NULL;
int main()
{
HMODULE ccdll = LoadLibrary(TEXT("D:\\Programming\\C++\\CCDLL\\Debug_VS\\CCDLL.dll")); //I built the DLL with Visual Studio and the EXE with GCC. Your paths may vary.
Ptr_GetCCDLL = (fnGetCCDLL)GetProcAddress(ccdll, (LPCSTR)"GetCCDLL");
CCDLL_v1* CCDLL_lib;
CCDLL_lib = Ptr_GetCCDLL(); //This calls the DLL's GetCCDLL method, which is an alias to the mangled function. By dynamically loading the DLL like this, we're completely bypassing the name mangling, exactly as expected.
pod<std::wstring> message = TEXT("Hello world!");
CCDLL_lib->ShowMessage(&message);
FreeLibrary(ccdll); //unload the library when we're done with it
return 0;
}
そして、その結果がこちらです。私たちの DLL は動作します。過去の STL ABI の問題、過去の C++ ABI の問題、過去のマングリングの問題、そして MSVC DLL が GCC EXE で動作するところまで到達することに成功しました。
結論として、もしあなたが絶対に しなければならない C++ オブジェクトを DLL の境界を越えて渡す必要がある場合、これがその方法です。しかし、この方法は、あなたのセットアップや他の人のセットアップで動作することを保証するものではありません。このどれもがいつでも壊れる可能性があり、おそらくあなたのソフトウェアがメジャーリリースされる予定の前日には壊れるでしょう。この方法は、ハックやリスク、そして一般的な馬鹿馬鹿しさに満ちており、おそらく私は撃たれるべきでしょう。もしこの道を選ぶのであれば、細心の注意を払ってテストしてください。そして、本当に......まったくやらないでください。
関連
-
[解決済み】C++ クラスヘッダが含まれているときに「不明な型」があるのはなぜですか?重複
-
[解決済み】'cout'は型名ではない
-
[解決済み】リンカーエラーです。"リンカ入力ファイルはリンクが行われていないため未使用"、そのファイル内の関数への未定義参照
-
[解決済み】標準ライブラリにstd::endlに相当するタブはあるか?
-
[解決済み] gdbを使用してもデバッグシンボルが見つからない
-
[解決済み] C++とCの融合 - #ifdef __cplusplus はどのように機能するのか?
-
[解決済み] 1ビットのセット、クリア、トグルはどのように行うのですか?
-
[解決済み] C++11では、標準化されたメモリモデルが導入されました。その意味するところは?そして、C++プログラミングにどのような影響を与えるのでしょうか?
-
[解決済み] std::vector<> からインデックスで要素を消すにはどうしたらいいですか?
-
[解決済み] STL」と「C++ Standard Library」の違いは?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】デバッグアサーションに失敗しました。C++のベクトル添え字が範囲外
-
[解決済み】fpermissiveフラグは何をするのですか?
-
[解決済み】C++の余分な資格エラー
-
[解決済み】エラー:free(): 次のサイズが無効です(fast)。
-
[解決済み】クラスのコンストラクタへの未定義参照、.cppファイルの修正も含む
-
[解決済み】Visual Studioのデバッガーエラー。プログラムを開始できません 指定されたファイルが見つかりません
-
[解決済み] C++でインターフェイスを宣言するには?
-
[解決済み] アプリケーション・バイナリ・インターフェース(ABI)とは何ですか?
-
[解決済み] STL」と「C++ Standard Library」の違いは?
-
[解決済み】#pragma pack の効果