Blog スタッフブログ
[Swift]Xcode15/Swift5.9でC++からSwiftへコールバックを実行する
こんにちは、株式会社MIXシステム開発担当のBloomです。
前回の記事からの続きになります。今度は逆にC++からSwiftのクラスで記述したコールバックを呼び出す方法について知見を共有させていただきたいと思います。
typedefでコールバックを定義
まずはCでお馴染みの関数ポインタによるコールバックを宣言するパターンを利用してみましょう。プロジェクト構成などは前回記事に準じています。
#ifndef CppClass_hpp
#define CppClass_hpp
#include <stdio.h>
#include <vector>
// [Build Setting] -> [Generated Header Name]で指定されている名称のヘッダファイルを読み込みましょう
#include <sandbox-Swift.h>
class CppClass {
public:
typedef void (*callback_t) (int);
void executeCallback(callback_t _Nonnull callback);
};
#endif /* CppClass_hpp */
#include "CppClass.hpp"
void CppClass::executeCallback(callback_t _Nonnull callback) {
callback(100);
}
では、このコールバックをSwift側で定義し渡してみましょう。
// 前略
override func viewDidLoad() {
super.viewDidLoad()
var cppClass = CppClass()
let callback_func: CppClass.callback_t = { result in
print("callback func called \(result)")
}
cppClass.executeCallback(callback_func)
}
ここでは「CppClass.callback_t」型として宣言しクロージャを渡しています。typedefによる型宣言はinteropabilityによりクラス内構造体に近い扱いになっているのでしょうか(具体的な機序について詳しい方、弊社XアカウントでDMをお待ちしています)
これで一旦C言語スタイルによるコールバックを書くことはできましたが、このコールバックではキャプチャができないためViewControllerのメンバなどへアクセスができません。
publicな中継クラスを利用する
ここでは中継するBridgeクラスを作成しC++のクラスへ渡しましょう。
public class CppBridge {
// これをやると循環参照でエラーになります
//var viewController: ViewController? = nil
var callback: ((NSObject) -> Void)?
var object: NSObject?
public func hello() {
print("hello")
if let object = object {
callback?(object)
}
}
}
publicとしてクラス宣言することで別モジュール、つまりC++のクラスからもアクセスができるようになります。
#ifndef CppClass_hpp
#define CppClass_hpp
#include <stdio.h>
#include <vector>
// [Build Setting] -> [Generated Header Name]で指定されている名称のヘッダファイルを読み込みましょう
#include <sandbox-Swift.h>
class CppClass {
public:
void executeCallback(sandbox::CppBridge bridge);
};
#endif /* CppClass_hpp */
#include "CppClass.hpp"
void CppClass::executeCallback(sandbox::CppBridge bridge){
bridge.hello();
}
// 前略
override func viewDidLoad() {
super.viewDidLoad()
var cppClass = CppClass()
let bridge = CppBridge()
bridge.object = self
/* // これでも動作しますが後述します
bridge.callback = { [self] object in
self.anyButton.setTitle("any", for: .normal)
}
*/
bridge.callback = { object in
if let self = object as? ViewController {
self.anyButton.setTitle("any", for: .normal)
}
print("hello from Swift")
}
cppClass.executeCallback(bridge)
}
Swift側で生成したCppBridgeクラスをC++側へ渡し、C++側のクラス関数実行をトリガーにコールバックを起動しています。
少々迂遠ですが、これでC++からSwiftのクラスへコールバックを実行することができました。良かったですね。
ちなみに
#include <sandbox-Swift.h>によるヘッダファイルの参照に失敗する場合、Swift側のコンパイルエラーが原因で[Project Name]-Swift.hが生成されていないといったケースが往々にしてあります。
その際は一旦includeを外しビルドを一度通し、生成させてから改めてincludeすると正常に動作するようです。
これと同じようにSwift側で記述した内容は一度ビルドしヘッダファイルを再生成するまでC++側で参照しても静的解析でエラーが発生します。
また、Xcode15自体に循環参照の検出についての不具合があるようです。ふたつのサンプルコードを掲載しますが、前者はエラーとなり後者はビルドできるなど不安定な部分があるため注意しましょう。
public class CppBridge {
var callback: (() -> Void)?
public func hello() {
print("hello")
callback?()
}
}
class ViewController {
override func viewDidLoad() {
super.viewDidLoad()
var cppClass = CppClass()
let bridge = CppBridge()
bridge.callback = { [self] in
self.anyButton.setTitle("any", for: .normal)
}
cppClass.executeCallback(bridge)
}
}
public class CppBridge {
var callback: ((NSObject) -> Void)?
var object: NSObject?
public func hello() {
print("hello")
if let object = object {
callback?(object)
}
}
}
class ViewController {
override func viewDidLoad() {
super.viewDidLoad()
var cppClass = CppClass()
let bridge = CppBridge()
bridge.object = self
bridge.callback = { [self] object in
self.anyButton.setTitle("any", for: .normal)
}
cppClass.executeCallback(bridge)
}
}