1. 分层设计
隔离平台相关的代码, 就像可测试性一样, 可移植性也要从设计抓起。一般来说, 最上
层和最下层都不具有良好的可移植性:
1). 最上层是 GUI, 大多数 GUI 都不是跨平台的, 如: Win32 SDK 和 MFC
2). 最下层是操作系统 API, 大部分操作系统 API 都是专用的
如果这两层的代码散布在整个软件中, 那么这个软件的可植性将非常的差, 这是不言自
明的。那么如何避免这种情况呢? 当然是分层设计了:
1). 最底层采用 Adapter 模式, 把不同操作系统的 API 封装成一套统一的接口(如:
KYLib 库), 至于封装成类还是封装成函数, 要看实际情况而定。如果在开发第
一个平台时就采用 KYLib, 可以大大减少移植的工作量。
2). 最上层采用分离界面表现与内部逻辑代码的模式, 把大部分代码放到内部逻辑里
面, 界面仅仅是显示和接收输入, 即使要换一套 GUI, 工作量也不大。这同时也
是提高可测试性的手段之一, 当然还有其它一些附加好处。所以即使你采用 QT
或者 GTK+ 等跨平台的 GUI 设计软件界面, 分离界面表现与内部逻辑也是非常
有用的。
2. 注意平台的特性
a. 目录分隔符: 在Windows下用 '\\', 在Linux下用 '/'。
b. 文本文件换行符: 在Windows下用 "\r\n", 在Linux下用 '\n'。
c. 在 Windows 中文件名不区分大小写字母, 而在 Linux 中则区分大小写字母。
d. 在 Windows 中线程可以 suspend 和 resume, 而在 Linux 中则不允许此操作。
e. 在 Windows 的动态库中, 除非明确指明为 export 的函数外, 其它函数对外都是不
可见的。
f. 在 Linux 的共享库中, 所有非 static 的全局变量和函数, 对外全部是可见的。这
要特别小心, 同名函数引起的问题, 让你查上两天也不为过。
g. 在 Linux 的共享库中, 如果想绑定共享库里的全局符号(变量, 函数和类等等), 则
在链接共享库的时候, 添加 gcc 选项 -Wl,-Bsymbolic 即可。
h. 在 Linux 的共享库中, 如果共享库存取主程序里定义的全局符号, 链接主程序的时
候, 使用参数 -Wl,--export-dynamic 即可。
3. 最好不要使用编译器特有的特性
a. 像在 VC 里, 你要实现线程局部存储, 在变量前加一个 __declspec( thread ) 就行
了, 然而尽管在 pthread 里有类似的功能, 却不能按这种方式实现, 所以无法移植
到 Linux 下。
b. 同样 gcc 也有很多扩展, 是在 VC 或者其它编译器里所没有的。如编译成多线程安
全的选项 -pthread, 此选项在编译源程序和链接时使用。
4. 数据类型差别
a. 在 VC 中64位整型是 __int64, 而在 Linux 中是 int64_t。
b. 在 VC 中函数指针默认情况下可以直接赋值给 void* 类型变量, 而在 Linux 中则不
允许直接赋值, 必须使用 (void*) 强制转换。
c. 在 Windows 中的原子锁相关函数 InterlockXXX 中的参数类型是 long*,
而在 Linux 中的原子锁相关函数 InterlockXXX 需要用AT&T内嵌汇编实现。
5. 调用外部库(静态库和动态库)差异
a. 在 VC 中调用外部库有 .lib 支持, 若是动态库则直接通过 .lib 关联。
b. 在 Linux 中调用静态库为 .a 文件, 库之间的先后顺序非常重要, 如 libKYLib.a
和 libkylin.a, 且 kylin 依赖 KYLib, 则在工程中加载库的顺序必须为: 先加载
libkylin.a, 再加载 libKYLib.a。
c. 在 Linux 中调用动态库为 .so 文件, 如果有好几个库, 它们之间有一些依赖关系的
话, 例如 X 依赖 Y, 那么你就要先加载那些被依赖的 Y, 然后加载 X。
d. 在 Linux 中混合调用静态库和动态库, 如使用 libKYLib.a 和 librc32c.so, 且
librc32c.so 中使用了 libKYLib.a, 则在加载库时必须先加载 libKYLib.a, 然
后再加载 librc32c.so。
6. 加载动态库时查找路径顺序的差异
a. Windows 库搜索路径和顺序
1). 应用程序目录
2). 当前工作目录
3). 系统目录 (%systemroot%, %systemroot%\system 和 %systemroot%\system32),
如: C:\WINNT\, C:\WINNT\system, C:\WINNT\system32
4). 路径变量 (系统的环境变量 Path)
b. Linux 库搜索路径和顺序
1). 链接时指定的路径, 如: -Wl,-rpath=./ 选项表示编译时 ld 路径
2). 环境变量 LD_LIBRARY_PATH 指明的路径
3). /etc/ld.so.cache中的函数库列表
4). /lib目录, 然后/usr/lib
5). 当前工作目录
7. 动态库入口函数的差异
a. Windows 中有 DllMain 入口函数, 而 Linux 中则没有。
b. Linux 中有特殊函数 _init 和 _fini, 主要是分别用来初始化函数库和关闭的时候
做一些必要的处理, 我们可以把自己认为需要的代码放到这两个函数里面, 它们分别
在函数库被加载和释放的时候被执行。具体说, 如果一个函数库里面有一个名字为
"_init" 的函数输出, 那么在第一次通过 dlopen() 函数打开这个函数库, 或者只是
简单的作为共享函数库被打开的时候, _init 函数被自动调用执行。与之相对应的就
是 _fini 函数, 当一个程序调用 dlclose() 去释放对这个函数库的引用的时候, 如
果该函数库的被引用计数器为 0 了, 或者这个函数库是作为一般的共享函数库被使
用而使用它的程序正常退出的时候, _fini就会被调用执行。
C语言定义它们的原型如下:
void _init(void);
void _fini(void);
当使用你自己的 _init 和 _fini 函数时, 会出现命名冲突, 就会得到一个
"multiple-definition" 的错误, 编译器提示已经存在这个名字, 可以通过几种方式
来解决:
1). 自定义 init 函数名字, 比如 myinit 用 -Wl, 选项给 ld 传递此名字:
gcc ... -Wl,-init=myinit
2). 当 GCC 编译源程序时, 可以使用选项 -nostartfiles 来使共享库不与系统
启动文件一起编译
gcc ... -nostartfiles
3). 使用上面的函数或 GCC 的 -nostartfiles 选项并不是很好的习惯, 因为这
可能会产生一些意外的结果。相反, 库应该使用
__attribute__((constructor)) 和 __attribute__((destructor)) 函数属
性来输出它的构造函数和析构函数。如下所示:
void __attribute__((constructor)) x_init(void);
void __attribute__((destructor)) x_fini(void);
构造函数会在dlopen()返回前或库被装载时调用;
析构函数会在这样几种情况下被调用: dlclose() 返回前, 或 main() 返回
后, 或装载库过程中 exit() 被调用时。
c. Linux 中的初始化和释放函数不建议使用。