2013年7月19日星期五

在VS2005+Win7SDK环境下编译现在的Firefox

注意:标题里所说的现在,是指2013年7月19日的mozilla-central hg revision 0d0263a58f06。
大概从2013年5月开始,由于官方采用VS2010已经很长时间,再加上在VS2005、2008环境下出现的问题越来越多,Mozilla官方干脆不再支持在VS2005和2008下编译Firefox(详见Bug 866425)。而过了两个月,到了2013年7月,我尝试在VS2005环境下编译时,发现已经需要大幅度的修改,才能编译成功了。
以下提到的所有路径,都是相对于mozilla-central的目录而言。

首先,是configure文件的修改。前面提到的Bug 866425的解决方案把configure文件里关于VS2005的部分给删除了,得通过修改给它加进去。
configure.in
      elif test "$_CC_MAJOR_VERSION" = "18"; then  
        _CC_SUITE=12  
        _MSVS_VERSION=2013  
这里把18改为14,_CC_SUITE改为8,2013改为2005
js/src/configure.in
      elif test "$_CC_MAJOR_VERSION" = "18"; then  
       _CC_SUITE=12  
修改如上。这样,configure就可以认出VS2005的编译器了。

虽然说微软的编译器对C99的支持一向不足,但是VS2010的CL16开始,加入了以前一直都缺少的一些C99的头文件,比如stdint.h。原本Firefox使用了一个自己的机制(StandardInteger.h和MSStdInt.h,如果判断系统没有stdint.h就使用自带的MSStdInt.h代替)来解决stdint.h不一定存在的问题,但自从Firefox官方不再支持低于VS2010的微软编译器后,有些开发者开始在新加入的文件中直接使用stdint.h了。比如
mfbt/Atomics.h
 #include <stdint.h>  
应该改为
 #include <"mozilla/StandardInteger.h">
Mozilla的开发者对直接使用stdint.h有关的这个修改,还影响到了工程中的其他东西。原本Mozilla使用的几个第三方库的头文件会根据微软编译器的版本自己定义一些标准整数,这些定义可以通过宏来屏蔽掉;由于现在Firefox不再支持低于VS2010的微软编译器,这些宏定义在一些makefile里被干掉了,导致编译到调用这些第三方库头文件的模块时发生重复定义的错误。我没有具体去找是哪些makefile导致这些问题,干脆把那几个头文件本身改掉了。
gfx/harfbuzz/src/hb_common.h

 #if !defined (HB_DONT_DEFINE_STDINT)  
前面加上
 #if defined(_MSC_STDINT_H_)  
 #define HB_DONT_DEFINE_STDINT  
 #endif //如果使用了MSStdInt.h,就屏蔽掉这个头文件自带的整数定义  
gfx/cairo/libpixman/src/pixman.h

 #if !defined (PIXMAN_DONT_DEFINE_STDINT)  
前面加上
 #if defined(_MSC_STDINT_H_)  
 #define PIXMAN_DONT_DEFINE_STDINT  
 #endif //如果使用了MSStdInt.h,就屏蔽掉这个头文件自带的整数定义  
这样应该就没问题了。

VS2005对于C++的标准一是支持有些不完善,二是可能会出现一些奇怪的Bug。在我编译的过程中,这些问题反复出现了多次。
首先是Firefox的一个新的第三方库webrtc,是为了支持浏览器中的视频通话而开发的。在VS2005编译的过程中,会出现一个继承类几个方法的参数定义和基类不同而造成的错误。
在media/webrtc/trunk/webrtc/modules/rtp_rtcp/source/rtp_rtcp_impl.h定义的类ModuleRtpRtcpImpl中的如下方法
   virtual int32_t SetRTXReceiveStatus(const bool enable,  
                     const uint32_t ssrc);  
   virtual int32_t SetRTXSendStatus(const RtxMode mode,  
                   const bool set_ssrc,  
                   const uint32_t ssrc);  
