21.1 关于库的基本知识

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P958

        如你所知,Windows 程序是一个可执行的文件,它通常创建一个或多个窗口,并使用消息循环来接收用户输入。动态链接库一般不能直接执行,而且它们一般也不接收消息。它们是包含许多函数的独立文件,这些函数可以被应用程序和其他 DLL 调用以完成某些特定的工作。一个动态链接库只有在另外一个模块调用其所包含的函数时才被启动

        “动态链接”(dynamic linking)这个术语是指 Windows 的链接过程,在这个过程中它把模块中的函数调用与在库模块中的实际函数链接在一起。“静态连接”(static linking)一般是在程序开发过程中发生的,用于把一些文件链接在一起创建一个 Windows 可执行文件。这些文件包括各种各样的对象模块(.OBJ),运行时库文件(.LIB),通常还有已编译的资源文件(.RES)。与其相反,动态链接则发生在程序运行时。

        动态链接库的例子有 KERNEL32.dll,USER32.dll 和 GDI32.DLL;以及各种驱动程序文件,如 KEYBOARD.DRV,SYSTEM.DRV 和 MOUSE.DRV;还有视频和打印机驱动程序等。这些是所有 Windows 程序都可以使用的库。

        一些动态链接库(如字体文件)被称为“只含资源的”(resource-only),它们只包含数据(通常以资源的形式存在),并没有代码。因此,动态链接库的目的之一是为许多不同的程序提供函数和资源。在传统的操作系统中,只有操作系统本身才包含了其他程序可以调用的例程(routine)。而在 Windows 中,某个模块调用其他模块内的函数的过程被推广了。实际上,在写动态链接库时,你是在扩展 Windows 的功能或者你也可以认为 DLL,包括 Windows 的库文件,为你的程序增加了功能

        虽然动态链接库模块可有任何扩展名(如 .EXE 或 .FON),但其标准扩展名是 .DLL。只有扩展名为 .DLL 的动态链接库才能被 Windows 操作系统自动加载。如果该文件有另外的扩展名,则程序必须明确地用 LoadLibrary 或 LoadLibraryEx 函数加载相应模块

        一般来说,你会发现动态库只有用在大型应用程序中才最有意义。举例来说,假设你要在 Windows 上编一个大型会计软件包,它由几个不同的程序组成。你可能会发现,这些程序使用了许多共同的例程。你可以把这些共同的例程放在一个普通的对象库中(扩展名为 .LIB),并在静态连接时用 LINK 将其加到每个程序模块中。但是,这种做法是一种浪费,因为软件包中每个程序都包含这些公同例程的相同代码。此外,如果你改变这个库中的任何一个例程,你都必须重新链接所有用到这些例程的程序。但是如果你把这些共同的例程放在动态链接库中,例如 ACCOUNT.DLL 中,这两个问题就都解决了。只有库模块需要包含所有其他程序所用到的例程,因此只要较少的磁盘空间来存放文件,以及较少的内存空间来同时运行两个或两个以上的应用程序。此外你还可以修改库模块而不需要重新链接任何一个程序。

        动态链接库自己就可以是产品。举例来说,假设你写了一组三维绘图的例程并将其放入一个叫 GDI3.DLL 的动态链接库。如果你后来吸引了其他软件开发商来使用你的库,你可以授权给他们,让他们把你的库包含在他们的图形程序中。拥有数个类似程序的用户,将只需要一个 GDI3.DLL 文件。

21.1.1  库:一词多义

        对于动态链接库的一些混淆理解来源于“库”这个字会出现于不同的场合。除了动态链接库,我们也来了解一下“对象库”(object library)和“导入库”(import library)。

        对象库是一个扩展名为 .LIB 的文件,这个文件中的代码在运行链接器进行静态链接时被添加到程序的 .EXE 文件中。例如,在 Microsoft Visual C++中,程序通常链接的 C 运行时对象库是 LIBC.LIB。

        导入库是一种特殊形式的对象库文件。如同对象库一样,导入库的扩展名为 .LIB,链接器用它来解析源代码中的函数调用。然而,导入库不包含任何代码。相反,它们只给链接器提供信息,以建立 .EXE 文件中用于动态链接的重定位表格。微软的编译器中的 KERNEL32.LIB、USER32.LIB 及 GDI32.LIB 文件是 Windows 函数的导入库。如果在程序中调用 Rectangle 函数,GDI32.LIB 会告诉链接器这个函数是在 GDI32.DLL 动态链接库中。这一信息被存放在 .EXE 文件中,所以 Windows 可以在你的程序运行时与 GDI32.DLL 动态链接库进行动态链接。

        对象库和导入库仅在程序开发时被使用,而动态链接库则是在运行时使用。当一个用到动态库的程序运行时,该库必须存在磁盘上。当 Windows 在运行程序前加载 DLL 模块的时候,该库文件必须存放在 .EXE 程序所在的目录下,活在当前目录、Windows 系统目录、Windows 目录、MS - DOS 环境下的 PATH 字符串中的某一目录下。(系统将按上述顺序来检索目录。)

