Morse's Site
10660 字
53 分钟
Swift 与 C++ 混合编程
2024-06-13

最近在学习 librime 的源码时,剥离了 librime 中的配置文件相关的代码作为对配置文件 Yaml 的处理逻辑,这样做,以后对配置文件的编译就不在需要调用 librime 的引擎了。

因剥离的代码是 C++,如果调用,就需要像「仓」调用 librime 引擎方法一样,通过 Objective-C 的代码作为桥接,即 Swift -> Objective-C -> C++

有了中间层 Objective-C 后,在调整底层接口时,需要 Swift 层与 Objective-C 层代码同时做调整。

我查阅了 Swift 的官方文档,里面有对 Swift C++ 混合编程的说明,对我来说,我的需求是 Swift 调用 C++ 代码,所以下面只翻译了这部分内容,对于 C++ 调用 Swift 的那部分,没有翻译。


C++ 互操作性是 Swift 5.9 的新功能。Swift 可以直接调用各种 C++ API,某些 Swift API 也可以从 C++ 中使用。

本文档是描述如何混合使用 Swift 和 C++ 的参考指南。它描述了如何将 C++ API 导入到 Swift 中,并提供了在 Swift 中使用各种 C++ API 的示例。此外,还描述了如何将 Swift API 暴露给 C++,并提供了在 C++ 中使用这些 Swift API 的示例。

C++ 互操作性是 Swift 的一个积极发展的功能。目前支持语言特性之间的互操作性。状态页面提供了当前支持的互操作性功能概述,并列出了现有的限制。

未来的 Swift 版本可能会改变 Swift 和 C++ 的互操作方式,因为 Swift 社区会根据实际使用反馈进行改进。请在 Swift 论坛或 GitHub 上提供您的反馈。未来对 C++ 互操作性的设计或功能的更改默认不会破坏现有代码库中的代码。

概览#

本节提供了混合使用 Swift 和 C++ 的高级概述。您可以通过在 Swift 中启用 C++ 互操作性来开始,然后了解 Swift 如何导入 C++ 头文件以及导入的 C++ 类型和函数如何由 Swift 编译器表示。之后,可以查看后续章节,了解如何在 Swift 中使用导入的 C++ API。您还应了解如何将 Swift API 暴露给 C++ 代码库。如果您有兴趣从 C++ 使用 Swift API,建议查看后续章节,了解如何在 C++ 中使用暴露的 Swift API。

启用 C++ 互操作性#

Swift 代码默认与 C 和 Objective-C API 互操作。如果要在 Swift 中使用 C++ API 或将 Swift API 暴露给 C++,则必须启用与 C++ 的互操作性。

以下指南描述了在特定构建系统或 IDE 中启用 C++ 互操作性的方法:

其他构建系统可以通过向 Swift 编译器传递所需的标志来启用 C++ 互操作性:

将 C++ 导入到 Swift#

头文件通常用于描述 C++ 库的公共接口。它们包含类型和模板定义,以及函数和方法的声明,其主体通常位于由 C++ 编译器编译的实现文件中。

Swift 编译器嵌入了 Clang 编译器。这使得 Swift 可以使用 Clang 模块导入 C++ 头文件。与直接使用 #include 指令包含头文件内容的预处理器模型相比,Clang 模块提供了更健壮和高效的 C++ 头文件语义模型。

Swift 目前无法导入 C++20 标准中引入的 C++ modules

创建 Clang 模块#

为了让 Swift 导入 Clang 模块,它需要找到一个 module.modulemap 文件,该文件描述了 C++ 头文件集合如何映射到 Clang 模块。

一些 IDE 和构建系统可以自动为 C++ 构建目标生成模块映射文件。Swift Package Manager 在 C++ 目标中找到 umbrella 头文件时,会自动生成模块映射文件。Xcode 会自动为框架目标生成模块映射文件,模块映射引用框架的公共头文件。在其他情况下,可能需要手动创建模块映射。

手动创建模块映射的推荐方法是列出要提供给 Swift 的特定 C++ 目标的所有头文件。例如,假设我们要为 C++ 库 forestLib 创建模块映射。该库有两个头文件:forest.htree.h。在这种情况下,我们可以按照推荐的方法创建一个包含两个 header 指令的模块映射:

module forestLib {
    header "forest.h"
    header "tree.h"

    export *
}

export * 指令是模块映射的另一个推荐添加项。它确保导入到 forestLib 模块的 Clang 模块中的类型对 Swift 也是可见的。

模块映射文件应放置在其引用的头文件旁边。例如,在 forestLib 库中,模块映射应放在 include 目录中:

forestLib
├── include
│   ├── forest.h
│   ├── tree.h
│   └── module.modulemap [NEW]
├── forest.cpp
└── tree.cpp

现在 forestLib 有了模块映射文件,Swift 可以在启用 C++ 互操作性时导入它。为了让 Swift 找到 forestLib 模块,构建系统必须在调用 Swift 编译器时传递指向 forestLib/include 的导入路径标志 (-I)。

For more information on the syntax and the semantics of module map files, please see Clang’s .

有关模块映射文件语法和语义的更多信息,请参阅 Clang 的 module map language documentation

使用导入的 C++ API#

导入 Clang 模块后,Swift 编译器使用 Swift 声明表示导入的 C++ 类型和函数。这使得 Swift 代码可以像使用 Swift 类型和函数一样使用 C++ 类型和函数。

例如,Swift 可以表示 forestLib 库中的以下 C++ 枚举和 C++ 类:

enum class TreeKind {
  Oak,
  Redwood,
  Willow
};

class Tree {
public:
  Tree(TreeKind kind);
private:
  TreeKind kind;
};

Swift 编译器内部使用 Swift 枚举表示 TreeKind

enum TreeKind : Int32 {
  case Oak = 0
  case Redwood = 1
  case Willow = 2
}

Swift 编译器内部使用 Swift struct 来表示 Tree

struct Tree {
  init(_ kind: TreeKind)
}

这种 struct 可以直接在 Swift 中使用,就像其他 Swift struct 一样:

import forestLib

let tree = Tree(.Oak)

Swift 直接使用 C++ 类型并调用 C++ 函数,而无需任何类型的间接或包装。在上述示例中,Swift 直接调用 Tree 类的 C++ 构造函数,并将结果对象直接存储到 tree 变量中。

本指南的后续章节提供了有关如何在 Swift 中使用导入的 C++ API 的详细信息。

将 Swift API 暴露给 C++#

除了导入和使用 C++ API 外,Swift 编译器还能够将 Swift 模块中的 Swift API 暴露给 C++。这使得在现有 C++ 代码库中逐步集成 Swift 成为可能。

Swift APIs can be accessed by including a header file that the build system generates when building a Swift module. Some build systems generate the header automatically. Xcode can automatically generate the header file for a framework or an App target. Other build configurations can generate the header manually by following the steps outlined on the.

Swift API 可以通过在构建 Swift 模块时生成的头文件进行访问。一些构建系统会自动生成头文件。Xcode 可以自动为框架或 App 目标生成头文件。其他构建配置可以通过遵循项目和构建设置页面中概述的步骤手动生成头文件。

生成的头文件使用 C++ 类型和函数表示 Swift 类型和函数。当启用 C++ 互操作性时,Swift 会为 Swift 模块中所有支持的公共类型和函数生成 C++ 绑定。例如,以下 Swift 函数可以从 C++ 调用:

// Swift module 'forestRenderer'
import forestLib

public func renderTreeToAscii(_ tree: Tree) -> String {
  ...
}

生成的头文件中将包含一个内联 C++ 函数,直接调用 renderTreeToAscii 的实现。只要 C++ 文件包含生成的头文件,就可以从 C++ 代码调用它:

#include "forestRenderer-Swift.h"
#include <string>
#include <iostream>

void printTreeArt(const Tree &tree) {
  std::cout << (std::string)forestRenderer::renderTreeToAscii(tree);
}

The C++ interoperability status page describes which Swift language constructs and standard library types can be exposed to C++. Some unsupported Swift APIs are mapped to empty unavailable C++ declarations in the generated header, so you’ll get an error in the C++ code when you try to use something that’s not exposed to C++.

C++ 互操作性状态页面描述了哪些 Swift 语言构造和标准库类型可以暴露给 C++。某些不受支持的 Swift API 在生成的头文件中映射为空的不可用 C++ 声明,因此当您尝试在 C++ 代码中使用不暴露给 C++ 的内容时,会收到错误。

混合语言代码库的源稳定性保证#

Swift 与 C++ 的互操作方式仍在不断发展。未来的 Swift 版本中的某些更改将需要对已经采用 C++ 互操作性的混合 Swift 和 C++ 代码库进行源代码更改。然而,Swift 不会在采用新版本 Swift 工具链时强制您采用新的或改进的 C++ 互操作性功能。为实现这一点,未来的 Swift 版本将提供多个 C++ 互操作性的兼容版本,就像 Swift 提供对基础 Swift 语言的多个兼容版本的支持一样。这意味着使用当前兼容版本的 C++ 互操作性的项目将与后续版本中的任何更改隔离开来,并且可以以自己的节奏迁移到较新的兼容版本。

在 Swift 中使用 C++ 类型和函数#

各种 C++ 类型和函数可以直接在 Swift 中使用。本节介绍了如何在 Swift 中使用支持的类型和函数的基本原理。

调用 C++ 函数#

可以使用 Swift 中熟悉的函数调用语法调用导入模块中的 C++ 函数。例如,以下 C++ 函数可在 Swift 中使用:

void printWelcomeMessage(const std::string &name);

Swift 代码可以像调用常规 Swift 函数一样调用该函数:

printWelcomeMessage("Thomas")

C++ 结构和类默认为值类型#

Swift 默认将 C++ 结构和类映射为 Swift struct 类型。Swift 将它们视为值类型。这意味着它们在 Swift 代码中传递时总是被复制。

当需要复制值或在值超出范围时处理值时,Swift 使用 C++ 结构或类类型的特殊成员。如果 C++ 类型具有复制构造函数,Swift 会在复制该类型的值时使用它。如果 C++ 类型具有析构函数,Swift 会在销毁该类型的 Swift 值时调用析构函数。

具有删除复制构造函数的 C++ 结构和类在 Swift 中表示为不可复制类型 (~Copyable)。

某些 C++ 类型始终使用指针或引用传递。在这种情况下,将它们映射为 Swift 中的值类型可能没有意义。可以在 C++ 中添加注释,指示 Swift 编译器将它们映射为Swift 中的引用类型

在 Swift 中构造 C++ 类型#

Public constructors inside C++ structures and classes that aren’t copy or move constructors become initializers in Swift. C++ 结构和类中的公共构造函数(非 copy 或 move 构造函数)在 Swift 中变为初始化器。

例如,以下 C++ Color 类的所有三个构造函数都可在 Swift 中使用:

class Color {
public:
  Color();
  Color(float red, float blue, float green);
  Color(float value);

  ...
  float red, blue, green;
};

上述 Color 构造函数在 Swift 中变为初始化器。Swift 代码可以调用它们来创建 Color 类型的值:

let theEmptiness = Color()
let oceanBlue = Color(0.0, 0.0, 1.0)
let seattleGray = Color(0.7)

访问 C++ 类型的数据成员#

C++ 结构和类的公共数据成员在 Swift 中变为属性。例如,可以像访问其他 Swift 属性一样访问上述 Color 类的数据成员:

let color: Color = getRandomColor()
print("Today I'm feeling \(color.red) red but also \(color.blue) blue")

调用 C++ 成员函数#

C++ 结构和类中的成员函数在 Swift 中变为方法。

常量成员函数为 nonmutating#