与其在基类RtpRtcp,定义于
media/webrtc/trunk/webrtc/modules/rtp_rtcp/interface/rtp_rtcp.h中的原型
    virtual int32_t SetRTXReceiveStatus(bool enable, uint32_t SSRC) = 0;  
    virtual int32_t SetRTXSendStatus(RtxMode mode, bool set_ssrc,  
                    uint32_t ssrc) = 0;  
比较,会发现基类里面的这两个函数参数都不带const关键字,VS2005编译到这里,会因为这个差异而把继承类ModuleRtpRtcpImpl里这些方法看成抽象方法而导致编译错误。不知为什么在VS2010之后的版本里没有这个问题,我C++忘得差不多了,求高人解释一下。我这里把上面列出的这些参数定义中的const关键字去掉,就能编译通过了。
在编译到content/base/src/nsDocument.h的时候,有一个宏NS_FORWARD_NSIDOMNODE_TO_NSINODE_OVERRIDABLE会编译出错。仔细检查,这个宏定义在content/base/public/nsINode.h中,其定义为
  #define NS_FORWARD_NSIDOMNODE_TO_NSINODE_OVERRIDABLE \  
   NS_FORWARD_NSIDOMNODE_TO_NSINODE_HELPER()  
而NS_FORWARD_NSIDOMNODE_TO_NSINODE_HELPER的定义超长,但反正是一个使用了“...”关键字和 __VA_ARGS__ 关键字的可变参数的宏。搜索了一下,貌似VS2005处理这种类型的宏存在bug,也有不少说这类可变参数宏是个C99的功能,微软一直都不能完美支持的云云。但是天无绝人之路,仔细搜索发现了lawlietfox的一个开发者@tete009_ 提供的关于这个的patch,具体是稍微修改了一下NS_FORWARD_NSIDOMNODE_TO_NSINODE_OVERRIDABLE这个宏定义:
 #define SLASH()   /  
 #define ASTERISK() *  
 #define NO_ARG()  SLASH()ASTERISK()ASTERISK()SLASH()  
 #define NS_FORWARD_NSIDOMNODE_TO_NSINODE_OVERRIDABLE \  
  NS_FORWARD_NSIDOMNODE_TO_NSINODE_HELPER(NO_ARG())  
这个NO_ARG()宏其实就是/**/,也就是个注释而已,为什么这样一改VS2005就不会报错了呢?难道是因为VS2005里面可变参数宏在输入0个参数的时候不行么。。但是输入NO_ARG宏一个“参数”和0个参数好像又没什么区别。这也许就是VS2005编译器预处理器的问题了。
layout/generic/nsGfxScrollFrame.cpp
编译到这里的时候,重头戏来了。这个模块中有个ClampAndAlignWithPixels函数,里面使用了C++11的一个新关键字decltype,作用是返回一个表达式的返回值类型。仔细看看用了这玩艺的那段代码:
   if (aBoundUpper == destUpper &&  
      static_cast<decltype(Abs(desired))>(aBoundUpper - desired) <  
     Abs(desired - aligned))  
   return aBoundUpper;  
   if (aBoundLower == destLower &&  
     static_cast<decltype(Abs(desired))>(desired - aBoundLower) <  
     Abs(aligned - desired))  
   return aBoundLower;  