21.1.2  一个简单的 DLL

        虽然动态链接库的完整构想是它们可以被多个应用程序使用,但通常最开始设计的动态链接库只会与一个应用程序相关联,而这个程序也许只是一个供学习 DLL 用的“试验”程序。

        这也是我们在这一节里要做的。我们将创建一个叫 EDRLIB.DLL 的 DLL。文件名中的“EDR”是“简单绘图例程”(Easy Drawing Routines)的英文缩写。我们的 EDRLIB 只包含一个函数(名为 EdrCenterText),但你可以添加其他函数来简化你的应用程序中的绘图函数。一个名为 EDRTEST.EXE 的应用程序将调用 EDRLIB.DLL 中所载的函数。

        为此我们要采取和我们先前所用的略有不同的方法,涉及在 Visual C++ 中我们还尚未接触的一项功能。Visual C++中,“客户区”(workspace)和“项目”(project)是不同的。一个项目通常用于建立一个应用文件(.EXE)或动态链接库(.DLL)。客户区则可以包含一至多个项目。到限制为止,我们所有的工作区都只包含一个项目。限制我们将创建一个名为 EDRTEST 的客户区。它将包含两个项目,一个用于产生 EDRTEST.EXE,另一个用于产生 EDRTEST 所用的动态链接库 EDRLIB.DLL。

        让我们开始吧。在 Visual C++ 中,在 File 菜单下选择 New。选择 Workspaces 选项卡。(之前我们还从未选过这个。)在 Location 文本框内选择用以存放客户区的目录,在 Workspace Name 文本框内输入 EDRTEST。按 Enter 键。

        这将创建一个空白客户区。Developer Studio 将创建一个名为 EDRTEST 的子目录和客户区文件 EDRTEST.DSW(以及其他几个文件)。

        现在让我们在这个客户区里创建一个项目。在 File 菜单下选择 New,选择 Projects 标签。过去曾在这里选择过 Win32 应用程序(Win32 Application),这一次选择 Win32 动态链接库(Win32 Dynamic-Link Library)。此外,请选中 Add To Current Workspace(添加到当前客户区)单选按钮。这样一来,此项目就成为 EDRTEST 工作区的一部分。在 Project Name 段键入 EDRLIB,但先不单击 OK 按钮。在 Project Name 字段输入 EDRLIB 时,Visual C++ 会自动替换 Location 框的内容,把 EDRLIB 设为 EDRTEST 的一个子目录。这并不是你希望!在 Location 框中,删除 EDRLIB 子目录,如此一来,此项目就会建立在 EDRTEST 目录中。现在单击 OK 按钮。你会看到一个对话框询问你想要创建什么样的 DLL。选择 Empty DLL Project,然后单击 Finish 按钮。Visual C++ 将创建一个项目文件 EDRLIB.DSP 以及 make  文件 EDRLIB.MAK(如果已在 ToolsOptions 对话框的 Build 选项卡中选择了 Export Makefile 选择的话)。

        现在可以为此项目添加几个文件了。在 File 菜单下选择 New 命令,然后选择 File 选项卡。选择 C/C++ Header File,并输入文件名 EDRLIB.H。输入如图 21-1 中所示的相应文件(或从本书的 CD-ROM 中复制)。再一次从 File 菜单下选择 New 命令,然后选择 File 选项卡。这一次选择 C++ Source  File,并输入文件名 EDRLIB.C。再次输入图21-1 中所示的相应文件。

<pre name="code" class="cpp">EDRLIB.H
/*----------------------------
    EDRLIB.H  header file
----------------------------*/
#ifdef __cplusplus
#define EXPORT extern "C" __declspec (dllexport)
#else 
#define EXPORT __declspec (dllexport)
#endif

EXPORT BOOL CALLBACK EdrCenterTextA(HDC, PRECT, PCSTR);
EXPORT BOOL CALLBACK EdrCenterTextW(HDC, PRECT, PCWSTR);

#ifdef UNICODE
#define EdrCenterText EdrCenterTextW
#else
#define EdrCenterText EdrCenterTextA
#endif

 
<pre name="code" class="cpp">EDRLIB.C
/*--------------------------------------------------
    EDRLIB.C -- Easy Drawing Routine Library module
                (c) Charles Petzold, 1998
--------------------------------------------------*/
#include <Windows.h>
#include "EdrLib.h"

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
    return TRUE;
}

EXPORT BOOL CALLBACK EdrCenterTextA(HDC hdc, PRECT prc, PCSTR pString)
{
    int        iLength;
    SIZE    size;

    iLength = lstrlenA(pString);

    GetTextExtentPoint32A(hdc, pString, iLength, &size);

    return TextOutA(hdc, (prc->right - prc->left - size.cx) / 2,
                         (prc->bottom - prc->top - size.cy) / 2,
                    pString, iLength);
}

