iOS 引用 C/C++ 项目:平台差异导致的代码移植

由于 Linux(包括 Android)在嵌入式和服务端的流行,很多 C/C++ 代码都是专为 Linux 而写;加上历史原因,很可能还是基于 GCC 编译;

而 iOS/MacOS 基于类 Unix 系统 BSD,并且从 2011 年的 OS X 10.7 开始,默认的 C++ Runtime 就从 GNU 的 libstdc++ 迁移到 LLVM 的 libc++,二者不完全二进制兼容(Xcode 编译时只能二选一);

同时编译器也从 GCC 切换到了 Clang,而 Clang 通常比 GCC 语法检查更严格:

Clang strives to both conform to current language standards (up to C11 and C++11) and also to implement many widely-used extensions available in other compilers, so that most correct code will “just work” when compiled with Clang.
However, Clang is more strict than other popular compilers, and may reject incorrect code that other compilers allow.

基于上述原因,我们需要对部分 C/C++ 代码做改动,以便移植到 iOS/MacOS 项目中。

下面仅梳理下最近项目中遇到的情况。

C/C++ 语法兼容问题

C/C++ 语法方面的问题目前碰到的并不多,主要是 staticconstexpr 相关;

static

头文件中如果定义了类的 static 成员变量和函数,在实现文件中,不能再带上 static 关键字,否则会报错:

‘static’ can only be specified inside the class definition

这应该就是典型的 Clang 检查更严格了,同事表示:“这也太 SB 了吧”,我只能摊手 ╮( ̄▽ ̄)╭

constexpr

初始化类的 static const 成员变量时,如果是 float 或者 double 类型,必须加上 constexpr 关键字,否则会报错:

in-class initializer for static data member of type ‘const double’ requires ‘constexpr’ specifier

进一步从 Xcode 编译日志可以看到是因为默认开启了 Clang 的 -Wstatic-float-init 选项;

我们知道,constexpr 用于修饰编译期可确定的常量表达式,但在这里这为什么是必须的呢?为什么其他类型的就不用加?

答案是:ISO 的 C++ Standard 就是这么规定的,参见 9.4.2:

If a non-volatile const static data member is of integral or enumeration type, its declaration in the class definition can specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression (5.20).
A static data member of literal type can be declared in the class definition with the constexpr specifier; if so, its declaration shall specify a brace-or-equal-initializer in which every initializer-clause that is an assignment-expression is a constant expression.

当然,最好还是将初始化放到类的实现中。

更多 C++ 语法兼容问题可参考:

系统库移植

epoll -> kqueue

epoll 是 Linux 系统的 IO 事件通知库(Android 的 Handler/Looper/Message 底层也是基于它),而 iOS/MacOS 上对应的是源自 FreeBSD 的 kqueue

关于二者的原理和特性比较,网上文章不少,这里不再赘述,总体来说二者非常相似(功能上 kqueue 似乎更胜一筹);

而 MacOS/iOS 的 64 位已普及, libdispatch 等系统库内部也用的 64 位 API,下面仅以 64 位 API 为例介绍 kqueue;

事件定义

epoll 的 epoll_event:

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t events;
    epoll_data_t data;
};

kqueue 中对应的 kevent64_s:

struct kevent64_s {
    uint64_t ident; /* 事件 ID */
	int16_t filter; /* 事件过滤器 */
	uint16_t flags; /* 行为标识 */
	uint32_t fflags; /* 过滤器标识 */
	int64_t data; /* 过滤器数据 */
	uint64_t udata; /* 应用透传数据 */
	uint64_t ext[2]; /* 过滤器扩展数据 */
};

这里要注意两点:

  • kevent64_sdata 不同于 epoll_eventdata,它是过滤器相关的数据,真正的应用透传数据是 udata字段;

  • epoll_eventevents 相当于既包含了事件标识又包含了行为标识, 而 kevent64_s 拆分为 flagsfilter,所以事件状态判断方式有差异:

    int isRead() {
    #ifdef __linux__
        return events & EPOLLIN;
    #elif __APPLE__
        return filter == EVFILT_READ;
    #endif
    }
    
    int isWrite() {
    #ifdef __linux__
        return events & EPOLLOUT;
    #elif __APPLE__
        return filter == EVFILT_WRITE;
    #endif
    }
    
    int hasError() {
    #ifdef __linux__
        return events & EPOLLERR;
    #elif __APPLE__
        return flags & EV_ERROR;
    #endif
    }
      
    int hasEnded() {
    #ifdef __linux__
        return events & EPOLLHUP;
    #elif __APPLE__
        return flags & EV_EOF;
    #endif
    }
创建队列

kqueue 的 kqueue() 对应 epoll 的epoll_create(),但不需要传参数:

int _max_events;
int _edge_trigger;
int _queue;
#ifdef __linux__
struct epoll_event *_events;
#elif __APPLE__
struct kevent64_s *_events;
#endif

