编写从 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 块捕获并写入调试日志。我已经尝试过不同的约定(不确定“约定”是否是正确的术语),例如 cdecl 或 safecall 而不是 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 : IIFlowContext 或 pContext : 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
,而不是 var
或 const
修饰符。
接口方法声明可能还是有问题;更多信息会更清楚:请参阅我对您的问题的最新评论。
【讨论】:
【参考方案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++ 接口成员函数会导致访问冲突的主要内容,如果未能解决你的问题,请参考以下文章