如何在xcode工程中调用现有的c++遗产代码

1,694 阅读4分钟

背景

最近因为项目需要,开始研究xcode iOS工程。因为部分功能需要借助开源库实现,所以项目中引入的一个基于C的开源工程;因为功能本身并不庞大,所以项目采用的方式是直接 将实现代码拷贝到工程本身目录下进行整合,这种形式相对简单且一劳永逸,因为外部依赖本身的修改也不会非常太频繁;

同时,此问题引发了另外一个思考,假如我们要整合利用一个比较庞大复杂的外部c/c++开源工程,这种方式未必是最合适的;有什么好的方式能兼顾可维护性和低耦合性呢?能想到的比较直接的方式是将外部工程编译成 动态链接库,然后在xcode工程中动态调用,这样xcode工程本身不用耦合如此庞大的代码;面向的修改还是可以集中在本身的业务逻辑上;

初步方案

将c/c++工程编译成独立的头文件和动态链接库,然后在xcode中配置动态链接库为启动依赖动态链接库,在业务逻辑中调用。

分步实施

整体主要分为这样几个步骤,比较清晰

  • STEP 1: 使用Makefile创建c++工程,编译出dylib动态链接库文件和头文件
  • STEP 2: 配置xcode,设置dylib为启动依赖动态链接库
  • STEP 3: 在业务代码中引入头文件,并调用动态链接库提供的API

这过程中也碰到了一些挑战,以及一些对问题认知不足导致的返工和回炉重造。

STEP 1

为了避免一开始就将问题复杂化,我们选择一个相对简单的c++工程,提供的API也比较简单,就一个函数。工程目录结构如下。

dylib_cpp
 ┣ src
 ┃ ┣ app.cpp
 ┃ ┣ repot_sys.cpp
 ┃ ┗ repot_sys.h
 ┗ Makefile

app.cpp是为了测试API调用。最终我仅仅是将repot_sys.cpp编译为独立的dylib文件。

repot_sys.cpp 文件

const char* repot_sys_os() {
  struct utsname u;
  uname(&u);

  size_t len = strlen(u.sysname);
  char* os = (char*)malloc(sizeof(char) * (len + 1));
  strcpy(os, u.sysname);
  os[len] = '\0';

  return os;
}

构建工具Makefile就够用了,拿出从网上淘来的万能小微工程Makefile构建模板(这里省去非关键部分代码)。

Makefile 文件

#...
# OBJS_WITHOUT_MAIN = repot_sys.cpp
# TARGET_SHARED = librepot.dylib

$(BUILD_DIR)/$(TARGET_SHARED): $(OBJS_WITHOUT_MAIN)
  $(CXX) -dynamiclib $< -o $@ $(LDFLAGS)

lib: $(BUILD_DIR)/$(TARGET_SHARED)
  @echo "library successfully generated"

# c++ source
$(BUILD_DIR)/%.cpp.o: %.cpp
  $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@  

通过在命令行调用

make lib

编译器通过-dynamiclib参数生成librepot.dylib动态链接库。有了librepot.dylibrepot_sys.h头文件,就可以直接在xcode工程中整合调用了。

STEP 2

这里以xcode 11作为例子,选中项目的构建TARGETS中的主target,在 Build Phases 标签中有几处选项需要配置。

设置一 Link Binary With Libraries

这里主要是告诉链接器需要链接的库文件,在这里添加的文件,实际在生成的是对应的clang的编译命令-lxxx。例如添加了librepot.dylib, 就会生成对应的编译命令 -lrepot

设置二 Embed Libraries

这里主要是告诉编译器,在链接期间去指定目录搜索库文件,并同时指定运行期间库文件的存放目标位置。实际是生成了对应的编译命令-L/path/to/lib。其中 Destionation 选项指定库文件的运行时存放目标位置。 比如我们讲 Destionation 设置为Frameworks实际上是将库文件同时拷贝到运行时目录中的Frameworks目录下,这样应用运行时,运行时会动态去Frameworks目录下查找库文件并 加载到内存中链接。

比如我们在这里添加 /Users/xxx/code/prjs/call_dylib/call_dylib/3rd中的librepot.dylib文件到这里,同时声明 DestinationFrameworks。Xcode会自动添加-L/Users/xxx/code/prjs/call_dylib/call_dylib/3rd的编译指令,链接时在此目录下查找动态库文件。同时拷贝库文件到运行时目录中Frameworks目录下。

设置三 Headers

在这里添加repot_sys.h头文件作为系统头文件搜索路径。

STEP 3

