转自:https://blog.rinc.xyz/posts/170606-objcxx/
最近终于有幸参与公司的 iOS 项目,其中有个 C/C++ 写的库需要调用;
之前对 Android JNI/NDK 调用 C/C++ 还算熟悉,但 iOS 混编 C/C++ 却是初次接触,各种被虐..
上个周末基本都在解决库的编译问题,爱人 Amble 也耐心帮我查资料、作分析,感动之余,决定把中途遇到的问题记录下来。
iOS 不支持调用第三方动态库(.dylib),应该是出于安全考虑,比如各种越狱工具就大量使用动态库;
一开始在这里耽误很久,.dylib 库编译没问题,但是 Xcode 项目引用后一直报 ‘library not load’、’symbols not found for architexture’ 之类的错误..
C++ 头文件去掉动态链接代码
C++ 动态库接口对应的头文件,需要将 __declspec(dllexport)
、__stdcall
等关键字去掉;
CMake 脚本改为动态链接
|
这里 SHARED
改为 STATIC
即可。
Mac 上直接跑 CMake 编译出来的是 X86 架构的,iOS 上无法运行,需要一个调用 xcodebuild 的 CMake 脚本做交叉编译;
Google Code 上的这个 iOS-CMake 项目年久失修,需要自己做一些改动。
修改 Xcode 路径
|
这里自动检测安装路径并设置,不需要写死,也不需要编译时指定编译选项;
编译器设置和检查
CMakeForceCompiler 已经过时,直接指定编译器为 Clang 就好:
|
后面需要跳过编译器检查,否则编译会报错中断:
|
修改 Architecture
|
这里可以根据实际情况做取舍,比如最新 iOS 11 就宣布不支持非 64 位架构,真机和模拟器可以分别只保留 arm64、x86_64;
另外,还新增了 WatchOS 的支持。
禁用 CMake 可执行编译选项
一般 C/C++ 项目每个模块下面都会添加包含 main()
的测试代码,需要在 CMakeLists.txt 文件中注释掉 #add_executable
相关代码,否则后面编译会报以下错误:
target specifies product type ‘com.apple.product-type.tool’, but there’s no such product type for the ‘iphoneos’ platform
调用 CMake 创建并编译 Xcode 项目
CMake 添加 -DCMAKE_TOOLCHAIN_FILE
选项指定 iOS 交叉编译脚本,-GXcode
选项指定生成项目类型为 Xcode:
|
上面是模拟器的库编译,真机类似。
使用 xcodebuild 自动编译项目
后面发现,如果存在库的依赖关系,即 CMakeLists.txt
中设置了 target_link_libraries
,上面生成的 Xcode 工程并不会自动添加库依赖,需要自己手动设在 Build Phases 的 Link Binary with Libraries 中设置;
设置完后,只要不增删 C/C++ 代码,就不用重新生成 Xcode 项目,可以通过 xcodebuild 自动编译:
|
这个工具在自动打包时经常用到。
符号表相关编译设置
我们知道,符号表(symbols) 包含 C/C++ 程序中的变量和函数信息,虽然可方便调试,但是会显著增加包大小;
Xcode 的 Build Settings 包含一些符号表相关设置,这里介绍两个常用的、和 Clang 编译选项有关的:
Generate Debug Symbols: 开启后,编译每个源文件时会自动带上编译选项 -g
和 -gmodules
,生成完整的调试信息,如果 C/C++ 层代码 crash 后会自动跳转到源代码行数;关闭后,crash 时会直接跳到汇编代码;一般建议至少调试阶段应该开启;
Debug Info Level: 如果设置为 Line Tables Only
,会自动带上编译选项 -gline-tables-only
,调试信息就只会包含函数名、文件名、行号,不包含变量;
也可以先不管这些设置,最后直接通过 strip -x -S old.a -o new.a
命令去掉符号表信息;
更多内容可参考:
Xcode Build Setting;
Clang Compiler User’s Manual;
Mac OSX / Darwin Man Pages - Strip;
库的合并与检查
macOS 自带的 lipo 命令可用于将真机和模拟器的库文件合并:
|
最后,再介绍两个有用的命令行工具:
file xxx.a
:可以查看库文件的架构信息;
lipo -info xxx.a
:也可以查看库文件的架构信息;
nm -a xxx.a
:可以查看库的符号表信息;
详细编译脚本参考这里。
C++ 头文件编译问题
C++ 工程的 .h 文件直接拷进 Xcode 是可能编译不过的,如果在头文件使用了 C++ 语法的话;
因为 Xcode 默认当做 C 头文件编译,解决方式有两个:
扩展名改为 .hpp 或 .hxx,让编译器知道这是 C++ 头文件;
在使用 C++ 语法的代码块前后分别添加 #ifdef __cplusplus
和 #endif
宏;
除了上面的问题,如果头文件定义了 const
常量,并且被多次 include
的话,Xcode 编译会报 ‘duplicate symbol’ 的异常;
原因是被认为重复定义,解决方式是在前面添加 static
关键字。
Objective-C 和 C/C++ 数据类型转换
主要是 NSString
/NSData
、std::string
、char*
这三者之前的相互转换,这里我定义了一些宏方便调用:
|
还有一点要注意,对于包含汉字等宽字节的 NSString
,如果要传字节长度到 C/C++ 层,不能直接调 [nsString length]
,因为它返回的是字符数而不是字节数;
可以先将 NSString*
转为 char*
,然后调用 C/C++ 的 std::strlen()
得到字节数:
|
内存管理
我们知道,Objective-C 的 init
和 dealloc
方法分别对应 C++ 的构造函数和析构函数,混编时他们的对象内存也是各自管理的;
Objective-C 中如果创建了全局 C/C++ 对象,需要在 dealloc
方法中释放;
同样,如果 C++ 创建了 Objective-C 对象,也需要在析构函数中释放;
二进制数据传递问题
通常 Android/iOS 调用底层 C/C++ 库最多的场景就是图像和音视频处理了,这就涉及到二进制数据的传递;
具体到我参与的项目,传递的数据是一个 std::map
类型参数;
一开始看到 std::string
很自然联想到 Objective-C 中对应的 NSString
,所以做了个“画蛇添足”的转换:先把 NSData
转换为 NSString
,再将 NSString
转为 std::string
;
为什么要先转换为 NSString
呢?因为一开始打算在 Objective-C++ 的 wrapper 类屏蔽掉 C++ 的类型,外部调用全部传入 Objective-C 的类型;
但既然要转为 NSString
就涉及到 encoding 的问题,但很快发现无论 NSUTF8StringEncoding
还是 NSASCIIStringEncoding
都有问题;
最后改为直接 NSData
和 std::string
之间转换,而且考虑到上面提到的内存管理问题,避免 EXC_BAD_ACCESS
,每次传递时都做下数据拷贝:
|
注意 std::string
构造函数要带上 length
参数,否则二进制数据很可能被截断。
C/C++ 回调 Objective-C
Objective-C 中的 C 回调函数是无法直接访问类的成员和方法的,甚至会报 ‘exc_bad_access’ 错误;
那么如何解决这个问题呢?
熟悉 JNI 的都知道,它的每个 JNI 函数第一个参数都是 JNIEnv*
,是访问 JVM 相关接口的桥梁;
同样,这里我们也可以在回调函数中增加 Objective-C 对象的指针参数,用来间接访问类成员和方法;
但是,这个 Objective-C 对象指针在 C/C++ 环境下是无法直接使用的,还得借助一个双方都能识别的桥梁 — void*
;
而 Objective-C 对象指针和 void*
的相互转换,则需要依赖 Bridged Cast:
__bridge
可用于类型互转,但是不改变对象的所有权(ownership);
__bridge_retained
用于将 Objective-C 对象指针转换为 C/C++ 对象指针,同时获得所有权;
__beidge_transfer
用户将 C/C++ 对象指针转换为 Objective-C 对象指针,同时放弃所有权;
其实系统的 Core Foundation 很多地方 也使用到这种类型转换。
最后看个 C++ 调用 Objective-C 的示例:
首先定义接口函数指针类型:
|
Objective-C 类的回调函数中,通过 __bridge_transfer
将 void*
还原为对象指针:
|
C++ 构造函数接收 Objective-C 传过来的 void*
和函数指针,用于执行该回调函数:
|
最终调用时,将 Objective-C 对象通过 __bridge_retained
转为 void*
传给 C++ 对象:
|
也可以将上面的 __bridge_transfer
和 __bridge_retained
全部改为 __bridge
。
[完整代码]