Написание библиотеки Delphi DLL, связанной с приложением C ++: доступ к функциям-членам интерфейса C ++ создает нарушение прав доступа - PullRequest
2 голосов
/ 04 ноября 2010

Мне нужно написать DLL (в Delphi 2009), которая должна быть связана с сторонним приложением, написанным на MS VC ++. Идея очень похожа на систему плагинов, что означает, что приложение прекрасно работает без DLL и загружает его, когда оно присутствует.

При определенных событиях приложение вызывает функции, которые экспортирует DLL. Документация содержит список определенных функций, и так называемый SDK предоставляет некоторый пример кода, конечно, также на C ++. У меня нет доступа к исходному коду самого приложения.

Ниже приведено несколько длинное введение вместе с некоторыми примерами кода. Вопрос (который будет снова задан в нижней части этого поста): как мне реализовать классы приложений C ++, передаваемые в виде указателей на интерфейсы, в Delphi DLL? Я уже прочитал несколько потоков о stackoverflow и других источниках, но большинство из них имеют дело с изменением обоих концов (приложение C ++ и Delphi DLL), что не вариант. Итак, я ищу того, кто может помочь в переводе кода C ++ DLL в код Delphi DLL.

Вызываемые функции обычно получают некоторые параметры (в основном TCHAR *, int и некоторые ENUM) и имеют возвращаемое значение типа HRESULT. Некоторые из них также имеют параметр, который описывается как указатель на «COM-подобный интерфейс», предназначенный для того, чтобы можно было вызывать функции-члены, определенные внутри приложения. Во время перевода я просто добавлял типы к «T» и объявлял соответствующие типы в отдельном блоке (TCHAR * становится PTCHAR и определяется как PTCHAR = PAnsiChar. Это облегчает замену типов, если это окажется необходимым) .

В настоящее время функции DLL уже вызываются приложением, и код в DLL имеет полный доступ к «стандартным» параметрам, таким как строки или целые числа. Возвращаемые значения передаются обратно в приложение, поэтому считайте, что реализация функций экспорта правильная. Один из более коротких примеров (который работает в реализации Delphi):

// C++ function defined in the SDK
DLLAPI int FPHOOK_OnStartFlowInstance(const TCHAR* strSvcAppName, 
           const TCHAR* strAppName,
           const FLOW_SECTION_TYPE eSectionType,
           IIFlowContext* pContext)
{
 return 0;
}


// Delphi translation of the same function
function FPHOOK_OnStartFlowInstance( const strSvcAppName : PTCHAR;
                                     const strAppName : PTCHAR;
                                     const eSectionType : TFLOW_SECTION_TYPE;
                                     pContext : PIIFlowContext) : Int; stdcall;
begin
  dbg('ENTER FPHOOK_OnStartFlowInstance: strSvcAppName = ''%s'', strAppName = ''%s''',[String(strSvcAppName),String(strAppName)]);
  result := 0;
end;

Теперь проблема в том, что мне нужно вызвать одну из функций-членов. Вот определение класса (C ++) соотв. интерфейс (Delphi). Я упустил большинство функций только для экономии места, но с удовольствием предоставлю больше исходного кода, если это будет полезно.

// (shortened) class definition from C++
class IIFlowContext : virtual public CIUnknown
{
   // Operation
   public:
   virtual HRESULT getContextID(/*[out]*/unsigned short* pContextId) = 0;
   virtual HRESULT cleanExecutionState() = 0;
   /* skipped some other 'virtual HRESULT ...' */
};

// (shortened) interface declaration from Delphi
type IIFlowContext = Interface(IUnknown)
       function getContextID(pContextId : Punsigned_short) : HRESULT; stdcall;
       function cleanExecutionState : HRESULT; stdcall;
       // skipped some other 'function ...'
end;

Если я сейчас попытаюсь получить доступ к одной из функций-членов:

function FPHOOK_OnStartFlowInstance( ...,pContext : PIIFlowContext) : Int; stdcall; 
var fphookResult : HRESULT;
begin
  try
    fphookResult := pContext.cleanExecutionState;
  except On E: Exception do
    dbg('FPHOOK_OnStartFlowInstance, pContext.cleanExecutionState: %s::%s',[E.ClassName,E.Message]);
  end;
  result := 0;
end;

ошибка EAccessViolation обнаруживается блоком исключений и записывается в журнал отладки. Я уже пробовал разные соглашения (не уверен, что здесь термин «конвенция» является правильным), например cdecl или safecall вместо stdcall , все с одинаковым результат.

Здесь я сейчас понятия не имею, на что мне обратить внимание ... Я никогда не был программистом на C ++ (или даже на C), поэтому мой перевод на Delphi вполне может быть неправильным. Может быть, я упускаю еще один момент.

В любом случае, я был бы рад, если бы кто-нибудь с небольшим (или намного) большим опытом дал бы мне несколько подсказок.

Заранее спасибо

Patrick

// 2010-11-05: что я извлек из комментариев, ответов и комментариев к ответам

Предложение Ремко определить параметр как

var pContext : IIFlowContext;

дает почти такой же результат, как и моя первоначальная попытка, как

pContext : PIIFlowContext;

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

