iOS — ObjectiveC混编 c++


一、Objective-c 和 C++为什么能混编

1. C++的“不跨平台性”与“跨平台性”

现在对于C++,有两种说法:“跨平台”和“不跨平台”。 对于这两种说法,先来做一个解释。
C++ 本身只是一种语言,并无“跨平台”与“不跨平台”之分。

1.1 C++的“不跨平台性”

C++不能跨平台,不是指源程序不能跨平台,而是可执行文件不能跨平台。因为C++源程序要经过预处理、编译、汇编和链接过程才生成可执行文件(与平台相关的机器码),但这过程中会受到平台限制,如汇编语言有不同版本,和平台有关(CPU、操作系统和编译器等);所以不能将windows平台生成的exe扔到Linux平台运行。所谓“不跨平台”,只是经过不同系统上的编译器编译出来的目标代码是机器相关,不能跨平台的。

1.2 C++的“跨平台性”

前面说过C++不能跨平台,不是指源程序不能跨平台。纯粹的C++语言可以跨平台开发,只要它的编译过程不依赖于平台即可。如果你的源程序不调用各个系统相关的API,只是使用C++的语言编写的源程序,那么只要在支持C++编译的机器平台上就都可以编译运行起来,这就是它的“跨平台性”。
所以很多跨平台的代码都是使用C++编写的,C++程序在Android、iOS、Windows、WP都可以正常运行,这是因为这些系统平台上都有支持C++编译的编译器。GCC编译器占据大多数。

1.3 不同的编译器

目前主要流行的两大编译器:GCC和Clang。这两者的区别很明显:GCC支持更为广泛,比较臃肿,Clang苹果研发,就支持C/OC/C++三种语言,但是速度快、内存占用少等。这两大编译器的发展历史,区别等可以参考博客:XCode编译器介绍详解三大编译器:gcc、llvm 和 clang

很多人又会问,怎么是两大编译器呢?不是有三大吗?那LLVM呢?
这个问题就涉及到LLVM的理解了,LLVM有广义和狭义之分。广义的LLVM其实就是指整个LLVM编译器架构,包括了前端、后端、优化器、众多的库函数以及很多的模块;而狭义的LLVM其实就是聚焦于编译器后端功能(代码生成、代码优化、JIT等)的一系列模块和库。那么Clang就相当于广义上的LLVM的前端。
两者的关系可以参考文章:Clang和LLVM的关系. LLVM编译器之Clang前端

很多在Linux上编写代码的人经常会用到CMake工具, 但是CMake工具其实只是一个辅助编译的工具,它本身是不具备编译、链接等功能的。它只是帮助编译器让我们所有的程序能够快速的编译。参考:编译器gcc、clang、make、cmake辨析

1.4 总结

还有很多其他的跨平台语言,比如之前曾经流行过的H5、ReactNative,还有现下最是火热的 Flutter,他们都属于解释型跨平台,约定好一种脚本语言,底层辅助以各平台的基础实现。比如ReactNative的开发中使用JavaScriptCore,但本质上还是在运行时解释js然后再执行native,真正参与编译的是 语法解释器+底层代码而不是js,他们或多或少都是通过c++实现的。
再比如Java编译是生成的中间文件,可以放到各个操作系统所定制的java虚拟机中去,之后再进一步编译成机器语言并执行,所以Java可跨平台,它的跨平台是建立在有对应JVM基础之上的。JVM集成了在此平台执行的指令集。所以看起来java只需要写一遍代码,经过一次编译之后,就可以各平台通用。但是java针对不同的平台,需要不同的安装文件(你可以从官网上看到同一版本的java针对各个系统有不同的安装文件)。相对于其他语言各系统但不能跨平台,java只是把这些放到了安装文件中而已。
现在计算机的架构都是基于冯诺依曼的架构来完成的,具体执行的格式都是二进制的格式,不同的操作系统生成不同格式的二进制文件,从代码到可执行的二进制代码之间还需要有一种工具存在,这就是编译器存在的价值。

2. Ojective-C

OC这门语言是基于C扩展的,所以它是向下兼容C语言的,可以下载官方的Objc源码下来查看,发现OC对象都是结构体。关于OC对象,可以参考文章:探寻OC对象的本质

3. 兼容原因

ObjectiveC 与 C++ 都向下兼容C ,是他们的共同点和纽带。OC中的对象虽然有ARC辅助内存管理,但本质上还是一个void *,同理C++也一样是void *,OC之所以调用函数叫做发送消息(sendMsg),是因为封装了层独有的runtime机制(这机制还是C的),但归根结底每个函数实体是一个IMP,依然是一个函数指针,这一点和C++也一样,所以他们之间的衔接才会如此通畅。

二、混编实践

OC和C++混编的情况有两种:OC调用C++和C++调用OC。两者调用的原则是尽可能解藕,隔离两种语言,避免开发上的混乱和困难。
混编过程中主要存在以下三种文件:

  • 纯C++类:只需要分别创建对应的 .h 和 .cpp 文件。
  • 混编文件 .mm 文件:如果你想创建一个既能识别C++又能识别OC的对象,只需要照常创建一个.h 文件和.m文件,然后将.m文件重命名成.mm文件,就是告诉编译器,这个文件可以进行混编 — ObjectiveC++。
  • 纯OC类: .m文件和对应的头文件。

1. OC调用C++

1.1 使用场景:

这种情况出现在已有的第三方SDK是用C++实现的,在OC项目中要用到这个SDK提供的类。或者说某些组件是跨平台的,确实需要C++来实现。

1.2 具体实践

OC调用C++的类比较简单,直接就在混编文件.mm文件中引入头文件即可。

