Passing C++ callback to Swift function that expects a closure

I'm trying to spin up an electron app that has access to some native macOS APIs. As such, I'm trying to write the core logic in Swift that watches for changes and pass in a callback that is fired every time a new event comes through. The project uses node-gyp to get the electron code to interface with some C++ code that is dynamically linked against a Swift library. Unfortunately I'm having a lot of trouble figuring out how to get my Swift code to invoke the callback and could use some advice! I'm including the subsection that seems problematic.

Swift code:

public typealias Callback = @convention(c) (UnsafePointer<CChar>?) -> Void

@_cdecl("watch_for_changes")
public func watchForChanges(_ callback: @escaping Callback) -> CBool {
    let arg = "arg"
    let argChars = UnsafePointer<CChar>((arg as NSString).utf8String)
    callback(arg)
    return true
}

C++ code:

// Dynamic linking taken from https://github.com/noppoMan/node-native-extension-in-swift
void *DLSymOrDie(void *lib, const string& func_name) {
  const auto func = dlsym(lib, func_name.c_str());
  const auto dlsym_error = dlerror();
  if (dlsym_error) {
    cerr << "Could not load symbol create: " << dlsym_error << endl;
    dlclose(lib);
    exit(EXIT_FAILURE);
  }
  return func;
}

void *DLOpenOrDie(const string& path) {
  const auto lib = dlopen(path.c_str(), RTLD_LAZY);
  if (!lib) {
    cerr << "Could not load library: " << dlerror() << endl;
    exit(EXIT_FAILURE);
  }
  return lib;
}

void testCB(const char *appName, const char *fileName) {
  printf("A: %s\n", appName);
  printf("F: %s\n", fileName);
}

Napi::Boolean RegisterForActiveAppChanges(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  const auto swiftLib = DLOpenOrDie(SWIFT_SHARED_LIBRARY_PATH);
  const auto _RegisterForChangesFunc = (RegisterForChangesFunc)DLSymOrDie(swiftLib, SWIFT_REGISTER_FOR_CHANGES_FUNC_SYMBOL);

  return Napi::Boolean::New(env, _RegisterForChangesFunc(testCB));
}

I can call into the Swift code just fine, but it aborts every time I try to pass anything to the testCB callback. When I bundle the same code together in one big project with a Bridging Header, everything compiles and runs the way that I would expect (invoking the callback correctly).

If there's something silly I'm doing, I'd appreciate the advice. Or alternatively, if there better ways to accomplish what I want, I'm all ears!

Thanks!

Where does it aborts exactly?
First that comes in mind, that you passing only one argument, but signature requires two, so you accessing garbage in printf("F: %s\n", fileName), but there may be other things.

You have a number of problems here. Firstly, here is the type you’ve given for the callback in Swift:

public typealias Callback = @convention(c) (UnsafePointer<CChar>?) -> Void

Here is the signature of the function you’re passing to it in C++:

void testCB(const char *appName, const char *fileName)

Notice that these signatures don’t match: your C++ function expects two arguments, but your Swift code only passes one. I’m astonished you say that it “runs as expected” in any mode, because this should only ever produce gibberish in the best case and pain in the worst.

Second, this code is almost certainly wrong:

let argChars = UnsafePointer<CChar>((arg as NSString).utf8String)

When attempting to get a pointer to the interior storage of a Swift structure you will almost always need to use a function with with in its name, e.g. withCString or similar. This is because Swift will often end up moving the location of the backing storage of Swift CoW structures. The with function has the effect of pinning the structure to the appropriate place in memory, but the pointer you get is only valid for the duration of that call.

I’d begin by trying to fix those two things and then see how that goes.

Agh, so sorry. Copy-paste error got me on testCB. I copied from the example that was working (where the callback typealias was public typealias Callback = @convention(c) (UnsafePointer<CChar>?, UnsafePointer<CChar>?) -> Void) instead of the reduced example. The reduced example code for testCB I used in testing was:

void testCB(const char *name) {
  printf("Name: %s\n", name);
}

I also updated the swift side code as you expect, so it now looks like:

@_cdecl("register_for_active_app_changes")
public func registerForActiveAppChanges(_ callback: @escaping Callback) -> CBool {
    let arg = "arg"
    arg.withCString { (cstr) in
        callback(cstr)
    }

    return true
}

