Подключение плагина на C++ к программе на Delphi
От: bordo Россия  
Дата: 01.09.09 09:48
Оценка:
Здравствуйте, коллеги!

Неделю мучаюсь над странным поведением SendMessage() из WinAPI -- растолкуйте кто знает.

Условия задачи:

Код модуля на Delphi:

unit TestFormUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

type
  TTestForm = class(TForm)
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  TestForm: TTestForm;

procedure ShowModForm(appWnd: HWND);

implementation

{$R *.dfm}

procedure ShowModuleForm(appWnd: HWND);
var
  rslt : Integer;
  msgCode : Integer;
begin
  with TTestForm.Create(Application) do
  try
    msgCode := RegisterWindowMessage('WM_ACME_REGISTERMODWND');

    {WM_ACME_REGISTERMODWND -- одно из сообщений для взаимодействия с приложением,
    {Возвращаемое значение должно быть 1 или 2}
    rslt := SendMessage(appWnd, msgCode, Handle, 0);

    Caption := 'DelphiTestModule - ' + IntToStr(rslt);
    ShowModal
  finally
    Free
  end
end;

end.


Процедура ShowModuleForm экспортируется так:
library ModDelphi;

uses
  ShareMem,
  SysUtils,
  Classes,
  TestFormUnit in 'TestFormUnit.pas' {TestForm};

{$R *.res}

exports ShowModuleForm;

begin
end.


При открытии формы этого модуля в заголовке будет написано DelphiTestModule — 1.


Вот код на C++ c использованием WTL:
#include "stdafx.h"

#define MOD_WTL_API __declspec(dllexport)

#include <windows.h>

#include <atlbase.h>
#include <atlapp.h>

extern CAppModule _Module;
#include <atlwin.h>
#include <atlgdi.h>
#include <atlmisc.h>

CAppModule _Module;

using namespace std;

// Функция вызова плагина
extern "C" MOD_WTL_API BOOL ShowModForm(HWND parent);

// Точка входа в DLL
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        // Инициализируем модуль
        _Module.Init(0, hModule, 0);

        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        _Module.Term();
        break;
    }
    return TRUE;
}

// Главное окно плагина
class CMainWindow :
    public CWindowImpl<CMainWindow, CWindow, CFrameWinTraits>
{
    BEGIN_MSG_MAP(CMainWindow)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
    END_MSG_MAP()

    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        PostQuitMessage(0);
        return 0;
    }
};


MOD_WTL_API BOOL ShowModForm(HWND parent)
{
    // Создаём главное окно модуля.
    CMainWindow wnd;
    wnd.Create(NULL, CWindow::rcDefault);


    const UINT WM_ACME_REGISTERMODWND = ::RegisterWindowMessage(L"WM_ACME_REGISTERMODWND");
    const LRESULT rslt = ::SendMessage(parent, WM_ACME_REGISTERMODWND, (WPARAM)((HWND)wnd), 0); ///< Возвращает всегда 0!


    char buffer[80] = {0};
    _itoa(rslt, buffer, 10);

    CString title(L"WtlTestModule - ");
    title += rslt;
    ::SetWindowText(wnd, title);

    wnd.ShowWindow(SW_SHOW);
    // Запускаем цикл сообщений
    CMessageLoop loop;
    loop.Run();

    return TRUE;
}


При открытии формы этого модуля в заголовке будет написано WtlTestModule — 0. Т.е. объявленный в программе API для подключения модулей не работает.

Подозреваю, что тайна может заключаться в использовании TTestForm.Create(Application), но как его использовать в C++?
dll plugin winapi
Re: Подключение плагина на C++ к программе на Delphi
От: savitar  
Дата: 01.09.09 10:28
Оценка:
Здравствуйте, bordo, Вы писали:

B>...


в паскаль-коде процедура ShowModForm не имеет явного указания на calling convention, что означает register. в C этому соответсвует __fastcall.
Re[2]: Подключение плагина на C++ к программе на Delphi
От: bordo Россия  
Дата: 01.09.09 11:20
Оценка:
Здравствуйте, savitar, Вы писали:

S>в паскаль-коде процедура ShowModForm не имеет явного указания на calling convention, что означает register. в C этому соответсвует __fastcall.


Точно, но если объявить
procedure ShowModuleForm(WPWnd: HWND); stdcall;

