C++のコールバックをSwiftのクロージャーで書くには

C++で実装されたライブラリがあり、それをSwiftで実装しているアプリから使いたいというときに、ライブラリに渡すコールバック処理をSwiftのクロージャーで書くにはどうしたらよいかについて解説します。

Swift 5.9でC++との相互運用機能が正式機能になりました。本記事はその前に書かれているので、Swift 5.9のC++相互運用については書いていません。後日、別記事で追加し、本記事からもリンクを張る予定です。

目次

SwiftとC++の相互運用性 (2022年8月4日時点)

SwiftC++には、この記事を執筆している時点では直接の相互運用性はありません。まだ、相互運用に必要な拡張は実装途中で、実験的な機能として入っています。

実験的な機能の範囲で、必要な機能が足りているならば、使って見ることもできますが、あくまで「実験的な実装」です。そのため、どのような不具合があるかは分からないので、配布するアプリや製品には使用するべきではないと思います。

実験的な実装でも良いから使って見たい方へ

ちなみに、実験的な実装でも良いから試したい場合には、次のように操作すると使えます。

(1) ビルド設定を開きます。

(2) 「Swift Compiler – Custom Flags」の「Other Swift Flags」に以下の設定を追加します。

-enable-experimental-cxx-interop

どこまで実装されているのかや、色々と興味があるという方は、次のリポジトリを参照してください。現状についてドキュメント化されています。

ちなみに私自身も試してみて、とても楽しみになりました。

Objective-C++経由

C++との相互運用が正式に実装されるまでは、Objective-C++経由でC++で実装された処理を呼ぶのが現実的です。Swiftのクロージャーでコールバック処理を実装するには、次のようにします。

  1. Objective-C++のクラスでC++のコードを実行するラッパークラスを実装する。
  2. C++のコールバック関数をObjective-C++で実装する。
  3. ラッパークラスからC++の処理を実行し、2のコールバック関数を渡す。
  4. 2のコールバック関数からブロックを実行する。
  5. 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」が追加されます。

「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に変更すると、サブディレクトリも潜るようになります。

サンプルコードのダウンロード

今回の記事で作成したサンプルコードはこちらからダウンロードできます。

著書紹介

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

Akira Hayashi (林 晃)のアバター Akira Hayashi (林 晃) Representative(代表), Software Engineer(ソフトウェアエンジニア)

アールケー開発代表。Appleプラットフォーム向けの開発を専門としているソフトウェアエンジニア。ソフトウェアの受託開発、技術書執筆、技術指導・セミナー講師。note, Medium, LinkedIn
-
Representative of RK Kaihatsu. Software Engineer Specializing in Development for the Apple Platform. Specializing in contract software development, technical writing, and serving as a tech workshop lecturer. note, Medium, LinkedIn

目次