EXPORT BOOL CALLBACK EdrCenterTextW(HDC hdc, PRECT prc, PCWSTR pString)
{
    int        iLength;
    SIZE    size;

    iLength = lstrlenW(pString);

    GetTextExtentPoint32W(hdc, pString, iLength, &size);

    return TextOutW(hdc, (prc->right - prc->left - size.cx) / 2, 
                         (prc->bottom - prc->top - size.cy) / 2,
                    pString, iLength);
}
 a 

        现在可以以发行配置(Release)或调试配置(Debug)来生成 EDRLIB.DLL。在生成之后,RELEASE 和 DEBUG 目录下将包含动态链接库的导入库 EDRLIB.LIB 和动态链接库本身 EDRLIB.DLL。

        贯穿本书始终,我们编写的程序都可以根据 UNICODE 标识符的定义编译成能够处理 UNICODE 或者非 UNICODE 字符串的程序。在创建一个 DLL 时,对于任何有字符或字符串参数的函数,它都应该包括 UNICODE 和非 UNICODE 两个版本。因此,DERLIB.C 包含名为 EdrCenterTextA(ANSI 版本)和 EdrCenterTextW(宽字符版本)的函数。EdrCenterTextA 被定义为采取 PCSTR(pointer to const string)参数,而 EdrCenterTextW 则采取 PCWSTR(pointer to const wide string)参数。EdrCenterTextA 函数显式地调用 lstrlenA、GetTextExtentPoint32A 和 TextOutA;而 EdrCenterTextW 显式地调用 lstrlenW、GetTextExtentPoint32W 和 TextOutW。如果有 UNICODE 标识符的定义,EDRLIB.H 文件就定义 EdrCenterText 为 EdrCenterTextW,否则就是 EdrCenterTextA。这就像在 Windows 头文件内一样。

        EDRLIB.C 包括一个名为 DllMain 的函数,在 DLL 中它取代的是 WinMain。此函数是用来执行初始化和逆初始化的,我将在下一节讨论这些。但对于我们现在来说,我们需要的仅仅是从 DllMain 返回 TRUE。

        在这两个文件里只剩下一个谜团,即 EXPORT 标识符的定义。DLL 中供应用程序所用的函数必须先导出。这和任何关税或商业条例无关,仅仅是几个用以确保函数名称被添加到 EDRLIB.LIB 中的关键字而已(使得链接器在链接使用这些函数的应用程序时可以正确解析该函数名),并且用于确保这些函数在 EDRLIB.DLL 中可见。如果头文件由 C++ 模式编译,则 EXPORT 标识符会包含存储关键字 __declspec(dllexport)和  extern "C"。这可以防止编译器对 C++ 函数进行“名称重整”,从而使 C 和 C++ 程序都能使用该DLL。

21.1.3  库的入口点和退出点

        当库第一次开始时和终止时,DllMain 函数都会被调用。DllMain 的第一个参数是库的实例句柄。如果你的库使用的资源需要实例句柄(如 DialogBox),则应该把 hInstance 作为一个全局变量来保存。DllMain 的最后一个参数是系统保留参数。

        fdwReason 参数可以是四个值中的一个,用来说明 Windows 调用 DllMain 函数的原因。在随后的讨论中请记住,一个程序可以被多次加载,并在 Windows 环境下同时运行。每一次程序加载都可以被认为是一个独立的进程

        当 fdwReason 值为 DLL_PROCESS_ATTACH 时,表明动态链接库已经被映射到一个进程的地址空间。这相当于一个初始化信号,让库针对所服务进程的后续请求进行初始化工作。例如,这种初始化可能包括内存分配等。在一个进程的整个生命周期内,DllMain 只有一次会在调用中带有 DLL_PROCESS_ATTACH 参数。任何其他进程如果使用同一个 DLL 都会以 DLL_PROCESS_ATTACH 参数另外调用 DllMain,但这代表的是新的进程。

        如果初始化成功,DllMain 应该返回非 0 值。返回 0 将导致 Windows 无法运行该程序

        当 fdwReason 值为 DLL_PROCESS_DETACH 时,意味着这个进程不再需要该 DLL 了。这给库提供了一个自清理的机会。往往在 32 位版本的 Windows 中这并不是必须的,但这是一个良好的编程习惯。

        类似地,当以 fdwReason 参数为 DLL_THREAD_ATTACH 调用 DllMain 时,意味着一个关联的进程创建了一个新的线程。当该线程终止时,Windows 会以 fdwReason 参数为 DLL_THREAD_DETACH 调用 DllMain。请注意,即便之前没有 DLL_THREAD_ATTACH 调用的情况下,DLL_THREAD_DETACH 仍有可能被调用,比如当动态链接库链接到一个进程时,该线程已被创建。

        DllMain 以参数 DLL_THREAD_DETACH 被调用时,线程仍然存在。它甚至还可以在此过程中发送线程消息。但他不应该再使用 PostMessage,因为线程可能在消息被收取之前就消失了。