常量成员函数在 Swift 中变为 nonmutating 方法,而没有 const 限定符的成员函数在 Swift 中变为 mutating 方法。例如,C++ Color 类中的此成员函数在 Swift 中视为 mutating 方法:

void Color::invert() { ... }

可变的 Color 值可以调用 invert

var red = Color(1.0, 0.0, 0.0)
red.invert() // red becomes yellow.

常量 Color 值不能调用 invert

另一方面,此常量成员函数在 Swift 中不是 mutating 方法:

Color Color::inverted() const { ... }

因此,常量 Color 值可以调用 inverted

let darkGray = Color(0.2, 0.2, 0.2)
let veryLightGray = darkGray.inverted()

常量成员函数不得修改对象#

Swift 编译器假定常量成员函数不会修改 this 指向的实例。如果 C++ 成员函数违反此假设,可能导致 Swift 代码未能观察到 this 指向的实例的修改,并在 Swift 代码执行的其余部分使用该实例的原始值。

C++ 允许在常量成员函数中修改 mutable 字段。具有此类字段的结构或类中的常量成员函数在 Swift 中仍变为 nonmutating 方法。Swift 不知道哪些常量函数会修改对象,哪些不会,因此为了更好的 API 可用性,Swift 仍假定此类函数不会修改对象。除非它们明确用 SWIFT_MUTATING 宏注释,否则应避免从 Swift 调用会修改 mutable 字段的常量成员函数。

Swift 5.9 将附带 SWIFT_MUTATING 自定义宏。然而,它尚未在可下载的 Swift 5.9 工具链中提供。该宏将允许您显式注释会修改对象的常量成员函数。此类函数将在 Swift 中变为 mutating 方法。以下 GitHub issue 跟踪 SWIFT_MUTATING 在 Swift 中的支持状态。

返回引用的成员函数默认不安全#

返回引用、指针或包含引用或指针的某些结构或类的成员函数通常返回指向调用函数的对象 this 内部的引用。此类成员函数在 Swift 中被视为不安全,因为返回的引用未与拥有对象关联,拥有对象可能在引用仍在使用时被销毁。Swift 自动重命名此类成员函数,以强调其不安全性。它们的 Swift 名称以两个下划线开头,并以 Unsafe 结尾。例如,以下成员函数在 Swift 中变为 __getRootTreeUnsafe 方法:

class Forest {
public:
  const Tree &getRootTree() const { return rootTree; }

  ...
private:
  Tree rootTree;
};

确定哪些函数不安全的规则集以及在 Swift 中安全调用此类方法的推荐指南记录在即将发布的章节中,描述如何在 Swift 中安全地使用 C++ 引用和视图类型

重载成员函数#

C++ 允许基于 const 限定符重载成员函数。例如,Forest 类可以有两个 getRootTree 成员,它们仅在常量性和返回类型上有所不同:

class Forest {
public:
  const Tree &getRootTree() const { return rootTree; }
  Tree &getRootTree() { return rootTree; }

  ...
private:
  Tree rootTree;
};

这两个 getRootTree 成员函数在 Swift 中变为方法。当 Swift 发现类型已经有一个具有相同 Swift 名称的 nonmutating 方法时,它会重命名 mutating 方法,以避免两个具有相同名称和参数的歧义方法。重命名会将 Mutating 后缀附加到 mutating 方法的名称上。在考虑方法的安全性之前,会进行此重命名。在上述示例中,这两个 getRootTree 成员函数在 Swift 中变为 __getRootTreeUnsafe__getRootTreeMutatingUnsafe 方法,因为它们返回指向 Forest 对象内部的引用。

虚成员函数#

目前虚成员函数在 Swift 中不可用。

静态成员函数#

静态 C++ 成员函数在 Swift 中变为 static 方法。

从 Swift 访问继承的成员#

C++ 类或结构在 Swift 中变为独立类型。其与基类 C++ 类的关系在 Swift 中不保留。Swift 尽力提供对从基类继承的成员的访问,C++ 类型的基类中的公共成员函数和数据成员在 Swift 中变为方法和属性,就像它们是在具体类中定义的一样。

例如,以下两个 C++ 类变为两个独立的 Swift 结构:

class Plant {
public:
  void water(float amount) { moisture += amount; }
private:
  float moisture = 0.0;
};

class Fern: public Plant {
public:
  void trim();
};

Fern Swift 结构有一个额外的方法 water,该方法调用 Plant C++ 类中的成员函数 water

struct Plant {
  mutating func water(_ amount: Float)
}

struct Fern {
  init()
  mutating func water(_ amount: Float) // Calls `Plant::water`
  mutating func trim()
}

确定何时将继承的基类类型的成员引入表示 C++ 结构或类的 Swift 类型的确切规则尚未在 Swift 5.9 中最终确定。以下 GitHub issue 跟踪其在 Swift 5.9 中的最终确定状态

使用 C++ 枚举#

带 class 的 C++ 枚举在 Swift 中变为具有原始值的 Swift 枚举。它们的所有枚举值都映射为 Swift 枚举值。例如,以下 C++ 枚举可在 Swift 中使用:

enum class TreeKind {
  Oak,
  Redwood,
  Willow
};

它在 Swift 中表示为以下枚举:

enum TreeKind : Int32 {
  case Oak = 0
  case Redwood = 1
  case Willow = 2
}

可以像使用任何其他 enum 一样在 Swift 中使用它:

func isConiferous(treeKind: TreeKind) -> Bool {
  switch treeKind {
    case .Redwood: return true
    default: return false
  }
}

未带 class 的 C++ 枚举在 Swift 中变为结构体。例如,以下未带 class 的 enum 变为 Swift 结构体:

enum MushroomKind {
  Oyster,
  Portobello,
  Button
}

未带 class 的 C++ 枚举的枚举值在 Swift 结构体外部变为变量:

struct MushroomKind : Equatable, RawRepresentable {
    public init(_ rawValue: UInt32)
    public init(rawValue: UInt32)
    public var rawValue: UInt32
}
var Oyster: MushroomKind { get }
var Portobello: MushroomKind { get }
var Button: MushroomKind { get }