Барри упомянул, что интерфейсы в Delphi (в отличие от C ++) уже являются указателями. В то время как C ++, следовательно, должен передавать указатель на класс (иначе говоря, reference ), Delphi уже ожидает ссылку на класс. Таким образом, параметр должен быть объявлен как

pContext : IIFlowContext;

То есть не как указатель на интерфейс и не с модификатором var .

Я выполнил следующие три тестовых примера, каждый из которых имел точку останова отладки в самой первой инструкции функции, экспортируемой dll:

1) объявил параметр как указатель на интерфейс

pContext : PIIFlowContext;

Результат: согласно отладчику, pContext содержит указатель на адрес памяти $ EF83B8.Вызов одного из методов интерфейса приводит к переходу к адресу памяти $ 560004C2t и вызывает исключение EAccessViolation.

2) объявляет параметр как ссылку на интерфейс

var pContext : IIFlowContext;

Результат:Отладчик показывает содержимое pContext как «Указатель ($ 4592DC) как IIFlowContext».Вызов метода interfaces приводит к переходу к тому же адресу памяти $ 560004C2, который затем вызывает то же исключение.

3) объявляет параметр как сам интерфейс (без модификатора)

pContext : IIFlowContext;

Результат: экспортированная функция dll даже не вызывается.EAccessViolation генерируется (и перехватывается отладчиком) до того, как происходит переход в функцию dll.

Исходя из вышеизложенного, я заключаю, что не должно быть большой разницы, если параметр объявлен как var pContext: IIFlowContext или pContext: PIIFlowContext , но это заметная разница, если она объявлена ​​как pContext: IIFlowContext .

По запросу, вотвывод отладочной разметки.В комментариях я отметил значения регистров после выполнения операции слева от них:

SystemHook.pas.180: fcnRslt := pContext.cleanExecutionState;
028A3065 8B4514           mov eax,[ebp+$14]        // EAX now = $00EF83D0
028A3068 8B00             mov eax,[eax]            // EAX now = $004592DC
028A306A 50               push eax
028A306B 8B00             mov eax,[eax]            // EAX now = $0041DE86
028A306D FF5010           call dword ptr [eax+$10]     // <-- Throws Exception, EAX+$10 contains $560004C2
028A3070 59               pop ecx
028A3071 8BD8             mov ebx,eax

Разборка точно такая же, независимо от того, является ли параметр указателем на интерфейс или var reference.

Есть ли что-нибудь еще, что я должен предоставить?

Еще один вопрос, который мне пришёл в голову ...

В оригиналезаголовочный файл из SDK, класс определяется как

class IIFlowContext : virtual public CIUnknown

CIUnknown, в свою очередь, определяется в другом заголовочном файле (win_unknown.h) как

class CIUnknown
{
//  Operation
public:
    virtual HRESULT QueryInterface(REFIID iid, void ** ppvObject) = 0;
    virtual unsigned long AddRef(void) = 0;
    virtual unsigned long Release(void) = 0;
    static bool IsEqualIID(REFIID iid1, REFIID iid2)
    {
        if (memcmp(&iid1, &iid2, sizeof(IID)) == 0)
            return true;

        return false;
    }
};

Можно лииспользовать IUnknown в качестве базы для интерфейсов Delphi?Я думаю, нет, потому что, насколько я знаю, IUnknown не реализует IsEqualIID , и поэтому в VMT будет сдвиг.Но как мне реализовать это в Delphi?Является ли C ++ static таким же, как Delphi функция класса ?

// 2010-11-18: некоторые обновления

К сожалению, я еще не нашел способ заставить это работать.Одна вещь, которая действительно изменила поведение, заключалась в передаче ссылки на интерфейс как

const pContext : IIFlowContext;

Как заявил Барри, это запрещает delphi «автоматически» вызывать _AddRef () в интерфейсе.Таким образом, я смог инициировать и отладить вызов функций-членов интерфейса.Теперь я могу следить за выполнением довольно долгое время и даже видеть некоторые вызовы в Windows API (например, CriticalSections), но через некоторое время он все еще вызывает ошибку EAccessViolation.

В настоящее время у меня нет дальнейших идей.Я думаю, что я попытаюсь получить компилятор MSVC ++, чтобы я мог собрать DLL, как рекомендовано SDK.Если это сработает, то, возможно, использование C ++ для создания обертки вокруг кода Delphi будет решением.

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

Ответы [ 2 ]

1 голос
/ 05 ноября 2010

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

IIFlowContext на стороне C ++ - это класс; IIFlowContext* pContext передает указатель на класс, как интерфейсы в стиле COM представлены в C ++.

Но интерфейсы Delphi уже являются указателями; подразумевается косвенность, так как классы Delphi никогда не передаются по значению, как классы C ++. Вы должны использовать IIFlowContext напрямую, без модификатора var или const в точке входа Delphi.

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

1 голос
/ 04 ноября 2010

Мой перевод будет:

function FPHOOK_OnStartFlowInstance( const strSvcAppName : TCHAR; 
                                     const strAppName : TCHAR; 
                                     const eSectionType : TFLOW_SECTION_TYPE; 
                                     var pContext : IIFlowContext) : Int; stdcall;

PS: как вы определили TCHAR это Ansi или Unicode / Wide?

...