21.1.4  测试程序

        现在让我们在 EDRTEST 客户区建立第二个项目。这个项目命名为 EDRTEST,它将使用 EDRLIB.DLL 。在 Visual C++ 中载入 EDRTEST 客户区之后,在 File 菜单下选择 New 命令。从 New 对话框中选择 Projects 标签。这一次选择 Win32 Application。务必确定选中 Add  To Current  Workspace 单选按钮。输入项目名称 EDRTEST。同样地,在 Locations 框中清除 EDRTEST 子目录。单击 OK 按钮,然后在弹出的对话框中选择 An Empty Project。最后单击 Finish 按钮。

        从 File 菜单中,再一次选择 New 命令。选择 Files 选项卡和 C++ Source File。确认 Add To Project 列表框中显示的是 EDRTEST 而不是 EDRLIB。输入文件名 EDRTEST.C,然后输入图 21-2 中所示的文件。此程序使用 EdrCenterText 函数将一个文本字符串放置到客户区中央。

/*---------------------------------------------------------
	EDRTEST.C -- Program using EDRLIB dynamic-link library
				(c) Charles Petzold, 1998
----------------------------------------------------------*/

#include <Windows.h>
#include "EdrLib.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
					PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("StrProg");
	HWND		 hwnd;
	MSG			 msg;
	WNDCLASS	 wndclass;

	wndclass.style = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc = WndProc;
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hInstance = hInstance;
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wndclass.lpszMenuName = NULL;
	wndclass.lpszClassName = szAppName;

	if (!RegisterClass(&wndclass))
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"),
			szAppName, MB_ICONERROR);
		return 0;
	}

	hwnd = CreateWindow(szAppName, TEXT("DLL Demostration Program"),
						WS_OVERLAPPEDWINDOW,
						CW_USEDEFAULT, CW_USEDEFAULT,
						CW_USEDEFAULT, CW_USEDEFAULT,
						NULL, NULL, hInstance, NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC			hdc;
	PAINTSTRUCT ps;
	RECT		rect;

	switch (message)
	{
	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		EdrCenterText(hdc, &rect,
				TEXT("This string was displayed by a DLL"));

		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}

        请注意,EDRTEST.C 包括了用于定义在处理 WM_PAINT 消息时所调用的 EdrCenterText 函数的头文件 EDRLIB.H。

        在编译此程序前,还有一些要完成的工作。首先,在 Project 菜单上选择 Set Active Project。看到 EDRLIB 和 EDRTEST 之后,应选择 EDRTEST。在生成这个客户区时,其实是想生成这个程序。此外,在 Project 菜单上选择 Dependencies。在 Select Project To Modify 列表框中,选中 EDRTEST。在 Dependent On The Following Project(s) 列表中,选中 EDRLIB。这意味着 EDRTEST 需要 EDRLIB 动态链接库。每当生成 EDRTEST 时,如果有必要,EDRLIB 也将在编译和链接 EDRTEST 前重新生成。

        从 Project 菜单中选中 Settings。选择 General 标签。在左窗口中选择 EDRLIB 或 EDRTEST 项目时,右窗格中的 Intermediate Files 和 Output Files 对应于 Win32 发行配置显示的应是 RELEASE 目录,对应于 Win32 调试配置的应该是 DEBUG 目录。如果不是这样,请纠正它们。这将确保 EDRLIB.DLL 最终与 EDRTEST.EXE 在同一目录下,使这个程序在使用该 DLL 时不会碰到任何问题。

        仍然在 Project Settings 对话框中,在确保选中 EDRTEST 的情况下,单击 C/C++ 标签。在 Preprocessor Definitions 中,在调试配置里添加 UNICODE,这是本书所有程序的惯例。

        现在,应该能够按调试和发行配置生成 EDRTEST.EXE。如有必要,Visual C++ 会首先编译并链接 EDRLIB。RELEASE 和 DEBUG 目录中将包含 EDRLIB.LIB(导入库)和 EDRLIB.DLL。当 Developer Studio 链接 EDRTEST 时,它会自动包含导入库。

        理解下面这一点是很重要的。即 EDRTEST.EXE 文件并不包含 EdrCenterText 的代码。相反,在这个可执行文件里只包括对 EDRLIB.DLL 文件和 EdrCenterText 函数的引用。EDRTEST.EXE 需要 EDRLIB.DLL 才能运行。

        在执行 EDRTEST.EXE 时,Windows 要对外部库模块里的函数进行修正。许多这些函数是放在普通的 Windows 动态库里的。但是 Windows 还会看到该程序调用了一个 EDRLIB 里的函数,所以 Windows 把 EDRLIB.DLL 文件加载到内存中,并调用 EDRLIB 的初始化例程。在 EDRTEST 中对 EdrCenterText 函数的调用会动态地与 EDRLIB 链接。

        与包括 WINDOWS.H 相类似,EDRTEST.C 源代码文件也要包含 EDRLIB.H。链接 EDRLIB.LIB 与链接 WIndows 导入库(如 USER32.LIB)相类似。当你的程序运行时,它用和链接 USER32.DLL 相同的方式链接 EDRLIB.DLL。祝贺你!你刚刚创建了一个对 Windows 的扩展!

        在继续下面的讨论之前,就动态链接库再讲几句。

        首先,尽管我刚刚将 DLL 归类于对 Windows 的扩展,但它也是一个对于你的应用程序的扩展。DLL 做的所有事情都代表应用程序的行为。例如,所有由它分配的内存由应用程序拥有,它所创建的任何窗口都属于应用程序,它所打开的任何文件也都属于应用程序。多个应用程序可以同时使用相同的 DLL,然而在 Windows 下这些应用程序不会互相干扰。

        多个进程可以共享动态链接库中相同的代码。然而,由 DLL 维护的数据对每个进程是不一样的。每个进程都有自己的、供 DLL 使用数据的地址空间。我们将在下一小节看到,进程间共享内存还需要作一些额外的工作。

21.1.5  在 DLL 中共享内存

        Windows 让同时使用相同动态链接库的进程相互隔离,这个想法非常不错。但是,有时候这并不可取。你或许想要写一个 DLL,它包含一些让各种应用程序或者相同应用程序的不同实例之间能共享的内存。这就涉及使用共享内存,实际上是内存映射文件

        让我们通过一个名为 STRPROG(“string program”)的程序和一个名为 STRLIB(“string library”)的动态链接库来了解一下共享内存是怎么回事。STRPROG 调用 STRLIB 里的三个导出函数。为了增加一点趣味性,STRLIB 中的一个函数将用到 STRPROG 里定义的一个回调函数。

        STRLIB 是一个动态链接库模块,用来存储和排序多达 256 个字符串。这些字符串全是大写的,并存放在 STRLIB 的共享内存中。STRPROG 可以使用 STRLIB 的三个函数来添加字符串、删除字符串,并获取 STRLIB 中所有当前字符串。STRPROG 测试程序有两个菜单项(Enter 和 Delete),可以通过对话框来添加和删除这些字符串。STRPROG 在它的客户区列出目前 STRLIB 里所存储的所有字符串。

        下面这个在 STRLIB 中定义的函数将一个字符串加入 STRLIB 的共享内存:

EXPORT BOOL CALLBACK AddString(pStringIn)

pStringIn 参数是指向该字符串的指针。在 AddString 函数中,字符串会转换成大写。如果相同的字符串已经存在 STRLIB 的字符串列表中,此函数会增加字符串的另一个副本。AddString 如果成功了,它将返回 TRUE(非 0),否则返回 FALSE。如果该字符串的长度为 0,或者无法给该字符串分配内存,或者 256 个字符串已经存满,则返回值是 FALSE。

        下面这个 STRLIB 函数删除 STRLIB 的共享内存中的字符串:

<pre name="code" class="cpp">EXPORT BOOL CALLBACK DeleteString(pStringIn)

 同样,pStringIn 参数是指向该字符串的指针。如果有一个以上的字符串匹配,则只有第一个被删除。DeleteString 如果成功了,它将返回 TRUE(非 0),否则返回 FALSE。返回值是 FALSE 表面字符串的长度是 0 或者匹配字符串无法找到。 

        下面这个 STRLIB 函数通过来自调用程序中的回调函数来枚举所有目前存放在 STRLIB 的共享内存中的字符串:

<pre name="code" class="cpp">EXPORT int CALLBACK GetStrings(pfnGetStrCallBack, pParam)

 该回调函数必须在调用程序中如下定义: 

<pre name="code" class="cpp">EXPORT BOOL CALLBACK GetStrCallBack(PSTR pString, PVOID pParam)

 GetStrings 中的 pfnGetStrCallBack 参数指向回调函数。对应于每个字符串,GetStrings 调用 GetStrCallBack 一次,直至回调函数返回 FALSE(0)。GetStrings 的返回值就是传给回调函数的字符串的数量。pParam 参数是一个指向程序员定义的数据的远指针。 

        当然,都是因为 UNICODE,或者是因为 STRLIB 必须同时支持 UNICODE 和非 UNICODE,才使程序变得这么复杂。跟 EDRLIB 一样,它的所有函数都有 A 和 W 版本。在内部,STRLIB 以 UNICODE 的形式存储所有的字符串。如果一个非 UNICODE 的程序使用 STRLIB(即程序调用 AddStringA、DeleteStringA 及 GetStringsA),字符串会从 UNICODE 来回转换。

        与 STRPROG 和 STRLIB 项目相关的客户区名为 STRPROG。这些文件以和 EDRTEST 客户区同样的方式组织在一起。图 21-3 显示了两个用于建立 STRLIB.DLL 动态链接库模块的文件。

/*----------------------------
STRLIB.H  header file
----------------------------*/

#ifdef __cplusplus
#define EXPORT extern "C" __declspec(dllexport)
#else
#define EXPORT __declspec(dllexport)
#endif

// The maximum number of strings STRLIB will store and their lengths

#define MAX_STRINGS 256
#define MAX_LENGTH 64

// The callback function type definition uses generic strings

typedef BOOL(CALLBACK * GETSTRCB) (PCTSTR, PVOID);

// Each function has ANSI and UNICODE versions

EXPORT BOOL CALLBACK AddStringA(PCSTR);
EXPORT BOOL CALLBACK AddStringW(PCWSTR);

EXPORT BOOL CALLBACK DeleteStringA(PCSTR);
EXPORT BOOL CALLBACK DeleteStringW(PCWSTR);

EXPORT int CALLBACK GetStringsA(GETSTRCB, PVOID);
EXPORT int CALLBACK GetStringsW(GETSTRCB, PVOID);

	// Use the correct version depending on the UNICODE identifier

#ifdef UNICODE
#define AddString		AddStringW
#define DeleteString	DeleteStringW
#define GetStrings		GetStringsW
#else
#define AddString		AddStringA
#define DeleteString	DeleteStringA
#define GetStrings		GetStringsA
#endif
/*--------------------------------------------------
	STRLIB.C -- Library module for STRPROG program
		(c) Charles Petzold, 1998
--------------------------------------------------*/

#include <Windows.h>
#include <wchar.h>			// for wide-character string functions
#include "StrLib.h"

	// shared memory section (requires /SECTION:shared,RWS in link options)

#pragma data_seg ("shared")
int		iTotal = 0;
WCHAR	szStrings[MAX_STRINGS][MAX_LENGTH + 1] = { '\0' };
#pragma data_seg()

#pragma comment(linker, "/SECTION:shared,RWS")

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
	return TRUE;
}