其实就是比较两个数之差是否小于另两个数之差的绝对值,这里desired、aBoundUpper、aBoundLower类型都是nscoord,定义在gfx/src/nsCoord.h中,可能是int32_t或者是float;Abs函数是mozilla自己的,定义在mfbt/MathAlgorithms.h中,输入为int32_t时返回uint32_t,输入为float时返回float。也许因为这个nscoord不一定是一种类型,开发者才决定要先把前面两个nscoord相减的值做一个强制转换。
但我认为,这个强制转换根本就是不必要的。首先,nscoord的类型是由宏NS_COORD_IS_FLOAT决定的,在同一个源文件的同一个函数内部,两个nscoord型变量的类型不可能不同;然后我们分两种情况来看上述代码中的表达式,如果nscoord是有符号的32位整数,那么小于号左边就是一个有符号整数,小于号右边Abs的返回值就是个无符号整数。尽管两者直接比较是不行的(这个在任何一本C语言入门的书里都会说为什么),但是一个实数的绝对值必然是非负数,任何一个负实数都小于任何一个实数的绝对值,因此可以分两种情况,在这个式子里,如果小于号左边是非负数,直接和右边比较即可;如果小于号左边是负数,可以直接认为其小于右边。如果nscoord是浮点数,上述逻辑一样成立。因此,上述代码可以改写为:
  if (aBoundUpper == destUpper &&  
       ((aBoundUpper - desired) < Abs(desired - aligned) || aBoundUpper - desired < 0) )  
   return aBoundUpper;  
  if (aBoundLower == destLower &&  
       ((desired - aBoundLower) < Abs(aligned - desired) || desired - aBoundLower < 0) )  
   return aBoundLower;  
这样就避免了使用decltype来确定小于号左边表达式要强制转换为什么类型,使其在VS2005下也能编译通过。

另外,我的编译环境本身,还会有一些bug。比如Win7 SDK和VS2005的intrin.h存在冲突(说明在此),一些在官方不再支持VS2005之后修改的源文件没有考虑到这个问题,比如
xpcom/ds/TimeStamp_windows.cpp
gfx/skia/src/ports/SkThread_win.cpp
中,在
 #include <intrin.h>  
前面,应当加上
 #if _MSC_VER <= 1400  
 #define _interlockedbittestandreset _interlockedbittestandreset_NAME_CHANGED_TO_AVOID_MSVS2005_ERROR  
 #define _interlockedbittestandset _interlockedbittestandset_NAME_CHANGED_TO_AVOID_MSVS2005_ERROR  
 #endif  
来解决这个问题。

gfx/cairo/cairo/src/moz-d2d1-1.h
gfx/2d/moz-d2d1-1.h
中,有一些函数声明用到了微软的SAL注释,但是却没有引用<sal.h>而导致编译错误。
    STDMETHOD_(void, PushLayer)(  
      _In_ CONST D2D1_LAYER_PARAMETERS1 *layerParameters,  
      _In_opt_ ID2D1Layer *layer   
      ) PURE;  

    void  
    PushLayer(  
      CONST D2D1_LAYER_PARAMETERS1 &layerParameters,  
      _In_opt_ ID2D1Layer *layer   
      )   
    {  
      PushLayer(&layerParameters, layer);  
    }  
我觉得,这个函数声明应该是直接从微软的例子里抄来的,因为SAL注释这种什么_In_、_Out_、_In_opt_之类的东西除了在MSDN里之外,在任何地方几乎都是看不到的。而这种东西除开方便微软的自动化生成文档工具解析之外,根本就没有用处。我的改法是,直接把这些东西去掉,就能编译通过了。
在编译到
gfx/thebes/gfxFontUtils.cpp的时候,发生了最可怕的错误——编译器内部错误。
编译器内部错误指的是,编译器在处理某些代码的时候,由于编译器发生了内存溢出、栈溢出,或者是编译器对某段代码进行优化失败,或者触发了编译器bug,而导致编译过程不能继续而出错。这种错误中,通常代码本身没有问题。VS2005的CL编译器在发生内部错误时,会指出在处理源代码哪一行时发生了错误,因此可以通过在出错点附近禁止优化等办法来解决。在这个源文件中,出错位置位于gfxFontUtils::MapCharToGlyphFormat4函数中,我的解决办法是在这个函数代码前面加上
 #pragma optimize( "", off )  
在这个函数代码的后面加上
 #pragma optimize( "", on )  
来单独对这一个函数禁用编译器优化,结果编译通过了。