то перестаёт работать и модуль на Дельфи -- в заголовке окна так же как и в случае с WTL выводится 0.

Я также поэкспериментировал со спецификациями вызова на С++ (Visual Studio 2005):
#define MOD_WTL_CALLSPEC
// __syscall -- ошибка компиляции
// __far __pascal -- ошибка компиляции
// WINAPI -- dll не загружается, выдаётся сообщение об ошибке
// __stdcall -- dll не загружается, выдаётся сообщение об ошибке
// __fastcall -- dll не загружается, выдаётся сообщение об ошибке
// __cdecl -- 0 в заголовке окна

extern "C" MOD_WTL_API BOOL MOD_WTL_CALLSPEC ShowModForm(HWND parent);


Выходит, программисты внешнего приложения "не учли" спецификации вызова и теперь к их приложению можно писать плагины на Дельфи? Если так, то придётся писать обёртку для стандартных dll.
Re: Подключение плагина на C++ к программе на Delphi
От: wallaby  
Дата: 01.09.09 11:33
Оценка:
Здравствуйте, bordo, Вы писали:

B>procedure ShowModForm(appWnd: HWND);


Аргумент appWnd передаётся в регистре EAX — такое соглашение используется в Delphi по умолчанию. Совет использовать __fastcall на С++ относится к C++ Builder'у. У Microsoft свой __fastcall, там параметр будет ожидаться в регистре ECX — http://msdn.microsoft.com/en-us/library/6xa169sk(VS.71).aspx

По нормальному конечно процедура должна была быть объявлена в программе на Delphi так:

procedure ShowModForm(appWnd: HWND);stdcall;

А если это не так, будут понятные проблемы при использовании не-борландовских компиляторов для написания плагинов.
---
The optimist proclaims that we live in the best of all possible worlds; and the pessimist fears this is true
Re[3]: Подключение плагина на C++ к программе на Delphi
От: Jack128  
Дата: 01.09.09 11:38
Оценка:
Здравствуйте, bordo, Вы писали:

B>Здравствуйте, savitar, Вы писали:


S>>в паскаль-коде процедура ShowModForm не имеет явного указания на calling convention, что означает register. в C этому соответсвует __fastcall.


B>Точно, но если объявить

B>
B>procedure ShowModuleForm(WPWnd: HWND); stdcall;
B>

B>то перестаёт работать и модуль на Дельфи -- в заголовке окна так же как и в случае с WTL выводится 0.

естественно. хост то ожидает, что функция фастсоловская, а ты стдкол пихаешь.

B>Я также поэкспериментировал со спецификациями вызова на С++ (Visual Studio 2005):

B>
B>#define MOD_WTL_CALLSPEC
B>// __syscall -- ошибка компиляции
B>// __far __pascal -- ошибка компиляции
B>// WINAPI -- dll не загружается, выдаётся сообщение об ошибке
B>// __stdcall -- dll не загружается, выдаётся сообщение об ошибке

// __fastcall -- dll не загружается, выдаётся сообщение об ошибке   
вот тут ты где то накосчил. нуно смотреть, что за ошибки.

B>// __cdecl -- 0 в заголовке окна

B>extern "C" MOD_WTL_API BOOL MOD_WTL_CALLSPEC ShowModForm(HWND parent);
B>


B>Выходит, программисты внешнего приложения "не учли" спецификации вызова и теперь к их приложению можно писать плагины на Дельфи? Если так, то придётся писать обёртку для стандартных dll.

ничего не выходит.
Re[4]: Подключение плагина на C++ к программе на Delphi
От: bordo Россия  
Дата: 02.09.09 02:17
Оценка:
Здравствуйте, Jack128, Вы писали:

S>>>в паскаль-коде процедура ShowModForm не имеет явного указания на calling convention, что означает register. в C этому соответсвует __fastcall.


B>>Точно, но если объявить

B>>
B>>procedure ShowModuleForm(WPWnd: HWND); stdcall;
B>>

B>>то перестаёт работать и модуль на Дельфи -- в заголовке окна так же как и в случае с WTL выводится 0.

J>естественно. хост то ожидает, что функция фастсоловская, а ты стдкол пихаешь.


Да, просто теперь я понял в чём корень проблемы.

B>>Я также поэкспериментировал со спецификациями вызова на С++ (Visual Studio 2005):

