2009年2月28日星期六

Unicode, A programmer should know

首先说说这个标题,因为我最近非常迷星战,所以顺带也喜欢上了Yoda大师说话的腔调,以后可能会用的更多,情别见怪(反正也没人看)。

“曹操”为什么会变成“变巨”一文中,我提到了Unicode是多语言共存的解决方案,本文将详细介绍Unicode,以及为什么解决了各种令人头疼的编码问题。

那unicode解决了哪些问题呢?其实最主要的是这几个问题:

  1. 人们希望有一个单一的字符集,能包含这个星球上所有的字符
  2. 每个字符都有一个独立的编号,不会出现一个编号对应不同字符的事情
  3. 可以根据需要变换不同的编码方式,但是不会影响字符编号
那么看看unicode是怎么解决这些问题的呢?

Unicode 是一个国际标准,它主要包含了:
  • 超大的字符集,可以有1,000,000个字符,code space为[0x0-0x10FFFF]
  • 每个字符的code point编码方法
  • 一个标准的字符集
  • 一组字符属性的枚举值
  • 提供字符集的多种编码
有很多人把unicode 与utf8,utf16这些编码混为一谈,看完本文后,应该不会再存在这样的混淆了。

Unicode有其标准的书写格式。要表示一个字符的Unicode码,可以这样写:
U+0041 
这就是大写字母A的Unicode码,U+表示这是Unicode码,后面的十六进制数字是这个字符的unicode code point。比如hello的code point就是U+0048 U+0065 U+006C U+006C U+006F。可以看到,这些码值去掉前面的00,就跟ASCII码一样了,是的,unicode最大限度跟ASCII保持兼容。

Unicode的字符集可以说包括了这个星球上所有的字符。包括一些我们可能永远也不会遇到的字符。不过Unicode标准把一些最常用的字符放在0x0000-0xFFFF这个区间中,这样有些系统实现的时候就不需要用那么多空间来存放不常用的字符了。这个区间被称为BMP(Basic Multilingual Plane)。Unicode一共有17个Plane,有6个已经被占用,剩下的都是空的。如下表所示:

Plane RangeDescriptionAbbreviation
0 0000-FFFFBasic Multilingual PlaneBMP
1 10000-1FFFFSupplementary Multilingual PlaneSMP
2 20000-2FFFFSupplementary Ideographic PlaneSIP
3-13 30000-DFFFFcurrently unassigned
14 E0000-EFFFFSupplementary Special-purpose PlaneSSP
15 E0000-EFFFFSupplementary Private Use Area-A 
16 E0000-EFFFFSupplementary Private Use Area-B 

在BMP中的任何一个code point都用4位16进制数来表示。其他的plane中的code point用5-6位。

Unicode的编码问题是我们最关心的。Unicode允许使用不同的编码方式,也就是你平时看到的utf8, utf16之类的名字。他们的特点在于编码出来的值各不相同,但是解码之后,都对应到一个唯一的unicode code point。这样保证了unicode编码之间可以无损(在BMP中)转换以及映射(mapping)的唯一性。Unicode中把一个值的code point及其使用某种编码得到的码值称为一个映射(mapping)。

在unicode发展过程中,出现过多种编码。我这里就介绍几种主要的编码,在此过程中出现过两个组织,各自发展出一套映射方法,称为Unicode Transformation Format(UTF)和 Universal Character Set(UCS)。 后来两个组织合并在一起,UTF和UCS也随之合并了。简单的说它们之间的对应关系是UTF后面跟的是一个编码的位数,UCS后面跟的数字是编码的字节数(以八位作为一个字节),比如UTF-16和UCS2几乎是同一个东西。但实际上UCS2只实现了BMP内的字符编码,而UTF16则实现了所有unicode编码。但为了简单期间,我们在这里认为它们是一个东西。

