编写从 C++ 应用程序链接的 Delphi DLL:访问 C++ 接口成员函数会导致访问冲突

Posted

技术标签:

【中文标题】编写从 C++ 应用程序链接的 Delphi DLL:访问 C++ 接口成员函数会导致访问冲突【英文标题】:Writing a Delphi DLL linked in from C++ Application: Access to C++ Interface Member Functions creates Access Violation 【发布时间】:2011-05-05 00:01:01 【问题描述】:

我需要编写一个 DLL(在 Delphi 2009 中),该 DLL 将链接到用 MS VC++ 编写的第三方应用程序中。这个概念非常类似于插件系统,这意味着应用程序在没有 DLL 的情况下运行良好,并在它存在时加载它。

在发生某些事件时,应用程序会调用 DLL 导出的函数。文档中有一个已定义函数的列表,所谓的 SDK 提供了一些示例代码,当然也包括 C++。我无权访问应用程序本身的源代码。

以下是一些冗长的介绍以及一些代码示例。 问题(将在本文底部再次提出)是:如何在 Delphi DLL 中实现应用程序 C++ 类,作为指向接口的指针传递?我已经阅读了有关 *** 和其他来源的几个线程,但其中大多数都涉及修改 both 端(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++)resp的定义。界面(德尔福)。为了节省空间,我省略了大部分功能,但如果有帮助,我很乐意提供更多源代码。

// (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 错误被 except 块捕获并写入调试日志。我已经尝试过不同的约定(不确定“约定”是否是正确的术语),例如 cdeclsafecall 而不是 stdcall,所有结果相同。

这是我目前完全不知道在哪里看的地方...我从来都不是 C++(甚至 C)程序员,所以我对 Delphi 的翻译很可能是错误的。也许还有一些我遗漏的地方。

无论如何,如果有一点(或更多)经验的人能给我一些提示,我会很高兴。

提前致谢

帕特里克

// 2010-11-05:我从 cmets 中提取的内容、答案和 cmets 的答案

Remko 建议将参数定义为

var pContext : IIFlowContext;

给出的结果与我最初的尝试几乎相同

pContext : PIIFlowContext;

两种情况都抛出异常,但变量的内容不同。下面给出了更多信息,我列出了不同的测试用例。

Barry 提到 Delphi(与 C++ 相对)中的接口已经是指针。虽然 C++ 需要传递一个指向类的指针(也称为传递 reference),但 Delphi 已经期望一个对类的引用。因此该参数应声明为

pContext : IIFlowContext;

即不作为接口的指针,也不带var修饰符。

我运行了以下三个测试用例,所有测试用例都在 dll 导出的函数的第一条指令处有一个调试断点:

1) 将参数声明为指向接口的指针

pContext : PIIFlowContext;

结果:根据调试器,pContext 包含指向内存地址 $EF83B8 的指针。调用其中一个接口方法会导致跳转到内存地址 $560004C2t 并引发 EAccessViolation 异常。

2) 将参数声明为对接口的引用

var pContext : IIFlowContext;

结果:调试器将 pContext 的内容显示为“Pointer($4592DC) as IIFlowContext”。调用接口方法会导致跳转到相同的内存地址 $560004C2,然后抛出相同的执行。

3) 将参数声明为接口本身(不带修饰符)

pContext : IIFlowContext;

结果:导出的 dll 函数甚至没有被调用。在跳转到 dll 函数发生之前,会引发 EAccessViolation(并被调试器捕获)。

从上面我得出的结论是,参数被声明为 var pContext : IIFlowContextpContext : PIIFlowContext 应该没有太大区别,但它如果它被声明为 pContext : IIFlowContext.

根据要求,这是调试器反汇编视图的输出。在 cmets 中,我记录了执行左侧操作后寄存器的值:

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引用。

我还有什么需要提供的吗?

我想到的另一个问题...

在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;

正如 Barry 所说,这会阻止 delphi 在接口上“自动”调用 _AddRef()。这样,我就能够启动和调试对接口成员函数的调用。现在我可以跟踪执行相当长的时间,甚至可以看到对 Windows API 的一些调用(例如,CriticalSections),但有时它仍然会引发 EAccessViolation 错误。