B>>
B>>#define MOD_WTL_CALLSPEC
B>>// WINAPI -- dll не загружается, выдаётся сообщение об ошибке
B>>// __stdcall -- dll не загружается, выдаётся сообщение об ошибке

J>// __fastcall -- dll не загружается, выдаётся сообщение об ошибке   
J>вот тут ты где то накосчил. нуно смотреть, что за ошибки.

B>>// __cdecl -- 0 в заголовке окна

B>>extern "C" MOD_WTL_API BOOL MOD_WTL_CALLSPEC ShowModForm(HWND parent);
B>>

С этой проблемой, я уже разобрался -- там декорируются имена функций и хост-программа их не видит. К сожалению, extern "C" не отменяет декорацию для __fastcall функций. К тому же, как правильно заметил wallaby fastcall в C++ &lt;&gt; register в Delphi.

B>>Выходит, программисты внешнего приложения "не учли" спецификации вызова и теперь к их приложению можно писать плагины на Дельфи? Если так, то придётся писать обёртку для стандартных dll.

J>ничего не выходит.
Именно так и выходит: они объявили возможность написания сторонних плагинов, но не сказали, что писать их можно только на Борландовских компиляторах, а также проигнорировали стандартные соглашения вызова экспортируемых функций (stdcall).
Re[5]: Подключение плагина на C++ к программе на Delphi
От: bordo Россия  
Дата: 02.09.09 02:18
Оценка:
Итак, всё дело было в разъезжании стека из-за несоответствия calling conventions. Проблему удалось решить с помощью простой обёртки на Дельфи:

unit TestFormUnit;

interface

type
 HWND = type LongWord;
 {$EXTERNALSYM HWND}

procedure ShowModuleFormProxy(appWnd: HWND);

implementation

procedure ShowModForm(appWnd: HWND); stdcall; External 'mod_wtl.dll';

procedure ShowModuleFormProxy(appWnd: HWND);
begin
 mod_wtl_show(appWnd);
end;

end.


Код на C++ остался без изменений.

Параллельно пытаюсь разобраться с вариантом, описанным в http://www.codeguru.com/forum/showthread.php?t=466266 . Если кто-нибудь поделится рабочим вариантом для Visual C++ 2005, а ещё лучше для gcc 3.4.5 под mingw, буду очень признателен, потому что в ассемблере я совершенно не разбираюсь.
Re[6]: Подключение плагина на C++ к программе на Delphi
От: wallaby  
Дата: 02.09.09 03:30
Оценка:
Здравствуйте, bordo, Вы писали:

B>Параллельно пытаюсь разобраться с вариантом, описанным в http://www.codeguru.com/forum/showthread.php?t=466266 . Если кто-нибудь поделится рабочим вариантом для Visual C++ 2005, а ещё лучше для gcc 3.4.5 под mingw, буду очень признателен, потому что в ассемблере я совершенно не разбираюсь.


Танцы с бубном, основанные на не- или слабо- документированных особенностях сишных компиляторов. Беда в том что неборландовские компиляторы C++ ни при каком соглашении о вызовах не ожидают аргумент функции в регистре EAX, а именно туда он передаётся. Если не разбирался на уровне ассемблера в коде, который компилятор генерирует при вызове функций, лучше этим не заниматься. Правильный вариант — это proxy dll.
---
The optimist proclaims that we live in the best of all possible worlds; and the pessimist fears this is true
Re[7]: Подключение плагина на C++ к программе на Delphi
От: bordo Россия  
Дата: 02.09.09 05:14
Оценка:
Здравствуйте, wallaby, Вы писали:

W>Танцы с бубном, основанные на не- или слабо- документированных особенностях сишных компиляторов. Беда в том что неборландовские компиляторы C++ ни при каком соглашении о вызовах не ожидают аргумент функции в регистре EAX, а именно туда он передаётся. Если не разбирался на уровне ассемблера в коде, который компилятор генерирует при вызове функций, лучше этим не заниматься. Правильный вариант — это proxy dll.


Не вижу танцев:

Осталось запрограммировать это на ассемблере, не зная его
Re[8]: Подключение плагина на C++ к программе на Delphi
От: wallaby  
Дата: 02.09.09 07:34
Оценка:
Здравствуйте, bordo, Вы писали:

B>Не вижу танцев:


Дело ваше, я предупредил

B>

B>Осталось запрограммировать это на ассемблере, не зная его