UTF-16/UCS2
最直观的编码方式就是一一映射。一个unicode码对应一个UTF-16编码。比如上面所说的hello,其编码为00 48 00 65 00 6C 00 6C 00 6F。但是问题马上来了,在大端法(big endian)和小端法(little-endian)的机器上编码得到的串的顺序不一样。在大端法的机器上,同样hello经过编码得到的是48 00 65 00 6C 00 6C 00 6F 00。而48 00也是一个合法的unicode code point,因此把在大端法机器上保存的文件拿到小端法机器上读就会解析成完全不同的东西。人们于是想到用最前面的两个字节来标明字节顺序。这两个字同样是unicode码,U+FEFF,一般被称为BOM(Byte Order Mark)。FE FF表明大端法,FF FE表明小端法。

UTF-16使用2个字节来进行编码,因此只能编码到BMP的code point。对于其他的plane,UTF-16采用一种额外的编码方式,达到3,4字节的编码。但是一般情况下,UTF16指的就是2字节的编码。这样的编码非常方便,所有的字符都占用一个unsigned short,对于传统的c字符串函数就非常方便。

在windows上,为了方便utf16被直接称为unicode编码,与ANSI对应。你在使用notepade时可以保存成unicode,指的便是utf16。

下图显示了在linux上使用utf16编码



比较下在windows下使用utf16(unicode)编码,注意到BOM了吗?




UTF-8
UTF-8可以说是用得最广泛的编码方式了,很多Linux的发布版都使用UTF-8作为其系统编码。UTF-8的产生原因可能是由于有人觉得UTF-16太占用空间了。在美国英语系统下,每一个字符都有一个00,字符串大小凭空增加了一倍。于是人们研究出UTF-8这个可以兼容ASCII的变长编码方式。UTF8编码出来的字符占1-4字节。

具体占位如下图所示:


对于在U+0000到U+007F区间(是不是很熟,对了,就是ASCII码的区间)内字符,使用1个字节来编码,因此完全兼容ASCII码,而且省空间。

[U+0080, U+07FF] 区间内的字符,基本上是欧洲各国字符集,使用2个字节来编码。
图中的yyy是code point中的高位字节,由于最多到7,因此只要3个bit就可以编码。
图中的xxxxxxxx是code point中的低位字节,从00到FF,需要用的所有8个bit,在编码时,把低位的6个bit与10结合作为一个字节,剩下的2个bit与yyy,110结合作为一个字节。

[U+0800, U+FFFF] 区间内的字符,包括中文,日文,韩文等都使用3个byte来编码。详细的编码规则在图中已经解释的很清楚了,我这里不再重复。

下图显示了在linux使用utf8编码,于utf16的截图相比,utf8对曹操这两个中文各用了3个byte进行编码。



比较下在windows上使用utf8编码,首先是字节顺序不同。然后你还可以看到utf8的BOM,0xEF 0xBB 0xBF。这BOM不仅和系统,还和使用的软件有关。notepad无论什么编码都会加上BOM。



UTF32/UCS4
这是最浪费空间的一种编码方式,它也是一一映射,但是UTF32映射了所有的unicode code point。对于每一个code point,UTF32如其名字所示,使用32bit也就是4个字节来进行编码。比如A=00 00 00 41。

很多程序使用utf32作为内部编码,在保存的时候它可以无损的转换成其他编码。而且因为现在的硬件总线的关系,这种编码的寻址和读写更快。比如Apache的Xerce2 就是这么做得。



还有其他很多编码,比如国标的GB18030。只要符合unicode标准,你也可以创造自己的编码。


讲了这么多,最初的问题也算回答得差不多了。Jeffrey Richter[1], Joey Spolsky[2], Charles Petzold[3]等人都在各自的著作中强调unicode对现代编程的重要性。虽然我人微言轻,但是我还是想附和一句:unicode以及编码的知识应该是每一个专业的程序员需要了解的知识。现在,停止ANSI程序,开始用unicode编写你的正式程序吧。




注1:《Programming Applications for Microsoft Windows》, chapter 2, Jeffrey Richter
注2:《Joel on software》, Charpter 4, Joel Spolsky
注3:《Programming windows》,chapter 2, Charles Petzold

2009年2月27日星期五

一张14.74亿像素的照片

美国总统奥巴马就职大典downloading
你可以看清大部分人的脸
http://gigapan.org/viewGigapanFullscreen.php?auth=033ef14483ee899496648c2b4b06233c

下面是缩略图

2009年2月21日星期六

“曹操”为什么会变成“变巨”?


以前在PC上玩过三国志的人大多数都会记得一个奇怪的词——“变巨”。但大家都知道,那是曹操,因为头像放在那,一看就知道是曹操,一张枭雄的脸。可是为什么会显示成“变巨”呢?有些人会说,编码问题呗。是的,那到底是怎么回事儿呢?

这个问题的根本原因可以说是编码和解码方式不匹配

要解释这个问题,首先要解释下Code Page的概念。在Windows下,打开控制面板->区域和语言选项->高级,里面就有代码页转换表。我们在里面会看到类似936  (ANSI/OEM - 简体中文 GBK) 这样的选项。这就是一个Code Page(代码页)。

Code Page最早始于IBM,当时个人PC上只有ASCII码,用0x00-0x7F这128个code point(代码点)来编码控制字符,数字,大小写字母,特殊字符和打印符号等。但是很快ASCII码就无法适应需求了,更多的西欧字符要求被加入码表。于是出现了EBCDIC codepages。这些Code Pages扩充了ASCII码,成为256位的一个码表。前面0x00-0x7F与ASCII码保持一致,后面的128根据不同的语言定义不同的code page。微软一开始借用了这些CODE PAGE,之后用自己的ANSI(或者说ISO 8859-1)的code page作为补充,成为现在我们在Windows操作系统上的CodePage。

在Windows操作系统中,对CJK(Chinese,Japanese,Korean) 也用Code Page进行处理。但是中文等语言的字符实在太多,不可能用256位的code page来表示,那怎么办呢?一个直观的想法是用双字节来编码一个字符,理论上就可以安置65536个字符。也可以用多字节来表示,单字节部分可以兼容ASCII码,双字节部分编码CJK字符。在code page设计时是怎么考虑的我不知道,但最后还是采用了后一种方式。

看下图:


这是一张Windows Codepage 936(以下简称CP936)的示意图。前面0x00-0x7F和ASCII码一致,和Windows上其他的codepage保持一致。后面128个字节被称为lead byte,用来和另一个byte组合成一个CJK字符的编码。每个lead byte又对应一张子表,比如0x81这个lead byte对应的子表如下图所示:


于是汉字“丢”的GBK码就是0x8147。但是子表并不一定都是256个字符。理论上GBK码按照这种方式可以编码128*256个=32768个字符。但实际上中文字符要多的多。因此出现了其他更好的编码,这里先按下不说。Windows的设计的这个字符集被称为MBCS(Multi Byte Character Set)。

知道了codepage的概念,我们就可以解释“曹操”-“变巨”的问题了。为什么我开头说“编码和解码方式不匹配”呢?想想看,曹操是存放在什么地方的?用什么编码存放的?

对了,曹操两个字是存放在资源文件里面的。而我们玩的三国志是第三波汉化的繁体中文版,也就是说,曹操两个字是以BIG5码(CP950)进行编码的。从以下的CP950_B1可以看到,“曹”字的编码是:B1E4。而我们是在简体中文的Windows上玩这个游戏的,对于磁盘上存放的B1E4,我们用CP936 GBK码来解码的。从下面的CP936_B1可以看到B1E4正好就是“变”字。



同理可以知道操对应的是巨,繁体字赵云对应的简体字是化冻。

事实上,这小小的一个问题,牵涉到Locale,字符集,codepage,编码等各种问题。
而CodePage在使用过程中也暴露出很多问题;最主要的就是一个代码点(code point)在不同的codepage中代表不同的字符,变成了一对多的关系,导致在一个程序中没法使用多语言。不同的厂商在设计code page时缺少文档,不利于交流等。

解决这些问题的,就是Unicode。我会在最近的blog中详细介绍Unicode。因为Unicode是每个程序员需要了解的概念。

最后,我总结下Code Page的概念:
  • Code Page只在Windows和IBM的mainframe上有,Linux/Unix等操作系统并没有这个概念。
  • Code Page是为了解决多语言及本地化的问题而产生的。
  • 使用Code Page,无法在一个程序中支持多语言。
  • 在Windows中,一旦换了Code Page,我们需要重启。
  • Code Page提供了字符集和编码/解码方式。

更新于2009.2.28: 关于unicode的介绍已经写好了--Unicode, a programmer should know

雷到我了

From Reference Photos

不知道三分钟后,他们是不是全部下场了

2009年2月8日星期日

log to console when programming windows application

我们在开发Windows界面程序的时候往往需要打印一些运行过程中的调试信息。很多人都使用OutputDebugString来把信息打印到WINDOWS内部的调试器上。然后通过SysInternal的DebugView来查看。

今天我来介绍另外一个非常有用的方法--打开一个控制台窗口

Impossible! 你是在开发窗口程序,怎么可能打开控制台呢?或许有人会这样说。如果不可能,我还写这么多干什么?事实上只要2行代码就可以搞定:

AllocConsole();
freopen( "CONOUT$" , "a" , stdout );
这两行代码打开了一个和当前应用程序共享同一个进程空间的控制台。因此你可以简单的用cout来输出任何你想输出的信息。如下图:

不过一般在这两句话只会用在debug版本下,因此最好在外面加上对DEBUG版本的判断:
#ifdef _DEBUG
AllocConsole();
freopen( "CONOUT$" , "a" , stdout );
#endif
最后提一下,这两个窗口无论关掉哪一个,都会同时关闭另外一个,相当于同时退出进程。

该技巧在编写WinForm的时候也是适用的。所要做的只是把项目设置(Project Setting)中的Output Type改为"Console Application", 如下图:

有了这个Console,你就可以在程序中随意的使用System.Console.WriteLine输出调试信息了。

写windows程序的朋友不妨试一下。

WM_PAINT no end

今天在写程序的时候,发现一个自定义控件的WM_PAINT被不断的调用,以至于CPU占用率上升到40%以上。

我觉得很奇怪,我并没有移动窗口或者做什么引起重绘的操作,WM_PAINT为什么会被不断调用了。之后我发现一开始该控件没有不断重绘,只有第一次重绘之后才开始不断调用。
我的重绘代码是这样写的:

HDC hdc = GetDC(m_hWnd);
....do some drawing
Release(m_hWnd, hdc);
我很老实的遵守了调用DC的对称性规则,有创建也有释放。看来问题不在这里。

再一次仔细看了WM_PAINT的说明其中有几句引起了我的注意:
a WM_PAINT message may have been caused by both a non-NULL update region
仔细体会一下,就得出了这个问题的背后黑手--update region。

我曾在Windows绘图概述一文中写道:
事实上WM_PAINT产生的条件就是有部分窗口失效了。如果不出现这种情况,就不会产生WM_PAINT
当窗口的Client Area发生改变的时候,系统给应用程序发送WM_PAINT消息。系统仅在消息队列中没有其他消息的时候发送该消息给应用程序。一般的用法是在WM_PAINT中调用BeginPaint获得DC,然后进行任何GDI绘图,最后通过EndPaint释放DC
BeginPaint会把需要更新的区域设为NULL,以防连续导致发送WM_PAINT消息。
这里的文字已经很清楚的告诉了我原因,很可惜,这些文字是翻译自MSDN,没有经过自己亲身经历,因此没有深刻的体会。而这次,我正是犯了这个错误。

我接收到的WM_PAINT的消息是由InvalidateRect(NULL)导致的。该函数把整个Client区域都变成了update region.而我在WM_PAINT中又恰恰用了GetDC而不是BeginPaint函数来获取DC,因此导致处理完WM_PAINT后,失效区域没有更新,让Window以为我一直没有重绘,因此DefWindowProc中不断的重复发送WM_PAINT到message queue中,导致了无限循环。