目前我没有进一步的想法。我想我会尝试使用 MSVC++ 编译器,这样我就可以像 SDK 推荐的那样构建 DLL。如果这可行,那么也许使用 C++ 创建一个由 Delphi 代码围绕的包装器将是解决方案。

无论如何,非常感谢您迄今为止的帮助!不过,我们将非常感谢您提供任何额外的意见。

【问题讨论】:

您是否将 C++ 版本截断太多?我没有看到调用约定说明符,因此 C++ 接口将默认为 Delphi 调用的 cdecl,除非另有说明(例如,一些奇怪的 C++ 寄存器(如 MS fastcall)调用约定,没有 Delphi 翻译)被指定直接到 C++ 编译器命令行。 FWIW,我调试的方法是转到 CPU 视图 (Ctrl+Alt+C) 并单步执行调用,观察 Delphi 推送参数并调度接口方法,然后然后单步执行 C++ 端(由 Delphi 调试器反汇编)并查看它如何访问参数。 Barry,我查看了 C++ 源代码,但没有找到任何调用约定,除了一些其他类(未在 DLL 中使用):virtual HRESULT STDMETHODCALLTYPE setErrorText(...)。 dll 函数中使用的类定义似乎没有这样的约定说明符。 MS VC++ 项目文件也不包含任何看起来像命令行选项的东西来指定它们。 我的调试问题是 C++ 应用程序实际上是一个 Windows 服务。我还没有找到一种附加调试器的方法,以便在调用 dll 中的导出函数时停止。但我仍在努力 - 感谢您的提示。 只要调试器有足够的权限,附加到服务应该没有问题(运行|附加到进程)。如果服务直到调用它之前才加载 DLL(因此不容易设置正确的断点),然后考虑在 DLL 的入口函数或类似函数中调用 DebugBreak(库开始/结束)给你时间设置断点等。 【参考方案1】:

根据您最近的评论,我想我知道发生了什么;我应该早点发现的。

IIFlowContext 在 C++ 端是一个类; IIFlowContext* pContext 正在传递一个指向类的指针,这就是 C++ 中 COM 样式接口的表示方式。

但是 Delphi 接口已经是指针了;假设是间接的,因为 Delphi 类永远不会像 C++ 类那样按值传递。您应该在 Delphi 入口点中直接使用 IIFlowContext,而不是 varconst 修饰符。

接口方法声明可能还是有问题;更多信息会更清楚:请参阅我对您的问题的最新评论。

【讨论】:

【参考方案2】:

我的翻译是:

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

PS:你是如何定义 TCHAR 是 Ansi 还是 Unicode/Wide ?

【讨论】:

TCHAR 被定义为 TAnsiChar。 PTCHAR(我用于 TCHAR*)被定义为 PAnsiChar。使用 PAnsiChar,字符串参数包含预期值,例如写入日志。无论如何我都会尝试使用 TCHAR。 什么是 TFLOW_SECTION_TYPE ?它是一个枚举吗?如果是这样设置 $MINENUMSIZE 4 好点。我已经设置了它,但是你的提示让我去仔细检查,只是为了发现我错过了一个单元。真丢脸……我会用 MINENUMSIZE = 4 再试一次。 幸运的我 ;-) 设置 MINENUMSIZE 并没有解决它,所以至少它不像忘记编译器指令那样愚蠢的错误。 回复我的第一条评论:变量 pContext 需要声明为指向接口的指针,将其声明为 pContext :一旦应用程序调用导出的函数,IIFlowContext 就会引发 EAccessViolation。字符串值确实作为 PAnsiChar 传递。

以上是关于编写从 C++ 应用程序链接的 Delphi DLL:访问 C++ 接口成员函数会导致访问冲突的主要内容,如果未能解决你的问题,请参考以下文章

Delphi链接器和C++链接器的区别

Delphi调用C++编写的DLL

从 C++ DLL 编辑 Delphi 记录

如何使用 C++ 挂钩 Delphi 函数

我应该在 Delphi 而不是 C++ Builder 中编写组件吗?如何向组件添加事件?

如何在 Visual C++ 中使用 Delphi 的寄存器调用约定调用函数?