Является ли структура, полная указателей на функции, хорошим решением для бинарной совместимости C ++?

У меня есть библиотека, написанная на C ++, которую мне нужно превратить в DLL. Эта библиотека должна быть в состоянии быть измененной и перекомпилированной с другими компиляторами и все еще работать.

Я читал, что очень маловероятно, что я достигну полной двоичной совместимости между компиляторами / версией, если я экспортирую все свои классы напрямую, используя __declspec (dllexport).

Я также читал, что чистые виртуальные интерфейсы могут быть извлечены из DLL, чтобы устранить проблему искажения имен, просто передав таблицу, полную указателей на функции. Однако я прочитал, что даже это может не сработать, потому что некоторые компиляторы могут даже изменять порядок функций в виртуальной таблице между последовательными выпусками.

Наконец, я решил, что могу просто реализовать свой собственный vtable, и вот где я нахожусь:

test.h

#pragma once
#include <iostream>
using namespace std;

class TestItf;
extern "C" __declspec(dllexport) TestItf* __cdecl CreateTest();

class TestItf {
public:
static TestItf* Create() {
return CreateTest();
}
void Destroy() {
(this->*vptr->Destroy)();
}
void Print(const char *something) {
(this->*vptr->Print)(something);
}
~TestItf() {
cout << "TestItf dtor" << endl;
}
typedef void(TestItf::*pfnDestroy)();
typedef void(TestItf::*pfnPrint)(const char *something);

struct vtable {
pfnDestroy Destroy;
pfnPrint Print;
};
protected:
const vtable *const vptr;
TestItf(vtable *vptr) : vptr(vptr){}
};

extern "C"__declspec(dllexport) void __cdecl GetTestVTable(TestItf::vtable *vtable);

test.cpp

#include "Test.h"
class TestImp : public TestItf {
public:
static TestItf::vtable TestImp_vptr;
TestImp() : TestItf(&TestImp_vptr) {

}
~TestImp() {
cout << "TestImp dtor" << endl;
}
void Destroy() {
delete this;
}
void Print(const char *something) {
cout << something << endl;
}
};

TestItf::vtable TestImp::TestImp_vptr =  {
(TestItf::pfnDestroy)&TestImp::Destroy,
(TestItf::pfnPrint)&TestImp::Print,
};

extern "C" {
__declspec(dllexport) void __cdecl GetTestVTable(TestItf::vtable *vtable) {
memcpy(vtable, &TestImp::TestImp_vptr, sizeof(TestItf::vtable));
}
__declspec(dllexport) TestItf* __cdecl CreateTest() {
return new TestImp;
}
}

main.cpp

int main(int argc, char *argv[])
{
TestItf *itf = TestItf::Create();
itf->Print("Hello World!");
itf->Destroy();

return 0;
}

Правильны ли мои вышеприведенные предположения о том, что невозможно достичь надлежащей совместимости с первыми двумя методами?

Является ли мое третье решение портативным и безопасным?

-В частности, меня беспокоит влияние использования указателей приведенных функций из TestImp на базовый тип TestItf. Кажется, он работает в этом простом тестовом примере, но я полагаю, что такие вещи, как выравнивание или изменение расположения объекта, могут сделать это небезопасным в некоторых случаях.

редактировать
Этот метод также можно использовать с C #. Несколько незначительных изменений были внесены в приведенный выше код.

Test.cs

struct TestItf {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct VTable {
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
public delegate void pfnDestroy(IntPtr itf);

[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
public delegate void pfnPrint(IntPtr itf, string something);

[MarshalAs(UnmanagedType.FunctionPtr)]
public pfnDestroy Destroy;

[MarshalAs(UnmanagedType.FunctionPtr)]
public pfnPrint Print;
}

[DllImport("cppInteropTest", CallingConvention = CallingConvention.Cdecl)]
private static extern void GetTestVTable(out VTable vtable);

[DllImport("cppInteropTest", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr CreateTest();

private static VTable vptr;
static TestItf() {
vptr = new VTable();
GetTestVTable(out vptr);
}

private IntPtr itf;
private TestItf(IntPtr itf) {
this.itf = itf;
}

public static TestItf Create() {
return new TestItf( CreateTest() );
}

public void Destroy() {
vptr.Destroy(itf);
itf = IntPtr.Zero;
}

public void Print(string something) {
vptr.Print(itf, something);
}
}

Program.cs

static class Program
{
[STAThread]
static void Main()
{
TestItf test = TestItf.Create();
test.Print("Hello World!");
test.Destroy();
}
}

2

Решение

Прежде всего: ваш деструктор TestItf должен быть виртуальным, потому что вы возвращаете тип-потомок в качестве базового предка. Без виртуальности произойдет утечка памяти на некоторых компиляторах.

Теперь по бинарной совместимости. Есть следующие общие подводные камни:

  1. Соглашения о вызовах. Если оба компилятора (ваш и клиентский) знают о выбранном соглашении о вызовах, это нормально (с тех пор обычное бесклассовое соглашение stdcall, как это делает Win32 API, является проверенным решением для многих лет и нескольких языков, не только C ++)
  2. Выравнивание конструкций. Упакуйте ваши опубликованные структуры с 1-байтовым выравниванием — большинство компиляторов имеют соответствующие настройки с помощью прагмы или ключей компиляции.

Помня об этих двух моментах, вы будете осторожны практически на любой платформе.

0

Другие решения

Нет.

Совместимость между языками удобным объектно-ориентированным способом была большой частью моей первоначальной мотивации для изучения этой идеи.

Хотя пример C #, использованный в первоначальном вопросе, работает под Windows, он не работает на Mac OSX. Размеры vtables между C # / Mono и C ++ не совпадают из-за разных размеров указателей на функции-члены. Mono ожидает 4-байтовый указатель на функцию, а компилятор xcode / c ++ ожидает, что они будут 8 байтами.

Очевидно, указатели на функции-члены — это больше, чем просто указатели. Иногда они могут указывать на структуры, которые содержат дополнительные данные для решения определенных ситуаций наследования.

Усечение 8-байтовых указателей на функции-члены до 4-х байтов и их отправка в моно в любом случае действительно работает. Это, вероятно, потому что я использую тип класса POD. Я не хотел бы полагаться на взлом, как это все же.

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

0

По вопросам рекламы [email protected]