Как быть если компилятор не поддерживает интерфейсы с нулевым смещением VMT

Для чего это надо


Часто бывает необходимо писать плагины для программ. Но из-за бинарной несовместимости классов эти плагины придётся писать на том же языке, что и основная программа. В С++ принято располагать таблицу виртуальных функций первой в классе. Если пользоваться определенными правилами (не использовать множественное наследование интерфейсов) и использовать абстрактные классы-то можно добиться возможности запуска плагинов, скомпилированных под разными компиляторами С++.

В этой статье я покажу как использовать плагин написанный с использованием компилятора Free Pascal Compiler в программе на с++ (только общая идея, а не реальный плагин).

Что такое VMT


Таблица виртуальных методов (англ. virtual method table, VMT) — координирующая таблица или vtable — механизм, используемый в языках программирования для поддержки динамического соответствия (или метода позднего связывания).

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

Обычно компилятор создает отдельную vtable для каждого класса. После создания объекта указатель на эту vtable, называемый виртуальный табличный указатель или vpointer (также иногда называется vptr или vfptr), добавляется как скрытый член данного объекта (а зачастую как первый член). Компилятор также генерирует «скрытый» код в конструкторе каждого класса для инициализации vpointer'ов его объектов адресами соответствующей vtable.
(Абзацы взяты из википедии.)

Реализация.


Для начала нам надо создать обертку вокруг кода на паскале.

plugin.hpp
#pragma once

#include "ApiEntry.hpp"

class IPlugin 
{
public:
  virtual void APIENTRY free () = 0;
  virtual void APIENTRY print () = 0;
};

class Plugin : public IPlugin
{
public:
  virtual void APIENTRY free ();
  virtual void APIENTRY print ();
  Plugin ();
  virtual ~Plugin ();
  
private:
  void* thisPascal;
};

extern "C" IPlugin* APIENTRY getNewPlugin ();

Где IPlugin это интерфейс плагина. А thisPascal это указатель на бинарный вариант класса реализации интерфейса в паскале.

И сам код обёртки: plugin.cpp
#include "plugin.hpp"
#include "pascalunit.hpp"
#include <iostream>

void APIENTRY Plugin::free ()
{
  IPlugin_release (thisPascal);
  delete this;
}

void APIENTRY Plugin::print ()
{
  IPlugin_print (thisPascal);
}

Plugin::Plugin ()
{
  std::cout << "Plugin::Plugin" << std::endl;
  thisPascal = IPlugin_getNewPlugin ();
}

Plugin::~Plugin ()
{
  std::cout << "Plugin::~Plugin" << std::endl;
}

extern "C" IPlugin* APIENTRY getNewPlugin ()
{
  Plugin* plugin = new Plugin ();
  return plugin;
}

Как видно код вызывает функции из библиотеки на паскале и передает им заранее сохраненный при создании класса указатель на реализацию плагина на паскале. getNewPlugin вызывается для создания экземпляра класса плагина в основной программе.

Теперь поговорим о реализации плагина на паскале.

library pascalunit;
{$MODE OBJFPC}

uses
  ctypes;
  
type
  IPlugin = interface
    procedure _release (); cdecl;
    procedure print (); cdecl;
  end;
  TPlugin = class (TInterfacedObject, IPlugin)
  public
    procedure _release (); cdecl;
    procedure print (); cdecl;
    constructor Create ();
    destructor Free ();
  end;
  PPlugin = ^TPlugin;

procedure TPlugin._release (); cdecl; 
begin
  Free;
end;

procedure TPlugin.print (); cdecl; 
begin
  writeln ('Hello World');
end;

procedure _release (this: PPlugin); cdecl;
begin
  this^._release ();
end;

procedure print (this: PPlugin); cdecl; 
begin
  this^.print ();
end;

constructor TPlugin.Create ();
begin
  inherited;
  writeln ('TPlugin.Create');
end;

destructor TPlugin.Free ();
begin
  writeln ('TPlugin.Free');
end;

function  getNewPlugin (): PPlugin; cdecl;
var
  plugin: PPlugin;
begin
  New (plugin);
  plugin^ := TPlugin.Create ();
  result := plugin;
end;

exports
  getNewPlugin name 'IPlugin_getNewPlugin', print name 'IPlugin_print', _release name 'IPlugin_release';

begin
end.

В данном файле реализуется почти этот же самый интерфейс на паскале и делается обертка вокруг функций плагина для возможности экспорта функций в библиотеку. Заметьте все функции реализации интерфейса содержат первым параметром указатель на класс. Этот параметр передаётся неявно для методов класса первым параметром и нужен для обращения к методам и полям класса. Функция getNewPlugin используется для получения указателя в С++ классе. Код на паскале подключается как библиотека.

