当前位置:编程学习 > C/C++ >>

程序移植与宏定义

 由于操作系统的差异,同一种操作系统本身版本的差异,目前C++标准库提供的功能仍然有限以及C++编译器产品不是完全兼容等问题,使得我们在移植大型应用程序的时候往往会出现很多难以解决的问题,如何合理的避免他们提高C++程序的移植性,本文作者从源代码的组织安排等方面提出了一些实用的建议。
 
当我们编写服务器端的软件产品时,我们往往需要为同一个软件产品推出多种不同平台版本。这是因为目前还没有哪个服务器操作系统可以一统天下。有不少服务器运行Windows 操作系统,但运行Linux和各种UNIX操作系统的服务器也很多,而且各种UNIX操作系统之间又有细微的差别。另外,在一些大企业(特别是大银行)中,运行关键业务的服务器往往是IBM 的大型机,它们的操作系统又会和一般的UNIX 有一些不同。
此外,软件依赖的中间件,调用的函数库,要求的编译器,都可看作平台的一部分。上述内容的任意组合会造成大量的可能性。如果平台移植性做得不好,那么很可能软件在你的开发环境能正常运行,但拿到客户的环境中会出现各种奇奇怪怪的问题。
或许你会说,这些都不是问题,用Java来写程序不就一切OK了?不幸的是,有时候一些遗产代码是用C写的,或者你必须依赖的某个关键函数库只提供了C API,经过评估又发现用Java重写,或者通过JNI以及其他可能的跨语言调用机制去封装这些遗产代码或者CAPI的工作量太大。那么这时候C++往往是更合适的选择。
用Java写程序可以跨平台的一大原因是Java有一个无所不包的标准库,而C++的标准库只提供了最基本的一些功能。要用C++写比较大的程序几乎一定会调用到标准库之外的API,而这些API未必可以跨平台。所以,编写易于移植的C++程序要注意的第一点是:如果能有选择,那么尽可能地使用跨平台的API。
比如,同样是对文件操作,Win 32 API 和UNIX操作系统提供的文件操作函数各不相同,选哪个呢?都不合适,最好还是依赖标准库,fstream或者fopen/fclose都可以。要创建线程并进行线程间同步,Win 32 API 和UNIX的做法又不一样。有没有跨平台的解决方案呢?有的,pthreads是跨平台的。如果你的系统需要有对字符串进行操作,是用MFC提供的CString还是标准库中的string呢?显然应该选后者,因为MFC 不是跨平台的。
那么,如果你不得不用到的某些API 没有跨平台的实现,只有各个平台自己的实现,怎么办呢?举个例子,在Windows平台,加载动态库是调用LoadLibrary;在UNIX平台,加载动态库是调用dlopen。似乎没有什么跨平台的实现。那么我们怎么办?可不可以在每处要加载动态库的地方都这么写?
#ifdef WIN32
HMODULE h = LoadLibrary(“libraryname”);
#elif defined(UNIX)
int h = dlopen(“libraryname”, RTLD_LAZY);
#endif
不少软件就是这么做的。但这样做很糟糕,因为把平台相关代码同其他的平易做图立代码混在了一起,而且代码中会散布很多的#ifdef,影响阅读;而且如果稍后需要把代码移植到另一个平台,那么可能需要修改每一处加载动态库的地方,增加一个#elif defined(?),工作量会比较大。
推荐的做法是:自己封装一个跨平台的实现,在平易做图立代码中只调用这个跨平台的API,把平台相关性隔离出去。当然,这层封装应该是很薄的,应该只需要用一两行的inline 函数以及几个typedef 即可。这样做的指导思想是,通过封装来增加间接层次,从而把平易做图立代码和平台相关代码分离。
下面来看一下,这样做是不是可以了呢?
在main.cpp 中(假设我们需要在这个文件中加载动态库)这样写:
#include “platform_specific.hpp”
int main() {
handle_t h = MyLoadLibrary("libraryname");
// 之后使用动态库,然后卸载
}
 
在platform_specific.hpp 中这样写:
#ifdef WIN32
typedef HMODULE /* WIN32 handle type */ handle_t;
inline handle_t MyLoadLibrary(const string& libname)
{
return LoadLibrary(libname.c_str());
}
#elif defined(UNIX)
typedef int /* UNIX handle type */ handle_t;
inline handle_t MyLoadLibrary(const string& libname)
{
return dlopen(libname.c_str(), RTLD_LAZY)
}
#endif
这样确实做到了“把平易做图立代码和平台相关代码分离”,main.cpp中是平易做图立代码,platform_specific.hpp中是平台相关代码,两者分离开来了。移植到新的平台时不需要对main.cpp做任何修改,只需要修改platform_specific.hpp中的MyLoadLibrary 的实现,而且只要改这一处就可以了。但这样做的一个问题是,platform_specific.hpp会变得非常混乱,充满了#ifdef。想象一下,除了MyOpenLibrary,可能还会有MyCloseLibrary,MyBindSymbol,等等,所有自己封装的跨平台API(也就是实现中需要写#ifdef(某种OS)的API)都在里面了。这个文件会变得难以维护,而且很可能是多个人在维护(每个人负责一个不同的平台),修改会非常频繁(特别是如果几个平台的版本同步开发的话)。有没有更好的做法呢?
不妨这样做:在platform_specific.hpp中,只放这些内容:
#ifdef WIN32
#include “win32_specific.hpp”
#endif
#ifdef UNIX
#include “unix_spefic.hpp”
#endif
 
