Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

12.3 Windows API

API的全称为Application Programming Interface,即应用程序编程接口。因此API不是一个专门的事物,而是一系列事物的总称。但是我们通常在Windows下提到API时,一般就是指Windows 系统提供给应用程序的接口,即Windows API。

Windows API是指Windows操作系统提供给应用程序开发者的最底层的、最直接与Windows打交道的接口。在Windows操作系统下,CRT是建立在Windows API之上的。另外还有很多对Windows API的各种包装库,MFC就是很著名的一种以C++形式封装的库。

很多操作系统是以系统调用作为应用程序最底层的,而Windows的最底层接口是Windows API。Windows API是Windows编程的基础,尽管Windows的内核提供了数百个系统调用(Windows又把系统调用称作系统服务(System Service)),但是出于种种原因,微软并没有将这些系统调用公开,而在这些系统调用之上,建立了这样一个API层,让程序员只能调用API层的函数,而不是如Linux一般直接使用系统调用。Windows在加入API层以后,一个普通的fwrite()的调用路径如图12-9所示。


图12-9 Linux和Windows的fwrite路径

12.3.1 Windows API概览

Windows API是以DLL导出函数的形式暴露给应用程序开发者的。它被包含在诸多的系统DLL内,规模上非常庞大,所有的导出函数大约有数千个(以Windows XP为例)。微软把这些Windows API DLL导出函数的声明的头文件、导出库、相关文件和工具一起提供给开发者,并让它们成为Software Development Kit(SDK)。

SDK可以单独地在微软的官方网站下载,也可能被集成到Visual Studio这样的开发工具中。当我们安装了Visual Studio后,可以在SDK的安装目录下找到所有的Windows API函数声明。其中有一个头文件"Windows.h"包含了Windows API的核心部分,只要我们在程序里面包含了它,就可以使用Windows API的核心部分了。

Windows API 版本

Windows API随着Windows 版本的升级也经历了好几个版本,每次Windows进行大升级的时候,也会引入新版本的API。最早期的Windows API是Win16,即16位Windows(Windows 3.x系列)所提供的API,Win16的核心部分是由3个16位DLL提供的:kernel.exe(或kernel286.exe或kernel386.exe)、user.exe和gdi.exe(虽然扩展名是exe,但实际上它们有导出函数,再说DLL和EXE其实就是一回事嘛)。

伴随32位Windows的API是Win32,它主要有3个核心DLL:kernel32.dll、user32.dll和gdi32.dll。Windows 3.x为了支持一部分Win32程序,还提供了一个Win32的子集叫做Win32s(s为Subset,即子集)。

64位的Windows提供了兼容Win32的API,被称为Win64。Win64与Win32没有增加接口的数量,只是所有的指针类型都改成了64位。

因为Win32是使用最广泛也是最成熟的Windows API版本,下文中如果我们不额外注明,则默认为Win32。

Windows API现在的数量已经十分庞大,它们按照功能被划分成了几大类别,如表12-2所示。



表12-2

我们可以在MSDN里找到每一个API的文档,很多API还可以找到使用示例,因此MSDN是学习Win32 API极佳的工具。

表12-2中所列的Kernel32.dll和User32.dll等DLL在不同的Windows平台上的实现都不一样,虽然它们暴露给应用程序的接口是一样的。在Windows NT系列的平台上,这些DLL在实现上都会依赖于一个更为底层的DLL叫做NTDLL.DLL,然后由NTDLL.DLL进行系统调用。NTDLL.DLL把Windows NT内核的系统调用包装了起来,它实际上是Windows系统用户层面的最底层,所有的DLL都是通过调用NTDLL.DLL,由它进行系统调用的。NTDLL.DLL的导出函数对于应用程序开发者是不公开的,原则上应用程序不应该直接使用NTDLL.DLL中的任何导出函数。我们可以根据dumpbin等工具来察看它的导出函数,比如Windows XP的NTDLL.dll大约有1 300个导出函数。它所导出的函数大多都以"Nt"开头,并提供给那些API DLL使用以实现系统功能,比如创建进程的函数叫做NtCreateProcess,位于Kernel32.dll的CreateProcess这个API就是通过NtCreateProcess实现的。

