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

When you have a library implemented in C++ and you want to use it from an application implemented in Swift, this article explains how to write a callback function to be passed to the library in Swift’s closure.

TOC

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 process of implementation and are included as experimental features.

Within the scope of experimental functionality, if it is sufficient for what you need, you can use it and see, but it is only an “experimental implementation”. Therefore, we do not know what kind of defects it may have, so we should not use it for applications or products that we distribute.

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

Incidentally, if you want to try an experimental implementation, you can use the following operation.

STEP
Open the build settings.
STEP
Add the following settings to “Other Swift Flags” in “Swift Compiler – Custom Flags”.
-enable-experimental-cxx-interop

If you are interested in how far it has been implemented and many other things, please refer to the following repository. The current status is documented.

By the way, I tried it myself and really enjoyed it.

Via Objective-C++

Until interoperability with C++ is formally implemented, it is practical to call processes implemented in C++ via Objective-C++; to implement 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. Let’s actually 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

Projects should be created as macOS apps using SwiftUI.

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

STEP
Open the target’s settings and go to the “Signing & Capabilities” tab.
STEP
Click the “Remove App Sandbox” button.
Click remove button
Click remove button
STEP
Click the Add button. The Capabilities window will appear.
STEP
Double-click “Hardened Runtime”. Hardened Runtime” will be added.
Add "Hardened Runtime"
Add “Hardened Runtime”

Implement directory scanning

Implement in C++. The code is 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 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. 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 then 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.

Let's share this post !

Author of this article

Akira Hayashiのアバター Akira Hayashi Representative, Software Engineer

I am an application developer loves programming. This blog is a tech blog, its articles are learning notes. In my work, I mainly focus on desktop and mobile application development, but I also write technical books and teach seminars. The websites of my work and books are here -> RK Kaihatsu.

TOC