Передайте класс Delphi функции/методу C++, который ожидает класс с методами __thiscall

У меня есть несколько скомпилированных библиотек DLL MSVC++, для которых я создал COM-подобные (облегченные) интерфейсы (абстрактные классы Delphi). Некоторые из этих классов имеют методы, которым нужны указатели на объекты. Эти методы C++ объявлены с использованием соглашения о вызовах __thiscall (которое я не могу изменить), которое аналогично __stdcall, за исключением того, что передается указатель this. в реестре ЕСХ.

Я создаю экземпляр класса в Delphi, а затем передаю его методу C++. Я могу установить точки останова в Delphi и увидеть, как он сработает с открытыми методами __stdcall в моем классе Delphi, но вскоре я получаю STATUS_STACK_BUFFER_OVERRUN, и приложение должно закрыться. Можно ли эмулировать/иметь дело с __thiscall на стороне Delphi? Если я передаю объект, созданный системой C++, тогда все хорошо, и методы этого объекта вызываются (как и следовало ожидать), но это бесполезно - мне нужно передать объекты Delphi.

Edit 2010-04-19 18:12 Вот что происходит более подробно: первый вызванный метод (setLabel) завершается без ошибок (хотя это метод-заглушка). Второй вызываемый метод (init) запускается, а затем умирает при попытке прочитать параметр vol.

Сторона C++

#define SHAPES_EXPORT __declspec(dllexport) // just to show the value
class SHAPES_EXPORT CBox
{
  public:
    virtual ~CBox() {}
    virtual void init(double volume) = 0;
    virtual void grow(double amount) = 0;
    virtual void shrink(double amount) = 0;
    virtual void setID(int ID = 0) = 0;
    virtual void setLabel(const char* text) = 0;
};

Сторона Делфи

IBox = class
public
  procedure destroyBox; virtual; stdcall; abstract;
  procedure init(vol: Double); virtual; stdcall; abstract;
  procedure grow(amount: Double); virtual; stdcall; abstract;
  procedure shrink(amount: Double); virtual; stdcall; abstract;
  procedure setID(val: Integer); virtual; stdcall; abstract;
  procedure setLabel(text: PChar); virtual; stdcall; abstract; 
end;

TMyBox = class(IBox)
protected
  FVolume: Double;
  FID: Integer;
  FLabel: String; //
public
  constructor Create;
  destructor Destroy; override;
  // BEGIN Virtual Method implementation
  procedure destroyBox; override; stdcall;             // empty - Dont need/want C++ to manage my Delphi objects, just call their methods
  procedure init(vol: Double); override; stdcall;      // FVolume := vol;
  procedure grow(amount: Double); override; stdcall;   // Inc(FVolume, amount);
  procedure shrink(amount: Double); override; stdcall; // Dec(FVolume, amount);
  procedure setID(val: Integer); override; stdcall;    // FID := val;
  procedure setLabel(text: PChar); override; stdcall;  // Stub method; empty.
  // END Virtual Method implementation      
  property Volume: Double read FVolume;
  property ID: Integer read FID;
  property Label: String read FLabel;
end;

Я бы наполовину ожидал, что использование одного stdcall сработает, но что-то не так, не знаю, что, возможно, что-то делать с используемым регистром ECX? Помощь будет принята с благодарностью.

Edit 2010-04-19 17:42 Может ли быть так, что регистр ECX необходимо сохранять при входе и восстанавливать после выхода из функции? Требуется ли указатель this для C++? Я, вероятно, просто достиг в данный момент, основываясь на некоторых интенсивных поисковых запросах Google. Я нашел что-то связанное, но, похоже, дело в обратном этого вопроса.


