10

異なるコンパイラでビルドされた C++ dll を相互に互換性を持たせる方法はありますか? クラスは作成と破棄のためのファクトリ メソッドを持つことができるため、各コンパイラは独自の新規/削除を使用できます (異なるランタイムには独自のヒープがあるため)。

次のコードを試しましたが、最初のメンバー メソッドでクラッシュしました。

インターフェイス.h

#pragma once

class IRefCounted
{
public:
    virtual ~IRefCounted(){}
    virtual void AddRef()=0;
    virtual void Release()=0;
};
class IClass : public IRefCounted
{
public:
    virtual ~IClass(){}
    virtual void PrintSomething()=0;
};

VC9 でコンパイルされた test.cpp、test.exe

#include "interface.h"

#include <iostream>
#include <windows.h>

int main()
{
    HMODULE dll;
    IClass* (*method)(void);
    IClass *dllclass;

    std::cout << "Loading a.dll\n";
    dll = LoadLibraryW(L"a.dll");
    method = (IClass* (*)(void))GetProcAddress(dll, "CreateClass");
    dllclass = method();//works
    dllclass->PrintSomething();//crash: Access violation writing location 0x00000004
    dllclass->Release();
    FreeLibrary(dll);

    std::cout << "Done, press enter to exit." << std::endl;
    std::cin.get();
    return 0;
}

g++ でコンパイルされた a.cpp g++.exe -shared c.cpp -o c.dll

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

class A : public IClass
{
    unsigned refCnt;
public:
    A():refCnt(1){}
    virtual ~A()
    {
        if(refCnt)throw "Object deleted while refCnt non-zero!";
        std::cout << "Bye from A.\n";
    }
    virtual void AddRef()
    {
        ++refCnt;
    }
    virtual void Release()
    {
        if(!--refCnt)
            delete this;
    }

    virtual void PrintSomething()
    {
        std::cout << "Hello World from A!" << std::endl;
    }
};

extern "C" __declspec(dllexport) IClass* CreateClass()
{
    return new A();
}

編集: 次の行を GCC CreateClass メソッドに追加しました。テキストはコンソールに正しく出力されたので、関数呼び出しは間違いなくそれを殺します。

std::cout << "C.DLL Create Class" << std::endl;

COM は、基本的にすべてのクラスが継承されているため (ただし単一のみ)、したがって仮想関数であるため、言語間でもバイナリ互換性をどのように維持するのか疑問に思っていました。基本的な OOP 要素 (つまり、クラスと単一継承) を維持できる限り、オーバーロードされた演算子/関数を使用できなくても、あまり気にしません。

4

9 に答える 9

10

期待を下げて単純な関数に固執する場合は、異なるコンパイラでビルドされたモジュールを混在させることができるはずです。

クラスと仮想関数の動作方法は C++ 標準で定義されていますが、実装方法はコンパイラ次第です。この場合、VC++ は、オブジェクトの最初の 4 バイト (32 ビットを想定しています) に "vtable" ポインターを持つ仮想関数を持つオブジェクトを構築し、それがメソッド エントリへのポインターのテーブルを指すことを知っています。ポイント。

したがって、行:dllclass->PrintSomething(); は実際には次のようなものと同等です:

struct IClassVTable {
    void (*pfIClassDTOR)           (Class IClass * this) 
    void (*pfIRefCountedAddRef)    (Class IRefCounted * this);
    void (*pfIRefCountedRelease)   (Class IRefCounted * this);
    void (*pfIClassPrintSomething) (Class IClass * this);
    ...
};
struct IClass {
    IClassVTable * pVTab;
};
(((struct IClass *) dllclass)->pVTab->pfIClassPrintSomething) (dllclass);

g++ コンパイラが MSFT VC++ とは異なる方法で仮想関数テーブルを実装している場合 (自由に実行でき、C++ 標準に準拠しているため)、これまでに説明したようにクラッシュします。VC++ コードは、関数ポインターがメモリ内の特定の場所 (オブジェクト ポインターに関連する) にあることを想定しています。

継承によってさらに複雑になり、多重継承と仮想継承では本当に、本当に、複雑になります。

Microsoft は、VC++ がクラスを実装する方法について非常に公開しているため、VC++ に依存するコードを記述できます。たとえば、MSFT によって配布される多くの COM オブジェクト ヘッダーには、ヘッダーに C と C++ の両方のバインディングが含まれています。C バインディングは、上記のコードのように vtable 構造を公開します。