新建xcode工程,这里选择创建mac app工程(iOS工程类似),创新主窗口的ViewController, 插入一个居中的Button,绑定action到ViewController中的回调callDylibFn

#include <repot_sys.h>

//...

-(IBAction)callDylibFn:(id)sender {
  //这里调用librepot.dylib中的 C API
  const char* os = repot_sys_os();
  printf("os is %s\n", os);
  free((char*)os);
}

添加头文件引用和调用逻辑,一切就绪。编译也可以顺利通过,但是启动调式应用时,得到的却是动态链接库找不到的提示,如下

dyld: Library not loaded: build/librepot.dylib

解决动态链接库依赖查看路径问题

要理解这里为什么编译可以通过,在运行时提示找不到动态链接库需要首先解释以下几个概念。

  • 动态链接库的加载链接时机

Mach-O是苹果公司的二进制可执行文件的通用文件格式。dylib动态链接库也是基于这种通用格式的。dylib动态链接库的加载链接时期分为两种类型。第一种是启动时链接,第二种是运行时主动链接。 上面的流程中,在xcode中配置Link Binary With Libraries就是一种启动时链接类型。xcode在编译链接期间,静态链接器会记录下通过-l参数指定的动态链接库作为引用的依赖链接库。并且在 应用的可执行文件中的首段的加命令中记录下这些动态链接库的依赖列表。同时在应用启动时,dyld会逐个加载这些依赖链接库。

通过命令行工具otool -L就可以查看应用本身的可执行文件的依赖列表(otool可以查看Mach-O文件的段信息)。

otool -L /path/to/call_dylib.app/Contents/MacOS/call_dylib:
	build/librepot.dylib (compatibility version 0.0.0, current version 0.0.0)
	/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1673.126.0)
	/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)
	/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit (compatibility version 45.0.0, current version 1894.10.126)
	/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1673.126.0)
  • @rpath, @load_path和@executable_path

重点说说@rpath, @rpath是一种路径通配符,是可以动态替换为可选路径列表的,也就是说,@rpath可以是一个待搜索的目录列表,在动态链接期间,可以动态替换为其他目录或者目录列表。它本身也是一种参数,可以在应用编译链接期间指定为链接器的参数,例如

clang -Xlinker -rpath -Xlinker @executable_path/../Frameworks

可以在xcode的设置中指定。 @executable_path也是一种路径通配符,它代表运行时应用本身的目录路径。@executable_path/../Frameworks意思是此dylib放置在应用本省的上一级目录下的Frameworks目录下。(还记得我们上面的设置,我们把配置的动态链接库拷贝到了Frameworks目录下)

回到上面找不到动态链接库的问题,otool工具显示,应用的动态链接库依赖项librepot.dylib的搜索路径为build/librepot.dylib,很明显在应用启动时,动态链接器找不到这个build目录。要解决这个问题,必须把应用的动态库依赖librepot.dylib的路径修改为运行时能找到的路径。比如我们利用@rpath把动态库的路径修改为如下这种形式

@rpath/librepot.dylib (compatibility version 0.0.0, current version 0.0.0)

要改变应用的动态库依赖路径,需要间接修改xcode中配置的动态库本身的依赖列表(动态链接库也是Mach-o文件,它也有依赖列表),利用otool查看链接库本身的依赖如下

otool -L build/librepot.dylib 
build/librepot.dylib:
	build/librepot.dylib (compatibility version 1.0.0, current version 1.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.4)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1)

可以看到动态链接库本身的依赖中,主要分为两部分,第一部分是首行,它被称为动态链接库的id名。第二部分除首行的剩余行,它们被称为动态链接库的依赖列表。 可以通过2种方式来修改:

  1. 使用install_name_tool命令行工具来修改现有dylib文件的依赖
    • 如果需要修改动态链接库的id名,可以使用命令install_name_tool -id
    • 如果需要修改动态链接库的依赖dependents,可以使用命令install_name_tool -change
  2. 在编译链接期间指定-install_name参数,设置动态链接库的id名

这里我们通过第2种方式修改,方法是修改Makefile中的编译命令,增加-install_name参数指定动态链接库的id名

$(BUILD_DIR)/$(TARGET_SHARED): $(OBJS_WITHOUT_MAIN)
  $(CXX) -dynamiclib -install_name -current_version 1.0.0 -compatibility_version 1.0.0 @rpath/librepot.dylib $< -o $@ $(LDFLAGS)

修改完成之后,通过otool工具查看修改生否生效

otool -L build/librepot.dylib 
build/librepot.dylib:
	@rpath/librepot.dylib (compatibility version 1.0.0, current version 1.0.0)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.4)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1)

在应用启动时期,动态链接器会将@rpath替换为@executable_path/../Frameworks(回忆上文提到的),从而能正确找到依赖库的位置。

定位和解决了动态链接库依赖的路径查找问题之后,重新运行xcode工程,编译运行,此时又出现了新的问题,工程能通过编译,应用也能启动运行,但是在API调用期间找不到符号,如下:

dyld: Symbol not found: _repot_sys_os

解决找不到符号的问题

出现上述问题的原因是C和C++的符号(symbol)生成逻辑不同导致。动态链接器链接期间,尝试去绑定C兼容的符号表_repot_sys_os,但是我们的c++库导出的却是c++的符号表。 这里涉及到的是c/c++的符号生成逻辑(Symbol mangling)差异和跨语言绑定接口(ABI, Application Bindary Interface),内容较为高深复杂,感兴趣的可以自行google研究。

因此,我们需要调整c++工程代码,导出C兼容的符号表

调整头文件

repot_sys.h

#ifndef REPOT_SYS_
#define REPOT_SYS_

#ifdef __cplusplus
extern "C" {
#endif

  const char* repot_sys_os(void);

#ifdef __cplusplus
}
#endif

#endif

extern关键字告诉编译器,按照C代码编译处理。

调整代码实现

repot_sys.cpp

#include "repot_sys.h"
#include <cstring>
#include <cstdlib>
#include <sys/utsname.h>

extern "C" {
  const char* repot_sys_os() {
    struct utsname u;
    uname(&u);

    size_t len = strlen(u.sysname);
    char* os = (char*)malloc(sizeof(char) * (len + 1));
    strcpy(os, u.sysname);
    os[len] = '\0';

    return os;
  }
}

重复以上STEP 1STEP 2,再次启动xcode调式工程应用,就可以顺利看到调用成功的日志了

os is Darwin

补充topic 1 符号表的可见性问题

通常作为公共库,你可能不想让代码的实现细节产生的符号表对使用方可见,这就需要在编码期间和编译期间做一些设置,只导出API对应的符号表就可以了。

在编译期间隐藏符号表

CPPFLAGS ?= $(INC_FLAGS) -MMD -MP -fvisibility=hidden

visibility flag告诉编译器隐藏符号表。

在代码中,只导出需要的符号表

// ...

#define EXPORT __attribute__((visibility("default")))

// ...
extern "C" {
  EXPORT const char* repot_sys_os() {
    //...
  }
}

EXPORT宏告诉编译器导出指定的符号表。

补充topic 2 如何将c++实现封装为bind友好的C API

通常我们在端上想借助整合c++能力的时候,更希望的是整合社区中或者公司内部已有的c++成熟工程,而且通常这样的工程code base不会小。那么就涉及到设计一层C API到打通应用(这里比如Objc)和c++工程。 同时我们又不想对现有的c++做过多的修改和封装,这就需要在c和c++ API之间进行切换,来最大程度的利用现有代码(比如c++代码中大量的class file等)。

举一个例子,例如我们想调用一个既有的具备多国语言翻译功能的c++代码,代码的实现可能是这样的

class AmazingTranslator {
  private:
    char* from_lang;
    char* to_lang;

  public:
    AmazingTranslator(char* from_lang, char* to_lang);
    ~AmazingTranslator();
    char* translate(char* sentense) const;
    void config_lang(char* from, char* to);
}

如何设计C API呢,在不过多封装,同时具备可操作性的原则下,C API设计可能是这样的。

typedef struct _TranslatorHandler {} TranslatorHandler;

extern "C" {
  TranslatorHandler* get_amazing_translator();

  void config_amazing_translator_lang(TranslatorHandler* translator, char* from_lang, char* to_lang);

  char* amazing_translator_translate(TranslatorHandler* translator, char* sentense);

  void destory_amazing_translator(TranslatorHandler* translator);
}

在实现的过程中,需要借助reinterpret_cast指针重解析功能,骗过调用环境。

EXPORT TranslatorHandler* get_amazing_translator() {
  AmazingTranslator* translator = new AmazingTranslator("en", "zh");
  return reinterpret_cast<TranslatorHandler*>(translator);
}

EXPORT void destory_amazing_translator(TranslatorHandler* translator) {
  AmazingTranslator* translator = reinterpret_cast<AmazingTranslator*>(translator);
  delete translator;
}

最后

整合过程很曲折,但借此了解了许多细节,如有疏漏,不吝指正。