person Atorian    schedule 19.04.2010    source источник
comment
Вы говорите, что скоро получите ошибку STATUS_STACK_BUFFER_OVERRUN. Как скоро? Можете ли вы опубликовать пример кода, чтобы показать, где возникает ошибка? Это происходит во всех методах? (Вы проверили все методы?)   -  person Mason Wheeler    schedule 19.04.2010
comment
@ Мейсон, вызывающий объект помещает в стек N аргументов, включая this. Получатель удаляет только N-1 аргументов, потому что считает, что this находится в регистре ECX, а не в стеке. В конечном итоге это обязательно приведет к переполнению стека.   -  person Rob Kennedy    schedule 19.04.2010
comment
Первый вызываемый метод (setLabel) завершается без ошибок (хотя это метод-заглушка). Второй вызываемый метод (init) входит в систему, а затем умирает при попытке прочитать параметр vol.   -  person Atorian    schedule 19.04.2010
comment
Заметил кое-что странное: если я сломаю init(), то смогу проверить vol и получить какое-то случайное число, но не смогу проверить FVolume, который является защищенным членом класса. Как будто Delphi не знает, на какой экземпляр ссылаться. Это потому, что он получает this не в стеке, а вместо этого в регистре ECX?   -  person Atorian    schedule 19.04.2010
comment
Ваш диагноз правильный, Алан. Код C++ помещает свой параметр this в ECX, но ваш код Delphi ожидает, что он будет в первом параметре стека. Все остальные параметры стека также отключены на единицу. У меня есть техника, которая может решить эту проблему. Единственное, что меня беспокоит при использовании его с этим кодом, — это деструктор класса. Сегодня постараюсь написать описание. Он будет основан на моей работе над API расширенного редактирования без окон: pages.cs .wisc.edu/~rkennedy/windowless-rtf   -  person Rob Kennedy    schedule 19.04.2010
comment
Это было бы здорово, Роб, с нетерпением жду этого. Почему деструктор может быть проблемой? Не будет ли интерфейсный деструктор-заглушка destroyBox() вызываться на стороне C++, ничего не делая, или в конечном итоге будет вызван реальный деструктор Delphi Destroy()?   -  person Atorian    schedule 19.04.2010
comment
Вот именно, Алан. Конструкторы и деструкторы имеют особое значение как в C++, так и в Delphi, и они не взаимозаменяемы между языками. Когда код C++ вызывает деструктор C++, на стороне Delphi должен быть метод, который выглядит и действует как деструктор C++. Деструктор Delphi, вероятно, не справится.   -  person Rob Kennedy    schedule 19.04.2010
comment
Итак, заглушка procedure destroyBox; виртуальный; стандартный вызов; abstract во главе класса будет недостаточно для этого? Он занимает ту же позицию в VMT, что и деструктор C++ ~CBox(), верно?   -  person Atorian    schedule 19.04.2010
comment
Да, он занимает правильный слот VMT, насколько мне известно. Правильно ли он ведет себя как деструктор C++, это другой вопрос, на который я пока не знаю ответа.   -  person Rob Kennedy    schedule 20.04.2010


Ответы (6)


Предположим, вы создали класс MSVC++ с VMT, который идеально соответствует VMT класса Delphi (я никогда этого не делал, я просто верю вам, что это возможно). Теперь вы можете вызывать виртуальные методы класса Delphi из кода MSVC++, единственная проблема заключается в соглашении о вызовах __thiscall. Поскольку __thiscall не поддерживается в Delphi, возможное решение — использовать виртуальные прокси-методы на стороне Delphi:

ОБНОВЛЕНО

type
  TTest = class
    procedure ECXCaller(AValue: Integer);
    procedure ProcProxy(AValue: Integer); virtual; stdcall;
    procedure Proc(AValue: Integer); stdcall;
  end;

implementation

{ TTest }

procedure TTest.ECXCaller(AValue: Integer);
asm
  mov   ecx,eax
  push  AValue
  call  ProcProxy
end;

procedure TTest.Proc(AValue: Integer);
begin
  ShowMessage(IntToStr(AValue));
end;

procedure TTest.ProcProxy(AValue: Integer);
asm
   pop  ebp            // !!! because of hidden delphi prologue code
   mov  eax,[esp]      // return address
   push eax
   mov  [esp+4],ecx    // "this" argument
   jmp  Proc