由于Windows API所提供的接口还是相对比较原始的,比如它所提供的网络相关的接口仅仅是socket级别的操作,如果用户要通过API访问HTTP资源,还需要自己实现HTTP协议,所以直接使用API进行程序开发往往效率较低。Windows系统在API之上建立了很多应用模块,这些应用模块是对Windows API的功能的扩展,比如对HTTP/FTP等协议进行包装的Internet模块(wininet.dll)对WinSocket API进行了扩展,这样程序开发者就可以通过Internet模块直接访问HTTP/FTP资源,而不需要自己实现一套HTTP/FTP协议。除了wininet.dll之外,Windows还有许多类似的对Windows API的包装模块,比如OPENGL模块、ODBC(统一的数据库接口)、WIA(数字图像设备接口)等。

12.3.2 为什么要使用Windows API

能省一事则省一事,微软为什么放着好好的系统调用不用,又要在CRT和系统调用之间增加一层Windows API层呢?

微软不公开系统调用而决定使用Windows API作为程序接口的原因也很简单,其实还是第1章里的"要解决问题就加层的万能法则"。Windows作为一个成功的商业操作系统,它对应用程序的向后兼容性可以说是非常好,这一点从Windows XP等这种较新的Windows版本还仍然支持20多年前的DOS程序/Windows 3.1/Windows 95的程序可以看出来。虽然它没有完全做到向后兼容,但是我们看得出Windows系统为向后兼容所付出的努力及Windows系统为此所背负的历史包袱。

系统调用实际上是非常依赖于硬件结构的一种接口,它受到硬件的严格限制,比如寄存器的数量、调用时的参数传递、中断号、堆栈切换等,都与硬件密切相关。如果硬件结构稍微发生改变,大量的应用程序可能就会出现问题(特别是那些与CRT静态链接在一起的)。那么直接使用系统调用作为程序接口的系统,它的应用程序在不同硬件平台间的兼容性也是存在较大问题的。

硬件结构发生改变虽然较少见,可能几年甚至十几年才会发生一次,比如16位CPU升级至32位,32位升级至64位,或者由Sysenter/Sysexit代替中断等,但是一旦发生改变,所付出的代价无疑是惊人的。

为了尽量隔离硬件结构的不同而导致的程序兼容性问题,Windows系统把系统调用包装了起来,使用DLL导出函数作为应用程序的唯一可用的接口暴露给用户。这样可以让内核随版本自由地改变系统调用接口,只要让API层不改变,用户程序就可以完全无碍地运行在新的系统上。

除了隔离硬件结构不同之外,Windows本身也有可能使用不同版本的内核,比如微软在Windows 2000之前要同时维护两条Windows产品线:Windows 9x和Windows NT系列。它们使用的是完全不同的Windows内核,所以系统调用的接口自然也是不一样的。如果应用程序都是直接使用系统调用,那么后来Windows 9x和Windows NT这两条产品线合并成Windows 2000的时候估计不会像现在这么顺利。

Windows API以DLL导出函数的形式存在也自然是水到渠成,我们知道DLL作为Windows系统的最基本的模块组织形式,它有着良好的接口定义和灵活的组合方式。DLL基本上是Windows系统上很多高级接口和程序设计方法的基石,包括内核与驱动程序、COM、OLE、ActiveX等都是基于DLL技术的。

银弹

很多时候人们把这种通过在软件体系结构中增加层以解决兼容性问题的做法又叫做"银弹"。古老相传,只有银弹(silver bullet)才能杀死巫士、巨人、有魔力的动物,譬如狼人。在现代软件工程的巨著《人月神话》中,作者把规模越来越大的软件开发项目比作无法控制的怪物,希望有一样技术,能够像银弹彻底杀死狼人那样,彻底解决这个问题。因而现在计算机界中的银弹,指的就是能够迅速解决各种问题的"万灵药"。

当某个软件某个层面要发生变化,却要保持与之相关联的另一方面不变时,加一个中间层即可。Windows API层就是这样的一个"银弹"。

Windows API的实例

我们知道Windows NT系列与Windows 9x系列是两个内核完全不同的操作系统,它们分别属于两个不同的Windows产品线,前者的目的主要为商业应用,它的内核以稳定高效著称;而后者是以家庭和多媒体应用为目标,注重体系应用程序的兼容性(支持DOS程序)和多媒体功能。

