参考资料:

1. http://www.codeforge.cn/read/146318/WinDef.h__html

windef.h头文件

2. http://www.codeforge.cn/read/146318/WinNT.h__html

winnt.h头文件

3. https://msdn.microsoft.com/en-us/library/windows/desktop/aa383681%28v=vs.85%29.aspx

微软官网中关于STRICT的内容

4.http://wenku.baidu.com/link?url=j0ubLizIjhgmxthACfwBa4IpXrdqyyFg84a9MPmwusN4XhalR94kVaDAeR6GlFCMVD_AQORTLyfEC84-tqUWo27dziBXKjNdAqXe8Ich0eu

C语言宏中"#"和"##"的用法

5. http://www.cnblogs.com/kerwinshaw/archive/2009/02/02/1382428.html

typedef和#define的用法与区别

6. http://blog.csdn.net/geekcome/article/details/6249151

void及void指针含义的深刻解析

写在前面:

本文是对在下上一篇文章《图解说明——究竟什么是Windows句柄》的扩充。同样地,本文依然是面向初学者的。让编程语言变得平易,让初学者学起来你更加舒服,在交流中与广大读者同勉共进,是在下的一贯宗旨和追求。所以,在对源码的解释中,在下会尽可能做到详细,具体到句,对于与句柄不是特别相关的内容,也会加以解释说明。和上一篇一样,我们仍然把窗口、位图、画笔等统称为对象。

还有一点必须要交代,在下对Windows句柄这一细节的研究,还存在一些疑问,这将在文末进一步说明。

先看winnt.h中关于HANDLE(句柄)的定义:

typedef void *PVOID;

#ifdef STRICT

typedef void *HANDLE;

#define DECLARE_HANDLE(name) struct name##__ {

int unused;

};

typedef struct name##__ *name

#else

typedef PVOID HANDLE;

#define DECLARE_HANDLE(name) typedef HANDLE name

#endif

以上代码typedef void *PVOID;来自winnt.h(参考资料2)中的第178行,其余代码来自winnt.h中的第285~293行,考虑到易读性,在下对代码格式稍稍做了调整。

在分析源代码之前,再说一点,那就是typedef和#define的区别的问题。typedef用来定义一个标识符及关键字的别名,而#define是宏定义,简单说,就是字符串替换。如果有读者还不是很明白,可以参阅参考资料5。在下面的叙述中,我们将两者都译为“定义”。因为在下觉得这样可以带来叙述上的方便,并且如果大家理解了typedef和#define的区别,这样做并不会造成理解上的误会。

下面我们开始逐句分析代码。

首先,typedef void *PVOID;,这里将PVOID定义为void*型,以后,PVOID a,b;就相当于void *a,*b;(注意不是void *a,b;)。这里再简单说一下void*,简单说,void *就是“无类型指针”,可以指向任何数据类型,详情可参阅参考资料6。

下面的一段总体上是if——else结构。我们先看if部分。

#ifdef STRICT

如果定义了STRICT,就执行后面的代码。关于STRICT,在后面我们还会进行详细的讲解,这里我们暂时将其跳过,先看条件成立时的代码。

#define DECLARE_HANDLE(name) struct name##__ {

int unused;

};

这里是一个带参数的宏定义,name是参数,##为粘贴符号,表示把左右两边的内容连接起来。关于带参数的宏定义和##,读者可以参阅参考资料4。

这里将结构体

struct name##__

{

int unused;

};

定义为

DECLARE_HANDLE(name)。

接下来,

typedef struct name##__ *name

定义一个指针name,指向上面的结构体name##__。

下面我们以窗口句柄HWND为例,进一步说明。

在windef.h头文件(见参考资料1)的第196行有代码

DECLARE_HANDLE            (HWND);

我们将宏展开,就是

struct HWND__

{

int unused;

};

同样,根据typedef struct name##__ *name

有typedef struct HWND__ *HWND。

即句柄HWND是一个指针,指向结构体struct HWND__。

其它句柄的定义与HWND类似,这里不再赘述,读者可以参阅参考资料1中从195行往后的代码。

注意这里我们忽略了一个细节,那就是结构体中的int unused。关于这一点,我们先暂时忽略,在后面的“尚未解决”板块,在下将对这一问题作出交代。

有了前边的经验,分析else部分的代码就变得容易了,让我们一起来看。

typedef PVOID HANDLE;

由于前边有typedef void *PVOID;,所以这里HANDLE被定义为void*型。

接着,

#define DECLARE_HANDLE(name) typedef HANDLE name

这里将

typedef HANDLE name

定义为

DECLARE_HANDLE(name)。

还以HWND为例,在这种情况下,

DECLARE_HANDLE            (HWND);

宏展开为

typedef HANDLE HWND,

即此时HWND为void*型。

好了,说完这些,我们着重说一下STRICT。相关内容请参阅参考资料3。

在windef.h头文件的第13~17行定义了STRICT,源代码如下:

#ifndef NO_STRICT

#ifndef STRICT

