Dll совместимость между компиляторами - PullRequest
8 голосов
/ 13 января 2009

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

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

interface.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();
}

EDIT: Я добавил следующую строку в метод GCC CreateClass, чтобы текст был правильно выведен на консоль, поэтому вызов функции убивает его.

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

Мне было интересно, как COM удается поддерживать бинарную совместимость даже между языками, поскольку в основном это все классы с наследованием (хотя и только один) и, следовательно, виртуальные функции. Меня не беспокоит, не могу ли я перегружать операторы / функции, пока я могу поддерживать базовые вещи ООП (т.е. классы и одиночное наследование).

Ответы [ 9 ]

8 голосов
/ 13 января 2009

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

То, как ведут себя классы и виртуальные функции, определяется стандартом 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 (Plain Old Data; т.е. без виртуальных функций) и указателей на непрозрачные объекты.

5 голосов
/ 14 января 2009

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

3 голосов
/ 14 января 2009

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

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

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

3 голосов
/ 14 января 2009

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

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

2 голосов
/ 13 января 2009

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

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

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

2 голосов
/ 13 января 2009

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

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

0 голосов
/ 19 февраля 2016

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

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

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

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

0 голосов
/ 12 мая 2012

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

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

0 голосов
/ 13 января 2009

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

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

...