字符串资源的内部格式
相信很多人在第一次针对一个字符串资源使用FindResource的时候,都会这么调用:
FindResource(hinst, MAKEINTRESOURCE(IDS_STRINGID), RT_STRING);然后基本上你就会发现明明这个字符串是存在的,却就是找不到!是的,系统没问题,编译器没问题,你的眼睛也没有问题。事实上,正确的调用方法应该是:
FindResource(hinst, MAKEINTRESOURCE(IDS_STRINGID/16+1), RT_STRING);
这是由字符串资源特殊的组织方式决定的。
字符串和Section
让我们先来看一下MSDN中是怎么说的:
String resources are stored in sections of up to 16 strings per section. The strings in each section are stored as a sequence of counted (not null-terminated) Unicode strings.
每 16个连续的字符串组成一个section,而FindResource只会找到相应的section,而不是精确到你要找的字符串,因此需要做一个String ID到Section ID的转换:
SectionID = StringID/16 + 1
0-15是第一个section, 16到31是第二个section。一个section中只要有一个StringID是存在的,那么这个section就是有效的。所以当你调用FindResource时发现即使传入一个不存在的String ID时也能找到resource,不要觉得奇怪。(更多内容可以参考The format of string resources)
我们来看个例子,假设一个rc文件中定义了如下字符串资源:
IDS_STRING96 "" IDS_STRING97 "97" IDS_STRING99 "99"
IDS_STRING111 "111"
我们可以通过一个工具来查看其内容:
字符串ID为96, 其SectionID为96/16+1=7,这里我们可以看到在第7个block中所有的字符串,其中未定义的字符串全部为空。
但同时我们也注意到另外一个问题,即定义为空的字符串(如96)和未定义的字符串(如98)在这里都表示为空,也就是我们无法分辨一个空字符串与不存在的字符串,事实如此吗?
内存模型
我们可以通过rc文件编译出来的res中间文件来观察。以二进制的形式打开res文件并搜索字符串 "111":
我们看中间的红框,前两个字节"02 00"表示此字符串长度为2,"39 00"即为16进制0x39,是字符9的ASCII码,同理"37 00"表示字符7,即红框表示的正是字符串:"97"。于是我们可以推出两个绿框表示的内容:两个字节的"00 00"表示的就是字符串长度为0,所以空的字符串(96)和未定义的字符串(98)在目标文件中表示是一样的。因为最终的DLL或Exe是由链接此res中间文件产生,所以可以肯定最终运行时的内存模型中,这两者表示也是毫无二致的。
运行如下代码:
HRSRC hRsrc = FindResource(NULL,MAKEINTRESOURCE(96/16+1),RT_STRING);
HGLOBAL hg = LoadResource(NULL, hRsrc);
观察hg指向的内存地址:
和res文件中观察到的内容是一样的,也就是说,空串在编译的时候就已经被删除掉了。实质上,空字符串就是未定义字符串。
建议
这种把空串与未定义串混为一谈的作法在大多数情况下还是可以接受的,但在做本地化的时候就会遇到一些问题:
一个字符串在中文版中要显示相应的内容,而由于产品设计上的考虑,在英文版中需要显示为空字符串。这种本地化工作我们一般用替换资源DLL来完成的,真正LoadString的代码是同一份的,当我LoadString返回一个空串时,我该认为是正确的还是错误的呢?
如果认为是正确的,那么在中文版下字符串真的出错的情况就判断不出来了。
如果认为是错误的,那么在英文版下空字符串就永远是错的。
很显然,陷入这样的困境原因在于我们无法区分空串与未定义串,而其根本原因在于字符串内存模型的设计缺陷(可以理解其如此设计是可以节省内存空间和字符串加载时的速度)。
或许,我们可以对其稍加修改以解决这个问题:
空串:01 00 00 00
未定义串:00 00
这里,我们把空串解释为长度为1,内容为"00 00"(即"\0")的字符串,因为"\0"在字符串资源中本来就已经做了特殊处理,因此这样的修改不会引起什么冲突。如此,我们就能区分空串与未定义串了。而且,因为空串出现的几率比较小,对内存空间的消耗并不会增加多少。
为使这个方案能够工作,至少需要修改:
Windows API中的LoadString函数
Load不存在的字符串时报错(SetLastError)
VS中的Resource Compiler
把空串编译成 "01 00 00 00"