And the same problem persists. It crashes right when it attempts to invoke the callback.

1 Like

I've added a repo with both the working and non-working example if that is helpful: GitHub - nbhargava/swift-interop-repro

When you say “it crashes”, can you be more specific? What information do you see about the crash?

I get a segfault that happens when the code attempts to invoke the callback.

Output when running normally:

$ ./test-interop 
About to invoke swift code...
Started...
Segmentation fault: 11

When I run lldb to debug, I'm not sure whether it's possible to inspect what's going on under the hood with the closure invocation. It seems like an issue with memory management, but I'm a little unsure how to best handle it.

$ lldb ./test-interop 
(lldb) target create "./test-interop"
Current executable set to './test-interop' (x86_64).
(lldb) r
Process 48334 launched: '/Users/nikhil/src/swift-interop-repro/non-working-interop/test-interop' (x86_64)
About to invoke swift code...
Started...
Process 48334 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x7ffeefbff890)
    frame #0: 0x00007ffeefbff890
->  0x7ffeefbff890: addb   $0x0, (%rax)
    0x7ffeefbff894: addl   %eax, (%rax)
    0x7ffeefbff896: addb   %al, (%rax)
    0x7ffeefbff898: adcb   %dl, (%rax)
Target 0: (test-interop) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x7ffeefbff890)
  * frame #0: 0x00007ffeefbff890
    frame #1: 0x0000000100126fa2 libCallbackExample.dylib`closure #1 in registerForChanges(cstr=<unavailable>, callback=<unavailable>) at CallbackExample.swift:10:9
    frame #2: 0x0000000100126fcf libCallbackExample.dylib`thunk for @callee_guaranteed (@unowned UnsafePointer<Int8>) -> (@error @owned Error) at <compiler-generated>:0
    frame #3: 0x0000000100127034 libCallbackExample.dylib`partial apply for thunk for @callee_guaranteed (@unowned UnsafePointer<Int8>) -> (@error @owned Error) at <compiler-generated>:0
    frame #4: 0x00007fff6c061dd8 libswiftCore.dylib`Swift.String.withCString<A>((Swift.UnsafePointer<Swift.Int8>) throws -> A) throws -> A + 88
    frame #5: 0x0000000100126e4c libCallbackExample.dylib`registerForChanges(callback=<unavailable>) at CallbackExample.swift:9:9
    frame #6: 0x0000000100126ca9 libCallbackExample.dylib`register_for_changes at <compiler-generated>:0
    frame #7: 0x000000010000112a test-interop`main + 234
    frame #8: 0x00007fff6cad77fd libdyld.dylib`start + 1
(lldb) up
frame #1: 0x0000000100126fa2 libCallbackExample.dylib`closure #1 in registerForChanges(cstr=<unavailable>, callback=<unavailable>) at CallbackExample.swift:10:9
   7   	    print("Started...")
   8   	    let arg = "arg"
   9   	    arg.withCString { (cstr) in
-> 10  	        callback(cstr)
   11  	    }
   12  	    print("Finished.")
   13  	}
(lldb) p arg
Cannot create Swift scratch context (couldn't load the Swift stdlib)Cannot create Swift scratch context (couldn't load the Swift stdlib)Shared Swift state for test-interop could not be initialized.
The REPL and expressions are unavailable.

PS - I've updated the previously linked repo with a simpler example that demonstrates the error when trying to just get the C++ to interop with a dynamically linked swift library (no Electron calls in).

The problem is that your RegisterForChangesFunc definition is declaring the callback as an std::function<void(char*)>, which isn't necessarily bitwise-compatible with a @convention(c) function pointer.

Rewriting it like this works:

typedef void (*ChangesCallback)(const char *);
typedef bool (*RegisterForChangesFunc)(ChangesCallback);

C++'s std::function can wrap more than just C-style function pointers; it might be wrapping a member function or even an object which implements the callable operator:

Class template std::function is a general-purpose polymorphic function wrapper. Instances of std::function can store, copy, and invoke any Callable target -- functions, lambda expressions, bind expressions, or other function objects, as well as pointers to member functions and pointers to data members.

Aha! That fixed it. Thanks so much!