OpaquePointerでC++をSwiftにBridgeする。

C++をSwiftにBridgeする方法として、Objective-C++を使う方法が有名です。

Objective-C++を使わないならば、C関数でC++の処理をラップします。

C関数でC++の処理をラップするアプローチにおいて、C++のオブジェクトをSwift側で保持するためにはOpaquePointerが活用できます。


C++のオブジェクトを包む構造体とC関数を用意する。

例えばC++標準ライブラリにある mt19937_64 の乱数生成器を、Swiftで利用する場合を考えてみます。

mt19937_64は乱数を出力する毎に状態が変化するため、mt19937_64のオブジェクトをSwift側で保持できるようにする必要があります。

そこで、mt19937_64のオブジェクトを保持する構造体CppRandomGeneratorと、この構造体のIFとなるC関数を用意します。


CppRandomGenerator.cpp

#include "CppRandomGenerator.hpp"

#include <random>

struct CppRandomGenerator {
std::mt19937_64 engine;
};

CppRandomGeneratorRef CppRandomGenerator_Create(uint64_t seed) {
CppRandomGeneratorRef rng = new CppRandomGenerator();
rng->engine.seed(seed);
return rng;
}

uint64_t CppRandomGenerator_Next(CppRandomGeneratorRef rng) {
return rng->engine();
}

void CppRandomGenerator_Destroy(CppRandomGeneratorRef rng) {
delete rng;
}



ブリッジするためのヘッダを用意する。

上述実装に対応するC++ヘッダでは、構造体とC関数を以下の様に宣言します。

このC++ヘッダにおいてCppRandomGeneratorの定義は存在しておらず不完全な状態です。

この様な型は「不完全な型」と呼ばれます。そして不完全な型に対するポインタは、SwiftにおいてOpaquePointerとして扱うことができます。

不完全な構造体の型では、(定義がないので)メンバ変数にアクセスすることができません。しかしポインタとして扱う限りでは、構造体の中に潜むC++オブジェクトの存在を隠蔽しているので、Swift側で保持することができます。


CppRandomGenerator.hpp

#include <stdint.h>


typedef struct CppRandomGenerator* CppRandomGeneratorRef;

#ifdef __cplusplus
extern "C" {
#endif

CppRandomGeneratorRef CppRandomGenerator_Create(uint64_t seed);
uint64_t CppRandomGenerator_Next(CppRandomGeneratorRef rng);
void CppRandomGenerator_Destroy(CppRandomGeneratorRef rng);

#ifdef __cplusplus
}
#endif



Bridging-Header.h

//

// Use this file to import your target's public headers that you would like to expose to Swift.
//

#include "CppRandomGenerator.hpp"



Swiftで利用する。

ブリッジした型とC関数は、Swiftにおいて以下のように使用することができます。

SwiftにおけるCppRandomGeneratorRefOpaquePointerのtypealiasとして自動的に定義されます。

CppRandomGeneratorRefが示すオブジェクトの生成と破棄は自前で管理する必要があるため、Swiftクラスのinit/deinitで面倒を見ることにします。


main.swift

class MersenneTwisterEngine {

private let generator: CppRandomGeneratorRef

init(seed: UInt64) {
generator = CppRandomGenerator_Create(seed)
}

deinit {
CppRandomGenerator_Destroy(generator)
}
}

extension MersenneTwisterEngine: RandomNumberGenerator {
func next() -> UInt64 {
return CppRandomGenerator_Next(generator)
}
}

var rng = MersenneTwisterEngine(seed: 123)
print(Double.random(in: 0...1, using: &rng))



ブリッジ方法別の実行時間を、ざっくり測定してみる。

せっかくなので、ブリッジ方法別に乱数を1億回ほど出力させたときの実行時間を測ってみます。

(a)は、mt19937_64をObjective-C++のクラスでブリッジした場合です。(b)は本記事の方法でブリッジした場合です。(c)はブリッジせずに純粋にC++の世界でmt19937_64の乱数を1億回ほど出力した場合です。

ブリッジ方法
performance (s)

(a)
ObjC++
1.0112709999084473

(b)
OpaquePointer
0.7442220449447632

(c)
Pure C++
0.5961019992828369

OpaquePointerとC関数によるブリッジでは、純粋なC++処理よりも若干のオーバヘッドがあるようです。一方で ObjC++クラスで処理するよりはオーバヘッドが少ないようです。


検証環境

Swift4.2

Xcode10 beta


まとめ

UnsafeなSwiftに造詣が深いわけではないですが、興味本位としてOpaquePointerとC関数によるブリッジ方法を紹介してみました。

Objective-C++のクラスがSwiftのクラスとして利用可能な事を考えると、本記事の方法は実装の手間が少し増えるのが難点だと思います。とはいえ、C++をSwiftで利用するために、わざわざObjective-Cの知識が必要になるのもセンスの無い話です。

Objective-Cの事を知らない人や、パフォーマンスへのこだわり強めで繊細な人1などは、本記事の方法を試してみるのも良いかもしれません。


参考

Using Imported C Structs and Unions in Swift

https://developer.apple.com/documentation/swift/imported_c_and_objective_c_apis/using_imported_c_structs_and_unions_in_swift

Using Imported C Functions in Swift

https://developer.apple.com/documentation/swift/imported_c_and_objective_c_apis/using_imported_c_functions_in_swift

素晴らしいSwiftのポインタ型の解説

https://qiita.com/omochimetaru/items/c95e0d36ae7f1b1a9052

不完全な型

https://docs.oracle.com/cd/E19957-01/805-7885/6j7dqggd5/index.html

SwiftプロジェクトでC++を使う方法メモ

https://qiita.com/hiz_/items/2108344de95c1470b104

Swift Tip: OpaquePointer vs. UnsafePointer

https://www.objc.io/blog/2018/01/30/opaque-vs-unsafe-pointers/





  1. 余談ですが、Matt Gallagher氏によるメルセンヌ・ツイスタの注意深いSwift実装がC++よりも早かったするようです(1億回で0.3sくらい)。 ただし Double.random()で出力範囲を調整させると遅くなります(1億回で4sくらい)。