web-dev-qa-db-ja.com

dllの境界を越えてSTLベクトルへの参照を渡す

文字列の特定のリストを返す必要があるファイルを管理するためのNiceライブラリがあります。これから使用するコードはC++だけなので(そしてJavaですが、JNIを介してC++を使用しています)、標準ライブラリのベクトルを使用することにしました。ライブラリ関数次のようになります(FILE_MANAGER_EXPORTはプラットフォーム定義のエクスポート要件です)。

extern "C" FILE_MANAGER_EXPORT void get_all_files(vector<string> &files)
{
    files.clear();
    for (vector<file_struct>::iterator i = file_structs.begin(); i != file_structs.end(); ++i)
    {
        files.Push_back(i->full_path);
    }
}

戻り値の代わりにベクトルを参照として使用した理由は、メモリ割り当てを正常に保つための試みであり、WindowsがC++の戻り値の型の周りにextern "C"を持っていることは本当に不幸だったためです(理由はわかっていますが、私の理解では、すべてのextern " C "は、コンパイラでの名前のマングリングを防ぎます)。とにかく、これを他のc ++で使用するためのコードは一般的に次のとおりです。

#if defined _WIN32
    #include <Windows.h>
    #define GET_METHOD GetProcAddress
    #define OPEN_LIBRARY(X) LoadLibrary((LPCSTR)X)
    #define LIBRARY_POINTER_TYPE HMODULE
    #define CLOSE_LIBRARY FreeLibrary
#else
    #include <dlfcn.h>
    #define GET_METHOD dlsym
    #define OPEN_LIBRARY(X) dlopen(X, RTLD_NOW)
    #define LIBRARY_POINTER_TYPE void*
    #define CLOSE_LIBRARY dlclose
#endif

typedef void (*GetAllFilesType)(vector<string> &files);

int main(int argc, char **argv)
{
    LIBRARY_POINTER_TYPE manager = LOAD_LIBRARY("library.dll"); //Just an example, actual name is platform-defined too
    GetAllFilesType get_all_files_pointer = (GetAllFilesType) GET_METHOD(manager, "get_all_files");
    vector<string> files;
    (*get_all_files_pointer)(files);

    // ... Do something with files ...

    return 0;
}

ライブラリは、add_library(file_manager SHARED file_manager.cpp)を使用してcmakeを介してコンパイルされます。プログラムは、add_executable(file_manager_command_wrapper command_wrapper.cpp)を使用して別のcmakeプロジェクトでコンパイルされます。どちらにもコンパイルフラグは指定されておらず、これらのコマンドのみが指定されています。

これで、プログラムはMacとLinuxの両方で完全に正常に動作します。問題はウィンドウです。実行すると、次のエラーが発生します。

デバッグアサーションに失敗しました!

.。

式:_pFirstBlock == _pHead

これは、実行可能ファイルとロードされたdllの間の個別のメモリヒープが原因であることがわかりました。これは、メモリが一方のヒープに割り当てられ、もう一方のヒープに割り当て解除されたときに発生すると思います。問題は、私の一生の間、何が悪いのか理解できないことです。メモリは実行可能ファイルに割り当てられ、dll関数への参照として渡され、値は参照を介して追加され、次にそれらが処理され、最後に実行可能ファイルに割り当て解除されます。

できればもっと多くのコードを公開しますが、会社の知的財産ではできないと述べているので、上記のコードはすべて単なる例です。

私がこのエラーを理解するのを助け、それをデバッグして修正するための正しい方向に私を向けることができる主題についてのより多くの知識を持っている人はいますか?残念ながら、Linuxで開発しているため、Windowsマシンをデバッグに使用できません。その後、jenkinsを介してビルドとテストをトリガーするgerritサーバーに変更をコミットします。コンパイルとテスト時に出力コンソールにアクセスできます。

非stl型を使用して、c ++のベクトルをchar **にコピーすることを検討しましたが、メモリ割り当ては悪夢であり、Windowsはもちろん、Linuxでもうまく機能させるのに苦労し、恐ろしい複数のヒープがありました。

