SWIFT与C++的互操作性

2020-10-17 19:38:16

我们可以对SWIFT语言和标准库进行更改。提议的改变必须符合SWIFT的目标和理念。换句话说,提议的改变必须有合理的机会被SWIFT社区接受。例如,在ApplePlatforms上需要ABI中断的更改是不可能的。

派生SWIFT语言或标准库,或者创建没有派生的方言(因此,能够对SWIFT的目标、哲学、安全性或人体工学进行根本改变)都不是有趣的选择,因此不会考虑或讨论。

我们可以对C++代码、工具链、标准库实现和运行时环境进行有限的更改。必须考虑到这些变化的成本。对于控制完整链条的用户来说,需要Anabi Break for C++的更改可能会很有趣,但是对于整个社区来说,这些更改是不可能的;因此,这样的更改只能被视为优化。需要终端用户在现有C++代码上进行大量手动工作的更改也只能被认为是对人体工程学的优化或改进。

良好的互操作性层的属性是什么?增强互操作性本身并不是任何SWIFT或C++用户的目标。因此,对于API用户来说,互操作性应该在所有方面都最大限度地透明:API设计和人机工程学、编辑器集成和工具,以及性能。对于API供应商来说,互操作性不应该是一个重大的额外负担,同时允许API供应商拥有和管理导入的API图面。让我们讨论一下这几点是什么意思。

API设计和人体工程学。理想情况下,在SWIFT中工作的用户应该不会感觉到本地SWIFT API与导入的C++API之间有任何区别。

例如,虽然可以在SWIFT中编写自定义哈希表,但很少这样做,而且大多数代码都使用词汇类型Set和Dictionary。因此,如果使用STD::UNORDERED_MAP或FLAT_HASH_MAP类型的C++API在SWIFT中导入时会继续使用这些C++映射类型,则它们在SWIFT中看起来会很奇怪。使用Set和Dictionary的惯用SWIFT代码在调用导入的C++API之前必须将数据转换为这些外来哈希表类型。

又如,C++函数通常通过常量引用或按常量指针接收值。语义上,SWIFT中最接近的等价物是UnsafePointer。但是,UnsafePointer在SWIFT中并不常见,因为常量引用是Inc++;SWIFT API在耦合异常情况(例如,实现低级设施、对性能敏感的代码等)之外接受UnsafePointer是不习惯的。因此,如果C++API在导入SWIFT时会大量使用不安全指针,那么它们在SWIFT中会显得奇怪和陌生。导入的CAPI,当它们不在Objective-Ctype上操作时,已经使用了很多不安全指针,因此看起来不太习惯。SWIFT提供了一些费用,使呼叫他们变得更容易。

编辑器集成和工具应该透明地支持这两种语言的代码。在可能的范围内,所有的编辑器操作都应该看透互操作。例如,如果用户调用重命名函数重构,它应该重命名SWIFT和C++代码中的所有定义、声明和用法。SWIFT/Objective-C互操作在很大程度上已经实现了这一目标,我们计划依赖于SWIFT/C++互操作的相同机制。

表演。理想情况下,从SWIFT使用C++API时,其性能特征应与从C++使用时相同。

互操作不能成为API供应商的负担。使SWIFT代码能够调用特定的C++库应该会给该C++库的所有者带来最小的负担。理想情况下,这应该可以在没有业主参与的情况下进行。

例如,要求API所有者创建API描述文件(如CLIF for Python)或胶合层(如JNI for Java)是一项重大负担。如果没有用户的具体要求,大多数API所有者是不会这样做的,即使收到这样的请求,许多API所有者也会仔细考虑是否要承担这个额外文件的维护工作。

也许可以让用户完成向SWIFT公开C++库所需的工作,但是,这也不是一个很好的选择。一个C++库可能最终会有多个(不兼容的)质量不同的绑定,覆盖API的不同部分。此外,API的所有者将失去对API图面的控制。

允许API供应商拥有和管理导入的API表面。虽然向SWIFT公开C++库的最小工作量理想情况下应该是零(应该可以),但API供应商应该拥有其C++库的SWIFT API图面,并且应该能够在自动化互操作不能令人满意的地方对其进行调整。

目标之间的紧张和冲突。这些目标是相互冲突的。例如,API人机工程学与性能是冲突的:如果我们在API边界自动将C++类型桥接到相应的SwiftWorldger类型,我们可以提供更符合人机工程学的API,然而,这些类型转换将耗费CPU周期。解决方案将根据具体情况进行设计:有时,我们会选择权衡的一方,有时我们会选择一方但允许用户覆盖默认值,有时我们会添加多个工具来选择不同的一方,迫使用户做出选择。

SWIFT/C++互操作性建立在SWIFT/C互操作性之上,因此熟悉SWIFT导入C模块的策略很有帮助。

具有非标识符名称的成员函数在关于类一节中讨论。其他构造将在其各自的部分中讨论。

在C++中,作为函数参数传递的指针没有语言要求。例如,给定如下的函数签名:

作为一种语言,C++对值在函数调用期间的生存时间没有任何要求,甚至在调用函数时该值也指向有效内存;值可以是空的,也可以是无效的(例如,释放的)指针。对于被初始化的值所指向的内存也没有要求,或者在没有数据争用的情况下对取消引用是有效的。指针也可以作为其他内存的别名,只要它们的类型是兼容的。指针也不暗示任何所有权或非所有权。

C++中关于引用的规则稍微严格一些,但并不多。唯一的区别是引用不能为空,并且在最初创建时必须绑定到avalid对象。

在实践中,使用C++的工程师经常对第三方API做出以下假设,即使语言不能保证这些属性,并且API通常没有明确记录这些属性:

当函数运行时,几乎所有传递给函数的指针对于取消引用都是有效的。

几乎所有传递给函数的指针都指向初始化的数据。将apointer传递给未初始化的内存,并期望函数初始化它偶尔会发生,但这种情况并不常见。

在SWIFT中,将不安全的指针作为函数参数传递并不是惯用的做法。自然代码按值传递结构,按引用传递类。SWIFT中的INOUT参数允许函数读取和修改调用方指定的存储。InOut参数的实参必须是存储引用表达式。