// OCCFile.mm
#import "OCCFile.h"
#import "CppFile.h"

@interface OCCFile ()
{
	CppFile* mCppFile;//
}
@end

@implementation OCCFile
//内部定义cpp对象
- (void)doSomethingWithString:(NSString*)str
{
	const char* charStr = [str UTF8String];
	std::string _str = std::string(charStr);
	mCppFile = new CppFile(std::string("My name is starimer"));
	mCppFile->DoSomething(_str);
}

-(void)dealloc
{
	delete mCppFile;// 记得自己管理内存
	[super dealloc];
}

@end

如上源码所示,.mm文件直接能过识别出C++的类。
但是有人就问:我就想在OC类的头文件中使用C++类,那怎么办呢?那就在头文件中定义C++类为id类型 或者 c语言的 void * ,然后在 .mm 中通过使用__bridge进行类型转换;
void* 是指针的最原本形态,利用它我们随意的进行C++和OC的混编,但是因为OC 对象 存在内存管理,但是 C++ 是没有的,所以在ARC下需要进行下特殊处理。关于__bridge,有三种情况:

  • __bridge: ARC下OC对象和Core Foundation对象之间的桥梁,转化时只涉及对象类型不涉及对象所有权的转化;
  • __bridge_retained: 将内存所有权同时归原对象和变换后的对象持有,并对变换后的对象做一次reatain。
  • __bridge_transfer:将内存所有权彻底移交变换后的对象持有,retain变换后的对象,release变换前的对象。

代码如下(示例):

// OCCFile.mm
- (void)doSomethingWithCPPobj:(id)cppobj
{
	CppOCFile* cppOb = (__bridge CppOCFile*)cppobj;// id to pointer.
	cppOb->DoSomething1();
}

2. C++调用OC

C++ 调用OC 就不是那么简单了,有两种方式可以让C++调用OC:

  • void*类型做转换
  • 直接定义C++的类,但是实现采用.mm文件。

2.1 void*类型做转换

这里我们利用一个C 函数做桥,将OC 对象转化为 void * 类型的指针传入。

// InterfaceBridge.h
#pragma once
typedef void(*OCInterfaceCFunction)(void*, void*);
//InterfaceCC.h
#pragma once
#include "InterfaceBridge.h"
class  InterfaceCC{
public:
	InterfaceCC(void* ocObj, OCInterfaceCFunction interfaceFunction);
	~InterfaceCC();
public:
	void doSomthings();
private:
	void* mOCObj; // OC类
	OCInterfaceCFunction mInterfaceFunction;
};
//InterfaceCC.cpp
#include "InterfaceCC.h"
#include <stdio.h>

InterfaceCC::InterfaceCC(void* ocObj, OCInterfaceCFunction interfaceFunction)
{
	mOCObj = ocObj;// 传入的是oc的对象
	mInterfaceFunction = interfaceFunction;// 传入的是oc对象的函数
}

void InterfaceCC::doSomthings()
{
	printf("there is c++.\n");
	mInterfaceFunction(mOCObj,NULL); // 调用oc对象的函数,并且把oc对象传入进去
}
// OCInterfaceC.m (纯OC类)
#import "OCInterfaceC.h"
#include "InterfaceBridge.h"

void MyObjectDoSomethingWith(void* obj, void* param)
{
	[(__bridge id)obj dosomthing:param];
}
@implementation OCInterfaceC
- (id)init
{
	self = [super init];
	if (self)
	{
		_call = MyObjectDoSomethingWith;
	}
	return self;
}
- (int)dosomthing:(void *)param
{
	printf("hei, there is OC .....\n");
	return 0;
}
@end

C++中调用OC的函数用法:

// OCCFile.mm
OCInterfaceC* OCtoC = [[OCInterfaceC alloc] init];
void* CCPointer = (__bridge void*)OCtoC; // 把oc对象转换为 void*
InterfaceCC* cc = new InterfaceCC(CCPointer, OCtoC.call);// 并且传入到C++对象当中。
cc->doSomthings();// 调用C++对象的函数,这个函数内部调用OC的OCInterfaceC这个对象的call函数。

这个调用的方法就是相当于OC 消息发送的机制,把OC的类以及相应的函数传到C++当中。然后在C++中发送消息。

2.2 mm中实现C++类

这种方式其实就是让xcode知道编译的时候是objective-c++的模式编译。头文件中的定义是C++的定义,实现中混着OC的代码,这种方式就会导致在MM文件中两种代码混杂在一起,所以整体还是不怎么推荐的。
代码如下:

//InterfaceBridge.h
#pragma once
#ifdef __OBJC__
#define OBJC_CLASS(name) @class name
#else
#define OBJC_CLASS(name) typedef struct objc_object name
#endif
...

// CCTOOC.h
#pragma once
#include "InterfaceBridge.h"
OBJC_CLASS(NSString);

class  CCTOOC{
public:
    CCTOOC();
    ~CCTOOC();
    
public:
    NSString* testCombinedCode();
};
//CCTOOC.mm
#import "CCTOOC.h"
#import <Foundation/Foundation.h>
#include <stdio.h>

NSString* CCTOOC::testCombinedCode()
{
    NSString* string = @"test combined code";
    return string;
}

在上面的代码中,在InterfaceBridge.h 文件中加入了预编译的命令,所以在CCTOOC.h 中通过使用OBJC_CLASS(NSString); 达到在C++类的头文件中使用OC 类的目的。之后在.mm文件中引入OC的头文件即可。

总结

这篇文章主要讲了OC与C++的混编原理,以及具体的调用情况。


版权声明:本文为LilyNothing原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。