end;
person kludg    schedule 19.04.2010
comment
Тот факт, что я вижу вызов setLabel() перед init(), наводит меня на мысль, что карта VMT каким-то образом отключена. Я надеюсь, что это не так. - person Atorian; 19.04.2010
comment
@Alan G.: Просто для информации. Такие вещи, как облегченные COM-подобные интерфейсы, реализованы в Delphi с использованием интерфейсов, а не абстрактных классов. Отличие Delphi от C++ здесь в том, что интерфейсы в Delphi не являются классами, это отдельная концепция. Классы Delphi не поддерживают множественное наследование, но могут реализовывать несколько интерфейсов. - person kludg; 19.04.2010
comment
@Serg, вы также можете реализовать COM-интерфейсы с классами Delphi. В конце концов, это то, что сделала Delphi 2. Вы даже можете реализовать их с помощью старых простых записей. Это то, что делает Си. - person Rob Kennedy; 19.04.2010
comment
@Alan, причина, по которой вы видите, что методы называются неправильно, заключается в том, что деструктор Delphi не живет с положительным смещением в VMT. Код C++ предполагает, что деструктор будет первым виртуальным методом, который находится по смещению 0 в VMT, поэтому для этой цели вам необходимо объявить новый метод. Деструктор Delphi находится по смещению -4. - person Rob Kennedy; 19.04.2010
comment
@Serg, да, под COM-подобным / облегченным я имел в виду больше сторону ABI, то есть сопоставление записей VMT в зависимости от порядка. Попробовал кстати ваш ответ, но он не работает, хотя логика мне кажется отличной; это кажется хорошим общим направлением. Я вижу, что ECX содержит указатель Self/this, но я получаю нарушение прав доступа после входа в Proc из ProcProxy сразу после jmp Proc. - person Atorian; 19.04.2010
comment
@Alan G.: Я обнаружил ошибку и обновил сообщение. Я проверил код на стороне Delphi - теперь он работает нормально. - person kludg; 19.04.2010
comment
Кажется, теперь работает хорошо; большое спасибо. Однако теперь я получаю нарушение прав доступа в конце, отслеживая его до стороны C++, которая выполняет удаление объекта, что, очевидно, не хорошо. Не знаю, как мне обойти эту новую проблему, если я передаю объект Delphi. - person Atorian; 22.04.2010

Я не думаю, что вы можете разумно ожидать, что это сработает. В C++ нет стандартизированного ABI, а в Delphi нет ничего стандартизированного. Вы можете найти способ взломать что-то работающее, но нет гарантии, что он продолжит работать с будущими версиями Delphi.

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

Если вы не можете этого сделать, похоже, вам придется либо написать thunking layer, либо не использовать Delphi.

person Ori Pessach    schedule 19.04.2010
comment
На самом деле Delphi прекрасно поддерживает стандартные соглашения о вызовах ABI. Просто thiscall не входит в их число. - person Mason Wheeler; 19.04.2010
comment
-1 за что-либо стандартизированное, +1 за обе стороны, поддерживающие COM (эй, это считается стандартом, верно?). - person Jeroen Wiert Pluimers; 19.04.2010
comment
Эй, я люблю Delphi так же сильно, как и следующий парень, а может быть, даже больше, так как я действительно использовал его и считаю, что это отличный инструмент. Но диалект Pascal, который он реализует, не стандартизирован так, как, скажем, C++. Delphi использует свой собственный формат библиотек, свой собственный формат объектов, и его разработчики могут свободно определять ABI, соглашения о вызовах и многое другое по своему выбору. Отличный инструмент? Определенно. Среда, основанная на стандартах? Неа. - person Ori Pessach; 19.04.2010
comment
@Jeroen Pluimers - COM, возможно, считается специальным стандартом ... Сравните это с тем, как что-то вроде Ant используется в мире Java. IDE, поддерживающие Ant (я почти уверен, что все они) можно использовать для создания всего, что использует Ant. Наличие четко определенного стандарта обеспечивает такую ​​совместимость. Приносит ли это приятный опыт? Не по моему мнению. Я бы предпочел Delphi IDE стандартной Java IDE в любой день, если согласен с тем, что мне придется придерживаться Delphi для разработки моего проекта. - person Ori Pessach; 19.04.2010
comment
Та же история со многими поставщиками инструментов: иногда их инструменты появляются до того, как был установлен стандарт, а иногда они недостаточно велики, чтобы установить стандарт, но все же оказывают большое влияние на другие стандарты, которые предстоит разработать. И иногда они намного опережают свое время. Подумайте о том, что «сеть — это компьютер» по сравнению с облачными вычислениями. Многие инструменты (включая VB6 и Delphi 3) были разработаны специально для соответствия стандарту COM. В .NET 4.0 COM стал гражданином первого класса! Большая часть Windows API по-прежнему использует соглашение о вызовах, введенное давным-давно a.o. Андерс Хейлсберг. - person Jeroen Wiert Pluimers; 19.04.2010

