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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.35/5 (14投票s)

2013 年 12 月 5 日

CPOL

9分钟阅读

viewsIcon

30903

一篇关于混合 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++ 代码,或者调用 C 函数,例如,所以本文就是关于这个的。您还应该了解什么是 Objective-C++。它是一种在同一文件中混合 Objective-C 和 C++ 代码并实现它们之间互操作的方法。您可以在 XCode 中使用它——将您的文件名命名为 .mm 扩展名。

问题 1. 赋值返回 C++ 对象的 Block 时出错

描述

假设我们有一个简单的 Objective-C 类层次结构。

@interface Vegetable : NSObject 
@end 
@interface Carrot : Vegetable
@end 

然后我们可以这样写

Vegetable *(^getVegetable)();
Carrot *(^getCarrot)();
getVegetable = getCarrot;  

这意味着,对于 Objective-C 类而言,Block 在返回值方面是协变的:返回更具体内容的 Block 可以被认为是返回更通用内容的 Block 的“子类”。在这里,返回 Carrot 的 Block getCarrot 可以安全地赋值给 Block getVegetable。实际上,当您期望得到 Vegetable * 时,始终安全地获取 Carrot *。而且,逻辑上反向操作是行不通的:getCarrot = getVegetable; 无法编译,因为当我们真心想要一个胡萝卜时,我们并不真正喜欢得到一个蔬菜。

您也可以这样写

void (^eatVegetable)(Vegetable *);
void (^eatCarrot)(Carrot *);
eatCarrot = eatVegetable; 

这表明 Block 在参数方面是逆变的:能够处理更通用参数类型的 Block 可用在需要处理更具体参数的 Block 的位置。如果 Block 知道如何吃蔬菜,那么它也会知道如何吃胡萝卜。同样,反向操作是不可行的,也不会编译:eatVegetable = eatCarrot;

以上都是 Objective-C 的情况。现在尝试在 C++ 和 Objective-C++ 中做类似的事情。

class VegetableCpp {};
class CarrotCpp : public VegetableCpp {};  

然后以下代码行将无法编译

VegetableCpp *(^getVegetableCpp)();
CarrotCpp *(^getCarrotCpp)();
getVegetableCpp = getCarrotCpp; // error!
 
void (^eatVegetableCpp)(VegetableCpp *);
void (^eatCarrotCpp)(CarrotCpp *);
eatCarrotCpp = eatVegetableCpp; // error! 

Clang 产生以下错误

«Main.mm: 28:17: Assigning to 'VegetableCpp * (^) ()' from incompatible type 'CarrotCpp * (^) ()'

main.mm: 32:17: Assigning to 'void (^) (CarrotCpp *)' from incompatible type 'void (^) (VegetableCpp *)' »

GCC 产生类似的错误

«Main.mm: 28: Cannot convert 'CarrotCpp * (^) ()' to 'VegetableCpp * (^) ()' in assignment

main.mm: 32: Cannot convert 'void (^) (VegetableCpp *)' to 'void (^) (CarrotCpp *)' in assignment

main.mm: 28:17: Assigning to 'VegetableCpp * (^) ()' from incompatible type 'CarrotCpp * (^) ()'

main.mm: 32:17: Assigning to 'void (^) (CarrotCpp *)' from incompatible type 'void (^) (VegetableCpp *)' » 

解决方案

这个问题没有解决方案,并且预计在未来也不会出现。尽管我在此处 提交了一个关于此主题的 Clang 编译器错误。据我理解,这不会被实现。这种区别是故意的,原因是 Objective-C 和 C++ 的对象模型之间的差异。更具体地说:拥有一个 Objective-C 对象的指针,您可以将其转换为基类或派生类对象的指针,而无需实际更改指针的值:对象的地址保持不变。

由于 C++ 允许多重继承和虚拟继承,因此这一切都不适用于 C++ 对象:如果我有一个 C++ 类的指针,并将其转换为基类或派生类对象的指针,我可能需要调整指针的值。

考虑这个例子

class A { int _a; }
class B { int _b; } 
class C : public A, public B { }
 
B *getC() { 
  C *cObj = new C; 
  return cObj;
}  

例如,在方法 getC() 中,类 C 的新对象在地址 0x10 处分配。指针 cObj 的值为 0x10。在 return 语句中,指向 C 的指针应该被调整,以便它指向 CB 的部分。由于 B 在类 C 的继承列表中位于 A 之后,因此(通常)在内存中 B 会跟在 A 之后,这意味着将 4 字节的偏移量(== sizeof (A))添加到指针上,因此返回的指针是 0x14。类似地,将 B* 转换为 C* 会导致指针减去 4 字节。当我们处理虚拟基类时,原理是相同的,但偏移量是未知的。

现在,考虑一下所有这些对这样的赋值有什么影响

C* (^getC)(); 
B* (^getB)(); 
getB = getC;  

Block getC 返回一个指向 C 的指针。要将其转换为返回指向 B 的指针的 Block,我们必须在每次 Block 调用时通过添加 4 字节来调整返回的指针。这不是 Block 的调整,而是 Block 返回的指针值的调整。可以通过创建一个包装前一个 Block 的新 Block 来实现此目的,并进行更正,例如

getB = B* (^thunk)() { return getC(); }  

这对于编译器来说是可以实现的,它已经在覆盖返回协变类型且需要调整的虚函数时提供了类似的“技巧”。然而,在 Block 的情况下,它会导致额外的问题:允许使用相等比较运算符 == 来比较 Block,因此,为了确定“getB == getC”,我们应该能够查看此包装器,该包装器是通过将“getB = getC”赋值给 Block 返回的底层指针进行比较而生成的。同样,这是可以实现的,但需要为 Block 提供一个更重的运行时,以便能够创建执行这些返回值调整(与任何逆变参数相同)的唯一包装器。虽然这一切在技术上都是可行的,但成本(运行时的体积、复杂性和执行时间)大于收益。