EXPORT BOOL CALLBACK AddStringA(PCSTR pStringIn)
{
	BOOL	bReturn;
	int		iLength;
	PWSTR	pWideStr;

		// Convert string to UNICODE and call AddStringW

	iLength = MultiByteToWideChar(CP_ACP, 0, pStringIn, -1, NULL, 0);
	pWideStr = (PWSTR)malloc(sizeof(WCHAR) * (1 + iLength));
	MultiByteToWideChar(CP_ACP, 0, pStringIn, -1, pWideStr, iLength);
	bReturn = AddStringW(pWideStr);
	free(pWideStr);

	return bReturn;
}

EXPORT BOOL CALLBACK AddStringW(PCWSTR pStringIn)
{
	PWSTR	pString;
	int		i, iLength;

	if (iTotal == MAX_STRINGS - 1)
		return FALSE;

	if ((iLength = wcslen(pStringIn)) == 0)
		return FALSE;

		// Allocate memory for storing string, copy it, convert to uppercase

	pString = (PWSTR)malloc(sizeof(WCHAR) * (1 + iLength));
	wcscpy(pString, pStringIn);
	_wcsupr(pString);

		// Alphabetize the strings

	for (i = iTotal; i > 0; i--)
	{
		if (wcscmp(pString, szStrings[i - 1]) >= 0)
			break;
		wcscpy(szStrings[i], szStrings[i - 1]);
	}
	wcscpy(szStrings[i], pString);
	iTotal++;

	free(pString);
	return TRUE;
}