一方、GNU -- IIRC -- は、異なるリリースで異なる実装を使用するオプションを残しており、そのコンパイラ (のみ!) で構築されたプログラムが標準の動作に準拠することを保証するだけです。

簡単な答えは、単純な C スタイル関数、POD 構造 (Plain Old Data、つまり仮想関数なし)、および不透明なオブジェクトへのポインターに固執することです。

于 2009-01-13T20:17:27.303 に答える
6

これを行うと、ほぼ確実に問題が発生します-他のコメント作成者は、C++ ABIが場合によっては同じである可能性があることは正しいですが、2つのライブラリは異なるCRT、異なるバージョンのSTL、異なる例外スローセマンティクス、異なるものを使用しています最適化...あなたは狂気への道を進んでいます。

于 2009-01-13T21:35:23.247 に答える
5

関数のみを使用する限り可能extern "C"です。

これは、「C」ABIが明確に定義されているのに対し、C++ABIは意図的に定義されていないためです。したがって、各コンパイラは独自のコンパイラを定義できます。

一部のコンパイラでは、コンパイラのバージョンが異なる場合、またはフラグが異なる場合でも、C++ABIは互換性のないABIを生成します。

于 2009-01-13T20:43:53.243 に答える
5

コードを編成できる 1 つの方法は、アプリと dll の両方でクラスを使用することですが、2 つの間のインターフェイスを extern "C" 関数として保持することです。これは、C# アセンブリで使用される C++ dll で行った方法です。エクスポートされた DLL 関数は、次のように static class* Instance() メソッドを介してアクセス可能なインスタンスを操作するために使用されます。

__declspec(dllexport) void PrintSomething()
{
    (A::Instance())->PrintSometing();
}

オブジェクトのインスタンスが複数ある場合は、dll 関数でインスタンスを作成し、識別子を返します。これを Instance() メソッドに渡して、必要な特定のオブジェクトを使用できます。アプリと dll の間で継承が必要な場合は、エクスポートされた dll 関数をラップするクラスをアプリ側で作成し、そこから他のクラスを派生させます。このようにコードを編成すると、DLL インターフェイスがシンプルになり、コンパイラと言語の両方の間で移植性が保たれます。

于 2009-01-13T22:08:29.857 に答える
3

この MSDN の記事が役立つと思います

とにかく、あなたのコードを一目見ただけで、インターフェイスで仮想デストラクタを宣言するべきではないことがわかります。delete this代わりに、参照カウントがゼロになったときに A::Release() 内で行う必要があります。

于 2009-01-13T21:18:01.067 に答える
2

VC と GCC の間で互換性のある v-table レイアウトに大きく依存します。それは大丈夫の可能性が高いです。呼び出し規約が一致していることを確認する必要があります (COM: __stdcall、あなた: __thiscall)。

重要なのは、執筆中に AV を取得していることです。メソッド呼び出し自体を行うときは何も書き込まれていないため、operator<< が爆撃を行っている可能性があります。DLL が LoadLibrary() でロードされると、std::cout はおそらく GCC ランタイムによって初期化されますか? デバッガーが通知する必要があります。

于 2009-01-13T20:08:44.000 に答える
0

興味深い.. DLL を VC++ でもコンパイルするとどうなるか、CreateClass() にデバッグ ステートメントを入れるとどうなるでしょうか。

メソッド呼び出しではなく、cout の 2 つの異なるランタイム「バージョン」が競合している可能性があると思いますが、返された関数ポインタ/dllclass が 0x00000004 ではないことを信じていますか?

于 2009-01-13T18:58:45.433 に答える
0

あなたの問題はABIを維持することです。コンパイラは同じですが、バージョンは異なりますが、ABI を維持したいと考えています。COMはそれを解決する1つの方法です。COM がこれをどのように解決するかを本当に理解したい場合は、この記事CPP to COM in msdnをチェックしてください。この記事では、COM の本質について説明しています。

COM 以外にも、Plain old data や opaque ポインターを使用するなど、ABI を解決する (最も古いものの 1 つ) 方法があります。ABI を解決する Qt/KDE ライブラリ開発者の方法を見てください。

于 2012-05-11T21:40:26.053 に答える