使用 C++ 类型别名#

C++ 中的 usingtypedef 声明在 Swift 中变为 typealias。例如,以下 using 声明在 Swift 中变为 CustomString 类型:

using CustomString = std::string;

使用类模板#

类或结构模板的实例化在 Swift 中映射为不同类型。例如,以下未实例化的 C++ 类模板本身在 Swift 中不可用:

template<class T, class U>
class Fraction {
public:
  T numerator;
  U denominator;

  Fraction(const T &, const U &);
};

但是,类模板的实例化在 Swift 中可用。当它映射到 Swift 中时,处理方式与常规 C++ 结构或类相同。例如,Fraction<int, float> 模板特化变为 Swift 结构体:

struct Fraction<CInt, Float> {
  var numerator: CInt
  var denominator: Float

  init(_: CInt, _: Float)
}

返回类似 Fraction<int, float> 的特化的函数在 Swift 中可用:

Fraction<int, float> getMagicNumber();

此类函数可以像其他 Swift 函数一样从 Swift 调用:

let magicNum = getMagicNumber()
print(magicNum.numerator, magicNum.denominator)

C++ 类型别名可以引用类模板的特定特化。例如,为了从 Swift 构造 Fraction<int, float>,首先需要创建引用该模板特化的 C++ 类型别名:

// Bring `Fraction<int, float>` type to Swift with a C++ `using` declaration.
using MagicFraction = Fraction<int, float>;

然后可以直接从 Swift 使用此类型别名:

let oneEights = MagicFraction(1, 8.0)
print(oneEights.numerator)

本文档的后续章节描述了如何使用 Swift 泛型和协议扩展编写适用于类模板任何特化的泛型 Swift 代码。

自定义 C++ 到 Swift 的映射#

可以通过使用提供的自定义宏之一注释特定 C++ 函数或类型来更改确定 C++ 类型和函数如何映射到 Swift 的默认设置。例如,可以选择使用 SWIFT_NAME 宏为特定 C++ 函数提供不同的 Swift 名称。

<swift/bridging> 头文件定义了可用于注释 C++ 函数和类型的自定义宏。该头文件随 Swift 工具链一起提供。

在 Apple 和 Linux 平台上,系统的 C++ 编译器和 Swift 编译器应自动找到该头文件。在其他平台(如 Windows)上,可能需要添加额外的头文件搜索路径标志 (-I) 以确保找到该头文件。

本节仅描述了 <swift/bridging> 头文件中的两个自定义宏。其他自定义宏及其行为在本文档的后续章节中有详细说明。所有自定义宏的完整列表可在附录中找到。

重命名 Swift 中的 C++ API#

SWIFT_NAME 宏为 Swift 中的 C++ 类型和函数提供了不同的名称。可以通过在 SWIFT_NAME 宏内指定 Swift 类型名称来重命名 C++ 类型。例如,以下 C++ 类在 Swift 中重命名为 CxxLibraryError 结构体:

class Error {
  ...
} SWIFT_NAME("CxxLibraryError");

重命名函数时,需要在 SWIFT_NAME 宏内指定 Swift 函数名称(包括参数标签)。例如,以下 C++ 函数在 Swift 中重命名为 send

#include <swift/bridging>

void sendCopy(const std::string &) SWIFT_NAME(send(_:));

在 Swift 中调用此函数时,必须使用新名称:

send("Hello, this is Swift!")

将 Getter 和 Setter 映射到计算属性#

SWIFT_COMPUTED_PROPERTY 宏将 C++ 的 gettersetter 成员函数映射到 Swift 中的计算属性。例如,以下 gettersetter 对在 Swift 中映射到单个 treeKind 计算属性:

#include <swift/bridging>

class Tree {
public:
  TreeKind getKind() const SWIFT_COMPUTED_PROPERTY;
  void setKind(TreeKind kind) SWIFT_COMPUTED_PROPERTY;

  ...
};

因为有 setter,所以可以在 Swift 中修改此属性:

func makeNotAConiferousTree(tree: inout Tree) {
  tree.kind = tree.kind == .Redwood ? .Oak : tree.kind
}

getter 和 setter 需要对同一个底层 C++ 类型进行操作,此转换在 Swift 中才会成功。

仅映射 getter 到计算属性也是可能的,不需要 setter 也可以进行此转换。

扩展 Swift 中的 C++ 类型#

Swift 的 extensions 可以为 C++ 类型添加新功能。它们还可以使现有的 C++ 类型符合 Swift 协议。

扩展可以为 C++ 类型添加新功能,但不能覆盖 C++ 类型的现有功能。

使 C++ 类型符合 Swift 协议#

可以在定义 C++ 类型后(事后)添加 Swift 协议一致性。此类一致性使得以下使用场景在 Swift 中成为可能:

• 受协议约束的通用 Swift 函数和类型可以与符合协议的 C++ 值一起工作。 • 协议类型可以表示符合协议的 C++ 值。

例如,Swift 扩展可以为 C++ 类 Tree 添加 Hashable 一致性:

extension Tree: Hashable {
  static func == (lhs: Tree, rhs: Tree) -> Bool {
    return lhs.kind == rhs.kind
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(self.kind.rawValue)
  }
}

此类一致性使得可以在 Swift 字典中使用 Tree 作为键:

let treeEmoji: Dictionary<Tree, String> = [
  Tree(.Oak): "🌳",
  Tree(.Redwood): "🌲"
]

使类模板符合 Swift 协议#

Swift 扩展可以使特定的类模板特化符合协议。例如,以下类模板的实例化特化在 Swift 中可用:

template<class T>
class SerializedValue {
public:
  T deserialize() const;

  ...
};

using SerializedInt = SerializedValue<int>;
using SerializedFloat = SerializedValue<float>;

SerializedInt getSerializedInt();
SerializedFloat getSerializedFloat();

此类模板特化可以使用 Swift 扩展使其符合协议:

// Swift module 'Serialization'
protocol Deserializable {
  associatedtype ValueType

  func deserialize() -> ValueType
}

// `SerializedInt` specialization now conforms to `Deserializable`
extension SerializedInt: Deserializable {}

在上述示例中,SerializedInt 符合 Deserializable 协议。然而,类模板的其他特化(如 SerializedFloat)不符合 Deserializable

The SWIFT_CONFORMS_TO_PROTOCOL customization macro from the <swift/bridging> header can be used to conform all specializations of a class template to a Swift protocol automatically. For example, the definition of the SerializedValue class template can be annotated with SWIFT_CONFORMS_TO_PROTOCOL:

<swift/bridging> 头文件中的 SWIFT_CONFORMS_TO_PROTOCOL 自定义宏可用于使类模板的所有特化自动符合 Swift 协议。例如,可以使用 SWIFT_CONFORMS_TO_PROTOCOL 注释 SerializedValue 类模板的定义:

template<class T>
class SerializedValue {
public:
  using ValueType = T;
  T deserialize() const;

  ...
} SWIFT_CONFORMS_TO_PROTOCOL(Serialization.Deserializable);

SWIFT_CONFORMS_TO_PROTOCOL 注释使得所有特化(如 SerializedIntSerializedFloat)在 Swift 中自动符合 Deserializable。这使得可以通过协议扩展在 Swift 中为类模板的所有特化添加功能:

extension Deserializable {
  // All specializations of the `SerializedValue` template now have
  // `deserializedDescription` property in Swift.
  // 现在,`SerializedValue` 模板的所有特化在 Swift 中都有 `deserializedDescription` 属性。
  var deserializedDescription: String {
    "serialized value \(deserialize().description)"
  }
}

这还使得可以在约束泛型代码中使用任何特化,而无需额外的显式一致性:

func printDeserialized<T: Deserializable>(_ item: T) {
  print("obtained: \(item.deserializedDescription)")
}

// Both `SerializedInt` and `SerializedFloat` specializations automatically
// conform to `Deserializable`
printDeserialized(getSerializedInt())
printDeserialized(getSerializedFloat())

使用 C++ 容器#

C++ 容器类型(如 std::vector 类模板)通常为用户提供基于迭代器的 API。在 Swift 中使用 C++ 迭代器是不安全的,因为这种使用不与其拥有的容器相关联,而容器可能在迭代器仍在使用时被销毁。Swift 自动使某些 C++ 容器类型符合以下协议,以代替依赖于 C++ 迭代器:

• 使用标准 Swift API 安全访问底层容器。

• 提供将 C++ 容器转换为 Swift 集合类型的方法。

这些协议及其一致性规则如下所述。符合这些协议的 C++ 容器的推荐使用方法总结在后续部分

部分 C++ 容器是 Swift 集合#

Swift 会自动将提供随机访问元素的 C++ 容器(如 std::vector)转换为 Swift 的 RandomAccessCollection 协议。例如,此函数返回的 std::vector 容器会自动符合 Swift 的 RandomAccessCollection 协议:

std::vector<Tree> getEnchantedTrees();

The conformance to RandomAccessCollection makes it possible to traverse through the container’s elements safely in Swift, using familiar control flow statements like the for-in loop. Collection methods like map and filter are also available:

符合 RandomAccessCollection 协议使得可以在 Swift 中安全地遍历容器的元素,使用熟悉的控制流语句(如 for-in 循环)。集合方法(如 mapfilter)也可用:

let trees = getEnchantedTrees()

// Traverse through the elements of a C++ vector.
// 遍历 C++ vector 的元素。
for tree in trees {
  print(tree.kind)
}

// Filter the C++ vector and make a Swift Array that contains only
// the oak trees.
// 筛选 C++ vector 并生成仅包含 tree 的 Swift 数组。
let oakTrees = getEnchantedTrees().filter { $0.kind == .Oak }

Swift 的 count 属性返回此类集合中的元素数量。Swift 的下标操作符也可以用来访问集合中的特定元素。这使得可以修改 C++ 容器中的各个元素:

var trees = getEnchantedTrees()
for i in 0..<trees.count {
  trees[i].kind = .Oak
}

符合 RandomAccessCollection 协议的 C++ 容器可以轻松转换为 Swift 集合类型,如 Array

let treesArray = Array<Tree>(getEnchantedTrees())

Swift 不会 自动将 C++ 容器类型转换为 Swift 集合类型。任何从 C++ 容器类型(如 std::vector)到 Swift 集合类型(如 Array)的转换在 Swift 中都是显式的。

自动集合一致性的性能限制#

当前,当使用符合 RandomAccessCollection 的 C++ 容器时,Swift 不提供显式的性能保证。Swift 可能在以下情况下对容器进行深拷贝:

• 在 for-in 循环中使用容器时。 • 使用来自 Swift 的 Sequence 协议的 filterreduce 方法时。

此限制在状态页面中有记录。针对这一限制的几种解决方案在下文中介绍。

随机访问 C++ 集合的一致性规则#

要使 C++ 容器类型自动符合 Swift 的 RandomAccessCollection,必须满足以下两个条件:

• C++ 容器类型必须有 beginend 成员函数。两个函数必须是常量并返回相同的迭代器类型。 • C++ 迭代器类型必须满足 RandomAccessIterator 要求。必须可以在 C++ 中使用 operator += 前进迭代器,并且可以在 C++ 中使用 operator [] 进行下标访问。 • C++ 迭代器类型必须可以使用 operator == 进行比较。

当这些条件满足时,Swift 将代表底层 C++ 容器类型的 Swift 结构符合 CxxRandomAccessCollection 协议,从而添加 RandomAccessCollection 一致性。

C++ 容器可以转换为 Swift 集合#

The sequential C++ container types that do not provide random access to their elements are automatically conformed to the CxxConvertibleToCollection protocol in Swift. For example, the std::set container returned by this function is conformed to the CxxConvertibleToCollection protocol automatically by Swift:

不提供随机访问元素的顺序 C++ 容器类型在 Swift 中自动符合 CxxConvertibleToCollection 协议。例如,此函数返回的 std::set 容器在 Swift 中自动符合 CxxConvertibleToCollection 协议:

std::set<int> getWinningNumers();

符合 CxxConvertibleToCollection 协议使得可以轻松将 C++ 容器转换为 Swift 集合类型,如 ArraySet。例如,getWinningNumers 返回的 std::set 可以转换为 Swift Array 和 Swift Set

let winners = getWinningNumers()
for number in Array(winners) {
  print(number)
}
let setOfWinners = Set(winners)

自动符合 CxxRandomAccessCollection 协议的 C++ 容器也自动符合 CxxConvertibleToCollection 协议。

CxxConvertibleToCollection 协议的一致性规则#

要使 C++ 容器类型自动符合 Swift 的 CxxConvertibleToCollection,必须满足以下两个条件:

• C++ 容器类型必须有 beginend 成员函数。两个函数必须是常量并返回相同的迭代器类型。 • C++ 迭代器类型必须满足 InputIterator 要求。必须可以在 C++ 中使用 operator ++ 增量迭代器,并且可以在 C++ 中使用 operator * 进行解引用。 • C++ 迭代器类型必须可以使用 operator == 进行比较。

在 Swift 中使用关联容器 C++ 类型#

关联容器 C++ 类型(如 std::map)提供使用查找键高效访问存储元素的方法。执行此类查找的 find 成员函数在 Swift 中是不安全的。取而代之的是,Swift 自动使 C++ 标准库中的关联容器符合 CxxDictionary 协议。此类一致性使得在 Swift 中使用关联 C++ 容器时可以使用下标操作符。例如,此函数返回的 std::unordered_map 在 Swift 中自动符合 CxxDictionary 协议:

std::unordered_map<std::string, std::string>
getAirportCodeToCityMappings();

返回的 std::unordered_map 值可以像 Swift 字典一样使用,下标返回存储在容器中的值,或如果该值不存在则返回 nil

let mapping = getAirportCodeToCityMappings();
if let dubCity = mapping["DUB"] {
   print(dubCity)
}

提供的下标在其实现中安全地调用容器的 find 方法。

当需要在 Swift 中手动遍历其元素时,可以将关联 C++ 容器转换为 Swift 顺序集合类型,如 Array

Swift 不会自动使自定义关联 C++ 容器符合 CxxDictionary。可以编写手动的 Swift extension 来为自定义关联容器类型添加 CxxDictionary 一致性。

使用 C++ 容器的推荐方法#

以下总结了当前使用 C++ 容器在 Swift 中的推荐方法:

• 使用 for-in 循环遍历符合 RandomAccessCollection 的 C++ 容器。

• 在处理符合 RandomAccessCollection 的 C++ 容器时,使用集合 API,如 mapfilter

• 使用下标操作符访问符合 RandomAccessCollection 的 C++ 容器中的特定元素。

• 如果需要遍历其他顺序容器的元素,或如果希望使用集合 API(如 mapfilter),请将其转换为 Swift 集合。

• 在查找关联 C++ 容器中的值时,使用 CxxDictionary 协议中的下标操作符。

在性能敏感的 Swift 代码中使用 C++ 容器#

Swift’s current for-in loop makes a deep copy of the C++ container when traversing through its elements. You can avoid this copy by using the forEach method provided by the CxxConvertibleToCollection protocol. For example, the std::vector<Tree> container returned by getEnchantedTrees can be traversed using the forEach method in Swift:

Swift 当前的 for-in 循环在遍历容器的元素时会对 C++ 容器进行深拷贝。可以通过使用 CxxConvertibleToCollection 协议提供的 forEach 方法避免此拷贝。例如,可以使用 forEach 方法在 Swift 中遍历 getEnchantedTrees 返回的 std::vector<Tree> 容器:

let trees = getEnchantedTrees()
// Swift should not copy the `trees` std::vector here.
// Swift 在这里不应拷贝 `trees` std::vector。
trees.forEach { tree in
  print(tree.kind)
}

在 Swift 中使用 C++ 容器的最佳实践#

不要在 Swift 中使用 C++ 迭代器#

正如本节开头所述,在 Swift 中使用 C++ 迭代器是不安全的。例如:

• 容易在 C++ 容器销毁后使用迭代器。 • 容易解引用已超出容器最后一个元素的迭代器。

处理 C++ 容器时,应使用 CxxRandomAccessCollectionCxxConvertibleToCollectionCxxDictionary 协议,而不是依赖于 C++ 迭代器 API。

C++ 容器类型中的返回 C++ 迭代器的成员函数在 Swift 中被标记为不安全,就像返回引用的成员函数一样。其他 C++ API,如接受或返回迭代器的顶级函数,仍然可以直接在 Swift 中使用。在 Swift 中应避免使用这些函数。

在调用 Swift 函数时借用 C++ 容器#

C++ 容器类型在 Swift 中变为值类型。这意味着 Swift 调用容器的拷贝构造函数,每次在 Swift 中进行拷贝时都会复制所有元素。例如,Swift 将复制 std::vector<int>(由 CxxVectorOfInt 表示)中的所有元素到新 vector 中,每当它传递到这个 Swift 函数时:

func takesVectorType(_ : CxxVectorOfInt) {
  ...
}

let vector = createCxxVectorOfInt()
takesVectorType(vector) // 'vector' 被拷贝。

Swift 即将推出的参数所有权修饰符,将在即将发布的 Swift 版本中提供,这些修饰符将使你在将不可变值传递给函数时避免拷贝。可变值可以通过 inout 传递给 Swift 函数,从而避免 C++ 容器的深拷贝:

func mutatesVectorType(_ : inout CxxVectorOfInt) {
  ...
}

var vector = createCxxVectorOfInt()
takesVectorType(&vector) // 'vector' 未被拷贝!

将 C++ 类型映射到 Swift 引用类型#

Swift 编译器允许你注释一些 C++ 类型并将其作为引用类型(或 class 类型)导入 Swift。C++ 类型是否应导入为引用类型是一个复杂的问题,需要考虑两个主要标准。

第一个标准是对象标识是否是类型 “value” 的一部分。比较两个对象的地址只是询问它们是否存储在同一个位置,还是在更重要的意义上确定它们是否表示“同一个对象”?

第二个标准是 C++ 类的对象是否总是通过引用传递。对象是否主要使用指针引用类型传递,例如原始指针(*)、C++ 引用(&&&)或智能指针(如 std::unique_ptrstd::shared_ptr)?当通过原始指针引用传递时,是否期望该内存是稳定的并且将继续保持有效,还是期望接收者在需要独立保持值时复制对象?如果对象通常是分配的并保持在一个稳定的地址,即使该地址在语义上不是对象“value”的一部分,该类可能会成为引用类型。这有时需要程序员进行判断。

第一个也是最重要的标准通常无法通过仅查看代码自动回答。如果你希望 Swift 编译器将 C++ 类型映射到 Swift 引用类型,必须使用 <swift/bridging> 头文件中的以下自定义宏之一注释 C++ 类型:

Immortal Reference Types 永久引用类型#

永久引用类型不打算由程序单独管理。这些类型的对象被分配然后故意“leaked 泄漏”而不跟踪它们的使用。有时这些对象并非真正永久:例如,它们可能是区域分配的,期望只从区域内的其他对象引用它们。尽管如此,它们不应该单独管理。

对于永久引用类型,Swift 唯一合理的做法是将它们导入为未管理的类 unmanaged classes。当对象确实是永久的时,这是完全可以的。如果对象是区域分配的,这是不安全的,但鉴于 C++ API 的选择,这是不可避免的不安全级别。

要指定 C++ 类型是永久引用类型,请应用 SWIFT_IMMORTAL_REFERENCE 属性。以下是将 SWIFT_IMMORTAL_REFERENCE 应用于 C++ 类型 LoggerSingleton 的示例:

class LoggerSingleton {
public:
    LoggerSingleton(const LoggerSingleton &) = delete; // non-copyable 不可复制

    static LoggerSingleton &getInstance();
    void log(int x);
} SWIFT_IMMORTAL_REFERENCE;

现在,LoggerSingleton 被作为引用类型导入 Swift,程序员可以如下方式使用它:

let logger = LoggerSingleton.getInstance()
logger.log(123)

Shared Reference Types 共享引用类型#

共享引用类型 Shared reference types 是引用计数类型,通过指针引用在 C++ 中传递。它们通常使用以下任一方式:

• 自定义保留和释放操作,增加和减少存储在对象内部的引用计数。 • 或者,使用非侵入性的共享指针类型,如 std::shared_ptr,可以在对象外部存储引用计数。

目前,Swift 可以将使用自定义保留和释放操作以及在对象内部存储引用计数的 C++ 类或结构映射到 Swift 引用类型(其行为类似于 Swift class)。其他依赖 std::shared_ptr 进行引用计数的类型仍然可以作为 Swift 中的值类型使用。

要指定 C++ 类型是共享引用类型,请使用 SWIFT_SHARED_REFERENCE 自定义宏。此宏需要两个参数:一个保留函数和一个释放函数。这些函数必须是接受一个参数且返回 void 的全局函数。该参数必须是指向 C++ 类型的指针(而不是基类型)。Swift 将在保留和释放 Swift 类时调用这些自定义保留和释放函数。以下是将 SWIFT_SHARED_REFERENCE 应用于 C++ 类型 SharedObject 的示例:

class SharedObject : IntrusiveReferenceCounted<SharedObject> {
public:
    SharedObject(const SharedObject &) = delete; // non-copyable

    static SharedObject* create();
    void doSomething();
} SWIFT_SHARED_REFERENCE(retainSharedObject, releaseSharedObject);

void retainSharedObject(SharedObject *);
void releaseSharedObject(SharedObject *);

现在,SharedObject 被作为引用类型导入 Swift,程序员可以如下方式使用它:

let object = SharedObject.create()
object.doSomething()
// `object` 在此处释放。

Unsafe Reference Types 不安全引用类型#

SWIFT_UNSAFE_REFERENCE 注释宏与 SWIFT_IMMORTAL_REFERENCE 注释宏具有相同的效果。然而,它传达了不同的语义:该类型意图不安全使用,而不是在程序期间存在。

Unique Reference Types 唯一引用类型#

目前,Swift 不支持唯一引用类型,例如通过 std::unique_ptr 传递的类型。

在 Swift 中使用 C++ 标准库#

本节介绍如何导入 C++ 标准库,以及如何在 Swift 中使用该库提供的类型。

导入 C++ 标准库#

Swift 可以通过导入 CxxStdlib 模块来导入平台的 C++ 标准库。std 命名空间在 Swift 中成为 std 枚举,内部的函数和类型成为 std Swift 枚举中的嵌套类型和静态函数。

状态页面包含一个受支持的 C++ 标准库列表,其中介绍了 Swift 支持的平台上支持哪些 C++ 标准库。

Using std::string#

C++ 类型 std::string 在 Swift 中成为一个结构体。它遵循 ExpressibleByStringLiteral 协议,因此可以直接用字符串字面量初始化:

import CxxStdlib

let s: std.string = "Hello C++ world!"

Swift 的 String 可以轻松转换为 C++ 的 std::string:

let swiftString = "This is " + "a Swift string"
let cxxString = std.string(swiftString)

同样,可以将 C++ 的 std::string 转换为 Swift 的 String

let cxxString = std.string("This is a C++ string")
let swiftString = String(cxxString)

Swift 不会自动将 C++ std::string 类型转换为 Swift 的 String 类型。

在 Swift 中处理 C++ 引用(References)和视图(View)类型#