Не делайте этого.

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

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

class SHAPES_EXPORT CBox
{
  public:
    virtual void init(double volume) = 0;
    static void STDCALL CBox_init(CBox *_this, double volume) { _this->init(volume); }
    // etc. for other methods
};

(На самом деле статический метод технически должен быть объявлен extern "C", так как не гарантируется, что методы статического класса будут реализованы с помощью C ABI, но почти все существующие компиляторы делают это.)

Я совсем не знаю Delphi, поэтому я не знаю, как правильно справиться с этим на стороне Delphi, но это то, что вам нужно сделать на стороне C++. Если Delphi поддерживает соглашение о вызовах cdecl, вы можете удалить STDCALL выше.

Да, это раздражает тем, что вам приходится вызывать CBox_init вместо init из Delphi, но это просто то, с чем вам придется иметь дело. Вы можете переименовать CBox_init во что-то более подходящее, если хотите, конечно.

person Adam Rosenfield    schedule 19.04.2010

Вместо этого вы можете попробовать скомпилировать эти библиотеки DLL с помощью C++ Builder, C++ Builder имеет языковую поддержку для взаимодействия с Delphi. Начиная с версии BDS 2006, к компонентам, созданным в C++Builder, можно получить доступ в Delphi, так что старые простые классы будут работать нормально.

Если вы собираетесь использовать только MSVC, COM, возможно, является лучшим способом взаимодействия между двумя средами.

person rep_movsd    schedule 19.04.2010
comment
В прошлом я компилировал основные библиотеки с помощью C++ Builder, которые никогда не были предназначены для этого (как эта), и усилия были огромными по сравнению с результатами. Поиск непонятных сообщений компилятора и компоновщика и исправление кода, специфичного для MS, гораздо менее увлекательны, чем попытки найти более прямое (надеюсь, разумное) решение этой проблемы. Я знаю, что исправление, вероятно, окажется менее стандартным/чистым, но если оно работает, я буду более чем счастлив. - person Atorian; 19.04.2010
comment
Соответствие языку C++ Builders значительно увеличилось по сравнению с C++ Builder 6 дней, возможно, это будет работать намного проще. возможно, более чистый подход, чем пытаться исправить соглашение о вызовах, манипулируя стеком на ассемблере. - person rep_movsd; 20.04.2010

Я создал COM-подобные (облегченные) интерфейсы (абстрактные классы Delphi)

Почему вы не используете обычные COM-интерфейсы? Гарантируется бинарная совместимость с C++ и Delphi.

Единственная проблема заключается в том, что вы не можете избежать AddRef/Release/QueryInterface в Delphi. Но если вы реализуете подсчет ссылок как ничего не делающий (как это делает TComponent), то вы можете просто игнорировать эти методы со стороны C++.

person Alex    schedule 20.04.2010

В качестве дополнения к предложению по использованию c++Builder, которое может быть проблемой из-за возражений бюджета/наличия версии/"сборщика"

Я предлагаю простую оболочку MSVC, которая передает вызовы DLL Delphi. В этот момент вы можете выбрать, использовать COM или нет.

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

person Patrick Martin    schedule 09.08.2010