#define STRICT 1

#endif

#endif /* NO_STRICT */

这里仅仅是将STRICT定义为数值1,看不出什么名堂。关键在于编译器(注意不是系统)对STRICT的“解释”。

顾名思义,STRICT是“严格”、“严厉”的意思。当编译器“看到”定义了STRICT后,就会对Windows 应用程序中使用的句柄进行严格的类型检查。Windows官网中的原文为Enabling STRICT redefines certain data types so that the compiler does not permit assignment from one type to another without an explicit cast.

也就是说,如果定义了STRICT,除非显式强制类型转换,否则不允许将数据从一种类型转化到另一种类型。换句话说,定义STRICT可以禁止隐式类型转换。

那么,这是怎么实现的,又有什么用处呢? 以窗口句柄HWND和钩子句柄HHOOk为例。在windef.h头文件的第196、197行定义了HWND和HHOOK:

DECLARE_HANDLE            (HWND);

DECLARE_HANDLE            (HHOOK);

通过前面的分析,我们知道,如果定义了STRICT,那么自然执行#ifdef STRICT后的代码,这样,HWND就是HWND__*型的指针,而HHOOK就是HHOOK__*型的指针,两者类型不同。如果没有定义STRICT,那么将执行#else后的代码,可以发现,这段代码直接将所有句柄都定义为HANDLE,即PVOID,也就是void*型。在这种情况下,上面的HWND和HHOOK都是void*型,类型相同。那么,两种情况下有什么差别呢?我们举例说明。

现在一个函数要求一个HHOOK类型的参数,而我们传给它一个HWND类型的参数。在没有define STRICT的情况下,这将是合法的,因为HHOOK和HWND都是void*类型。而如果我们定义了STRICT,HHOOK和HWND就是两个不同类型的指针,上面的参数传递将变为不合法,并且在编译阶段就会报错,这就避免了直到程序出现了运行时错误,程序员才知道代码有错的情况。

顺便说一句,现在VC、VS都define了STRICT,即都默认进行严格的类型检查。

至此,相信大家已经明白了STRICT的作用以及为什么不直接用int unused而要用结构体将其封装起来。

最后,让我们一言以蔽之,来总结一下Windows句柄的本质:

      Windows句柄本质上就是一个指向结构体的指针(define STRICT的情况下)

而所谓“指针的指针”的说法并不正确,这只是一个逻辑上的理解。

尚未解决:

现在,让我们回到前面忽略掉的关于int unused的问题上来。

如果有读者看过了在下的上一篇文章《图解说明——究竟什么是Windows句柄》,那么相信有人会和在下最初的想法一样,认为这里的unused就是我们说的区域A。句柄指向一个结构体,而这个结构体中唯一的数据unused中存放着对象的地址(虽然unused不是指针类型,但int和指针同为4个字节,将对象的地址存到unused里,将来再用某种方式通过unused找到该对象,这也是可以实现的),这与我们先前的图示恰好吻合。但稍加琢磨,我们发现这样解释在某些地方还是有些说不过去的。理由起码有2:

①首先是名字问题,相信稍微细心的读者就会发现这一问题。通过前边的源代码看,每一个名字都恰如其分地反映了它应有的意义,照这么看,结构体中的int变量存放了一个有用的地址,那它就不应该叫unused。

②如果unused相当于区域A的话,在define STRICT的情况下,句柄指向了区域A,而在没有define STRICT的情况下,并没有定义结构体,句柄被定义为void*型,那么,这种情况下的区域A又在哪里,句柄又如何指向它?

所以,综合前面的分析,在下认为,unused并不是区域A。关于int unused,在下的一个猜想是:

进程创建时,系统在内存的一个地方存放各个对象的地址,同时系统为各个对象指定句柄,存放在内存中另一个地方,并使各个句柄指向相应对象的地址(即区域A)。至于如何指向,很可能是这样:

对于未define STRICT的情况,直接指向就可以,因为此时句柄是void*型。而对于define了STRICT的情况,可能采用强制类型转换或是相似的手段来使原先指向结构体的句柄指向一个32位的地址。注意到,原先句柄指向一个结构体,而这个结构体中只有一个int型数据,从内存的角度看,句柄其实指向了一段4字节的内存,而后来,句柄指向32位的地址,同样是指向一段4字节的内存。而如果我们去掉intunused,只保留一个空结构体,我们知道,空结构体占1个字节(对这一点有疑问的读者,可以写一个空结构体,用sizeof()函数实测一下),此时句柄指向1个字节的内存,而现在要让它指向4个字节的内存(32位地址),很可能会无法“转化”。然而,如果转化前后,句柄都指向4个字节的内存,那很可能就能够转化。所以,int unused的作用就是使句柄指向一个4字节的内存,以便将来句柄指向对象的地址时能够顺利“转化”。而从始至终,unused从来没有被显式地使用过,所以取名为unused。显然,这里unused的意思是“未被使用的”,而非“没用的”。