PS: Забыл упомянуть что код на паскале должен быть/желательно обернут в try/catch так как в данном способе плагирования исключения проходить не должны. Плагин должен обрабатывать свои исключения и выдавать результаты либо сразу либо отдельной функцией в виде простых типов.

PS2: Добавил коммент насчет функции free и изменил её код. Сюда изменения добавлять не буду, что бы сохранить соответствие с комментариями. И добавил коммент насчет использования функции getNewPlugin и удалении объекта в сторонних приложениях. Хотя человеку знающему об интерфейсах это и так будет понятно.

Исходники примера

Комментарии 9

    0
    Без обид, plugin.cpp сильно попахивает. Тут и delete this, и утечка в последнем методе. RAII уже давно придумали)
      0
      Тут и delete this

      Здесь просто идёт переназначение деструктора на другой метод free (он автоматически вызывается с помощью умного указателя при удалении объекта). Где конкретно вы увидели утечку? Посмотрите исходники полностью там это есть.
        0
        Я не говорю, что здесь 100% утечка, я говорю, что код попахивает. Вполне возможно, конкретно в этом вашем (предельно упрощённом) варианте утечек нет.
        Просто вы пытаетесь прикрутить свой плагин на Паскале к чужому коду на плюсах. И делаете это «общественно-опасным образом». После delete this объект вообще трогать нельзя, любое самое невинное обращение может окончится access violation. В том числе попытка вызвать деструктор (повторно!), что вообще на автомате может случиться. С другой стороны, если free не вызвать, будет утечка. Вы действительно так уверенны, что пользователи вашего плагина — программисты С++ — везде вот так строго соблюдают этот, мягко скажем, нестандартный для плюсов протокол?
        Как минимум, я бы перенёс содержимое Plugin::free внутрь деструктора, оставив там только delete this, если уж без него никак.
          0
          Вместо деструктора ВСЕГДА в данной плагинной системе должен вызываться метод free и только он. Это всё делается в загрузчике плагинов автоматом при удалении. Зачем вы думаете в COM технологии используется метод Release. Это система основанная на интерфейсах. А если плагин например написан на java (кажется в ней есть нормальная поддержка интерфейсов в том числе и без использования QueryInterface, AddRef с использованием jni можно прикрутить к с++) откуда программа на с++ будет знать где в памяти расположен java деструктор, для этого и нужен метод free. Он его и будет вызывать…
            0
            В статье нет никаких упоминаний о «данной плагинной системе» (какой, кстати)? Это раз.
            И, в любом случае это не повод писать говнокод и пропихивать его в туториалы для начинающих — это два. Надо «в данной плагинной системе» использовать free — используйте. Но и на правила языка класть болт не надо. Плюсы это вам не паскаль какой-нибудь или там джава, здесь можно отстрелить себе не только ногу, но и голову.
              0
              Ну так то я галку туториал не ставил. например вот описание более сложной системы плагинов там же и про delete this и про выстрел в ногу и почему будет осечка. вот описание com с ссылками на книжки. delete this используется в книге «Основы COM» Дейл Роджерсон. Думаю в книгах по ссылке то же самое… Вот вам скриншот из книги: image
                –1
                И да, я понимаю ваши чувства: неправильное использование delete this и все такое, но это реально используется в com (ActiveX DirectX) и не только, смиритесь с этим.
                  0
                  Не трогайте вы COM, ActiveX и прочие фекалии мамонта. Это не пример хорошей архитектуры.
                  По сути: моя претензия была не к delete this, а к его оформлению. В методе free должен быть только delete this (плюс страшный комент на тему «сюда больше ничего не писать»), вся остальная логика — в деструкторе (который будет этим delete вызвано).
                  Во-первых, потому что человек, пишущий плагин на паскале/java/..., может быть не силён в тонкостях плюсов, и по-ошибке написать что-нибудь после delete this. Причем в дебаге оно может и работать, а вот на релизе — трудноуловимо падать.
                  Во-вторых, потому что кто-то на другой стороне вполне может юзать ваш плагин так, как это принято в С++, а не в Си:
                  int main() {
                  auto plugin = *getNewPlugin();
                  // do something ...
                  } /* plugin будет убит автоматом, без всяких вызовов plugin.free() */
                  
                    0
                    По сути: моя претензия была не к delete this, а к его оформлению. В методе free должен быть только delete this (плюс страшный комент на тему «сюда больше ничего не писать»), вся остальная логика — в деструкторе (который будет этим delete вызвано).

                    Ну вот с этого и надо было сразу начинать с конкретного указания что к чему. Тут я с вами согласен, спасибо, исправлю. Коммент добавлю.
                    Насчет использования в другом приложении это вы конечно загнули. Если только это не приложение совместимое с этими плагинами (какой-нибудь форк). Но конечно такое тоже возможно и лучше тоже коммент добавить. Спасибо.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое