您的位置:首页 > 其它

剖析Xml4C源码,完美兼容中文XML

2005-03-03 20:34 537 查看
Xerces-C++及Xml4C这两套优秀的Xml解析器对多字节语言的兼容性问题一直是广大XML程序员心头无法愈合的“伤痛”。本文另辟蹊径,带着读者一步一步深入解析器源码,修正它的一处代码,从而完美解决了这个问题。在整个过程中,您还可以与笔者一起分享剖析源码的经验与乐趣。

一、背景介绍

在我们的数字生活中,XML称得上是一颗耀眼的明星。由于其开放性,越来越多的工业标准采用它作为描述语言;由于其平台无关性,越来越多的系统采用它作为数据传递媒介;由于其良好的数据变换能力,越来越多的集成软件使用它来整合不同的系统。随着XML及相关技术的逐渐成熟、大量基于XML的相关标准出台,更多的文档将以XML文件的形式出现,而拥有解析与处理XML文件的能力也将日益成为软件的必要功能之一。

二、问题的提出

提起C++的XML解析器,相信大多数人都会想起Xerces-C++和Xml4C这两个著名的开放源码项目。

Xerces-C++是Apache团队的开发成果,它不但严格遵循DOM与SAX规范,而且提供了良好的易用性和跨平台特性,并保证很高的执行效率,一直是国外很多项目的首选XML解析器。但对我们国内的C++程序员来说,对中文兼容性不好成了该解析器的致命缺陷,一直是我们又爱又恨的强大工具。其两条主要中文兼容性问题如下(以下缺陷针对Xerces-C++ 2.2.0,它也是目前最新的稳定版本):

支持的编码格式有限,GB2312是大多数中国人使用的本地编码,而Xerces-C++偏偏不支持,不能不说是一大遗憾。
对中文XML文档兼容性不够好,在处理中英文混合内容时,常常出现读出的字符串比原有字符串短的现象,并且不稳定,时好时坏,这个缺陷的确让人无法接受。

仅以上两条缺点就足以让我们无法选择该解析器,那Xml4C是否比Xercesc-C++有更上乘的表现呢?

Xml4C是IBM在Xerces-C++的基础上,结合自己的技术开发的XML解析器。它最大的优势不仅仅是与Xerces-C++有着完全相同的调用接口,更吸引人的是它支持更多的文字编码。因为IBM把自己完整的XML解析器技术整合到了Xerces-C++之上,所以支持的文字编码以10倍20倍甚至更大的的差距把Xerces-C++远远地抛在后面,这是该解析器的一大亮点。当然,它也直接支持GB2312编码的XML文档。对于中国程序员,这的确是一件天大的好事,但因为是在Xerces-C++基础上开发的,所以很不幸,它对中文XML文档内容的兼容性也有同样的问题。:-(

针对这些问题,很多勤劳的人们都在努力寻找着解决办法,网上的讨论更是多如牛毛,但一直没有太好的结论。在IBM developerWorks网站上,冯键博士在他的《如何利用Xerces-C++解析包含中文字符的XML文档》文章中,巧妙地使用了XMLFormatter和XMLFormatTarget两个类将XML文档先转换成本地字符编码再打印出来。这个办法虽然解决了XML文档的输出问题,但效率影响却不能忽视。众所周知,为了支持多语言,解析器的内部采用宽字符编码,而上述文中所示方法需要将整个XML文档的内容全部转成本地编码,转换的开销太大了。另外,因为使用了非标准的处理方法,所以解析器的用法变得比较复杂,处理文档内容时可能还会存在着这样那样的问题。

三、问题的解决

难道真的无法解决这个问题了吗?不,我们还有最后一招,分析源码!记得有位名人说过"源码之前,了无秘密",既然从外面已经找不出更好的答案,我们为什么不做一次内科手术,从内核"汉化"它,让它更适合我们呢?记住,我们的目标是:不需要修改任何调用方代码,让Xerces-C++和Xml4C支持中文。由于篇幅关系,我们仅以Win32平台/Visual C++ 6.0为例说明思路。思路有了,应用到其它平台还成问题吗?有一点需要说明,因为这两种解析器的特殊关系,我们完成其中的任何一个,对另一个如法炮制就可以了,因为他们不但调用接口完全相同,连源代码树结构都几乎完全相同。我们分析的对象是截稿为止Xml4C的最新稳定版本:Xml4C 5.2.0。

很多程序员对分析其它程序的源码心存恐惧。其实只要源码品质优秀,再使用一些有效的分析方法,自然会得心应手,几经曲折后,必然直取要害。对于这方面,笔者对候捷前辈分析源码的功底佩服得五体投地,感兴趣的读者可以参考他的相关文章和书藉,这次源码探险就算是一次实习,您准备好了吗?

剖析源码的过程犹如炼狱,我们要有充足的心理准备。同时,有效的分析方法也是必不可少。其实,像我们这种以"解决问题"为目的读源码,其难度远远底于以"分析架构"为目的读源码,而两者的分析方法也大相径庭。至于"分析架构"的方法将在今后的文章中讨论,这篇文章里,我给大家介绍一种以"解决问题"为目的读源码的方法。分析过程如下:

问题分析,了解本质

我们首先要将遇到的问题考虑清楚,并且要看到表面现象背后的本质,只有这样才有助于快速、准确地定位需要修改的源码范围。问题在哪呢?Xml4C在处理中英文混合内容时,常常出现读出的字符串比原有字符串短的现象。为什么会出现这种错误?一般我们都在程序中都使用窄字符,每个字符一字节,而解析器内部采用Unicode存储,在Win32平台上,每个字符两字节。宽字符串与窄字符串之间的转换必须通过调用XMLString类的静态成员函数transcode来完成,是不是转换过程有什么问题呢?测试后发现在调用方完全采用宽字符的情况下是不会发生上述错误的,而在调用方采用窄字符时才会不时的出现问题。这初步证实了我的想法,数据在解析器内部被正确保存,只是在编码转换的过程中出现了问题。

浏览目录,确定范围

首先浏览Xml4C的源码目录树,尽量了解每个目录的用处。从上述分析可知,我们感兴趣的源码内容是宽字符与窄字符互相转换的代码,而此类功能又很容易做成通用模块,所以源码目录中的util(工具)目录特别引起我的注意力。一路抓下去,很快把范围定位在源码目录中的util/ Transcoders/ Win32目录下。运气真好,该目录下只有三个文件,其中有一个cpp文件内容为空。

粗览代码,确定调用关系

我们已经确定了可能修改的代码范围,想进一步缩小范围,需要查找调用关系,分析出在编码转换过程中,哪些类与这三个文件中的内容有关系。首先尽量在头文件中找答案,也就是说这个阶段应该提纲掣领,尽可能从文档或函数声明中判断结果。千万不要在开始就一头扎在源码堆里,那样太伤身体,弄不好还容易血本无归。

让我们打开util/Transcoders/Win32/Win32TransService.hpp文件,浏览一遍后就会很容易发现Win32Transcoder类与Win32LCPTranscoder类提供的接口与我们期望的字符转换接口很相近(它们都有类似transcode之类的成员函数)。但应该修改哪个类呢?或者这两个类都需要修改?别着急!别忘了现在还不能"一头扎在源码堆里",我们已经将范围缩小到两个具体的类,这已经是很大的胜利了。

再接下来,可以写一个实际例子代码,用调试器跟踪进去,分析出Win32Transcoder或Win32LCPTranscoder类与XMLString类的调用关系(因为XMLString类有编码转换的功能,它肯定与这两个类有直接或间接的关系)。当然,没有调试器也可以直接用眼睛跟踪。这里面也有很多技巧,由于篇幅的关系,我将在以后的文章中讨论这些技巧。在分析的过程中,对于关键代码需要跟进相应cpp文件进行粗略浏览,但还是不要太过纠缠细节。经过一番周折,我在util/PlatformUtils.cpp文件中的XMLPlatformUtils::Initialize函数中发现了以下代码:

XMLLCPTranscoder* defXCode
= XMLPlatformUtils::fgTransService->makeNewLCPTranscoder();
if (!defXCode)
panic(Panic_NoDefTranscoder);
XMLString::initString(defXCode);
我注意到:XMLString类的初始化函数(initString)在这里被调用,而传入的参数正是Win32LCPTranscoder类的实例(defXCode)。下面,再打开util/XMLString.cpp文件,查看XMLString::initString的实现,我们需要知道XMLString类把Win32LCPTranscoder类的指针存放在什么地方:

void XMLString::initString(XMLLCPTranscoder* const defToUse)
{
// Store away the default transcoder that we are to use
gTranscoder = defToUse;
}
果然不出所料,它把Win32LCPTranscoder对象保存在一个全局变量中。

到现在,已经与上面分析过的问题衔接上了,下一步需要打开util/XMLString.cpp文件查看XMLString类的静态成员函数transcode的实现。结果我惊喜地发现transcode函数直接调用Win32Transcoder成员函数transcode来完成功能。

已经有结论了:Win32LCPTranscoder是完成编码转换的核心,所以必须剖析Win32LCPTranscoder::transcode函数才可能找出问题所在。

看,洋洋洒洒数万行代码,经过分析后,实际需要精读的只是那么一、两个函数!

剖析源码细节,解决问题

重新打开util/Transcoders/Win32/Win32TransService.cpp文件,现在是句句推敲、字字斟酌的时候了。在transcode函数中,调用calcRequiredSize(const char* const srcText)函数计算从窄字符编码转换成宽字符编码后的尺寸,问题就出在此。下面直接将修改后的函数源码列出,注释掉的就是它最初的内容。读过之后,我们一起分析该函数导致问题出现的原因。

unsigned int Win32LCPTranscoder::calcRequiredSize(const char* const srcText)
{
/*
if (!srcText)
return 0;
unsigned charLen = ::mblen(srcText, MB_CUR_MAX);
if (charLen == -1)
return 0;
else if (charLen != 0)
charLen = strlen(srcText)/charLen;
if (charLen == -1)
return 0;
return charLen;
*/
if ( ! srcText ) {
return 0;
}
unsigned int retVal = ::mbstowcs( 0, srcText, 0 );
if ( retVal == -1 ) {
return 0;
}
return retVal;
}
聪明的您,从源码中应该很容易看出它对多字节语言支持的问题吧?注意那句带外框的代码,它错误地假设多字节语言中每个文字占用的字符是等宽的。而现实生活中我们经常需要中英文混合使用,每个文字占用的字符肯定不会完全等宽。如:"我&你"实际包含3个文字,但错误的计算结果却是strlen("我&你")/mblen("我&你", MB_CUR_MAX)=5/2=2个文字,返回的长度比实际需要的长度短,导致字符串被截断。设想一下,如果是"&我你"会怎么样呢?strlen("&我你")/mblen("&我你", MB_CUR_MAX)=5/1=5个文字,这回又太长了,不过只是空间浪费一些,不会导致出错。这就是该错误存在并且不稳定的原因。

现在,问题已经解决了吗?是的。不信?您试试。

读到这儿,也许您会说:"我们查出了窄字符转换成宽字符的错误,那宽字符转换成窄字符会不会有问题呀?"我看过,这个过程没有问题。

四、一个不容忽视的细节

因为C/C++语言支持多语种的方式与众不同,所以在使用Xml4C过程中有一处需要非常注意的地方,那就是在使用前一定要设置好本地locale,否则默认locale为“C”,是不可能正确处理非英文语种的。比如在默认locale下,mblen(“我&你”)函数将返回-1表示失败。在Win32平台上做测试时,我在使用该库之前调用了:

setlocale( LC_ALL, "chinese-simplified" );
五、结束语

IBM的Xml4C项目源自Apache的Xerces-C++项目,并在其基础上支持大量编码格式,所以我个人比较喜欢使用Xml4C。如果您已经按照文中所述的方法修正了它的代码,那么重新编译以后,您的Xml4C就能够在Win32下正确处理多字节语言了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: