[Swift] Swift 5.9 と C++ の互換性


# はじめに

`Swift 5.9` から `C++ interoperability` という機能が入りました。
これにより、`C++` と `Swift` に互換性ができ、直接呼び出せるようになりました！

https://www.swift.org/documentation/cxx-interop/

詳細は `WWDC2023` の動画を見てると面白いと思います。

https://developer.apple.com/videos/play/wwdc2023/10164/?time=1679

https://developer.apple.com/videos/play/wwdc2023/10172/

では、何が変わったのかを見ていきましょう！


# 実装

まずは従来の方法を紹介します。

（Xcode でアプリ開発をしている前提で話します）

## Swift 5.9 未満で C++ を扱う

今までは３通りの方法がありました。（他にあればコメントくださいmm）

1. `C` でブリッジする（C++ の機能 extern "C" を使う）
2. `Objective-C++` でブリッジする
3. `SPM` から `module.modulemap` で公開する

１つずつ見ていきましょう。

### ① C でブリッジする（C++ の機能 `extern "C"` を使う）

前提として `Swift` は `C` と互換性があります。
`Objective-C` と同じ要領で、`C` のコードをヘッダーに追加して呼び出すだけです。

#### - Swift から C を呼び出す簡単な例

以下簡単な例を用意しました。
まず、`C` のヘッダーと実行部分を用意します。

```CMethod.h
#ifndef CMethod_h
#define CMethod_h

void run_from_c(const char *str);

#endif /* CMethod_h */
```

実装は何かを出力する簡単な処理を入れておきます。

```CMethod.c
#include "CMethod.h"
#include "stdio.h"

void run_from_c(const char *str)
{
    printf("%s\n", str);
}
```

最後にアプリのヘッダーに追加します。

```App-Bridging-Header.h
#import "CMethod.h"
```

あとは `Swift` で呼び出すだけです。

```.swift
run_from_c("Hello, World! from C".cString(using: .utf8))
// 出力結果：　"Hello, World! from C"
```

とても簡単です！

#### - Swift から C 経由で C++ を呼び出す簡単な例

`C` を経由する（`C` として振る舞う）ことで `C++` は先の方法と同じ要領で呼び出すことができます。

使うのは `extern "C"` という `C++` の言語機能で、簡単に言えば `C++` 上でコードを `C言語` として扱うようにさせるもので、これを使って実行します。

以下は簡単な例です。

まず、`C++` のヘッダーと実行部分を用意します。
ヘッダーファイルで `extern "C"` を使用します。

```CppMethod.hpp
#ifndef CppMethod_hpp
#define CppMethod_hpp

#include <stdio.h>

#ifdef __cplusplus
extern "C" {
#endif

    void run_from_cpp();

#ifdef __cplusplus
}
#endif

#endif /* CppMethod_hpp */
```

メソッド自体（今回は`run_from_cpp`）を `__cplusplus` で囲ってしまうと、`Swift` 側で認識できなくなるので注意してください。

こちらでも、文字を出力する簡単な実装を入れておきます。

```CppMethod.cpp
#include "CppMethod.hpp"
#include "iostream"

void run_from_cpp() {
    std::cout << "Hello, World! from C++";
}
```

最後にアプリのヘッダーに追加します。
`#include` なことに注意してください。

```App-Bridging-Header.h
#include "CppMethod.hpp"
```

あとは `Swift` で呼び出すだけです。

```.swift
run_from_cpp()
// 出力結果：　"Hello, World! from C++"
```

とても簡単でしたが、これにはいくつかの**制約**があります。例えば `C` から `C++` を呼び出す場合は、`C++` 側で対応する型を使用する必要があったりします。

### ② Objective-C++ でブリッジする

① は少しテクニックのような感じでしたが、こちらはシンプルに `Objective-C++` を経由するものになります。

#### - Swift から Objective-C++ 経由で C++ を呼び出す簡単な例

まずは、先ほどと同じように `C++` のヘッダーと実行部分を用意します。
今回はクラスにしてみます。

```CppClass.hpp
#ifndef CppClass_hpp
#define CppClass_hpp

#include <stdio.h>

class CppClass {
public:
    void run(void);
};

#endif /* CppClass_hpp */
```

```CppClass.cpp
#include "CppClass.hpp"
#include "iostream"

void CppClass::run(void) {
    std::cout << "Hello, World! from Objective-C++";
}
```

文字列を出力するだけの簡単なメソッドを持ったクラスです。

次にブリッジする部分を作ります。

```CppClassWrapper.h
#ifndef CppClassWrapper_hpp
#define CppClassWrapper_hpp

#include <stdio.h>
#import <Foundation/Foundation.h>

@interface CppClassWrapper : NSObject
- (void) run_from_objective_cpp;
@end

#endif /* CppClassWrapper_hpp */
```

```CppClassWrapper.mm
#include "CppClassWrapper.h"
#include "CppClass.hpp"

@implementation CppClassWrapper {
    CppClass *wrapper;
}

-(id)init {
    wrapper = new CppClass();
    return self;
}

-(void)dealloc {
    delete wrapper;
}

-(void)run_from_objective_cpp {
    wrapper->run();
}

@end
```

`#include "CppClass.hpp"` で `C++` のクラスを参照しています。
また、`C++` のクラスを保持できるように、クラスにしています。

あとは、ヘッダーに `Objective-C++` のヘッダーを追加して参照できるようにしましょう。

```App-Bridging-Header.h
#import "CppClassWrapper.h"
```

これで完了です！

```.swift
let wrapper = CppClassWrapper()
wrapper.run_from_objective_cpp()
// 出力結果：　"Hello, World! from Objective-C++"
```

少し記述する量は増えましたが、基本的には橋渡しをしているだけでシンプルな実装できます。

### ③ `SPM` から `module.modulemap` で公開する

方法 ①と② をそのまま `SPM` 上に組み込むことで `C++` を呼び出すことは可能です。

それを使用せずとも、`module.modulemap` を使用することで、`Objective-C++` のブリッジを使用する必要がなくなります。

#### module.modulemap とは?

> Module Map は Bridging Header の上位互換で、Objective-C または C で書かれたライブラリの場合は、Modules ディレクトリの中に module.modulemap というファイルを設定することで、Swift 側からシンボルにアクセスできるようにします。
Swift が登場して以降の Xcode で Objective-C 製のライブラリなどをビルドした場合は自動的に module.modulemap ファイルが生成されます。

（引用：[iOS開発におけるライブラリ、SDK、フレームワークなど](https://zenn.dev/kamimi01/scraps/21080d85e9a6dc)）

以下は簡単な例です。

```.modulemap
module ExampleCppLibrary [system] {
    header "ExampleCppLibrary.hpp"
    export *
}
```

同じような感じなので実装は割愛します。
少し調べたら出てくる ＆ 生成AI(ChatGPTなど)に聞いてください笑


## Swift 5.9 以上で C++ を扱う

ここまでは、`C++` を `Swift` 上で動かすための従来の方法を紹介してきました。
今までの方法（特に①, ② ）は **ブリッジするコードが必要**となり、若干の手間がありました。

`C++ interoperability` では、これが必要なくなり、直接 `C++` を呼ぶことができます！


### - プロジェクト上にある C++ を参照する

`C++` のヘッダーと実行部分を用意します。
ここでは、先ほど作成したコード例とほぼ同じものを使用して進めます。

<details><summary>コード例はこちら：CppClass.hpp, CppClass.cpp</summary><div>

```CppClass.hpp
#ifndef CppClass_hpp
#define CppClass_hpp

#include <stdio.h>

class CppClass {
public:
    void run(void);
};

#endif /* CppClass_hpp */
```

```CppClass.cpp
#include "CppClass.hpp"
#include "iostream"

void CppClass::run(void) {
    std::cout << "Hello, World! from C++";
}
```

</div></details>

<br>

これをそのままヘッダーに追加します。

```App-Bridging-Header.h
#include "CppClass.hpp"
```

次に Xcode 側でコンパイルの設定を変更します。

![Screenshot 2023-12-27 at 22.43.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/677d9bfe-a445-4763-6a72-8d0bc1fcec0b.png)

interoperability の設定を `C/Objective-C` -> `C++/Objective-C++` にしましょう。

あとは、そのまま `Swift` から呼び出すだけです。

```.swift
var cppClass = CppClass()
cppClass.run()
// 出力結果：　"Hello, World! from C++"
```

**ブリッジコードが不要**になり、とても簡単です！

注意点として、`C` のコードはビルドできなくなります。

なので、もし `C` と `C++` のコードを混在させたプロジェクトがある場合は、安易にフラグを変えてしまうとビルドできなくなります。（ただし、`Objective-C` はビルドできる）


# その他

取り上げませんでしたが

- `C++` から `Swift` を参照する
- `SPM` で `C++` を参照する
- コマンドラインで使う
etc.

この辺はドキュメントに書いてあり、`Swift` と `C++` をもっと快適に、面白くできそうなことがたくさん載っています！




## サンプルコード

試したサンプルを置いておきます。
説明した大部分のコードはこちらに入っています。

https://github.com/crane-hiromu/SwiftToCpp

フラグの設定や読み込み部分は、試したいものを各自で変更してくださいませmm


## 関連

### - Vision Pro での動作


`Stack Overflow` のコメントでは `Vision Pro` でビルドできないとありました。

https://stackoverflow.com/questions/77300704/visionos-support-for-mixing-swift-and-c-interoperability

> May be I am doing something wrong, but the below code snippet from the official swift doc is working fine when the run destination is set to Mac, but when I change the run destination to Apple Vision Pro, it doesn't compile and throws some errors. 
`
let cxxString = std.string("This is a C++ string") 
let swiftString = String(cxxString)
`
No exact matches in call to initializer So, what I get from this is that the mixing Swift and C++ doesn't support visionOS for now. 



手元の `Xcode 15.2` と `Vision Pro` の Simulator では、紹介したサンプルコードは動作しました。`C++` のユニークな型や機能を使った場合に動作するのかは分かりません。

### - 興味深い記事


以下ピックアップで、紹介しませんでしたが、興味がある人は読んでみてください。

https://www.swift.org/blog/swift-everywhere-windows-interop/

https://kateinoigakukun.hatenablog.com/entry/2020/12/01/083949


### 参考

https://www.koikikukan.com/archives/2017/07/04-000300.php

https://qiita.com/nukka123/items/a1c029e07cb6cdb26a2f

https://nao-bamboo.hatenablog.com/entry/2015/12/13/204334