65.9K
CodeProject 正在变化。 阅读更多。
Home

混合 C/C++/Objective-C 开发中的互操作性问题,第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (14投票s)

2014年3月25日

CPOL

8分钟阅读

viewsIcon

21934

一篇关于混合 C/C++/Objective-C 开发中互操作问题的文章

引言

本文概述了我们在 C、Objective-C、C++ 和 Objective-C++ 代码互操作时可能遇到的几个复杂问题。所有代码示例都在 XCode 4.6.2(Apple LLVM 4.2 (Clang) 和 LLVM GCC 4.2)以及最新的 XCode 5.0 的编译器上进行了测试。由于 XCode 5.0 不再包含 GCC 编译器,因此示例仅在最新的 Apple LLVM 5.0 (Clang) 上进行了检查。如果最新的 Clang 结果不同,我会明确说明。

背景

在许多情况下,例如,我们需要从 Objective-C 代码中使用 C++ 代码,或者从 Objective-C 调用 C 函数,本文将对此进行介绍。您还应该了解 Objective-C++ 是什么。它是一种在同一个文件中混合 Objective-C 和 C++ 代码并使其能够相互操作的方法。您当然可以在 XCode 中使用它——将您的文件名命名为 .mm 扩展名。

问题 1. 在 Objective-C++/C++ 代码中包含模板 MAX/MIN

描述

假设您在 CppTemplate.h 文件中有以下代码

template<class T>
inline const T &MAX(const T &a, const T &b)
{ return b > a ? (b) : (a); } 

文件 ObjCppClass.h

#import <Foundation/Foundation.h>
@interface ObjCppClass : NSObject
@end 

文件 ObjCppClass.mm

#import "ObjCppClass.h"
#include "CppTemplate.h"
@implementation ObjCppClass
@end 

尝试构建时,您会遇到类似 Clang 的编译错误

«CppTemplate.h:17:17: Expected unqualified-id
CppTemplate.h:17:17: Expected ')'» 

如果将 CppTemplate.h 包含在 .cpp 文件中,则不会出现错误。

解决方案

这个问题 arises from the fact that there are macros called MAX/MIN in Objective-C, which look like this (from a file NSObjCRuntime.h)

#define MAX(x,y) ((x) > (y) ? (x) : (y))

所以模板实例化会变成如下形式

template<class T> 
inline const T& ((const T& a) > (const T& b) ? (const T&a) : (constT&b)) (const T& a, const T& b) { ... } 

这完全没有意义且无法编译。

要修复此错误,请将模板重命名,或使用 std::max

问题 2. ARC 禁止在结构体中使用 Objective-C 类型的对象

描述

下面是一个包含两个 Objective-C 类 NSString 对象的结构体

typedef struct SStrings
{
    NSString* firstName;
    NSString* secondName;
} SStrings; 

在 ARC 项目(自动引用计数)中,您将收到编译错误 «main.m: 15:15: ARC forbids Objective-C objects in structs or unions»,即使您为该特定文件在项目设置中设置了 -fno-objc-arc 标志。

解决方案

首先,我应该简要解释一下 ARC(自动引用计数)是什么。它是一种机制,使开发者无需手动管理内存。也就是说,编译器会在代码中插入 retain/release/autorelease 的调用,而不是您自己。

ARC 机制介于垃圾回收器和手动内存管理之间。与垃圾回收器一样,ARC 使开发者无需编写 retain/release/autorelease 的调用。但是,与垃圾回收器不同的是,ARC 不识别强循环引用(retain)。互相拥有强引用的两个对象即使没有其他引用指向它们,ARC 也不会将其处置。开发者仍然需要避免或销毁对象到对象的强循环引用。ARC 仅由 Clang 支持。

现在回到主题。要避免此错误,您需要在结构体中声明 NSString 对象之前加上 __unsafe_unretained 属性:

typedef struct SStrings{ 
    __unsafe_unretained NSString* firstName; 
    __unsafe_unretained NSString* secondName;
} SStrings; 

接下来,我应该解释一下 __unsafe_unretained 是什么。

默认情况下,ARC 中的所有对象都是 __strong 类型的。这意味着当您将一个对象赋值给一个变量时,它的引用计数会增加,并且只要有对它的引用,该对象就会被保留。这为循环引用提供了可能。例如,当一个对象将另一个对象作为类变量时,而第二个对象又以强引用的方式指向第一个对象(例如作为代理),那么这两个对象将永远不会被释放。

__unsafe_unretained__weak 限定符正是为此目的而设计的。它们最常用于代理。这意味着代理实例仍然指向第一个对象,但对于这个对象,引用计数不会增加,从而打破了循环引用并允许两个对象被释放。

这两种修饰符都可以阻止对象的保留,但方式略有不同。对于 __weak,在对象被移除后,变量将被赋为 nil,这是一种非常安全的方式。正如其名称所示,带有 __unsafe_unretained 限定符的变量在对象移除后仍会指向该对象所在的内存。这可能导致在访问已释放的对象时崩溃。

那么,为什么您可能想使用 __unsafe_unretained 呢?不幸的是,__weak 仅支持 iOS 5.0 和 Lion 作为平台。如果您想在 iOS 4.0 和 Snow Leopard 上运行应用程序,则必须使用 __unsafe_unretained 限定符。

现在假设您可以在使用 ARC 的情况下编写这样的代码

typedef struct { 
    __strong NSObject *obj; 
    int ivar;
} SampleStruct; 

那么您可以编写如下代码

SampleStruct *thing = malloc(sizeof(SampleStruct)); 

问题在于 malloc 不会将返回的内存清零。因此 thing->obj 是一个随机值,不一定是 NULL。然后您像这样赋值:

thing->obj = [[NSObject alloc] init];

实际上,ARC 会将此代码转换为类似如下的内容

NSObject *temporary = [[NSObject alloc] init];
[thing->obj release];
thing->obj = temporary; 

这里的问题是,您的程序刚刚向一个随机值发送了 release。应用程序很可能会在此处崩溃。

您可能会说 ARC 应该识别 malloc 的调用并负责将 obj 设置为 nil 以防止这种情况。关键在于 malloc 可以被封装到其他函数中:

void *customAllocate(size_t size) {
    void *p = malloc(size); 
    if (!p) { 
        // malloc failed.  Try to free up some memory. 
        clearCaches(); 
        p = malloc(size); 
    } 
    return p;
} 

好的,现在 ARC 也应该知道您的函数 customAllocate ,而且它可能是一个您以二进制形式收到的静态库。

您的应用程序也可以使用自定义内存分配器,这些分配器会重用旧的分配而无需使用 freemalloc。因此,即使更改 malloc 使其清零分配的内存,也不会起作用。ARC 应该了解您程序中的所有特殊分配器。

要可靠地实现这一点将非常困难。因此,ARC 的创建者们选择放弃,并禁止在结构体中使用 __strong

这就是为什么您只能在结构体中放置带有 __unsafe_unretained 限定符的对象,以告诉 ARC “不要尝试控制此变量引用的对象的拥有权”。

仅对 Clang 有效,因为 GCC 完全不支持 ARC。

问题 3. 带有块的代码在 Objective-C 中编译成功,但在 Objective-C++ 中无法编译

描述

首先,关于“块”的几点说明。“块”这个词有点歧义,所以我只是说我不是指 C 中存在的、用大括号组合成一个单元的运算符组。我指的是 Apple 提出的对 Objective-C 语言的补充,它允许使用匿名函数(或 lambda)。

因此,一个典型的简单块看起来是这样的

void (^block)()  = ^{ printf("Hello world\n"); } 

它只是打印字符串。

大括号前的井号 (^) 符号区分了我们的语句和经典的运算符块。定义了块之后,您可以如下调用它

block ();

您会在控制台上看到打印的行“Hello world”。

现在让我们回到我们的主题。假设您在 Objective-C 中有以下代码

@interface TestClass1 : NSObject
- (void)test;
@end
@implementation TestClass1
- (void)test
{ 
    void (^d_block)(void) = 
    ^{ 
        int n; 
    };
}
@end 

C++ 中的情况大致相同

class TestClass2
{
public:
    void TestIt();
};
void TestClass2::TestIt()
{ 
    void (^d_block)(void) = 
    ^{ 
        int n; 
    };
} 

Objective-C++ 中的情况也是如此

class TestClass3
{
public:
    void TestIt();
};
void TestClass3::TestIt()
{
    void (^d_block)(void) = 
    ^{ 
        int n; 
    };
}  

Clang 编译了所有 3 个代码片段,但分别生成了“Unused variable 'n'”警告 3 次。在 C++ 版本中,Clang 应该生成一个错误消息,因为块是 Objective-C 的特性,但它并没有这样做。这看起来像是一个 bug,但更像是一个特性。由于块被认为是有用的工具,因此它们被设计成可以在 C++ 代码中识别。

GCC 在 Objective-C++ 和 C++ 版本中都发出了几乎相同的抱怨

«TestClass2.cpp:15: 'int TestClass2::n' is not a static member of 'class TestClass2'»
«TestClass3.mm:15: 'int TestClass3::n' is not a static member of 'class TestClass3'». 

在 C++ 代码中,GCC 应该说它也不知道块。这似乎也是一个特性。

解决方案

这些是 GCC 中的 bug。关于这个主题有一个 bug:http://lists.apple.com/archives/xcode-users/2011/Mar/msg00232.html。您可以通过使变量为静态变量或切换到 Clang 来解决此问题:

class TestClass3
{
    static int n;
public:
    void TestIt();
};
void TestClass3::TestIt()
{
    void (^d_block)(void) =
    ^{
    };
}  

问题 4. 无法将块分配给 C++11 lambda

描述

Clang 支持 C++11。这会带来一些有趣的问题。

您可以将 lambda 分配给块。

void (^block)() = []() -> void {
NSLog(@"Inside Lambda called as block 1!");
};
block();   

您可以将块分配给 std::function

std::function<void(void)> func = ^{
NSLog(@"Block inside std::function");
}; 
func(); 

但是,您无法将块分配给 lambda。

auto lambda = []() -> void {
NSLog(@"Lambda!");
};
lambda = ^{ // error!
NSLog(@"Block!");
};
lambda();  

然后您将收到编译错误 «main.mm: 40:12: No viable overloaded '='»。

解决方案

此问题没有解决方案。您不能将一个 lambda 分配给另一个 lambda(即使结构相同)。

auto lambda1 = []() { return 1; };
auto lambda2 = []() { return 1; };
lambda1 = lambda2; //Error  

编译时错误 «main.mm: 67:13: No viable overloaded '='» 发生。

Lambda 甚至不能分配给自己。

auto lambda = []() -> void { printf("Lambda 1!\n"); };
lambda = lambda; 

代码也无法编译,出现错误 «main.mm: 63:12: Overload resolution selected implicitly-deleted copy assignment operator»。

每个 lambda 都有其自己实现定义的类型。

对于 XCode 5 中的 Clang 来说,情况都是一样的,但错误消息略有不同。

Visual Studio 2010 编译了将 lambda 分配给自己的操作,但 IntelliSense 产生了一个奇怪的错误消息

«IntelliSense: function" lambda [] void () ->void :: operator = (const lambda [] void () -> void &) "(declared at line 9) cannot be referenced - it is a deleted function».

原因是 Visual Studio 2010 不支持已删除的方法。

关于已删除方法的简要离题。它们管理默认行为。

现在,标准的“不可复制”惯用法可以显式表达如下

class X { 
    // ... 
    // Noncopyable 
    X& operator=(const X&) = delete;
    X(const X&) = delete;
}; 

反之,您可以显式声明您希望使用复制的默认行为

class Y { 
    // ... 
    // Default copy semantics 
    Y& operator=(const Y&) = default;
    Y(const Y&) = default;
};  

结论

很明显,不同语言的代码互操作并非易事,但遗憾的是有时却是必需的。我希望我的文章能有趣、有用,并帮助许多人避免犯类似的错误。

© . All rights reserved.