至此,所有关于windows句柄这一细节的内容都讲解完了。

写在后面:

1.在下知识浅薄、能力有限,讲解过程中难免有错误疏漏之处,这里恳请大家务必批评指正,在下先行谢过。

2.遗憾的是,到最后,还是有一些疑问没有解决。这里在下请求路过的大神不吝赐教,也诚望广大读者各抒己见,让我们一起思考,共同进步。

源码剖析——深入Windows句柄本质的更多相关文章

  1. Nodejs事件引擎libuv源码剖析之:句柄(handle)结构的设计剖析

    声明:本文为原创博文,转载请注明出处. 句柄(handle)代表一种对持有资源的索引,句柄的叫法在window上较多,在unix/linux等系统上大多称之为描述符,为了抽象不同平台的差异,libuv ...

  2. socket_server源码剖析、python作用域、IO多路复用

    本节内容: 课前准备知识: 函数嵌套函数的使用方法: 我们在使用函数嵌套函数的时候,是学习装饰器的时候,出现过,由一个函数返回值是一个函数体情况. 我们在使用函数嵌套函数的时候,最好也这么写. def ...

  3. 侯捷STL课程及源码剖析学习1

    1.C++标准库和STL C++标准库以header files形式呈现: C++标准库的header files不带后缀名(.h),例如#include <vector> 新式C hea ...

  4. Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

    声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...

  5. STL"源码"剖析-重点知识总结

    STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...

  6. 自己实现多线程的socket,socketserver源码剖析

    1,IO多路复用 三种多路复用的机制:select.poll.epoll 用的多的两个:select和epoll 简单的说就是:1,select和poll所有平台都支持,epoll只有linux支持2 ...

  7. Node 进阶:express 默认日志组件 morgan 从入门使用到源码剖析

    本文摘录自个人总结<Nodejs学习笔记>,更多章节及更新,请访问 github主页地址.欢迎加群交流,群号 197339705. 章节概览 morgan是express默认的日志中间件, ...

  8. DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求

    转载:http://blog.csdn.net/zssureqh/article/details/39213817 背景: 上一篇专栏博文中针对PACS终端(或设备终端,如CT设备)与RIS系统之间w ...

  9. 【转载】STL"源码"剖析-重点知识总结

    原文:STL"源码"剖析-重点知识总结 STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点 ...

随机推荐

  1. docker好文收藏

    深入浅出Docker(一):Docker核心技术预览 2. 核心技术预览 Docker核心是一个操作系统级虚拟化方法, 理解起来可能并不像VM那样直观.我们从虚拟化方法的四个方面:隔离性.可配额/可度 ...

  2. kafka Failed to send messages after 3 tries 问题解决

    kafka Failed to send messages after 3 tries. 在kafka0.8开发过程中 生产者测试用例碰到了 Exception in thread "mai ...

  3. servlet的匹配规则,兼谈/与/*

    客户端通过URL地址访问服务器(servlet容器)资源,所以servlet若要能对外提供服务,必须要将程序按照java规范将其映射到对应的URL上,映射的规则是需要开发人员在WEB.XML中显示指定 ...

  4. git pull 冲突解决

    这个意思是说更新下来的内容和本地修改的内容有冲突,先提交你的改变或者先将本地修改暂时存储起来. 处理的方式非常简单,主要是使用git stash命令进行处理,分成以下几个步骤进行处理. 1.先将本地修 ...

  5. SSIS 连接ORACLE 无法从 SQL 命令中提取参数的解决方案

    第一步:  定义包变量:maxdate 类型为String  定义包变量:sqlStatement类型为String,值为:select * from i_out_serv_mon 第二步:  取&q ...

  6. HBA相关知识

    HBA使用详解: 一般的AIX客户端支持的HBA为Emulex HBA卡和交换机硬件确保连接成功的标志: A. 如果是 Emulex卡,卡上的绿灯常亮,黄灯闪烁. B. 如果是 QLogic卡,卡上的 ...

  7. oracle归档模式和非归档模式的切换

    Oracle从未归档日志改成归档日志: SQL> shutdown immediate; 数据库已经关闭. 已经卸载数据库. Oracle 例程已经关闭. SQL> startup mou ...

  8. linux scp 远程复制文件

    1.从本机复制文件到远程scp 文件名 远程计算机用户名@远程计算机的ip:远程计算机存放该文件的路径2.从远程复制文件到本机:scp 远程计算机用户名@远程计算机ip:文件名 存放该文件的本机路径3 ...

  9. 5.3监听请求:使用eclipse的tcp/ip工具(端口转换)

    1.改用wsdl文件生成响应文件 运行浏览器输入发布的地址,获得wsdl源码保存在项目路径下, 2.创建接口转换器,window-property-tcpip 客户端执行结果:

  10. linux Centos下安装 sqlserver

    我使用的是Centos7在虚拟机中完成测试 1.下载设置mssql的yum源,执行以下代码,现在sqlserver的linux版本130多兆,网速慢的请等待 curl https://packages ...