回到 Objective-C,我注意到单重继承模型永远不需要对对象指针进行任何调整:Objective-C 对象只有一个地址,静态指针的类型并不重要,因此协变/逆变永远不需要任何包装,Block 赋值只是一个指针赋值(使用 ARC 时为 _Block_copy / _Block_release)。

问题 2. 在 ARC 模式下将 Objective-C 对象传递给 C 函数时出错

描述

假设我们想将某个 Objective-C 对象作为 void* 类型参数传递给 C 函数,例如传递给 pthread_create

pthread_t thread;
NSObject *obj = [[NSObject alloc]init];
pthread_create(&thread, NULL, startThread, obj); 

当 ARC 开启时,我们将收到编译错误

«main.m:36:48: Implicit conversion of Objective-C pointer type 'NSObject *' to C pointer type 'void *' requires a bridged cast»

ARC 不会轻易允许进行这种转换。

解决方案

您需要使用桥接转换来在两个内存模型之间移动。这种转换最简单的形式是(如果您将鼠标悬停在代码中的错误上,XCode 本身会提供它)

pthread_create(&thread, NULL, startThread, (__bridge  void*)obj); 

这与第一个代码等效,但现在,由于我们在 ARC 模式下,还有额外的注意事项。问题在于 ARC 会认为如果 obj 是对它的最后一个引用,则可以释放它。不幸的是,pthread_create 对对象一无所知,因此不会保留该参数。到线程启动时,对象可能已被移除。正确的解决方案如下

pthread_create(&thread, NULL, startThread, (__bridge_retained  void*)obj); 

这将在传递参数之前调用 objc_retain()

另一方面,线程函数应该看起来像这样

void* startThread(void *arg)
{
    id anObject = (__bridge_transfer id)arg;
    arg = NULL;
    //…
}  

这将使对象重新回到 ARC 的控制之下。这种形式的桥接转换期望对象已被保留,因此将在 Block 结束时释放它(调用 release)。行 arg = NULL 是可选的,但这是一个好的风格。您已将 arg 所包含的引用置于 ARC 的控制之下,因此重置指针是显而易见的。

注意

这是 Clang 的自动引用计数文档的内容

«桥接转换是 C 风格的转换,并用以下三个关键字之一进行注释

  • (__bridge T) op 将操作数转换为目标类型 T。如果 T 是一个可保留对象指针类型,则 op 必须是非可保留指针类型。如果 T 是非可保留指针类型,则 op 必须是可保留对象指针类型。否则,转换是无效的。不进行所有权转移,ARC 不会插入任何保留操作。
  • (__bridge_retained T) op 将操作数(必须是可保留对象指针类型)转换为目标类型(必须是非可保留指针类型)。ARC 会保留该值,但会受到本地值通常的优化。接收者负责平衡这个 +1。
  • (__bridge_transfer T) op 将操作数(必须是非可保留指针类型)转换为目标类型(必须是可保留对象指针类型)。ARC 将在封闭的完整表达式结束时释放该值,但会受到本地值通常的优化。

这些转换是为了将对象移入和移出 ARC 控制范围所必需的;请参阅关于可保留对象指针转换的理由部分。

仅出于说服 ARC 发出非平衡的保留或释放而使用 __bridge_retained__bridge_transfer 转换,这是不好的做法。»

仅对 Clang 有效。

问题 3. 如何从 Objective-C 代码调用内联 C 函数

描述

假设我们有文件 Foo.hFoo.c

文件 Foo.h
inline void foo(); 

文件 Foo.c

inline void foo()
{
    printf("Hello, World\n");
} 

main.m 文件如下

#import <Foundation/Foundation.h>
#include "Foo.h"
 
int main(int argc, const char * argv[])
{
 
    foo();
 
    // insert code here...
    NSLog(@"Hello, World!");
 
    return 0;
} 

然后我们将收到一个链接器错误,表明找不到符号 foo。

这个问题有不同的解决方案。C99 提供了这种方法:它要求程序员提供一个包含 extern 内联函数可调用副本的模块。默认情况下,C99 中没有存储说明符的内联函数被视为 extern。如果模块中所有内联函数的声明都没有存储说明符,则该函数被视为 extern inline,并且不会为其生成主体。另一方面,如果模块中内联函数的声明之一明确包含 extern 关键字,那么正是该模块将生成该函数的可调用副本。这导致了源文件的以下组织。

解决方案

在头文件中插入 extern 内联函数的定义,而不使用 extern 关键字。此头文件可以包含在任意数量的模块中。在只有一个模块中,您应该包含此头文件并使用 extern 声明函数原型,以获取函数的(可调用)副本(在此模块中,不一定需要重复 inline 关键字)。

示例将更改如下

文件 Foo.h

inline void foo()
{
    printf("Hello, World\n");
}   

文件 Foo.c

extern inline void foo();  

文件 main.m 保持不变。

问题得到解决。还有另一种解决方案——您可以将环境设置中的 C 方言选项切换到 GNU 89,而无需修改源文件。这两种编译器都适用。使用最新的 clang(XCode 5.0),此解决方案无效。您需要将 static 放在函数之前。我在此处 提交了一个此问题的错误,但我没有获得任何关于此问题的信息,除了需要放置 static

结论

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

历史

  • 2013.12.05:初始版本
© . All rights reserved.