Совместимость Dll между компиляторами

Есть ли способ сделать библиотеки С++, созданные с помощью разных компиляторов, совместимыми друг с другом? Классы могут иметь фабричные методы для создания и уничтожения, поэтому каждый компилятор может использовать свои собственные операции new/delete (поскольку разные среды выполнения имеют свои собственные кучи).

Я попробовал следующий код, но он разбился на первом методе-члене:

интерфейс.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;
};

test.cpp, скомпилированный с помощью VC9, 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;
}

a.cpp, скомпилированный с помощью g++ 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 удается поддерживать двоичную совместимость даже между языками, поскольку в основном все его классы имеют наследование (хотя и только одно) и, следовательно, виртуальные функции. Меня не сильно беспокоит, если я не могу иметь перегруженные операторы/функции, пока я могу поддерживать базовые вещи ООП (например, классы и одиночное наследование).


person Fire Lancer    schedule 13.01.2009    source источник
comment
Как COM это делает? С облегченными вызовами RPC вы можете создать свое приложение, используя dce-rpc, и вы получите те же результаты. COM ни в коем случае не предоставляет указатель на память внешней dll, он вызывает функции для этой dll.   -  person gbjbaanb    schedule 14.01.2009
comment
Следующие статьи здесь, здесь и < href="http://chadaustin.me/cppinterface.html" rel="nofollow noreferrer">здесь может оказаться полезным. Ваш пример кода почти готов, за исключением встроенного виртуального деструктора. Насколько я знаю, все методы в вашем абстрактном интерфейсе должны быть чисто виртуальными =0.   -  person greatwolf    schedule 13.03.2013


Ответы (9)


У вас должна быть возможность смешивать модули, созданные с помощью разных компиляторов, если вы снижаете свои ожидания и придерживаетесь простых функций.

То, как ведут себя классы и виртуальные функции, определяется стандартом C++, но то, как это реализовано, зависит от компилятора. В этом случае я знаю, что VC++ создает объекты, которые имеют виртуальные функции с указателем «vtable» в первых 4 байтах объекта (я предполагаю, что это 32-разрядный), и это указывает на таблицу указателей на запись метода точки.

Таким образом, строка: 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++ реализует классы, поэтому вы можете писать код, зависящий от него. Например, многие заголовки COM-объектов, распространяемые MSFT, имеют в заголовке привязки как C, так и C++. Привязки C раскрывают свою структуру vtable, как это делает мой код выше.

С другой стороны, GNU -- IIRC -- оставил открытой возможность использования разных реализаций в разных выпусках и просто гарантирует, что программы, созданные с помощью его компилятора (только!) будут соответствовать стандартному поведению,

Короткий ответ — придерживаться простых функций в стиле C, структур POD (обычные старые данные, т. е. никаких виртуальных функций) и указателей на непрозрачные объекты.

person Die in Sente    schedule 13.01.2009
comment
Я бы предпочел не использовать везде простые функции, я бы просто сказал всем, что они должны скомпилировать dll с VC9, прежде чем я предприму этот шаг... - person Fire Lancer; 13.01.2009
comment
Возможно, вы сможете делать с CORBA все, что хотите, но я мало о ней знаю. - person Die in Sente; 14.01.2009

Вы почти наверняка напрашиваетесь на неприятности, если сделаете это - в то время как другие комментаторы правы в том, что C++ ABI может быть одним и тем же в некоторых случаях, две библиотеки используют разные CRT, разные версии STL, разную семантику генерации исключений, разные оптимизация... вы идете по пути к безумию.

person Ana Betts    schedule 13.01.2009

Один из способов организовать код — использовать классы как в приложении, так и в dll, но сохранить интерфейс между ними как внешние функции «C». Именно так я сделал это с dll С++, используемыми сборками С#. Экспортированные функции DLL используются для управления экземплярами, доступными через методы статического класса* Instance(), например:

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

Для нескольких экземпляров объектов функция dll создает экземпляр и возвращает идентификатор, который затем можно передать методу Instance() для использования конкретного необходимого объекта. Если вам нужно наследование между приложением и dll, создайте класс на стороне приложения, который обертывает экспортированные функции dll, и получите от него другие классы. Такая организация кода сделает интерфейс DLL простым и переносимым как между компиляторами, так и между языками.

