How To write C++ callbacks with Swift’s closure

This article provides instructions on how to write a callback function using Swift’s closure for interfacing with a library written in C++ from Swift application.

Interoperability with C++ is now an official feature in Swift 5.9. This article was written before that, so it does not describe C++ interoperability in Swift 5.9. It will be added in a separate article at a later date and linked to from this article.

目次

Swift and C++ Interoperability (as of August 4, 2022)

Swift and C++ do not have direct interoperability as of this writing. The extensions necessary for interoperability are still in the implementation process and are included as experimental features.

You can utilize the experimental functionality if it meets your needs. However, remember it’s still in the “experimental implementation” stage, meaning we are still investigating potential defects. Hence, using it for applications or products intended for distribution is not advisable.

For those who would like to try an experimental implementation and see how it works

If you want to try an experimental implementation, follow the steps below.

(1) Open the build settings.

(2) Add the following settings to “Other Swift Flags” in “Swift Compiler – Custom Flags”.

-enable-experimental-cxx-interop

For more information on the implementation progress and other related aspects, please refer to the designated repository where the current status is documented.

On a personal note, I’ve tried this out and found it quite engaging.

Via Objective-C++

Until interoperability with C++ is formally implemented, it is practical to call processes implemented in C++ via Objective-C++; to implement the callback function in Swift’s closure, do the following.

  1. Implement a wrapper class that executes C++ code in an Objective-C++ class.
  2. Implement C++ callback functions in Objective-C++.
  3. Execute C++ callback functions from the wrapper class and pass the callback function of 2.
  4. Execute a block from the callback function in 2.
  5. Use a wrapper class from Swift and implement the block with a Swift closure.

It is hard to understand in writing. So let’s write a test program.

Implement a test program

Here we implement the process of getting a list of files in a directory using FTS as a C++ class. This class executes a callback function for each file or directory it finds.

Implement the wrapper class for this C++ class as an Objective-C++ class. Also, implement the callback function in a C++ class.

Implement the Swift code that uses the Objective-C++ wrapper class; the Swift part will be a macOS app implemented in SwiftUI. The part that outputs to the console will be implemented in a Swift closure.

Create a project

You should create a project as a macOS app utilizing SwiftUI.

Since this is a local test program, please also remove the sandbox. Operate as follows.

(1) Open the target’s settings and go to the “Signing & Capabilities” tab.

(2) Click the “Remove App Sandbox” button.

Click remove button
Click remove button

(3) Click the Add button. The Capabilities window will appear.

(4) Double-click “Hardened Runtime”. Hardened Runtime” will be added.

Add "Hardened Runtime"
Add “Hardened Runtime”

Implement directory scanning

Implement in C++. The code is the following.

//
//  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)
        {
            // Don't go to the sub directory
            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);
    }
}

Wrapper Class and Callback Function Implementation

Implement the wrapper class, which is used by Swift and the callback function, which is passed to `DirectoryScanner::scan` of C++ in Objective-C++. The code is the following.

//
//  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

The bridge header does not include the C++ header, only the Objective-C++ header. Therefore, it will look like this.

// FTSExample-Briding-Header.h

#import "DirectoryScannerWrapper.h"

Swift app-side code

Implement app-side code as follows in 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()
    }
}

The process when a button is pressed, ScanButtonAction.swift is created and separated from the view as follows. The code is as follows.

//
//  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
            }
        }
    }
}

Running Test

Run the application and click the “Scan” button. A file selection dialog will appear, so select the directory from which you want to retrieve the file list. The paths of the files/directories in the selected directory will be output to the console.

In the DirectoryScanner::scan() method, change the entry->fts_level == 1 to entry->fts_level > 0 so that subdirectories will also be scanned.

Download the sample code

The sample code created for this article can be downloaded here.

著書紹介

よかったらシェアしてね!
  • 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

目次