編集:ファイルベクトルがスコープから外れるとすぐに間違いなくクラッシュします。私の現在の考えは、ベクターに入れられた文字列はdllヒープに割り当てられ、実行可能ヒープに割り当て解除されるというものです。この場合、誰かがより良い解決策について私に教えてもらえますか?

17
SmallDeadGuy

あなたの主な問題は、C++タイプをDLL境界を越えて渡すのが難しいことです。次のものが必要です。

  1. 同じコンパイラ
  2. 同じ標準ライブラリ
  3. 例外についても同じ設定
  4. Visual C++では、同じバージョンのコンパイラが必要です
  5. Visual C++では、同じデバッグ/リリース構成が必要です
  6. Visual C++では、同じイテレータデバッグレベルが必要です

等々

それが必要な場合は、C++で最も簡単な方法を提供するcppcomponents https://github.com/jbandela/cppcomponents というヘッダーのみのライブラリを作成しました。 C++ 11を強力にサポートするコンパイラが必要です。 Gcc4.7.2または4.8が機能します。 Visual C++ 2013プレビューも機能します。

Cppcomponentsを使用して問題を解決する方法を説明します。

  1. 選択したディレクトリのgit clone https://github.com/jbandela/cppcomponents.git。このコマンドを実行したディレクトリをlocalgitと呼びます。

  2. interfaces.hppというファイルを作成します。このファイルでは、コンパイラー間で使用できるインターフェースを定義します。

次のように入力します

#include <cppcomponents/cppcomponents.hpp>

using cppcomponents::define_interface;
using cppcomponents::use;
using cppcomponents::runtime_class;
using cppcomponents::use_runtime_class;
using cppcomponents::implement_runtime_class;
using cppcomponents::uuid;
using cppcomponents::object_interfaces;

struct IGetFiles:define_interface<uuid<0x633abf15,0x131e,0x4da8,0x933f,0xc13fbd0416cd>>{

    std::vector<std::string> GetFiles();

    CPPCOMPONENTS_CONSTRUCT(IGetFiles,GetFiles);


};

inline std::string FilesId(){return "Files!Files";}
typedef runtime_class<FilesId,object_interfaces<IGetFiles>> Files_t;
typedef use_runtime_class<Files_t> Files;

次に、実装を作成します。これを行うには、Files.cppを作成します。

次のコードを追加します

#include "interfaces.h"


struct ImplementFiles:implement_runtime_class<ImplementFiles,Files_t>{
  std::vector<std::string> GetFiles(){
    std::vector<std::string> ret = {"samplefile1.h", "samplefile2.cpp"};
    return ret;

  }

  ImplementFiles(){}


};

CPPCOMPONENTS_DEFINE_FACTORY();

最後に、上記を使用するためのファイルがあります。 UseFiles.cppを作成します

次のコードを追加します

#include "interfaces.h"
#include <iostream>

int main(){

  Files f;
  auto vec_files = f.GetFiles();
  for(auto& name:vec_files){
      std::cout << name << "\n";
    }

}

これでコンパイルできます。コンパイラ間で互換性があることを示すために、Visual C++コンパイラでclを使用してUseFiles.cppUseFiles.exeにコンパイルします。 MingwGccを使用してFiles.cppFiles.dllにコンパイルします

cl /EHsc UseFiles.cpp /I localgit\cppcomponents

ここで、localgitは、上記のようにgit cloneを実行したディレクトリです。

g++ -std=c++11 -shared -o Files.dll Files.cpp -I localgit\cppcomponents

リンクステップはありません。 Files.dllUseFiles.exeが同じディレクトリにあることを確認してください。

次に、実行可能ファイルをUseFilesで実行します

cppcomponentsはLinuxでも動作します。主な変更点は、exeをコンパイルするときに、フラグに-ldlを追加する必要があり、.soファイルをコンパイルするときに、フラグに-fPICを追加する必要があることです。

ご不明な点がございましたら、お気軽にお問い合わせください。