函数增量(_value:InOut Int){value+=1}struct TwoInts{var x:int=0var y:int=0}Funcc Caller(){var int=TwoInts()Increment(&;INT.。X)//ints.x为1//ints.y为0}

要了解SWIFT对InOut参数施加的约束,让我们看看所有权声明和SE-0176中引入的的心理模型。当调用方将存储引用绑定到InOut参数时,它会立即开始访问占用存储的整个值。此访问在被调用方返回时结束。不允许重叠访问,因此,当InOut参数处于活动状态时,可能无法访问原始值。

(请注意,在所有权声明之前,InOut参数使用了一个可能更容易理解的不同模型进行了解释。然而,这两种模式是等价的。旧模型规定,在函数进入时,值从调用方指定的存储引用移到inout参数中,然后从InOut参数移回函数退出时的原始存储中。在函数执行期间,原始存储区未初始化,因此,占用该存储区的整个值在函数返回之前不可访问。)。

在实践中,InOut参数被实现为传递指向应该改变的存储的指针,同时保持与上面解释的模型相同的行为。作为这些模型的结果,输入输出参数提供以下保证:

支持INOUT参数的内存在函数执行期间有效访问。

除非函数本身已与多个线程共享参数,否则在函数执行过程中读取和修改In Out参数都是有效的,而不会发生数据争用。

INOUT由函数进入和函数退出时的初始化内存支持(拷贝入/拷贝出语义的含义)。销毁InOut中的对象需要使用不安全的构造。因此,在安全的SWIFT中,输入输出参数在整个函数执行过程中都由初始化内存提供支持。

InOut参数不会彼此别名,也不会给程序在该点上允许访问的任何其他值设置别名。请记住,绑定到InOut参数的原始值在inoutbinding期间是不可访问的。

在实践中,函数参数中的非常数指针和引用通常用于与SWIFT的InOUT相同的目的,因此最好将它们相互映射。

//SWIFT中导入的C++头(可能是映射)。函数增量Both(_value1:InOut Int,_Value2:InOut Int)。

这样导入的incrementBoth函数在SWIFT中比在C++中有更多的限制(例如,参数可能没有别名)。从SWIFT调用此C++函数不会在SWIFT中产生新的安全问题,因为限制较多的语言调用限制较少的语言。从API的观点来看,这也不应该是问题,除非调用者需要传递别名的参数(SWIFT对排他性的强制执行将阻止这一点)。

基于以上比较,看起来SWIFT的InOut提供的保证比C++的非常数指针和引用严格得多。因此,在所有情况下将它们相互映射是不安全的。例如,当SWIFT实施向C++公开的函数时,我们不能应用此映射,例如:

//SWIFT中导入的C++头(可能是映射)。Protocol Incrementer{func incrementBoth(_value1:InOut Int,_value2:InOut Int)}struct MyIncrementer:Incrementer{func incrementBoth(_value1:InOut Int,_value2:InOut Int){//语言要求`value1`和`value2`不使用别名。}}。

通过C++签名公开Incrementer.incrementBoth的SWIFT实现是不安全的,除非C++函数已经具有类似SWIFT的指针参数前提条件。对于C++中真正的inout参数,这些前提条件实际上应该成立,即使C++函数没有正式记录它们。

在C++中,函数参数中的常量引用最常用于避免复制传递给函数的对象。SWIFT通过两种方式解决此问题:

通过提供允许工程师引入间接地址的语言特征(例如,引用类型、存在类型、间接枚举情况、用于定义写入时复制类型的工具),

将C++常量引用自动映射到特定于SWIFT的间接指令(如类类型)是不可行的,因为这需要通过非常重要的桥接来更改内存布局。

我们可以很容易地将函数参数中的C++常量引用映射到SWIFT中的UnsafePointer<;T>;,但是,生成的API看起来不那么地道。它们将可从SWIFT使用,因为SWIFT代码可以使用&;运算符创建不安全指针,但是,它只能应用于可变存储位置。因此,在SWIFT中有更多变量是可变的,这是不必要的。此外,要使某些值可变,代码将需要创建一个副本,该副本将存储在可变变量中--这与C++API采用常量引用的观点不符。

//使用示例。Void caller(){var x=42 printInt(&;x)//OK let y=42 printInt(Y)//错误:类型不匹配:`Int`不是`UnsafePointer<;Int>;`printInt(&;y)//错误:可以';t将`&;`应用到不可变值}。

要消除代码为使某些值可变而必须执行的额外副本,我们可以扩展SWIFT语言,以允许将&;应用于不可变值,从而生成一个不安全的指针T>;。

一种更符合人体工程学的方法是将C++常量引用导入到值类型as值,但仍在幕后使用按引用调用约定。

导入的接受Int by值的printInt函数将是调用实际C++入口点的thunk,该入口点通过常量引用接受Int。强制优化传递会将该thunk内联到其调用方,并消除现在变得不必要的副本。

重要的是要理解,这种方法只有在编译器可以插入thunk的情况下才有效。以下是编译器可以透明地将函数指针包装到thunk中的一个示例,因此不能将const int导入为Int32:

一个缺点是,这种方法提供的控制较少:调用者可以指定或预测传递给C++函数的确切地址(因为SWIFT可以复制或在内存中移动它们)。大多数接收常量T&;参数的函数不应该关心确切的地址,但有些函数可能会关心。

这种方法的另一个缺点是,在实践中,它在许多情况下是安全的,但是用户可以构造不安全的案例。具体地说,如果被调用的C++代码将SWIFT传递的引用持久化到函数调用的持续时间之外,则该引用可能无效(因为SWIFT编解码器可以传递由编译器隐式物化的临时地址)。选项1、选项2和现有的SWIFT/C互操作的不安全性质是相同的,但是,在选项2中,我们将C++引用作为SWIFT值导入,被调用的函数接受地址对于SWIFT程序员来说是不可见的。

C++具有复杂的重载解析规则,部分原因是为了支持某些API设计模式。一些API设计模式作为重载解决规则的结果出现。因此,在C++中,API ATOM";是一个重载集合,而不是单个函数(CppCon 2018:Ttus Winters";Modern C++Design(Part 1 Of 2)";)。SWIFT的情况也是如此,因此,从根本上说,这里不存在阻抗失配。

但是,C和Objective-C互操作性分别导入每个函数和方法。对于C++互操作性,我们应该识别使用重载集的设计模式,并将它们适当地映射到SWIFT。

一种这样的C++模式提供了两个重载:一个接受常量T&;,另一个接受T&;。如果我们试图天真地将它们映射到SWIFT,它们就会映射到相同的签名。如果我们只导入其中一个,而忽略另一个,我们将把性能放在桌面上。

映射此类重载集的最简单方法是将T&;&;重载作为按值接受参数的函数导入,并忽略常量T&;重载。此方法确实会产生少量的性能开销(Extra Move),但不需要更改SWIFT语言或类型检查器来识别临时的概念。SIL优化器似乎是机会主义地回收性能的合适位置,它在可能的情况下删除了不必要的临时移动,并将对thunk的调用替换为对最合适的C++入口点的调用。

//SWIFT中导入的C++头。Struct张量{...}函数process张量(_:张量){//调用`void process张量(张量&;&;)`}。

//优化后的SIL代码的等价物。//void process张量(Const张量&;);func processTensorByConstRef(_:UnsafePointer<;张量>;)//void processTensorByRvalueRef(_:UnsafeMutablePointer<;Tvar>;)func processTensorByRvalueRef(_:UnsafeMutablePointer<;Tvar>;)func processTensorByConstRef(X)processTensorByRvalueRef(X)//自动移动值,因为它显然不再使用。}。

一旦将仅移动类型添加到SWIFT中,同样的映射技术也适用于C++仅移动值类型,唯一的区别是导入函数的参数将被标记为正在使用。

与C中的内联函数相比,C++中使用内联函数(自由函数和成员函数)的次数要多得多。对于C++中的内联函数,SWIFT应使用它目前在C中使用的相同策略:使用Clang的CodeGen库将内联函数的定义与SWIFT代码一起发送到一个LLVM模块中。

C++中的命名空间和模块是正交概念。C++中的命名空间可以跨越多个模块;C++命名空间也可以嵌套。在SWIFT中,模块是命名空间。

在SWIFT中使用空枚举作为命名空间是有先例的(例如,标准库中的CommandLine、Unicode、Unicode.UTF8)。但是,空枚举不是C++命名空间的完美替代品:

SWIFT没有允许代码避免限定嵌套在枚举中的名称的类Using-Like构造(换句话说,在使用枚举时,嵌套在枚举中的名称必须始终使用该枚举的名称)。

C++命名空间可以跨越多个模块。我们不希望每个导入的C++模块都定义自己的空枚举,因为这将导致名称冲突,这将需要用户进行更多的限定。

//用法示例:只导入`CppButton`,一切正常。导入CppButton函数makeButton(){var b1=Button()//错误:没有这样的类型var b2=widgets。按钮()//确定}。

//用法示例:同时导入`CppButton`和`CppTextbox`时的歧义。Import CppButton import CppTextbox func makeButton(){var b1=Button()//错误:没有这样的类型var b2=widgets。Button()//错误:`widgets`名称不明确,是指`CppButton.widgets`还是`CppTextbox.widgets`?Var b3=CppButton。小工具。按钮()}。

我们可以解决由多个C++模块定义的同名命名空间枚举之间的歧义,方法是合成一个额外的SWIFT模块,该模块将只包含组成命名空间结构的枚举,然后使所有其他C++模块定义这些枚举的扩展。

//使用示例。Import CppButton import CppTextbox//隐式注入:import CppNamespaces func makeButton(){var b1=Button()//错误:没有这样的类型var b2=widgets。按钮()//确定}

使用枚举扩展的一个附带的可读性优势是,C++模块的Pretty打印模块接口将使用名称空间的关键字扩展,而不是枚举,从而减少混淆的可能性。

一些C++库使用长名称或深度嵌套的名称空间来定义名称空间。这类库的C++用户通常倾向于避免在他们的代码中键入和看到这样的命名空间限定符。为了帮助在SWIFT中使用这些库,我们可以添加一个类似于C++Using的构造,将namelLookup扩展到给定的空枚举。

//使用示例。导入CppButton导入CppTextBox//含义。

.