void create(int max_events, int edge_trigger) {
    _max_events = max_events;
    _edge_trigger = edge_trigger;
    if(_events != NULL) {
        delete[] _events;
    }
#ifdef __linux__
    _queue = epoll_create(_max_events + 1);
    _events = new struct epoll_event[_max_events + 1];
#elif __APPLE__
    _queue = kqueue();
    _events = new struct kevent64_s[_max_events + 1];
#endif
}
事件控制

kqueue 的 EV_SET64() 对应 epoll 的 epoll_ctrl(),事件控制方式基本类似;

不过要注意几点:

  • EV_SET64 内部会自动设置各个字段:

     #define EV_SET64(kevp, a, b, c, d, e, f, g, h) do {\
    struct kevent64_s *__kevp__ = (kevp);\
    __kevp__->ident = (a);\
    __kevp__->filter = (b);\
    __kevp__->flags = (c);\
    __kevp__->fflags = (d);\
    __kevp__->data = (e);\
    __kevp__->udata = (f);\
    __kevp__->ext[0] = (g);\
    __kevp__->ext[1] = (h);\
       } while(0)

    所以不必像 epoll_ctrl 一样去手动设置;

  • epoll 可以直接一次传入读/写事件(EVFILT_READ|EVFILT_WRITE),而 kqueue 需要分两次;

  • epoll 有专门用于修改的字段 EPOLL_CTL_MOD,而 kqueue 没有,但是从 kqueue 的 EV_ADD 字段注释可以看到:

    Re-adding an existing event will modify the parameters of the original event, and not result in a duplicate entry.

    即可以通过重复添加实现修改;

  • 事件触发类型默认是 Level Triggered,即:如果没有处理或者一次性做完读写操作,下次 wait 时会被一直通知;

    epoll 可以通过在 events 字段附上 EPOLLET 指定为 Edge Triggered;

    而 kqueue 同样可以通过在 flags 附加相关参数实现:

    • EV_DISPATCH: 事件送达后置为 disable(可通过 EV_ENABLE 恢复);

    • EV_ONESHOT: 事件送达后直接删除;

下面是示例代码:

#define EV_READ 0x1<<1
#define EV_WRITE 0x1<<2

void ctrl(int fd, long long data, int events, int op) {
#ifdef __linux__
    struct epoll_event ev;
    ev.data.u64 = data;
    ev.events = events;
    if(_edge_trigger) ev.events |= EPOLLET;
    epoll_ctl(_queue, op, fd, &ev);
#elif __APPLE__
    struct kevent64_s change[2];
    int num = 0;
    //if (_edge_trigger) op |= EV_ONESHOT;
    if (events & EV_READ) {
        EV_SET64(&change[num++], fd, EVFILT_READ, op, 0, 0, data, 0, 0);
    }
    if (events & EV_WRITE) {
        EV_SET64(&change[num++], fd, EVFILT_WRITE, op, 0, 0, data, 0, 0);
    }
    kevent64(_queue, change, num, NULL, 0, 0, NULL);
#endif
}

void addRead(int fd, long long data) {
#ifdef __linux__
    ctrl(fd, data, EPOLLIN, EPOLL_CTL_ADD);
#elif __APPLE__
    ctrl(fd, data, EV_READ, EV_ADD | EV_ENABLE);
#endif
}
    
void addWrite(int fd, long long data) {
#ifdef __linux__
    ctrl(fd, data, EPOLLOUT, EPOLL_CTL_ADD);
#elif __APPLE__
    ctrl(fd, data, EV_WRITE, EV_ADD | EV_ENABLE);
#endif
}
    
void addReadWrite(int fd, long long data) {
#ifdef __linux__
    ctrl(fd, data, EPOLLIN | EPOLLOUT, EPOLL_CTL_ADD);
#elif __APPLE__
    ctrl(fd, data, EV_READ | EV_WRITE, EV_ADD | EV_ENABLE);
#endif
}
    
void modRead(int fd, long long data) {
#ifdef __linux__
    ctrl(fd, data, EPOLLIN, EPOLL_CTL_MOD);
#elif __APPLE__
    addRead(fd, data);
#endif
}
    
void modWrite(int fd, long long data) {
#ifdef __linux__
    ctrl(fd, data, EPOLLOUT, EPOLL_CTL_MOD);
#elif __APPLE__
    addWrite(fd, data);
#endif
}
    
void delRead(int fd, long long data) {
#ifdef __linux__
    ctrl(fd, data, EPOLLIN, EPOLL_CTL_DEL);
#elif __APPLE__
    ctrl(fd, data, EV_READ, EV_DELETE);
#endif
}
    
void delWrite(int fd, long long data) {
#ifdef __linux__
    ctrl(fd, data, EPOLLOUT, EPOLL_CTL_DEL);
#elif __APPLE__
    ctrl(fd, data, EV_WRITE, EV_DELETE);
#endif
}
注册事件队列并等待

kqueue 的 kevent64() 对应 epoll 中的 epoll_wait(),关于参数这里要注意两点:

  • 有两个事件列表相关的参数,第二个 changelist 表示 EV_SET64() 设置过的事件列表,第四个 eventlist 表示(用户需要处理的)触发的事件列表;

  • timeout 参数需要自己构造 struct

