使用 InstallShield for MSVC 6.0 Professional 调试自定义 DLL






4.60/5 (5投票s)
2003年5月30日
12分钟阅读

80360

904
本文介绍如何使用 InstallShield 的 CallDLLFx 函数来开发、集成和调试自定义 DLL。
引言
使用 Visual C++ 6.0 Professional 版本附带的 InstallShield 可以轻松生成安装程序。但是,有时您可能需要 InstallShield 未包含的额外功能。本文将介绍如何创建和调试自定义 DLL,从而在 InstallShield 项目中添加自己的功能。示例源代码演示了如何执行应用程序、注册 ActiveX 控件以及将注册信息上传到 FTP 服务器。
注意:自定义 DLL 仅可在 Windows NT 4.0、2000 和 XP 上进行调试。
背景
在我将应用程序的所有文件添加到新的 InstallShield 项目后不久,我发现我所使用的 InstallShield 版本中禁用了一个名为 LaunchApp
的方法。由于需要在设置结束时启动文档,所以我发现了一个名为 CallDLLFx
的方法,该方法允许您调用自定义 DLL 中的函数。
经过一些研究,我编写了一个自定义 DLL 来复制 LaunchApp
的功能。我还发现调试自定义 DLL 也将是一个挑战。
我调试自定义 DLL 的第一次尝试是尝试从调试器运行 setup.exe 文件。令我惊讶的是,Visual C++ 无法使用 InstallShield 安装程序使用的安装程序。我意识到我必须以不同的方式调试安装程序。本文的其余部分将讨论我所采用的方法,并解释如何创建自己的自定义 DLL。
注意:假定您知道如何使用 InstallShield 生成安装程序。但是,如果您不知道,可以在 CodeProject 网站上找到 Bryce 编写的 简单的 InstallShield 教程。
必备组件
为了更轻松地阅读本文的其余部分,建议您在继续之前确保已完成以下项目。
- 安装 Visual C++ 6.0 Professional 以及随附的 InstallShield 版本。
- 下载并解压随附的源代码(使用文件夹名称)到您的 C:\ 目录。InstallShield 项目依赖于此目录结构。
- 编译位于 C:\My Installations\Projects\GreatProduct 文件夹下的 GreatProduct.dsw 项目工作区中提供的源代码。您需要为 ActiveX_Control 和 GreatProduct 项目编译发布版本。为了调试,请为 IS_CustomDll 项目构建 DEBUG 版本。
Great Product 将作为 InstallShield 的安装程序将要安装到客户计算机上的产品。
Visual C++ 项目
Great Product 的源代码包含一个 ActiveX 控件(一个 ATL 完全控件)和一个基于对话框的 MFC 应用程序。这两个应用程序本质上包含 Microsoft 提供的默认样板代码。唯一不同的是,ActiveX 控件的文本已被更改,并且 ActiveX 控件已添加到基于对话框的应用程序中。
自定义 DLL
自定义 DLL 的源代码可以在以下文件中找到:C:\My Installations\GreatProduct\Projects\IS_CustomDll\IS_CustomDll.cpp
该 DLL 的项目由 Microsoft Visual C++ 生成为 Win32 动态链接库。它导出了五个供 InstallShield 安装程序使用的函数。它们的原型如下:
int __stdcall StartProgram(HWND hWnd, LPLONG val, LPSTR str);
int __stdcall RegisterServer(HWND hWnd, LPLONG val, LPSTR str);
int __stdcall UnregisterServer(HWND hWnd, LPLONG val, LPSTR str);
int __stdcall UploadRegistrationInfo(HWND hWnd, LPLONG val, LPSTR str);
int __stdcall InsideDll(HWND hWnd, LPLONG val, LPSTR str);
StartProgram
StartProgram
函数尝试使用 ShellExecute
打开 str
参数中指定的文件。以下代码演示了如何完成此操作。
int __stdcall StartProgram(HWND hWnd, LPLONG val, LPSTR str) { if (ShellExecute(hWnd, "open", str, NULL, NULL, SW_SHOW) <= (HINSTANCE)32) { return -1; } return 0; }
RegisterServer / UnregisterServer
RegisterServer
将由安装脚本用于注册 Great Product 中使用的 ActiveX 控件。我 确实 意识到 InstallShield 可以注册 ActiveX 控件。本示例演示了如何使用 LoadLibrary
和 GetProcAddress
来获取 ActiveX 控件导出的 DllRegisterServer
函数的地址。
查看 RegisterServer
的原型,str
参数包含要注册的控件的名称。如果使用 LoadLibrary
加载控件并找到 RegisterServer
的地址,则会尝试注册该控件。否则会返回错误。
请注意,DllMain
函数中会先调用 CoInitialize
。调用 CoInitialize
对于控件的注册是至关重要的。
另外,由于 RegisterServer
和 UnregisterServer
之间的相似性,因此不讨论 UnregisterServer
。
typedef HRESULT (__stdcall *__DllRegisterServer)(); typedef HRESULT (__stdcall *__DllUnregisterServer)(); BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: CoInitialize(NULL); break; case DLL_THREAD_ATTACH: DisableThreadLibraryCalls((HINSTANCE)hModule); break; case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } int __stdcall RegisterServer(HWND hWnd, LPLONG val, LPSTR str) { // Attempt to load the ActiveX control using LoadLibrary. HMODULE hModule = LoadLibrary(str); HRESULT hResult = S_OK; if (NULL == hModule) { // the ActiveX couldn't be loaded. return 1; } // Obtain a pointer to the DllRegisterServer exported function __DllRegisterServer pDllRegisterServer = (__DllRegisterServer)GetProcAddress(hModule, "DllRegisterServer"); if (NULL == pDllRegisterServer) { // The function address couldn't be found return 2; } // Call the returned function address as you would a regular function hResult = pDllRegisterServer(); if (S_OK != hResult) { MessageBox(hWnd, "DllRegisterServer failed", str, MB_ICONEXCLAMATION); } // Free the resources. FreeLibrary(hModule); return hResult; }
UploadRegistrationInfo
提供 UploadRegistrationInfo
函数是为了让您的用户有机会将他们的注册信息上传到您的 FTP 服务器。此代码使用 WININET.DLL 登录并将用户的姓名、公司和序列号传输到本地主机上运行的 FTP 服务器(显然,这一点需要更改)。以下列表显示了此函数的实现。
#include "stdafx.h" #include "wininet.h" int __stdcall UploadRegistrationInfo(HWND hWnd, int value, char *registrationInfo) { DWORD timeout = 60000; HINTERNET hInternet = NULL; HINTERNET hFtpSession = NULL; HINTERNET hFtpFile = NULL; // you'd want to change the IP address to something other // than the local host. No sense in uploading the registration // to the customer's own PC. :-) char FtpServerName[100] = "127.0.0.1"; int FtpServerPort = 21; HANDLE hFile = INVALID_HANDLE_VALUE; char fileName[MAX_PATH]; DWORD bytesWritten = 0; DWORD transferSize = 0; // Set the name for the transferred file to "reg.txt" // // NOTE: This file does not exist on your hard drive. The string data in // the registrationInfo will be stored on the FTP server as this // filename. // strcpy(fileName, "reg.txt"); // Set the wait cursor to let the user know something's happening. SetCursor(::LoadCursor(NULL, IDC_WAIT)); // Attempt to open an internet connection hInternet = InternetOpen("HomeServer", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); // Let the user know if it wasn't successful. if (hInternet == NULL) { MessageBox(hWnd, "There was an error opening a connection to the internet.", "FTP Error", MB_ICONINFORMATION); SetCursor(::LoadCursor(NULL, IDC_ARROW)); return false; } // set timeout durations for connects, sends, and receives. if (FALSE == InternetSetOption(hInternet, INTERNET_OPTION_RECEIVE_TIMEOUT, &timeout, sizeof(DWORD))) { // not necessarily a serious error, but report it anyway. MessageBox(hWnd, "Couldn't set receive timeout.", "FTP - warning", MB_ICONINFORMATION); } if (FALSE == InternetSetOption(hInternet, INTERNET_OPTION_SEND_TIMEOUT, &timeout, sizeof(DWORD))) { // not necessarily a serious error, but report it anyway. MessageBox(hWnd, "Couldn't set send timeout.", "FTP - warning", MB_ICONINFORMATION); } if (FALSE == InternetSetOption(hInternet, INTERNET_OPTION_CONNECT_TIMEOUT, &timeout, sizeof(DWORD))) { // not necessarily a serious error, but report it anyway. MessageBox(hWnd, "Couldn't set connect timeout.", "FTP - warning", MB_ICONINFORMATION); } // Attempt to connect to the FTP server. This is hard-coded to the // local host (127.0.0.1) IP address. // // For your convenience, a sample FTP server has been provided and is // located in the c:\My Installations\GreatProduct\FTP folder. hFtpSession = InternetConnect(hInternet, FtpServerName, FtpServerPort, "registered", "registered", INTERNET_SERVICE_FTP, 0, NULL); if (NULL == hFtpSession) { MessageBox(hWnd, "There was an error connecting to the FTP server.", "FTP Connect Failed", MB_ICONINFORMATION); InternetCloseHandle(hInternet); SetCursor(::LoadCursor(NULL, IDC_ARROW)); return 1; } hFtpFile = FtpOpenFile(hFtpSession, fileName, GENERIC_WRITE, FTP_TRANSFER_TYPE_BINARY, 0); if (NULL == hFtpFile) { MessageBox(hWnd, "There was an error uploading the registration information.", "FTP Upload Failed", MB_ICONINFORMATION); InternetCloseHandle(hFtpSession); InternetCloseHandle(hInternet); SetCursor(::LoadCursor(NULL, IDC_ARROW)); return 2; } transferSize = strlen(registrationInfo); if (FALSE == InternetWriteFile(hFtpFile, registrationInfo, transferSize, &bytesWritten)) { MessageBox(hWnd, "There was an error uploading the registration information.", "FTP Upload Failed", MB_ICONINFORMATION); InternetCloseHandle(hFtpFile); InternetCloseHandle(hFtpSession); InternetCloseHandle(hInternet); SetCursor(::LoadCursor(NULL, IDC_ARROW)); return 3; } if (bytesWritten != transferSize) { MessageBox(hWnd, "There was an error uploading all of the registration information.", "FTP Upload Incomplete", MB_ICONINFORMATION); InternetCloseHandle(hFtpFile); InternetCloseHandle(hFtpSession); InternetCloseHandle(hInternet); SetCursor(::LoadCursor(NULL, IDC_ARROW)); return 3; } InternetCloseHandle(hFtpFile); InternetCloseHandle(hFtpSession); InternetCloseHandle(hInternet); SetCursor(::LoadCursor(NULL, IDC_ARROW)); MessageBox(hWnd, "Your registration information was uploaded successfully.\n" "Thanks for registering the software.", "Registration Complete", MB_ICONINFORMATION); return 0; }
UploadRegistrationInfo
函数将尝试使用 registered
作为用户名和 registered
作为密码登录 FTP 服务器。如果成功,将尝试创建一个名为 reg.txt 的文件。如果文件创建成功,将把存储在指针 registrationInfo
中的注册信息发送到 FTP 服务器。
注意:为了方便起见,源代码下载中提供了一个演示 FTP 服务器。
在 C:\My Installations\GreatProduct\FTP 文件夹中查找 FTP 服务器演示的 ZIP 文件。要使用演示,请将这两个文件解压缩到您选择的文件夹中。双击文件 FtpServerDemo,并在主屏幕显示后按 Start 按钮启动服务器。演示运行时间约为一天。
InsideDll
最后一个函数 InsideDll
是一个虚拟函数,在 代码方面 不执行任何操作。它的主要目的是允许符号表加载到 Visual C++ 调试器中。它将在用户按下初始 欢迎 屏幕上的“下一步”按钮后立即由安装程序调用。
InstallShield 设置项目
您的 Great Product 的 InstallShield 项目位于文件 C:\My Installations\GreatProduct\GreatProduct.ipr 中。
要构建设置安装,请先选择菜单“Build->Compile”编译脚本。然后通过选择“Build->Media Build Wizard....”来生成实际的设置文件。在 Existing Media 部分中选择 Default 列表。
当您进入 Build Type 对话框时,选择“Full Build”。否则,请按“Next”接受所有默认设置。
仔细查看...
本教程中提供的安装项目包含三个屏幕:欢迎屏幕、目标屏幕和安装完成屏幕。其中两个屏幕通过调用 DialogShowSdWelcome
和 DialogShowSdAskDestPath
在 ShowDialogs
函数中显示。
// this code can be found in the following path // C:\My Installations\GreatProduct\Script Files\setup.rul //////////////////////////////////////////////////////////////////////// // // // Function: ShowDialogs // // // // Purpose: This function manages the display and navigation // // the standard dialogs that exist in a setup. // // // //////////////////////////////////////////////////////////////////////// function ShowDialogs() NUMBER nResult; begin Dlg_Start: // beginning of dialogs label Dlg_SdWelcome: nResult = DialogShowSdWelcome(); if (nResult = BACK) goto Dlg_Start; Dlg_SdAskDestPath: // ... nResult = DialogShowSdAskDestPath(); if (nResult = BACK) goto Dlg_SdWelcome; // This indicates the type of installation to be used. // There is only one possibility, so a dialog is not needed. svSetupType = "Typical"; return 0; end;
最后一个函数 DialogShowSdFinishReboot
包含对 CallDLLFx
的调用。CallDLLFx
接受四个参数:
- 自定义 DLL 的名称,IS_CustomDll.dll,它必须与安装程序位于同一目录。
- 要调用的函数(可以是
StartProg
、RegisterServer
或UploadRegistrationInfo
) - 一个
LONG
值(未使用) - 一个
STRING
,其中包含要注册的 ActiveX 控件的名称、用于 FTP 上传的注册信息,或者要启动的可执行文件。
//////////////////////////////////////////////////////////////////////////// // // // Function: DialogShowSdFinishReboot // // // // Purpose: This function will show the last dialog of the product. // // It will allow the user to reboot // // and/or show some readme text. // // // //////////////////////////////////////////////////////////////////////////// function DialogShowSdFinishReboot() NUMBER nResult, nDefOptions; STRING szTitle, szMsg1, szMsg2, szOption1, szOption2; STRING str, dllName, dllFunction; LONG value, retVal; NUMBER bOpt1, bOpt2; begin if (!BATCH_INSTALL) then bOpt1 = FALSE; bOpt2 = FALSE; szMsg1 = "Setup has installed My Great Product on your PC"; szMsg2 = ""; dllName = SRCDIR ^ "\\IS_CustomDll.dll"; value = 0; szOption1 = ""; szOption2 = ""; nResult = SdFinish( szTitle, szMsg1, szMsg2, szOption1, szOption2, bOpt1, bOpt2 ); str = svName ^ "\n" ^ svCompany ^ "\n" ^ svSerial ^ "\n"; dllFunction = "UploadRegistrationInfo"; retVal = CallDLLFx(dllName, dllFunction, value, str); str = TARGETDIR ^ "\\controls\\ActiveX_Control.dll"; dllFunction = "RegisterServer"; retVal = CallDLLFx(dllName, dllFunction, value, str); if (0 == retVal) then str = TARGETDIR ^ "GreatProduct.exe"; dllFunction = "StartProgram"; retVal = CallDLLFx(dllName, dllFunction, value, str); endif; return 0; endif; nDefOptions = SYS_BOOTMACHINE; szTitle = ""; szMsg1 = ""; szMsg2 = ""; nResult = SdFinishReboot( szTitle, szMsg1, nDefOptions, szMsg2, 0 ); return nResult; end;
在上面的代码中,您会注意到在运行已安装的应用程序之前会检查 CallDLLFx
的返回值。如果找不到 DLL,此返回值将为 -1(负一)。如果函数因任何原因失败,返回值将非零。当然,确定失败的如何或为何可能更困难。尤其是如果您仅依赖 InstallShield 调试器。
调试自定义 DLL
***************************************************************************
DEBUGGING THE CUSTOM DLL MAY ONLY BE PERFORMED ON WINDOWS NT, 2000 and XP.
OTHER VERSIONS OF WINDOWS CANNOT SAFELY ATTACH THE SETUP.EXE PROCESS TO
THE VISUAL C++ 6.0 DEBUGGER.
***************************************************************************
我们将从 InstallShield 开始调试过程。首先,确保安装脚本已编译。要编译项目,请选择 Build->Compile 菜单。编译后,使用 Media Build Wizard 构建一个完整的安装。完成所有这些操作后,请确保自定义 DLL IS_CustomDll.DLL 可以在安装目录中找到。该目录是:C:\My Installations\GreatProduct\Media\Default\Disk Images\Disk1
如果文件不存在,您需要构建它。Visual C++ 项目工作区 GreatProduct.dsw 包含 IS_CustomDll 项目。构建此项目将自动将 DLL 放入正确的位置。
请确保您构建的是DEBUG 版本。如果您不确定哪个版本在设置目录中,可以选择 Build 菜单中的“Rebuild All”。
一旦自定义 DLL 构建完成并且 InstallShield 项目已编译,您就可以启动 InstallShield 调试器。要启动调试器,请按 F5 或从 InstallShield Build 菜单中选择 Debug Setup。启动后,转到 ShowDialogs
函数中 Dlg_SdAskDestPath:
之后直接调用 CallDLLFx
的行(第 171 行)设置一个断点。断点应类似于本文开头的图像。按“Go”按钮启动调试器。您现在应该会看到“My Great Program”欢迎屏幕。
在按 Next 之前,如果 Visual C++ 尚未运行,请启动它。我们现在将设置程序附加到 MSVC 调试器。
要将设置程序附加到调试器,请选择以下菜单项:Build->Start Debug->Attach to Process...
这将显示一个包含所有可用进程的对话框。选择标题为 My Great Program 的进程,然后按 OK。
如果您看不到此文本,请确保“My Great Program”设置屏幕已激活(切换回设置程序,如有必要激活屏幕,然后返回 Visual C++ 调试器)。打开 Attach to Process... 对话框并将设置进程附加到调试器。
一旦附加,设置程序现在应该已激活。如果您收到一条错误消息,指出一个或多个断点无法设置,那没关系。
请记住,该程序只能由 Windows NT 4.0、2000 或 XP 调试。
仔细观察...
好的,让我们花点时间看看正在发生什么。您现在应该有三件事情正在进行:
- InstallShield 调试器正在运行您的安装项目。
- 安装程序对话框“My Great Program”正在耐心等待您按 Next。
- Visual C++ 已将设置项目附加到其调试器。
继续在安装程序中单击 Next。这将触发 InstallShield 中的断点。在单步执行 CallDLLFx
调用之前,花点时间看看发生了什么。
CallDLLFx
的第一个参数是自定义 DLL 的名称。此名称通过将 \\IS_CustomDll.DLL
附加到 SRCDIR
全局变量并将其存储在 STRING
变量 dllName
中来获得。
下一个参数 dllFunction
是另一个 STRING
变量,其中包含要调用的导出函数的名称。在这种情况下,我们只是调用虚拟函数 InsideDll
。
其余参数 value
和 str
已赋值,但 InsideDll
函数未使用它们。
继续 逐行执行 CallDLLFx
的调用。现在切换到 Visual C++ 调试器。
在调试器的输出窗口中,您应该会看到类似以下内容的行:
Loaded symbols for
'C:\My Installations\GreatProduct\...\disk1\IS_CustomDll.dll'
为了确保 DLL 已成功加载,将测试变量 retVal
是否等于 -1,如下所示:
//////////////////////////////////////////////////////////////////////////// // // // Function: ShowDialogs // // // // Purpose: This function manages the display and navigation // // the standard dialogs that exist in a setup. // // // //////////////////////////////////////////////////////////////////////////// function ShowDialogs() NUMBER nResult; STRING dllName; STRING dllFunction; LONG value, retVal; STRING str; begin str = "Debug running setup app by attaching to process in Visual C++."; value = 0; dllFunction = "InsideDll"; dllName = SRCDIR ^ "\\IS_CustomDll.dll"; Dlg_Start: // beginning of dialogs label Dlg_SdWelcome: nResult = DialogShowSdWelcome(); if (nResult = BACK) goto Dlg_Start; Dlg_SdAskDestPath: retVal = CallDLLFx(dllName, dllFunction, value, str); if (retVal == -1) then MessageBox ("Couldn't load the custom dll. Make sure you build it first.", INFORMATION); return -1; endif; nResult = DialogShowSdAskDestPath(); if (nResult = BACK) goto Dlg_SdWelcome; // This indicates the type of installation to be used. // There is only one possibility, so a dialog is not needed. svSetupType = "Typical"; return 0; end;
假设 DLL 加载成功,请继续单击 InstallShield Debugger 上的 Go 按钮。这将导致下一个屏幕“Choose Destination Location”出现。单击 Back 按钮返回 Welcome 屏幕。在 Welcome 屏幕上,单击 Next 以触发 InstallShield Debugger 中的断点。现在我们将在 MSVC Debugger 中为 InsideDll
的代码设置一个断点。
位于 C:\My Installations\GreatProduct\Projects\IS_CustomDll\IS_CustomDll.cpp 源文件中,滚动到文件末尾找到 InsideDll
函数。在说 return 0;
的行上设置一个断点。
int __stdcall InsideDll(HWND hWnd, LPLONG val, LPSTR str) { return 0; }
现在返回 InstallShield 调试器并按 Go。MSVC 调试器应该会在 InsideDll
函数内中断。将 str
变量添加到监视列表。它应该包含以下文本:
"Debug running setup app by attaching to process in Visual C++"
此时,您可以尝试在 Visual C++ 自定义 DLL 或 InstallShield 项目中设置其他断点。特别值得关注的是 StartProgram
和 RegisterServer
函数。这些函数会忽略 value 参数,但要求 str
变量包含一个有效的文件名。在这些函数中设置断点以验证变量的内容。
如果您对文件如何上传到 FTP 服务器感兴趣,请在 UploadRegistrationInfo
函数中设置一个断点。此函数位于文件 IS_UseFtp.cpp 中。
需要记住的一些要点
至此,我希望您对如何构建和测试自定义 DLL 有了很好的了解。诚然,实际调试安装程序的方法有点复杂。但是,这总比无法调试您的 DLL 要好。请记住,当您需要调试 DLL 时,您需要确保安装程序能够找到包含调试信息的 DLL 版本。您还应该将文件放在与安装文件相同的目录中。
编写设置脚本时,InstallShield 的两个全局变量 SRCDIR
和 TARGETDIR
在构建文件名时非常有用。
要创建自己的 DLL 函数,您需要像这样声明它们:int __stdcall FunctionName(HWND hWnd, LPLONG val, LPSTR str)
另外,请确保在项目定义文件的 EXPORTS
部分中包含它们。下面显示了一个示例文件。
LIBRARY "IS_CustomDll.dll" EXPORTS StartProgram @ 1 UnregisterServer @ 2 RegisterServer @ 3 UploadRegistrationInfo @ 4 InsideDll @ 5
您的自定义函数的返回值将由 CallDLLFx
返回。由于如果无法加载 DLL,将返回负一 (-1) 的值,因此建议您的函数不要返回此值。这样,您就可以区分是 DLL 函数失败还是 DLL 根本无法加载。
结论
本文试图解释如何同时使用 InstallShield 和 Visual C++ 来调试用于 InstallShield 安装程序的自定义 DLL。我希望它能为您在使用产品设置应用程序构建和测试自己的自定义函数方面提供一些启示。
调试愉快!