正如之前所述,返回引用指针或包含引用指针结构/类的成员函数在 Swift 中被认为是不安全的。这些成员函数通常返回指向 this 对象内部或由 this 对象拥有的内存的引用或视图类型。在这种情况下,返回引用或视图指向的对象的生命周期取决于其拥有对象(传递给成员函数的 this 对象)的生命周期。C++ 当前没有指定哪些成员函数返回依赖引用视图,哪些成员函数返回完全独立的引用视图。因此,Swift 假设任何成员函数返回的引用或视图类型都依赖于 this 对象

在 Swift 中,依赖引用和视图类型是不安全的,因为引用或视图与其拥有对象没有关联。因此,当引用仍在使用时,拥有对象可能会被销毁。由于这种不安全性,以及假设所有此类引用和视图都是依赖的,Swift 重命名了此类成员函数,以强调它们的不安全性,并阻止在 Swift 中使用它们。

本节介绍 Swift 用于确定哪些成员函数返回依赖引用或视图类型的确切规则,并建议如何编写 Swift 封装程序,以便从 Swift 安全地调用此类成员函数。本节还介绍了两个新的自定义宏,可用于 C++ 代码,指示 Swift 将其认为不安全的某些成员函数视为安全函数。

Swift 视为引用(References)或视图(View)类型的 C++ 类型#

Swift 假设返回以下类型之一的 C++ 成员函数在 Swift 中是不安全的:

• C++ 引用 • 原始指针 • 没有用户定义的拷贝构造(user-defined copy)函数且包含上述类型字段的 C++ 类或结构

例如,以下两个 C++ 结构在 Swift 看来是视图(view)类型:

struct PairIntRefs {
  int &firstValue;
  const int &secondImmutableValue;

  PairIntRefs(int &, const int &);
};

// Also a view type, since its `refs` field is a view type.
// 由于 `refs` 字段是视图类型,因此也是视图类型
struct BagOfValues {
  PairIntRefs refs;
  int x;

  BagOfValues(PairIntRefs, int);
};

上述规则定义了 Swift 用于检测可能返回依赖引用或视图类型的成员函数的启发式方法。这并不能保证所有返回依赖引用或视图的 C++ 成员函数都能被 Swift 检测到,因此有些成员函数可能在 Swift 中看起来是安全的。

安全地访问具有依赖生命周期的引用#

调用返回具有依赖生命周期的引用或视图类型的成员函数的推荐方法是将它们封装在返回被引用对象副本的 Swift API 中。

For example, consider the Forest class whose getRootTree members return references:

例如,考虑 Forest 类,getRootTree 会返回引用:

class Forest {
public:
  const Tree &getRootTree() const { return rootTree; }
  Tree &getRootTree() { return rootTree; }

  ...
private:
  Tree rootTree;
};

如前所述,这两个成员函数在 Swift 中分别成为 __getRootTreeUnsafe__getRootTreeMutatingUnsafe 方法。这些方法不应直接在代码中调用。相反,应该编写一个封装器,以实现所需的目标而不暴露依赖引用,然后可以在整个 Swift 代码库中使用。例如,假设您想在 Swift 中检查底层 rootTree 值。可以通过扩展 Forest 类并添加一个返回 Tree 值的 rootTree 计算属性来实现:

import forestLib

extension Forest {
  private borrowing func getRootTreeCopy() -> Tree {
    return __getRootTreeUnsafe().pointee
  }

  var rootTree: Tree {
    getRootTreeCopy()
  }
}

上述使用的 borrowing 所有权修饰符是 Swift 5.9 中的新添加。一些 Swift 5.9 的开发版本可能不允许对可拷贝的 C++ 类型(如 Forest)使用 borrowing。在这种情况下,在 Swift 5.9 发布之前,可以使用一个 mutating 方法调用链来安全地复制 getRootTree 返回的 Tree:

import forestLib

extension Forest {
  private mutating func getRootTreeCopy() -> Tree {
    return __getRootTreeUnsafeMutating().pointee
  }

  var rootTree: Tree {
    var mutCopy = self
    return mutCopy.getRootTreeCopy()
  }
}

使用返回独立生命周期引用和视图的方法#

某些返回引用或视图类型的 C++ 成员函数可能返回一个生命周期独立于 this 对象的引用。Swift 仍将假设这些成员函数是不安全的。要改变这种情况,可以通过注释您的 C++ 代码来指示 Swift:

• 假设某个特定的 C++ 成员函数返回一个完全独立的引用或视图。这些成员函数将被视为安全。 • 假设某个特定的 C++ 类或结构是自包含 self contained的。所有返回此自包含类型的成员函数将被视为安全。

注解返回独立引用或视图的方法#

可以向 C++ 成员函数添加 <swift/bridging> 头文件中的 SWIFT_RETURNS_INDEPENDENT_VALUE 自定义宏,以让 Swift 知道它不会返回依赖引用或视图。这样的成员函数将被认为在 Swift 中是安全的。

例如,NatureLibrary C++ 类中的 getName 成员函数就是 SWIFT_RETURNS_INDEPENDENT_VALUE 的一个很好的候选者,因为其定义明确表明它返回一个指向常量静态字符串文字的指针,而该字符串并未存储在 NatureLibrary 对象内部:

class NatureLibrary {
public:
  const char *getName() const SWIFT_RETURNS_INDEPENDENT_VALUE {
    return "NatureLibrary";
  }
};

注解为自包含(Self Contained)的 C++ 结构或类#

可以向 C++ 结构或类添加 <swift/bridging> 头文件中的 SWIFT_SELF_CONTAINED 自定义宏,以让 Swift 知道它不是视图类型。所有返回此自包含类型的成员函数将被视为安全。

原文#

Swift 与 C++ 混合编程
https://fuwari.vercel.app/posts/rime/mixing_c_swift/
作者
Morse Hsiao
发布于
2024-06-13
许可协议
CC BY-NC-SA 4.0