На msdn есть примеры naked-функций — http://msdn.microsoft.com/en-us/library/4d12973a(VS.71).aspx

Поскольку на C не пишу, могу только набросать идею:

__declspec(naked) int __fastcall  proxyfunc() 
{
    __asm {
        mov   ecx,eax
/* вызываем рабочую функцию из asm-кода */
        jmp   workfunc
    }
}

int __fastcall  workfunc(hWnd: DWORD)
{
...
}


Или так:
__declspec(naked) int __fastcall  proxyfunc() 
{
  dword hWnd;
   __asm      /* prolog */
      {
      push   ebp
      mov    ebp, esp
      sub    esp, __LOCAL_SIZE
      mov    hWnd,eax
    }
{
/* вызываем рабочую функцию из C-кода */
    workfunc(hWnd)
}
   __asm      /* epilog */
      {
      mov    esp, ebp
      pop    ebp
      ret
      }
}
---
The optimist proclaims that we live in the best of all possible worlds; and the pessimist fears this is true
Re[9]: Подключение плагина на C++ к программе на Delphi
От: bordo Россия  
Дата: 02.09.09 09:12
Оценка:
Здравствуйте, wallaby, Вы писали:

W>Дело ваше, я предупредил

На то есть две причины: деплой на 1 длл меньше (у меня их и так несколько), плюс нет лицензионной Дельфи, а есть требование чистоты.
Пробовал собрать под Lazarus, но он отказался экспортировать процедуру без stdcal.

B>> объявляем рабочую функцию в Visual C++ как __stdcall;

W> Проще __fastcall, тогда не нужно будет возиться со стеком
VC++ декорирует __fastcall-функции, поэтому пришлось использовать DEF-файл, но это проще, чем возиться со стеком.

B>> в proxy-функции перетасовываем регистры и стек и делаем jmp на рабочую функцию;

W>Просто нужно переслать EAX в ECX, стек не трогаем
Я так написал потому что совсем не разбираюсь в ассемблере.

B>>Осталось запрограммировать это на ассемблере, не зная его

W>На msdn есть примеры naked-функций — http://msdn.microsoft.com/en-us/library/4d12973a(VS.71).aspx
Обязательно почитаю.

W>Поскольку на C не пишу, могу только набросать идею:


W>
W>__declspec(naked) int __fastcall  proxyfunc() 
W>{
W>    __asm {
W>        mov   ecx,eax
W>/* вызываем рабочую функцию из asm-кода */
W>        jmp   workfunc
W>    }
W>}

W>int __fastcall  workfunc(hWnd: DWORD)
W>{
W>...
W>} 
W>


W>Или так:

W>
W>__declspec(naked) int __fastcall  proxyfunc() 
W>{
W>  dword hWnd;
W>   __asm      /* prolog */
W>      {
W>      push   ebp
W>      mov    ebp, esp
W>      sub    esp, __LOCAL_SIZE
W>      mov    hWnd,eax
W>    }
W>{
W>/* вызываем рабочую функцию из C-кода */
W>    workfunc(hWnd)
W>}
W>   __asm      /* epilog */
W>      {
W>      mov    esp, ebp
W>      pop    ebp
W>      ret
W>      }
W>}
W>

Восхитительно, оба способа работают! Теперь буду переписывать то же самое под mingw-gcc (да, вот такой я извращенец)

Огромное спасибо!
Re[10]: Подключение плагина на C++ к программе на Delphi
От: bordo Россия  
Дата: 07.09.09 10:33
Оценка:
B>Восхитительно, оба способа работают! Теперь буду переписывать то же самое под mingw-gcc (да, вот такой я извращенец)

Провозился с ассемблером два дня впустую. После чего родилось простое решение -- воспользоваться дебаггером (например, OllyDbg) и скопировать ассемблерный код из proxy-dll на Delphi. Всё заработало

Итак, нашёлся окончательный ответ из двух вариантов:
  1. использовать proxy-dll на Delphi;
  2. написать proxy-функцию на ассемблере (на Visual C++ можно воспользоваться __naked функциями).


Плюсы и минусы:
  1. Пишем на высокоуровневых языках ObjectPascal и C++, но в поставке идёт две dll на один плагин, и нужно иметь лицензионную Дельфи;
  2. нужно немного позаморачиваться с ассемблером и DEF-файлами, но зато всё компактно.


Ещё раз всем огромное спасибо за помощь!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.