而把平台相关的实现部分放在各个平台自己的头文件中去。比如,win32_speific.hpp 是这样的:
typedef HMODULE /* WIN32 handle type */ handle_t;
inline handle_t MyLoadLibrary(const string& libname)
{
return LoadLibrary(libname.c_str());
}
在unix_specific.hpp 是这样的:
typedef int /* UNIX handle type */ handle_t;
inline handle_t MyLoadLibrary(const string& libname)
{
return dlopen(libname.c_str(), RTLD_LAZY)\
}

这样就极大地减少了# i f d e f 的数目。除了在platform_specific.hpp中会出现#ifdef(需要支持几个平台就有几个),其他所有文件中都不再需要。而且也分离了关注焦点:负责实现平易做图立功能的人就专注于编写和维护main.cpp,而负责移植到各个平台的人就编写和维护各自平台的os_specific.hpp。不会造成多人修改同一个文件的冲突,平易做图立代码和平台相关代码也得到了很好的分离。
有两点值得注意:
第一点,platform_specific.hpp中没有用到#elif,而是用了独立的#ifdef #endif 块。这样做的目的是为了支持下面这样的拓扑结构:
#ifdef WIN32
#include “win32_specific.hpp”
#endif
#ifdef WINCE
#include “wince_specific.hpp”
#endif
#ifdef UNIX
#include “unix_spefic.hpp”
#endif
#ifdef SOLARIS
#include “solaris_specific.hpp”
#endif
#ifdef AIX
#include “aix_specific.hpp”
#endif
 
WIN32和WINCE不冲突,WINCE是特殊的WIN32;Solaris和AIX是两种特殊的UNIX,和UNIX也不冲突。如果用了#elif就无法同时#include,但用上面这种拓扑结构就可以做到,而且可以把各个UNIX平台都一样的东西实现在unix_specific.hpp中,而把Solaris和AIX有差异的东西实现在solaris_specific.hpp和aix_specific.hpp 中,实现进一步的平台细分。
第二点,win32_specific.hpp、unix_specific.hpp等只能用来封装平台相关的API,不能包含过多的平易做图立逻辑。
下面举一个反例:
在unix_specific.hpp 中:
int main()
{
// 做平台无关的事情
int h = dlopen(“library”, RTLD_LAZY);
// 继续做平台无关的事情
}
在win32_specific.hpp 中:
int main()
{
// 做平台无关的事情
HMODULE h = LoadLibrary(“library”);
// 继续做平台无关的事情
}
 
这样做是很不好的。有一部分平台无关代码会被拷贝粘贴,重复出现在了两个地方。拷贝粘贴是编程之大忌。所以一定要注意,那些封装函数只能是很简单的只有一两行的inline 函数,而且不能出现平易做图立的代码。
采用这种源文件拓扑结构,可以极大地提高软件的可移植性,而且给编写第一个平台版本带来的麻烦也不大。如果你的开发策略是各个平台同步开发,那么这样做可以让各个平台以及跨平台模块的开发者毫不冲突地工作于不同的源代码文件;如果你的开发策略是先全力发布一个平台的版本,然后移植到另一个平台,那么用这样的源代码结构同样可以给你带来极大的好处:假设第一个版本是Windows 的,稍候发布Linux 版本,那么一开始只有main.
cpp(在这里代表所有的平易做图立代码)和win32_specific.hpp。移植的时候只要照着win32_specific.hpp的实现,编写一个linux_specific.hpp 即可。
维护起来也很省心,以后出升级版本或者出patch/servicepack,都只需要在一棵代码树上工作,而没有很多合并修改分支的烦恼。而且还有一个好处是,如果一个bug只在某个平台出现而在其他平台没有,那么找bug基本上只要在那个平台对应的os_specific.hpp中看即可,这是分离关注焦点带来的好处。
正如我前面说过的,平台除了指操作系统,也可以指更广泛的概念,比如中间件或者你依赖的某个第三方库。只要你对平台的依赖是局部性的,而非全局性(比如对框架的依赖),那么这种方法都可适用。我在这里选择了用#ifdef和#inc

补充:软件开发 , C语言 ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,