从 Delphi 调用 C++ DLL 时发生访问冲突

Access violation when invoking a C++ DLL from Delphi

我在 Visual C++ 6.0 中编写了一个 Unicode DLL。然后尝试从 Delphi XE3.

调用 DLL 函数

当我在Delphi中调试时,当跨行调用DLL函数时,我总是会得到一个访问冲突异常。

然而,当我在 Visual C++ 中进行调试时,我可以看到从 Delphi 传递的所有参数都是正确的,并且我可以毫无例外地跨过所有代码行。

如果 运行 在调试器之外,那么我将看不到任何“访问冲突异常。

试了很多方法还是没搞清楚在Delphi中debuggin时的异常如何消除。

下面是Visual C++ 6.0部分的代码:

TestDLL.cpp:

extern "C"  VOID WINAPI Test(CONST MESSAGEPROC lpMessageProc, LPVOID lParam)
{
    if (lpMessageProc != NULL)  
        (*lpMessageProc)(1500, (const LPVOID)(LPCTSTR)CString((LPCSTR)IDS_MYTEST), lParam);
    /*
    if (lpMessageProc != NULL)  
        (*lpMessageProc)(1500, (const LPVOID)(LPCTSTR)CString(_T("Test")), lParam);*/
}

TestDLL.h:

// TestDLL.h : main header file for the TESTDLL DLL
//

#if !defined(AFX_TESTDLL_H__38054A53_5CEE_4ABF_9BA8_BCE427FCB8E1__INCLUDED_)
#define AFX_TESTDLL_H__38054A53_5CEE_4ABF_9BA8_BCE427FCB8E1__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#ifndef __AFXWIN_H__
    #error include 'stdafx.h' before including this file for PCH
#endif

#include "resource.h"       // main symbols

#ifdef __cplusplus
extern "C" {
#endif  /* __cplusplus */

    typedef BOOL (CALLBACK* MESSAGEPROC)(CONST DWORD dwMessageId, CONST LPVOID lp, LPVOID lParam);

    VOID WINAPI Test(CONST MESSAGEPROC lpMessageProc, LPVOID lParam);

#ifdef __cplusplus
}
#endif


/////////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_TESTDLL_H__38054A53_5CEE_4ABF_9BA8_BCE427FCB8E1__INCLUDED_)

下面是Delphi XE3部分的代码:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  public
    { Public declarations }
  end;

  PForm1 = ^TForm1;

  TMessageProc = function (const dwMessageId: DWORD; const lp: Pointer; lParam: Pointer): BOOL; stdcall;
  {$EXTERNALSYM TMessageProc}

var
  Form1: TForm1;

procedure Test(const lpMessageProc: TMessageProc; lParam: Pointer); stdcall;

implementation

{$R *.dfm}

procedure Test; external 'TestDLL.dll' index 2;

function MessageProc(const dwMessageId: DWORD; const lp: Pointer; lParam: Pointer): BOOL; stdcall;
begin
  Result := True;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Test(MessageProc, @Self);  //  <---- This code line will cause "access violation
end;

我相信问题出现在 DLL 测试函数中,当它尝试使用 CString((LPCSTR)IDS_MYTEST) 从资源中加载字符串时。如果我将代码更改为 CString(_T("Test")),那么问题就会消失。

谢谢

如您所料,此语句无效:

CString((LPCSTR)IDS_MYTEST)

尽管 CString 的这个构造函数确实允许您将资源 ID 传递给它,但它会尝试在调用进程(即 Delphi EXE)的资源中查找资源,而不是在DLL 的资源。从 DLL 的资源加载字符串时,您需要使用 DLL 的 HINSTANCE,如 D​​LL 的 DllMain() 所提供的。您可以为此使用 CString::LoadString() 方法,例如:

HINSTANCE hInst;

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    hInst = hinstDLL;
    return TRUE;
}

extern "C" VOID WINAPI Test(CONST MESSAGEPROC lpMessageProc, LPVOID lParam)
{
    if (lpMessageProc != NULL)
    {
        CString str;
        str.LoadString(hInst, IDS_MYTEST);
        (*lpMessageProc)(1500, (LPCTSTR)str, lParam);
    }
}

终于弄明白这是MFC代码的bug(VC6.0版本)

不知道能不能post MFC 源代码所以我就只贴函数头和相关部分了。

在Microsoft Visual Studio\VC98\MFC\SRC\STRCORE.CPP中,我们可以看到以下3个函数:

//////////////////////////////////////////////////////////////////////////////
// More sophisticated construction

CString::CString(LPCTSTR lpsz)   // Function 1
{
    Init();
    if (lpsz != NULL && HIWORD(lpsz) == NULL)
    {
        UINT nID = LOWORD((DWORD)lpsz);
        if (!LoadString(nID))
            TRACE1("Warning: implicit LoadString(%u) failed\n", nID);
    }
    else
    {
            // Construct string normally
    }
}

/////////////////////////////////////////////////////////////////////////////
// Special conversion constructors

#ifdef _UNICODE
CString::CString(LPCSTR lpsz)   // Function 2
{
       // Construct string normally
}
#else //_UNICODE
CString::CString(LPCWSTR lpsz)  // Function 3
{
       // Construct string normally
}
#endif //!_UNICODE

在上面的代码片段中我们可以看到,只有函数1包含对lpsz进行特殊处理并检查它是否是字符串资源ID的代码,如果是,则从资源中加载字符串。功能2和3都没有这种特殊的过程。

我们在VS6中创建工程时,工程默认设置为_MBCS,这样的话,函数1就会变成

CString::CString(LPCSTR lpsz)

所以 CString((LPCSTR)nResID) 将实际调用函数 1 并正确加载字符串资源。

函数 2 将被禁用,因为 _UNICODE 未定义。函数 3 适用于宽字符字符串。

因此,对于 _MBCS 项目,一切都与 MSDN 文档完美一致。

但是,当我将_MBCS 更改为_UNICODE 时,函数1 将变为

CString::CString(LPCWSTR lpsz)

将启用功能 2,将禁用功能 3。

所以 CString((LPCSTR)nResID) 实际上会调用函数 2,它没有特殊的进程来加载字符串资源,这就是问题所在。

这个问题有两种解决方法:

  1. 始终使用 CString((LPCTSTR)nResID) 而不是 CString((LPCSTR)nResID) 从资源中加载字符串。然而,这种用法与MSDN文档不一致,所以我们不得不称其为未记录的用法。

  2. 始终使用 LoadString 加载字符串资源。

虽然方案一稍微简单一点,但它是一个未记录的用法,所以我最终选择了方案二来解决我的问题。

非常感谢您为解决此问题提供的所有帮助。