我们维护一个 C++ 编写的滤镜和特效库, 可跨平台运行在 Windows,iOS,Android 上. Windows 上使用 Visual Studio 2013 或 2017 编译, iOS 是 Xcode 带的 clang,Android 使用 gcc.
抛开 iOS 和 Android 的平台差异, 只从 C++ 语言来看, clang 和 gcc 的行为是很相似的, 一个移动端编译运行没有问题, 另一个就基本没有问题. 但 VS 编译器的表现却很不同. 此文对比 VS 编译器和 clang 编译器的一些差异, 顺便记录我们踩过的坑.
1. 字符编码
在我的开发环境, clang 编码默认是 utf8, VS 是 GB2312(代码页是 936), 它们都兼容 ASCII.
假如代码文件中只出现英文, 两端都可编译. 假如代码中出现中文, 文件编码为 utf8, iOS 编译没有问题, VS 会出现编译错误 error C2001. 假如设置编码为 utf16, VS 编译没有问题, 而 iOS 会出现编译错误 encoding is not supported. 因此假如代码有中文, 需要将源文件编码修改为 Unicode(UTF8 带签名)- 代码页 65001.
参见 vs 编译 error C2001: 常量中有换行符 中文无法通过编译
另外假如包含中文字符串, 直接读取使用, 程序运行起来很容易出现乱码. 类似这样的代码:
const char* str = "你好啊, 世界";
想将 str 的文字在不同平台都显示正确, 是不可控的. 跨平台代码不应该使用中文, 绝对不能用中文定义字符串再读取, 更严格的甚至不能用中文写注释. 假如要显示中文字符串, 应该将其从程序中分离出来, 写在一个 utf8 编码的配置文件中, 再动态读取.
我们就碰坑了, 我们本意是在导出一个 lua API 的时候, 自动生成对应的文档. 有类似这样的代码:
- ADD_METHOD_WITH_DOC(Context,
- nv12ToRGBPass, "获取颜色空间 nv12 到 rgb 的着色器程序",
- "3.2",
- "[Program](#program)", "program",
- 0)
后来发觉在 Windows 上编译通过, iOS 编译不过. 修改编码后, iOS 编译过了, Windows 上又编译不过. 当两端都编译过了, 但又乱码了. 最后只好都写成英文, 自动生成英文文档.
2. int8_t 和 char
在 VS 上, int8_t 实际上是 char 的 typedef, 也就是说 int8_t 和 char 是同类型的. 但是在 iOS 上, int8_t 和 char 是不同类型的. 下列代码
printf("%d\n", (int)std::is_same<int8_t, char>::value);
在 VS 上输出 1, 在 iOS 上输出 0. 这刷新我认知, 我一直以为 char 和 int8_t 是相同的. 因为这区别, 又踩坑了.
为了方便写 lua 导出, 我们用了 LuaCpp 的库, 里面有这代码
- typedef LOKI_TYPELIST_15(
- bool, char, unsigned char, short, unsigned short,
- int, unsigned int, long, unsigned long, float, double,
- std::string, luaObject, luatable, int64_t) SupportType;
这里定义了一个 Loki 的 typelist, 包含支持自动转换的类型. typelist 参见书籍 C++ 设计新思维 https://book.douban.com/subject/1119904/ .
LuaCpp 基本都是模板代码, 假如类型 T 属于 SupportType, 就可执行自动转换的代码, 不然就需要手写转换, 假如没有手写转换, 对于此类型 T, 就会直接崩掉.
这里的代码很老了, 一直都运行正常. 直到某个接口出现了 int8_t, 于是 VS 上运行正常, iOS 上崩溃了.
同理, int8_t, uint8_t, int16_t, uint16_t 也需要注意.
类似的模板代码, 最好还是乖乖地使用标准库中的 std::is_integral 吧. 不要那么聪明自己手写 typelist 了.
3. __VA_ARGS__
__VA_ARGS__ 可用于不定参数的宏. 但是它的行为在 VS 和 clang 上是有区别的. 如下面代码
- #include <iostream>
- #define MY_PRINT(format, ...) printf(format, __VA_ARGS__)
- int main(int argc, const char* argv[])
- {
- MY_PRINT("Hello, World");
- return 0;
- }
在 VS 上可以编译通过. 但在 clang 上确实编译失败, clang 编译器的 __VA_ARGS__ 不能展开 0 个变长参数的. 写成
MY_PRINT("Hello, World, %d", 1);
才可以正确展开. 为了展开 0 个参数, 需要写成 ##__VA_ARGS__, 定义为
#define MY_PRINT(format, ...) printf(format, ##__VA_ARGS__)
参考 Variadic macros with zero arguments
4. 跨 dll 模块的静态变量
一个工程经常有多个动态模块. 在 VS 上, 动态模块为 dll 文件; iOS 上为 framework 或者 dylib.VS 在跨模块时, 默认符号是不导出的. clang 默认符号都是导出的.
在 VS 上, 当想在 A 模块中定义某个类或者某个函数, 让 B 模块使用, 就需要使用 __declspec(dllexport),__declspec(dllimport) 标明. 通常会定义一些宏, 比如.
- #if defined(OF_WIN32) || defined(_WIN32) || defined(WIN32)
- # ifdef MODULE_A_API_LIB
- # define MODULE_A_API __declspec(dllexport)
- # else
- # define MODULE_A_API __declspec(dllimport)
- # endif
- #else
- # define MODULE_A_API
- #endif
之后需要跨模块使用的函数或者类写成
- class MODULE_A_API TestClass {
- };
- MODULE_A_API void myfunction(int a, int b);
通常都没有问题, 假如忘记写导出, 就会链接错误. 但一旦涉及到模板和静态变量, 这种平台的差别, 就会是个坑.
模板代码通常会直接写在头文件中, 比如下代码.
- // myheader.h
- template <typename T>
- class TemplateClass {
- public:
- static std::string str;
- };
- template <typename T>
- std::string TemplateClass<T>::str;
在模块中, 包含了头文件 myheader.h, 就可以使用 TemplateClass 了. 假如这时模块 A 使用语句设置 str 的值
TemplateClass<int>::str = "Hello, World";
之后模块 B 读取 str 的值.
std::string str = TemplateClass<int>::str;
在 VS 中, 模块 A 和模块 B 虽然都使用 TemplateClass<int>, 但因为没有导出, 实际是分离的两个类, 他们的静态变量并不会共享. 于是就是模块 A 设置了 TemplateClass<int>::str, 模块 B 读取的还是默认的空值.
而在 clang 编译器中, 默认是导出的. 于是模块 A 和模块 B 看到的是相同的 TemplateClass<int>, 静态变量是共享的. 于是模块 A 设置了 TemplateClass<int>::str, 模块 B 读取的是设置后的 "Hello, World"
这种 Bug 比较隐蔽, 可以正常编译, 也可以运行, 但实际结果就是不对. 我们就踩过类似的坑.
前文说过, 我们使用了 LuaCpp 这个库来导出 lua. 这个库是个模板库, 它包含一些静态变量, 用来实现自动注册. 我们在模块 A 中注册了一批 lua 类. 之后在模块 B 中往 lua 虚拟机压注册过的类对象, 在 iOS 上运行正常, 但在 Windows 上就异常. 因为在模块 B 中看来, LuaCpp 的记录中, 这些类根本就没有被注册过.
结论就是, 跨模块不要直接导出静态变量. 也尽量不要在模板代码中包含静态变量, 因为你不知道这份模板代码啥时候就被两个不同模块包含, 到时这些静态变量就容易出问题.
来源: https://juejin.im/entry/5bdaceb75188257f5637bba9