在编译到
content/canvas/src/WebGLContextNotSupported.cpp
的时候说WebGL2Context::Create()的WebGL2Context没有声明,我明明因为没有安装DXSDK而使用了--disable-webgl的参数的,怎么还会有错误?仔细查看相关代码,发现WebGL2在目前的源代码中只是个刚刚写出来的空壳,不如干脆把和这个类相关的东西去掉好了。
去掉content/canvas/src/WebGLContextNotSupported.cpp中的WebGL2Context::Create(),
再去掉content/html/content/src/HTMLCanvasElement.cpp中的
   if (WebGL2Context::IsSupported() &&  
    aContextId.EqualsLiteral("experimental-webgl2"))  
   {  
    Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_USED, 1);  
    nsRefPtr<WebGL2Context> ctx = WebGL2Context::Create();  
    if (ctx == nullptr) {  
     return NS_ERROR_NOT_IMPLEMENTED;  
    }  
    ctx->SetCanvasElement(this);  
    ctx.forget(aContext);  
    return NS_OK;  
   }  
这段代码,就能顺利编译通过了。也许在之后的源代码中,这些WebGL2相关的代码在禁用webgl时都会被宏屏蔽掉,我现在这修改就没有必要了。事实上,已经有人提出了Bug 895855,估计在今天的新代码中已经修正。

最后,在链接gkmedias.dll的时候,又出现了更离奇的错误,GrGpuGL.obj中有重名的符号。用WinHex打开一看,真的有三个符号重名。这三个符号名都是根据VC对C++函数的符号规则重命名了的,但不知为什么C++符号名最后几个字符居然一样。查了一下def文件,发现这个函数是非导出函数,非导出函数并不靠符号名来调用,直接用WinHex把三个重名符号名的最后一个字符改成不同的,就链接通过了。
如果你觉得我上述的改法太hackish,还有一个办法,就是编译时--disable-skia,这个GrGpuGL.obj是第三方图形库Skia里的。但是这样又会触发Bug 891709,因此我这样尝试的时候还是修改了几个相关的文件。
gfx/2d/Factory.cpp
 #if defined(WIN32) && defined(USE_SKIA)  
 #include "ScaledFontWin.h"  
 #endif  
看了ScaledFontWin.cpp和ScaledFontWin.h,发现就算没有skia,这个类也是有实现的。因此需要把要求Skia存在的这个条件去掉。
但是在生成Makefile的时候,是否编译ScaledFontWin.cpp也是由是否使用Skia来决定的。因此也得改:
gfx/2d/moz.build
  elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':  
    CPP_SOURCES += [  
      'DrawTargetD2D.cpp',  
      'SourceSurfaceD2D.cpp',  
      'SourceSurfaceD2DTarget.cpp',  
      'PathD2D.cpp',  
      'ScaledFontDWrite.cpp',  
    ]  
    if CONFIG['MOZ_ENABLE_DIRECT2D1_1']:  
     CPP_SOURCES += [  
        'RadialGradientEffectD2D1.cpp',  
        'DrawTargetD2D1.cpp',  
        'SourceSurfaceD2D1.cpp'  
      ]  
    if CONFIG['MOZ_ENABLE_SKIA']:  
      CPP_SOURCES += [  
        'ScaledFontWin.cpp',  
      ]  
把这里的ScaledFontWin.cpp移到前面ScaledFontDWrite.cpp那行后面去。这样,--disable-skia时编译就能通过了,运行也没有问题。

在费尽九牛二虎之力后,我终于在VS2005+Win7SDK环境下成功编译了7月19日的Firefox源代码。
也许随着Mozilla代码的继续改进,类似的错误会越来越多;但是有些错误,比如和那个MSStdInt.h相关的,可能会因为这些为兼容老VC而存在的头文件最终被删除,而可以更简单地来解决。另外,还有一些死硬派的第三方开发者,比如之前提到的lawlietfox的@tete009_,会继续研究老版本VS环境下Firefox的编译。不过,VS2005也许确实已经老了。。。