EXPORT BOOL CALLBACK DeleteStringA(PCSTR pStringIn)
{
	BOOL	bReturn;
	int		iLength;
	PWSTR	pWideStr;

		// Convert string to UNICODE and call DeleteStringW

	iLength = MultiByteToWideChar(CP_ACP, 0, pStringIn, -1, NULL, 0);
	pWideStr = (PWSTR)malloc(sizeof(WCHAR) * (1 + iLength));
	MultiByteToWideChar(CP_ACP, 0, pStringIn, -1, pWideStr, iLength);
	bReturn = DeleteStringW(pWideStr);
	free(pWideStr);

	return bReturn;
}

EXPORT BOOL CALLBACK DeleteStringW(PCWSTR pStringIn)
{
	int i, j;

	if (0 == wcslen(pStringIn))
		return FALSE;

	for (i = 0; i < iTotal; i++)
	{
		if (_wcsicmp(szStrings[i], pStringIn) == 0)
			break;
	}

		// If given string not in list, return without taking action

	if (i == iTotal)
		return FALSE;
		// Else adjust list downward

	for (j = i; j < iTotal; j++)
		wcscpy(szStrings[j], szStrings[j + 1]);

	szStrings[iTotal--][0] = '\0';
	return TRUE;
}

EXPORT int CALLBACK GetStringsA(GETSTRCB pfnGetStrCallBack, PVOID pParam)
{
	BOOL	bReturn;
	int		i, iLength;
	PSTR	pAnsiStr;

	for (i = 0; i < iTotal; i++)
	{
			// Convert string from UNICODE

		iLength = WideCharToMultiByte(CP_ACP, 0, szStrings[i], -1, NULL, 0, NULL, NULL);

		pAnsiStr = (PSTR)malloc(iLength);
		WideCharToMultiByte(CP_ACP, 0, szStrings[i], -1, pAnsiStr, iLength, NULL, NULL);

			// Call callback function
		
		bReturn = pfnGetStrCallBack((PCTSTR)pAnsiStr, pParam);

		if (bReturn == FALSE)
			return i + 1;

		free(pAnsiStr);
	}
	return iTotal;
}

EXPORT int CALLBACK GetStringsW(GETSTRCB pfnGetStrCallBack, PVOID pParam)
{
	BOOL	bReturn;
	int		i;

	for (i = 0; i < iTotal; i++)
	{
		bReturn = pfnGetStrCallBack((PCTSTR)szStrings[i], pParam);
		if (bReturn == FALSE)
			return i + 1;
	}
	return iTotal;
}

        除 DllMain 函数之外,STRLIB 仅包含 6 个可供其他程序使用的导出函数。所有这些函数都被定义为 EXPORT,这样链接程序会将它们列入 STRLIB.LIB 导入库中。

21.1.6  STRPROG 程序

        如图 21-4 所示,这个 STRPROG 程序相当简单。两个菜单项,Enter 和 Delete,调用对话框以输入一个字符串。STRPROG 然后调用 AddString 或 DeleteString。当程序需要更新客户区时,便调用 GetStrings,并利用 GetStrCallBack 函数来枚举字符串。

/*--------------------------------------------------------
STRPROG.C -- Program using STRLIB dynamic-link library
	(c) Charles Petzold, 1998
--------------------------------------------------------*/

#include <Windows.h>
#include "StrLib.h"
#include "resource.h"
#pragma comment(lib, "StrLib.lib")

typedef struct
{
	HDC hdc;
	int xText;
	int yText;
	int xStart;
	int yStart;
	int xIncr;
	int yIncr;
	int xMax;
	int yMax;
}
CBPARAM;

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

TCHAR szAppName[] = TEXT("StrProg");
TCHAR szString[MAX_LENGTH + 1];

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)
{
	HWND     hwnd;
	MSG      msg;
	WNDCLASS wndclass;

	wndclass.style = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc = WndProc;
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hInstance = hInstance;
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wndclass.lpszMenuName = szAppName;
	wndclass.lpszClassName = szAppName;

	if (!RegisterClass(&wndclass))
	{
		MessageBox(NULL, TEXT("This program requires Windows NT!"),
			szAppName, MB_ICONERROR);
		return 0;
	}

	hwnd = CreateWindow(szAppName, TEXT("DLL Demonstration Program"),
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, hInstance, NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return msg.wParam;
}

BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_INITDIALOG:
		SendDlgItemMessage(hDlg, IDC_STRING, EM_LIMITTEXT, MAX_LENGTH, 0);
		return TRUE;

	case WM_COMMAND:
		switch (wParam)
		{
		case IDOK:
			GetDlgItemText(hDlg, IDC_STRING, szString, MAX_LENGTH);
			EndDialog(hDlg, TRUE);
			return TRUE;

		case IDCANCEL:
			EndDialog(hDlg, FALSE);
			return TRUE;
		}
	}
	return FALSE;
}

BOOL CALLBACK GetStrCallBack(PTSTR pString, CBPARAM * pcbp)
{
	TextOut(pcbp->hdc, pcbp->xText, pcbp->yText,
		pString, lstrlen(pString));

	if ((pcbp->yText += pcbp->yIncr) > pcbp->yMax)
	{
		pcbp->yText = pcbp->yStart;
		if ((pcbp->xText += pcbp->xIncr) > pcbp->xMax)
			return FALSE;
	}
	return TRUE;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HINSTANCE  hInst;
	static int        cxChar, cyChar, cxClient, cyClient;
	static UINT       iDataChangeMsg;
	CBPARAM           cbparam;
	HDC               hdc;
	PAINTSTRUCT       ps;
	TEXTMETRIC        tm;

	switch (message)
	{
	case WM_CREATE:
		hInst = ((LPCREATESTRUCT)lParam)->hInstance;
		hdc = GetDC(hwnd);
		GetTextMetrics(hdc, &tm);
		cxChar = (int)tm.tmAveCharWidth;
		cyChar = (int)(tm.tmHeight + tm.tmExternalLeading);
		ReleaseDC(hwnd, hdc);

		// Register message for notifying instances of data changes

		iDataChangeMsg = RegisterWindowMessage(TEXT("StrProgDataChange"));
		return 0;

	case WM_COMMAND:
		switch (wParam)
		{
		case IDM_ENTER:
			if (DialogBox(hInst, TEXT("EnterDlg"), hwnd, &DlgProc))
			{
				if (AddString(szString))
					PostMessage(HWND_BROADCAST, iDataChangeMsg, 0, 0);
				else
					MessageBeep(0);
			}
			break;

		case IDM_DELETE:
			if (DialogBox(hInst, TEXT("DeleteDlg"), hwnd, &DlgProc))
			{
				if (DeleteString(szString))
					PostMessage(HWND_BROADCAST, iDataChangeMsg, 0, 0);
				else
					MessageBeep(0);
			}
			break;
		}
		return 0;

	case WM_SIZE:
		cxClient = (int)LOWORD(lParam);
		cyClient = (int)HIWORD(lParam);
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		cbparam.hdc = hdc;
		cbparam.xText = cbparam.xStart = cxChar;
		cbparam.yText = cbparam.yStart = cyChar;
		cbparam.xIncr = cxChar * MAX_LENGTH;
		cbparam.yIncr = cyChar;
		cbparam.xMax = cbparam.xIncr * (1 + cxClient / cbparam.xIncr);
		cbparam.yMax = cyChar * (cyClient / cyChar - 1);

		GetStrings((GETSTRCB)GetStrCallBack, (PVOID)&cbparam);

		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		if (message == iDataChangeMsg)
			InvalidateRect(hwnd, NULL, TRUE);
		break;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);
}
STRPROG.RC (excerpts)

// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"

/
//
// Dialog
//

ENTERDLG DIALOG DISCARDABLE  20, 20, 186, 47
    STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
    CAPTION "Enter"
    FONT 8, "MS Sans Serif"
    BEGIN
    LTEXT           "&Enter:", IDC_STATIC, 7, 7, 26, 9
    EDITTEXT        IDC_STRING, 31, 7, 148, 12, ES_AUTOHSCROLL
    DEFPUSHBUTTON   "OK", IDOK, 32, 26, 50, 14
    PUSHBUTTON      "Cancel", IDCANCEL, 104, 26, 50, 14
END

DELETEDLG DIALOG DISCARDABLE  20, 20, 186, 47
    STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
    CAPTION "Delete"
    FONT 8, "MS Sans Serif"
    BEGIN
    LTEXT           "&Delete:", IDC_STATIC, 7, 7, 26, 9
    EDITTEXT        IDC_STRING, 31, 7, 148, 12, ES_AUTOHSCROLL
    DEFPUSHBUTTON   "OK", IDOK, 32, 26, 50, 14
    PUSHBUTTON      "Cancel", IDCANCEL, 104, 26, 50, 14
END

/
//
// Menu
//

STRPROG MENU DISCARDABLE
BEGIN
    MENUITEM "&Enter!", IDM_ENTER
    MENUITEM "&Delete!", IDM_DELETE
END
RESOURCE.H (excerpts)

// Microsoft Visual C++ generated include file.
// Used by StrProg.rc
#define IDC_STRING                      1000
#define IDM_ENTER                       40001
#define IDM_DELETE                      40002
#define IDC_STATIC                      -1

        STRPROG.C 包括 STRLIB.H 头文件,这个头文件定义了 STRPROG 将调用的 STRLIB 的三个函数。

        这个程序最有趣的地方,在于同时运行多个 STRPROG 实例时的表现。STRLIB 把字符串及其指针存储于共享内存中,让所有 STRPROG 的实例分享此数据。下面来看看它的具体实现。

21.1.7  多个 STRPROG 实例共享数据

        Windows 在 Win32 进程的地址空间周围构造了一堵墙。通常情况下,一个地址空间的数据是私有的,其他进程看不见。但运行多个 STRPROG 的实例则表明,STRLIB 允许在该程序

的所有实例之间共享其数据。当你从一个 STRPROG 窗口添加或删除一个字符串时,这个改变会立即反映在其他窗口中。

        STRLIB 在所有实例中共享两个变量:一个字符串数组和一个用来表示存储中有效字符串个数的整数。STRLIB 在一个特殊的共享内存区中保存着这两个变量:

#pragma data_seg ("shared")
int        iTotal = 0;
WCHAR    szStrings[MAX_STRINGS][MAX_LENGTH + 1] = { '\0' };
#pragma data_seg()
第一条 #pragma 语句声明创建数据区,这里命名为 shared。你也可以选择任意名称。在这条 #pragma 语句后定义的所有已初始化变量都被放入 shared 区。第二条 #pragma 表示数据区定义的结束。 重要的是变量都要专门初始化,否则编译器将把它们放在通常的未初始化的区中,而不是 shared 区

        链接程序必须知道 shared 区。在 Project Settings 对话框中,选择 Link 标签。在 STRLIB 的 Project Options 字段(包括发行和调试配置),包括如下链接选项:

/SECTION:shared,RWS
RWS 表面该数据区具有可读、可写和可共享的属性。或者,也可以直接再 DLL 的源代码中指定链接选项,就像 STRLIB.C 中的那样:

#pragma comment(linker, "/SECTION:shared,RWS")
该共享内存区允许 iTotal 变量和 szStrings 字符串数组被所有 STRLIB 实例共享。由于 MAX_STRINGS 等于 256 而 MAX_LENGTH 等于 63,所以共享内存区共有 32772 字节长——包括 iTotal 的 4 个字节和每个需要 128 字节的 256 个指针。

        采用共享内存区可能是多个应用程序中共享数据的最方便的方式。如果你需要动态分配共享内存空间,则应该考虑使用文件映射对象。相关详情请参考 /Platform SDK/Windows  Base Services/Interprocess Communication/File Mapping(译注:/MSDN Library/Win32 and COM Development/System Services/Memory Management/File Mapping)。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值