person Tim Butterfield    schedule 13.01.2009
comment
Хорошо, есть ли способ сделать это, по крайней мере, автоматически, вместо того, чтобы писать 4 вещи для каждого метода (загрузка метода C, чтобы интерфейс мог его найти, перевод вызова интерфейса в метод C, переход от C метода к методу oop в dll и, наконец, к методу dll? - person Fire Lancer; 14.01.2009
comment
Вы могли бы частично автоматизировать это. Вам нужно будет сохранить список файлов классов и методов. Сценарий может обработать этот файл, генерируя файлы, которые будут #include, где это необходимо. Имейте в виду, однако, что функции C dll не смогут напрямую принимать параметры экземпляра C++*. - person Tim Butterfield; 24.01.2009

Вы можете, если используете только extern "C" функции.

Это связано с тем, что "C" ABI хорошо определен, а C++ ABI намеренно не определен. . Таким образом, каждому компилятору разрешено определять свои собственные.

В некоторых компиляторах C++ ABI между разными версиями компилятора или даже с разными флагами будет генерировать несовместимый ABI.

person Martin York    schedule 13.01.2009
comment
@Shy: Двоичный интерфейс приложения. - person Die in Sente; 14.01.2009

Я думаю, вы найдете эту статью MSDN полезной

В любом случае, бегло взглянув на ваш код, я могу сказать вам, что вам не следует объявлять виртуальный деструктор в интерфейсе. Вместо этого вам нужно выполнить delete this в A::Release(), когда количество ссылок упадет до нуля.

person Nemanja Trifunovic    schedule 13.01.2009

Вы критически зависите от макета v-таблицы, совместимого между VC и GCC. Это, вероятно, будет в порядке. Вы должны проверить соответствие соглашения о вызовах (COM: __stdcall, вы: __thiscall).

Примечательно, что вы получаете AV при написании. Когда вы вызываете сам метод, ничего не пишется, поэтому вполне вероятно, что оператор ‹‹ выполняет бомбардировку. Может ли std::cout инициализироваться средой выполнения GCC, когда DLL загружается с помощью LoadLibrary()? Отладчик должен сказать.

person Hans Passant    schedule 13.01.2009
comment
Как я могу проверить, правильно ли создан cout в dll GCC с помощью VC? Кроме того, как именно COM работает через __stdcall, поскольку я думал, что даже базовые классы должны работать через __thiscall под VC? - person Fire Lancer; 13.01.2009

Проблема с вашим кодом, который вызывает сбой, - это виртуальные деструкторы в определении интерфейса:

virtual ~IRefCounted(){}
    ...
virtual ~IClass(){}

Удалите их, и все будет в порядке. Проблема вызвана тем, как организованы таблицы виртуальных функций. Компилятор MSVC игнорирует деструктор, но GCC добавляет его первой функцией в таблицу.

Посмотрите на COM-интерфейсы. У них нет конструкторов/деструкторов. Никогда не определяйте никаких деструкторов в интерфейсе, и все будет в порядке.

person Artyom Chirkov    schedule 19.02.2016

интересно .. что произойдет, если вы скомпилируете dll в VC++, и что, если вы поместите некоторые операторы отладки в CreateClass ()?

Я бы сказал, что возможно, что ваши две разные «версии» cout во время выполнения конфликтуют вместо вызова вашего метода, но я верю, что возвращаемый указатель функции/dllclass не равен 0x00000004?

person gbjbaanb    schedule 13.01.2009
comment
Нет, отладчик VC показал разумное значение для указателя, насколько я могу судить (я никогда раньше не пытался отлаживать двоичный файл без VC с помощью VC), он никогда не попадал в метод PrintSomething, по крайней мере, кадр стека указывает на это. никогда не входил в dll на данный момент. - person Fire Lancer; 13.01.2009
comment
Когда вы отлаживаете код, который не был создан с помощью VC, или даже когда вы используете его, но не имеете символов отладки, вы не можете полностью доверять тому, что отладчик говорит вам о стеке вызовов. - person Die in Sente; 14.01.2009

Ваша проблема заключается в поддержании ABI. Хотя один и тот же компилятор, но разные версии, вы все равно хотите поддерживать ABI. COM является одним из способов решения этой проблемы. Если вы действительно хотите понять, как COM решает эту проблему, ознакомьтесь с этой статьей CPP to COM в msdn, который описывает суть COM.

Помимо COM, существуют и другие (один из самых старых) способов решения ABI, например, использование простых старых данных и непрозрачных указателей. Посмотрите, как разработчики библиотек Qt/KDE решают проблему ABI.

person Gnu Engineer    schedule 11.05.2012