当Windows 版本升级至2000时,微软计划停止Windows 9x系列产品,而将Windows统一建立在较可靠的NT内核之上。这时候两条产品线将合并成同一个Windows版本,即Windows 2000。Windows 2000就必须承担起能够同时兼容Windows 9x和之前Window NT的应用程序的任务。由于Windows 2000使用的是NT的内核(内核版本5.0),所以要做到兼容之前的Windows NT(NT 4.0及之前)的应用程序应该不是很成问题的。但是要兼容Windows 9x则不是一件容易的事,因为它的内核与NT完全不同,它们各自使用的中断号都不一样,NT内核使用的是INT 0x2E,而9x内核则使用INT 0x20,所以,如果某个9x的应用程序一旦使用了任何系统调用,那么它就无法在Windows 2000下运行。

除了它们的内核中断号不同以外,即使同一个接口,有可能参数也不同。

Windows 9x系统的内核是并不原生支持unicode的,因此它的系统调用涉及的字符串都是ANSI字符串,即参数都是使用char*作为类型,比如与CreateFile这个API相对应的系统调用要传入一个文件名,那么这个字符串在最终传递给内核时应该是一个ANSI字符串。而Windows NT内核是原生支持unicode的,所有的系统调用涉及的字符串相关的参数都是unicode字符串,即参数是wchar_t*类型的(wchar_t是一种双字节的字符类型)。那么同样的系统调用,所需要的字符串类型却不一样,这也会造成程序兼容性的问题。

幸运的是,Windows API层阻止了这样的事情发生。大家如果留意的话,会注意到Windows 下所有有字符串作为参数的API都会有两个版本,一个是ANSI字符串版本,另外一个是unicode字符串版本。例如,与Windows API 的CreateFile相对应的两个版本分别为CreateFileA和CreateFileW,"A"表示ANSI版,"W"表示宽字符(Wide character),即unicode版,kernel32.dll实际上导出了这两个函数,而CreateFile仅仅是一个宏定义。下面的代码摘自Windows SDK的"winbase.h":

WINBASEAPI
HANDLE
WINAPI
CreateFileA(
    IN LPCSTR lpFileName,
    IN DWORD dwDesiredAccess,
    IN DWORD dwShareMode,
    IN LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    IN DWORD dwCreationDisposition,
    IN DWORD dwFlagsAndAttributes,
    IN HANDLE hTemplateFile
    );
WINBASEAPI
HANDLE
WINAPI
CreateFileW(
    IN LPCWSTR lpFileName,
    IN DWORD dwDesiredAccess,
    IN DWORD dwShareMode,
    IN LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    IN DWORD dwCreationDisposition,
    IN DWORD dwFlagsAndAttributes,
    IN HANDLE hTemplateFile
    );
#ifdef UNICODE
#define CreateFile  CreateFileW
#else
#define CreateFile  CreateFileA
#endif

可见根据编译的时候是否定义UNICODE这个宏,CreateFile会被展开为CreateFileW或CreateFileA,而这两个函数唯一的区别就是第一个参数lpFileName的类型不同,分别为LPCWSTR和LPCSTR,即const wchar_t*和const char*。CreateFileA/CreateFileW这个API才是真正的Windows API导出函数,它们在不同的操作系统版本上实现会有所不同。

例如在Windows 2000下,由于NT内核只支持unicode版的系统调用,所以CreateFileW的实现是最直接的,它只要直接调用内核即可。而CreateFileA则在实现上需要把第一个参数从ANSI字符串转换成unicode字符串(Windows提供了MultiByteToWideChar这样的API用于转换不同编码的字符串),然后再调用CreateFileW。Windows 2000的kernel32.dll中的CreateFileA的实现大概如下面的代码所示:

HANDLE STDCALL CreateFileA (
    LPCSTR lpFileName,
    DWORD dwDesiredAccess,
    DWORD dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD dwCreationDisposition,
    DWORD dwFlagsAndAttributes,
    HANDLE hTemplateFile)
{
    PWCHAR FileNameW;
    HANDLE FileHandle;

    // ANSI to UNICODE
    FileNameW = MultiByteToWideChar( lpFileName );

    FileHandle = CreateFileW (FileNameW,
                          dwDesiredAccess,
                          dwShareMode,
                          lpSecurityAttributes,
                          dwCreationDisposition,
                          dwFlagsAndAttributes,
                          hTemplateFile);

    return FileHandle;
}

对上面的代码我们进行了简化,但是它表达的思想与实际的实现是一致的。可以想象,在Windows 9x的kernel32.dll所进行的恰恰是相反的步骤,CreateFileW函数中的宽字符串通过WideCharToMultiByte()被转换成了ANSI字符串,然后调用CreateFileA。API层在这一过程中所扮演的角色可以如图12-10所示。


图12-10 Windows NT和Windows 9x的API层次结构对比

所以不管内核如何改变接口,只要维持API层面的接口不变,理论上所有的应用程序都不用重新编译就可以正常运行,这也是Windows API存在的主要原因。

12.3.3 API与子系统

作为一个商业操作系统,应用程序兼容性是评价操作系统是否有竞争力最重要的指标之一。一方面从用户的角度看,如果一个商业操作系统只能运行数量很少的应用程序,是不会有人使用的;从应用程序的开发者角度看,他们投入了巨大的精力在应用程序上,如果操作系统不支持这些应用程序,无疑会使开发者的努力白费。微软最初在开发Windows NT的时候除了考虑向后兼容性之外(兼容其他版本Windows),它还考虑到了兼容Windows之外的操作系统。

为了操作系统的兼容性,微软试图让Windows NT能够支持其他操作系统上的应用程序。在设计Windows NT的时候,与它同一时期的操作系统有各种UNIX(posix标准)、IBM的OS/2、微软自家的DOS和Windows 3.x等。于是Windows NT提出子系统(Subsystem)的概念,希望提供各种操作系统的执行环境,以兼容它们的应用程序。

子系统又称为Windows环境子系统(Evironment Subsystem),简称子系统(Subsystem)。我们知道,原生的Windows程序是通过CreateProcess这个API来创建进程的,而UNIX的程序则是通过fork()来创建的,子系统就是这样一个中间层,它使用Windows的API来模拟fork()这样的系统调用,使得应用程序看起来与UNIX没有区别。

子系统实际上又是Windows架设在API和应用程序之间的另一个中间层。前面讲到API这个中间层是为了防止内核系统调用层发生变化导致用户程序也必须随之变化而增加的,而子系统则是用来为各种不同平台的应用程序创建与它们兼容的运行环境。

当然,子系统要实现二进制级别的兼容性是十分困难的,于是它的目标就是源代码级别的兼容。也就是说每个子系统必须实现目标操作系统的所有接口,比如Windows NT要创建一个能够运行UNIX应用程序的子系统,它必须实现UNIX的所有系统调用在C语言源代码层面的接口。

在Windows里,最开始支持3种子系统:Win32子系统、POSIX子系统和OS/2子系统,而OS/2子系统在Windows 2000里已经被去除。DOS程序和16位Windows程序也是通过类似于子系统的模式实现在32位Windows下运行的。16位的Windows程序运行在32位Windows下被称为WoW(Windows On Windows),这使我们联想到现在32位Windows程序运行于64位的Windows操作系统,也是通过WoW技术实现的。

和内核直接打交道的只有Win32子系统,其他的子系统如Posix子系统和OS/2子系统都是直接将请求发送给Win32子系统处理。Win32子系统在系统运行的时候始终是运行的,而其他的子系统则是在需要的时候才启动。

后来随着Windows的市场地位逐渐巩固,它对于兼容其他操作系统和早期的DOS/Windows 3.1及Windows 9x的应用程序的需求已经极大地减弱,现在运行于Windows系统上的应用软件基本上都是使用Win32子系统的程序,所以子系统的概念已经逐渐地被弱化,除了Win32子系统之外,其他的子系统基本上形同虚设。我们在本书中提及子系统这一概念,也仅仅是为了帮助读者了解一些背景,以便于在Windows系统下碰到相关内容时不至于困惑,但并不打算深入介绍它,因为Windows子系统在实际上已经被抛弃了。