14
John Bandela

メモリは実行可能ファイルに割り当てられ、dll関数への参照として渡され、値は参照を介して追加され、次にそれらが処理され、最後に実行可能ファイルに割り当て解除されます。

スペース(容量)が残っていない場合に値を追加すると、再割り当てが行われるため、古いものは割り当てが解除され、新しいものが割り当てられます。これは、ライブラリのstd :: vector :: Push_back関数によって実行されます。この関数は、ライブラリのメモリアロケータを使用します。

それ以外に、明らかなcompile-settings-must-match-exactlyがあり、もちろんそれらはコンパイラ固有の依存関係にあります。ほとんどの場合、コンパイルに関してそれらを同期させておく必要があります。

6
dascandy

誰もがここで悪名高いDLL-compiler-incompatibilityの問題に悩まされているようですが、これがヒープ割り当てに関連していることについては正しいと思います。何が起こっているのかと思うと、ベクター(メインexeのヒープスペースに割り当てられている)には、DLLのヒープスペースに割り当てられている文字列が含まれています。ベクターがスコープ外に出て割り当てが解除されると、文字列の割り当ても解除されます。これはすべて.exe側で発生するため、クラッシュが発生します。

私は2つの本能的な提案があります:

  1. 各文字列をstd::unique_ptrでラップします。これには、unique_ptrがスコープ外になったときにコンテンツの割り当て解除を処理する「deleter」が含まれています。 unique_ptrがDLL側で作成されると、その削除機能も同様になります。したがって、ベクターがスコープ外になり、その内容のデストラクタが呼び出されると、文字列はDLLによって割り当て解除されます-バインドされた削除機能があり、ヒープの競合は発生しません。

    extern "C" FILE_MANAGER_EXPORT void get_all_files(vector<unique_ptr<string>>& files)
    {
        files.clear();
        for (vector<file_struct>::iterator i = file_structs.begin(); i != file_structs.end(); ++i)
        {
            files.Push_back(unique_ptr<string>(new string(i->full_path)));
        }
    }
    
  2. ベクトルをDLL側に保持し、その参照を返すだけです。DLL境界を越えて参照を渡すことができます:

    vector<string> files;
    
    extern "C" FILE_MANAGER_EXPORT vector<string>& get_all_files()
    {
        files.clear();
        for (vector<file_struct>::iterator i = file_structs.begin(); i != file_structs.end(); ++i)
        {
            files.Push_back(i->full_path);
        }
        return files;
    }
    

半関連: 「ダウンキャスト」unique_ptr<Base>からunique_ptr<Derived>(DLL境界を越えて)

5
d7samurai

この問題は、MS言語の動的(共有)ライブラリがメインの実行可能ファイルとは異なるヒープを使用するために発生します。 DLLに文字列を作成するか、再割り当ての原因となるベクターを更新すると、この問題が発生します。

この問題の最も簡単な修正は、ライブラリを静的ライブラリに変更することです(CMAKEにどのようにそれを行わせるかは定かではありません)。そうすると、すべての割り当てが実行可能ファイルと単一のヒープで行われるためです。もちろん、MS C++の静的ライブラリの互換性の問題がすべてあり、ライブラリの魅力が低下します。

John Bandelaの応答の上部にある要件はすべて、静的ライブラリの実装の要件と同様です。

別の解決策は、ヘッダーにインターフェイスを実装し(これにより、アプリケーションスペースでコンパイルされます)、それらのメソッドにDLLで提供されるCインターフェイスを使用して純粋関数を呼び出させることです。

3
Bryan Donaldson

そこでのベクトルは、デフォルトのstd :: allocatorを使用します。これは、割り当てに:: operatornewを使用します。

問題は、ベクターがDLLのコンテキストで使用される場合、そのDLLによって提供される:: operatornewを認識しているDLLのベクターコードでコンパイルされることです。

EXEのコードは、EXEの:: operatornewを使用しようとします。

