C++
で実装されたライブラリがあり、それをSwift
で実装しているアプリから使いたいというときに、ライブラリに渡すコールバック処理をSwift
のクロージャーで書くにはどうしたらよいかについて解説します。
SwiftとC++の相互運用性 (2022年8月4日時点)
Swift
とC++
には、この記事を執筆している時点では直接の相互運用性はありません。まだ、相互運用に必要な拡張は実装途中で、実験的な機能として入っています。
実験的な機能の範囲で、必要な機能が足りているならば、使って見ることもできますが、あくまで「実験的な実装」です。そのため、どのような不具合があるかは分からないので、配布するアプリや製品には使用するべきではないと思います。
実験的な実装でも良いから使って見たい方へ
ちなみに、実験的な実装でも良いから試したい場合には、次のように操作すると使えます。
(1) ビルド設定を開きます。
(2) 「Swift Compiler – Custom Flags」の「Other Swift Flags」に以下の設定を追加します。
-enable-experimental-cxx-interop
どこまで実装されているのかや、色々と興味があるという方は、次のリポジトリを参照してください。現状についてドキュメント化されています。
ちなみに私自身も試してみて、とても楽しみになりました。
Objective-C++経由
C++
との相互運用が正式に実装されるまでは、Objective-C++
経由でC++
で実装された処理を呼ぶのが現実的です。Swift
のクロージャーでコールバック処理を実装するには、次のようにします。
Objective-C++
のクラスでC++
のコードを実行するラッパークラスを実装する。C++
のコールバック関数をObjective-C++
で実装する。- ラッパークラスから
C++
の処理を実行し、2のコールバック関数を渡す。 - 2のコールバック関数からブロックを実行する。
Swift
からラッパークラスを使用し、ブロックをSwift
のクロージャーで実装する。
文章にすると分かりづらいですね。実際にテストプログラムを書いてみましょう。
テストプログラムの実装
ここでは、FTSを使ったディレクトリ内のファイルリスト取得処理をC++
のクラスとして実装します。このクラスは見つけたファイルやディレクトリごとにコールバック関数を実行します。
このC++
のクラスのラッパークラスをObjective-C++
のクラスとして実装します。また、コールバック関数をC++
のクラスで実装します。
Objective-C++
のラッパークラスを使用するSwiftのコードを実装します。Swiftの部分はSwiftUI
で実装したmacOSアプリとします。コンソールに出力する部分は、Swiftのクロージャーで実装します。
プロジェクトの作成
プロジェクトはSwiftUI
を使ったmacOS
アプリとして作成してください。
ここでは、ローカルのテストプログラムなので、サンドボックスも外してください。次のように操作します。
(1) ターゲットの設定を開き、「Signing & Capabilities」タブを開きます。
(2) 「App Sandbox」の削除ボタンをクリックします。
(3) 追加ボタンをクリックします。「Capabilities」ウインドウが表示されます。
(4) 「Hardened Runtime」をダブルクリックします。「Hardened Runtime」が追加されます。
ディレクトリのスキャン処理を実装する
C++
で実装します。コードは次の通りです。
//
// DirectoryScanner.hpp
// FTSExample
//
// Created by Akira Hayashi on 2022/08/05.
//
#ifndef DirectoryScanner_hpp
#define DirectoryScanner_hpp
#include <string>
class DirectoryScanner {
public:
typedef bool (*ItemCallBackProc)(const std::string &path, void *pParam);
DirectoryScanner() {}
virtual ~DirectoryScanner() {}
void scan(const std::string &directoryPath, ItemCallBackProc callbackProc, void *pParam);
};
#endif /* DirectoryScanner_hpp */
//
// DirectoryScanner.cpp
// FTSExample
//
// Created by Akira Hayashi on 2022/08/05.
//
#include "DirectoryScanner.hpp"
#include <sys/types.h>
#include <sys/stat.h>
#include <fts.h>
#include <set>
void DirectoryScanner::scan(const std::string &directoryPath, ItemCallBackProc callbackProc, void *pParam)
{
int options = (FTS_NOSTAT | FTS_NOCHDIR | FTS_PHYSICAL);
char *paths[] = {const_cast<char *>(directoryPath.c_str()), NULL};
FTS *fts = fts_open(paths, options, NULL);
if (fts)
{
std::set<std::string> pathSet;
FTSENT *entry = NULL;
while ((entry = fts_read(fts)) != NULL)
{
// サブディレクトリまでは潜らない
if (entry->fts_level == 1)
{
std::string path(entry->fts_path);
if (pathSet.find(path) == pathSet.end())
{
pathSet.insert(path);
if (!(*callbackProc)(path, pParam))
{
break;
}
}
}
}
fts_close(fts);
}
}
ラッパークラスとコールバック関数の実装
Swift
から使用するラッパークラスとC++
のDirectoryScanner::scan
メソッドに渡すコールバック関数をObjective-C++
で実装します。コードは次の通りです。
//
// DirectoryScannerWrapper.h
// FTSExample
//
// Created by Akira Hayashi on 2022/08/05.
//
#import <Foundation/Foundation.h>
@interface DirectoryScannerWrapper : NSObject
- (void)scanWithDirectoryPath:(nonnull NSString *)path
block:(nonnull BOOL (^)(NSString * _Nonnull directoryPath))block;
@end
//
// DirectoryScannerWrapper.m
// FTSExample
//
// Created by Akira Hayashi on 2022/08/05.
//
#import "DirectoryScannerWrapper.h"
#import "DirectoryScanner.hpp"
struct Context {
BOOL (^block)(NSString * _Nonnull dirPath);
};
static bool ScannerCallBack(const std::string &path, void *pParam)
{
@autoreleasepool
{
Context *context = reinterpret_cast<Context *>(pParam);
return context->block([NSString stringWithUTF8String:path.c_str()]);
}
}
@implementation DirectoryScannerWrapper
- (void)scanWithDirectoryPath:(nonnull NSString *)path
block:(BOOL (^)(NSString * _Nonnull directoryPath))block
{
Context context = {};
context.block = block;
DirectoryScanner scanner;
scanner.scan(std::string(path.UTF8String), &ScannerCallBack, &context);
}
@end
ブリッジヘッダーには、C++
のヘッダーを含めず、Objective-C++
のヘッダーだけを含めます。次のようになります。
// FTSExample-Briding-Header.h
#import "DirectoryScannerWrapper.h"
Swiftのアプリ側のコード
アプリ側のコードを実装します。ContentView.swift
に以下の様に実装します。
//
// ContentView.swift
// FTSExample
//
// Created by Akira Hayashi on 2022/08/05.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("DirectoryScanner")
.font(.largeTitle)
.padding()
Text("Click the 'Scan' button.")
.padding()
Button("Scan") {
let action = ScanButtonAction()
action.execute()
}
}
.frame(width: 400, height: 300, alignment: .center)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ボタンが押されたときの処理は次のようにScanButtonAction.swift
を作成して、ビューから分離しました。コードは次の通りです。
//
// ScanButtonAction.swift
// FTSExample
//
// Created by Akira Hayashi on 2022/08/05.
//
import AppKit
struct ScanButtonAction {
func execute() {
let openPanel = NSOpenPanel()
openPanel.canChooseFiles = false
openPanel.canChooseDirectories = true
if openPanel.runModal() == .OK {
let scanner = DirectoryScannerWrapper()
scanner.scan(withDirectoryPath: openPanel.url!.path) { path in
print("\(path)")
return true
}
}
}
}
動作テスト
アプリを実行し、「Scan」ボタンをクリックします。すると、ファイルの選択ダイアログが表示されるので、ファイルリストを取得するディレクトリを選択します。選択されたディレクトリ内のファイル・ディレクトリのパスをコンソールに出力します。
DirectoryScanner::scan()
メソッドでentry->fts_level == 1
という部分をentry->fts_level > 0
に変更すると、サブディレクトリも潜るようになります。
サンプルコードのダウンロード
今回の記事で作成したサンプルコードはこちらからダウンロードできます。