解决的方法有两个,一个是在WM_PAINT中用BeginPaint和EndPaint来处理。另外一种是在WM_PAINT中使用ValidateRect函数。

Bjarne Stroustrup说过,一知半解的程序员是危险的,这个问题没有花去我很多时间,但是它告诉我,不要小看任何一个小小的函数。它或许牵涉到很大的问题。

2009年2月5日星期四

我的第一次就这么没了

好吧,我承认,我有点标题党了。

今天去体检,遇到了一个以前从来没有过的体检项目--查痔疮。结果第一次被人爆了菊花,这感觉真是恶心,不过还算值得,因为查出了轻微的外痔。犯我菊花者说让我不要久坐,常起来走动,少吃辛辣食物。看来我也要注意了,做我这个工作的坏处就是一直坐着,容易引起腰椎问题不说,也容易得痔疮,还容易引起前列腺问题。建议和我一样干程序员的朋友常起来走动走动。

另一个第一次就是第一次洗牙。洗牙这东西,我从来没有好好了解过,一直自以为是的认为是一种奢侈的行为,或者是那些牙齿泛黄的人为了美观才去做的事情。但是今天却是好好得上了一堂课。牙科医生检查了一下我的牙齿,然后让我拿着一面镜子看我的牙齿内侧,真是不看不知道,一看吓一跳。我牙齿内侧就像糊着一层泥一样恶心。医生说这就是牙结石,而且我的牙结石已经很厉害了。当牙齿上的牙结石增多,牙龈(我们常说的牙肉)就会被挤下去,慢慢露出牙根,牙龈对牙齿的保护作用就随之退化和消失。这样就会产生牙周炎,牙龈肿痛之类的问题,牙齿也会慢慢损坏。她建议我尽快做洗牙,把这些牙结石全部洗掉。同时告诉我,洗掉之后,牙齿间就会露出缝隙,因为原来牙龈的部分都已经被牙结石代替了,结石没了之后,牙龈不会立刻恢复。而且随着年龄的增加,牙龈的恢复能力会越来越差,所以这个事情越早越好。

她告诉我,在发达国家,一般人都有这个意识,每半年去检查一次牙齿,如果有牙结石就洗掉它,这是一种常识性的东西。很可笑的是,我活了快30年了,这是生平第一次听到医生跟我解释这个事情。有人可能说她想赚我钱,但是她至少说服了我,虽然洗牙还挺贵(300块全套,因为我比较严重),但是我还是心甘情愿的掏了钱。而且我了解了这个常识,以后我每半年会去检查一次看看。我好吃,所以我不想我的牙齿有什么问题,否则美食在眼前,却无福享用,那可比杀了我还难受啊。

洗牙的过程不多说了,稍微有点小痛。结束之后医生说我的牙结石钙化非常严重,很多地方要洗好几次才能洗掉。现在回想起来,我以前自己用舌头舔牙齿内侧的时候,总归感觉牙缝之间好像被什么东西填住了。还有两个牙齿根部好像有硬硬的东西,当时还傻乎乎的以为自己又长了小牙齿出来。后来在洗牙过程中,医生用力的在牙根中间冲刷,我才知道那是一块钙化物,是牙结石最顽固也是危害最大的地方。如果你和我有同样的经历,建议你去医院检查一下牙齿。

最后由于我门牙附近几颗牙齿钙化特别严重,导致洗牙之后牙龈大量出血,医生给我用了进口药(感觉有点骗钱,不过大刀也宰了,就不在乎这点小刀了)又加了150元。第一次洗牙总共花了450块。最后医生说牙龈恢复很慢,而且像我这种情况也不大可能完全恢复,这是我唯一遗憾的地方了。

医生最后建议我用软一点的牙刷,因为我不抽烟喝酒,因此色素很少,用硬的牙刷还容易损坏牙齿,刷牙的正确方法我想也不用我说了,大家都知道,要上下刷,不要左右刷,里外都要刷,不能只刷外面。

最后,建议从来没有洗过牙的朋友也去医院检查一下,有备无患么,说不定还可以体验你的“第一次”。