これがWindowsではなくMac/Linuxで機能する理由は、Windowsではコンパイル時にすべてのシンボルを解決する必要があるためだと思います。

たとえば、VisualStudioが「未解決の外部シンボル」のようなエラーを表示するのを見たことがあるかもしれません。 「foo()という名前のこの関数が存在すると言われましたが、どこにも見つかりません」という意味です。

これはMac/Linuxが行うことと同じではありません。ロード時にすべてのシンボルを解決する必要があります。これが意味するのは、:: operatornewが欠落している.soをコンパイルできるということです。また、プログラムは.soをロードし、.soに新しい::演算子を提供して、解決できるようにします。デフォルトでは、すべてのシンボルはGCCでエクスポートされるため、:: operator newはプログラムによってエクスポートされ、.soによってロードされる可能性があります。

ここには興味深いことがあります。Mac/ Linuxでは循環依存が許可されています。プログラムは、.soによって提供されるシンボルに依存する可能性があり、同じ.soは、プログラムによって提供されるシンボルに依存する可能性があります。循環依存はひどいものなので、Windowsメソッドがこれを行わないように強制するのが本当に好きです。

しかし、そうは言っても、本当の問題は、境界を越えてC++オブジェクトを使用しようとしていることです。それは間違いなく間違いです。 DLLで、EXEが同じで、設定が同じである場合にのみ機能します。'extern "C" 'は、名前のマングリングを防止しようとする場合があります(何がわからないかはわかりません)。 std :: vectorのような非Cタイプの場合も同様です)。ただし、反対側のstd :: vectorの実装がまったく異なる可能性があるという事実は変わりません。

一般的に言って、そのような境界を越えて渡される場合は、単純な古いCタイプにする必要があります。 intやsimpletypesのようなものであれば、それほど難しいことではありません。あなたの場合、おそらくchar *の配列を渡したいでしょう。つまり、メモリ管理には注意が必要です。

DLL/.soは、独自のメモリを管理する必要があります。したがって、関数は次のようになります。

Foo *bar = nullptr;
int barCount = 0;
getFoos( bar, &barCount );
// use your foos
releaseFoos(bar);

欠点は、境界で物事をC共有可能な型に変換するための余分なコードがあることです。また、実装を高速化するために、これが実装に漏れることがあります。

しかし、利点は、人々が任意の言語、任意のコンパイラバージョン、および任意の設定を使用してDLLを作成できることです。また、適切なメモリ管理と依存関係に注意を払う必要があります。

私はそれが余分な仕事であることを知っています。しかし、それは境界を越えて物事を行うための適切な方法です。

2
ProgramMax

おそらくバイナリ互換性の問題が発生しています。 Windowsでは、DLL間でC++インターフェイスを使用する場合は、たとえば、多くのものが正常であることを確認する必要があります。

  • 関連するすべてのDLLは、同じバージョンのVisualStudioコンパイラでビルドする必要があります
  • すべてのDLLは、同じバージョンのC++ランタイムをリンクする必要があります(VSのほとんどのバージョンでは、これはプロジェクトプロパティの[構成]-> [C++]-> [コード生成]のランタイムライブラリ設定です)
  • イテレータのデバッグ設定は、すべてのビルドで同じである必要があります(これは、リリースDLLとデバッグDLLを混在させることができない理由の一部です)

残念ながら、これは完全なリストではありません:(

2

私の-部分的な-解決策は、すべてのデフォルトコンストラクターをdllフレームに実装することでした。そのため、プログラムに応じて、明示的に(impelement)コピー、代入演算子、さらにはコンストラクターを移動します。これにより、正しい:: newが呼び出されます(__declspec(dllexport)を指定すると仮定します)。削除を照合するためのデストラクタの実装も含めます。 (dll)ヘッダーファイルに実装コードを含めないでください。 dllインターフェイスクラスのベースとして非dllインターフェイスクラス(stlコンテナを使用)を使用することについての警告が引き続き表示されますが、機能します。これは、明らかにWindows上でネイティブコードにVS2013RCを使用しています。

0
Jan