int wait(int msec) {
#ifdef __linux__
    return epoll_wait(_queue, _events, _max_events + 1, msec);
#elif __APPLE__
    struct timespec timeout = {.tv_sec = msec / 1000, .tv_nsec = (msec % 1000) * 1000 * 1000};
    return kevent64(_queue, NULL, 0, _events, _max_events, NULL, &timeout);
#endif
}

更多 kqueue 内容可参考:

网络相关

getHostByName

这个函数的定义有很大差异,包括参数和返回值;

Linux 是将 name 和 result 都作为参数传入:

int gethostbyname_r(const char *name,
        struct hostent *ret, char *buf, size_t buflen,
        struct hostent **result, int *h_errnop);

而 iOS/MacOS 是传入 name 返回 struct:

struct hostent *gethostbyname(const char *);

代码移植:

struct hostent *pstHostent;
#ifdef __linux__
    struct hostent stHostent;
    char buf[2048] = "\0";
    int iError;
    gethostbyname_r(sAddr.c_str(), &stHostent, buf, sizeof(buf), &pstHostent, &iError);
#elif __APPLE__
    pstHostent = gethostbyname(sAddr.c_str());
#endif
socket 常量

Linux 上 setsockopt() 函数的第二个参数通常传入 SOL_IP,但是 iOS/MacOS 上没有这个常量,最后在 ip - Linux manual page 找到了答案:

Using the SOL_IP socket options level isn’t portable; BSD-based stacks use the IPPROTO_IP level.

可见 iOS/MacOS 应该使用 IPPROTO_IP,位于 netinet/in.h

arp 头文件

可以看到 sdk net 目录下找不到这个头文件,但是从 opensource.apple.com 却可以找到,所以直接把这个头文件拷贝进项目。

另外发现 GitHub 上这个项目也是将其拷贝放进了 missing 目录,个人猜测可能是考虑到安全性?毕竟 ARP 被很多人认为是最不安全的协议..

其他

pthread

iOS/MacOS 基于类 Unix 系统,pthread 当然是支持的,不过下面这段代码在 MacOS 上编译不过:

map<int, string> buffer;
pthread_t ThreadID  = pthread_self();
buffer.push_back(make_pair(ThreadID, buffer.second));

因为 pthread_t 的定义有差异;

Linux 上定义为 int:

typedef unsigned long int pthread_t;

MacOS/iOS 上定义为 struct*:

struct _opaque_pthread_t {
	long __sig;
	struct __darwin_pthread_handler_rec  *__cleanup_stack;
	char __opaque[__PTHREAD_SIZE__];
};
typedef struct _opaque_pthread_t *__darwin_pthread_t;
typedef __darwin_pthread_t pthread_t;

所以需要做下类型转换:

intptr_t ThreadID_p = reinterpret_cast<intptr_t>(ThreadID);
hash_map

使用了 GNU libstdc++ 的 ext/hash_map 的代码会有警告信息:

warning Use of the header <ext/hash_map> is deprecated.  Migrate to <unordered_map>

所以需要去掉这个头文件引用,换成 std::unordered_map 或者 std::map

前者和 hash_map 一样基于链表实现,适合频繁访问已知元素或者增删元素,但是无序;而后者基于平衡二叉树(通常是红黑树)实现,元素有序;

具体实现细节可参考:

可以根据实际需求去权衡选择;

byteswap

这个库在 iOS/MacOS 只是头文件和函数名前缀不同,所以用宏判断即可:

#ifdef __linux__
    #include <byteswap.h>
    #define __bswap_64 bswap_64
#elif __APPLE__
    #include <libkern/OSByteOrder.h>
    #define __bswap_64 OSSwapInt64
#endif
libiconv

libiconv 是 GNU 的一个字符集转码库,MacOS/iOS 也内置了,但需要在 Xcode 的 Build Phases 中添加;

同样,接口命名也有不同,需要去掉 lib 前缀,如 libiconv_close() 改为 iconv_close

直接加宏判断:

#ifndef __APPLE__
#define iconv libiconv
#define iconv_t libiconv_t
#define iconv_open libiconv_open
#define iconv_close libiconv_close
#define iconv_open_into libiconv_open_into
#define iconvctl libiconvctl
#endif
allocator

很多老的 C++ 代码可能引入了 GNU 的 libstdc++ 的 etx/pool_allocator.h,如果用 LLVM 的 libc++ 编译,就会提示找不到头文件;

其实直接去掉这个头文件引用即可,因为系统的 memory 库就包含了 std::allocator

同样直接加宏判断:

#ifndef __APPLE__
#ifdef __GNUC__
    #if __GNUC__ > 3 || __GNUC_MINOR__ > 3
        #include <ext/pool_allocator.h>
    #endif
#endif
#endif
zlib

我们在引用很多第三方库可能都会报类似 crc_32deflate 的 symbol 找不到的问题,其实就是调用了 zlib 这个数据压缩库;

同样是 MacOS/iOS 内置,在 Xcode build phases 中添加即可;