1

first of all I'm new in Stackoverflow and I'd like to thanks all of you for all the great info!

I have some experience on C/C++ under Linux and Win and recently I'm trying to develop/port an app for MacOS since I've just bought the new MacBook Air M1.

I've create a new MacOS App in Xcode and I'm using some C++ Classes with extern "C" wrapper... From my Swift code (push button pressed), I'm calling a C function that takes an int* as parameter that is the percentage progress of the operation it does:

void doSomething(int *progress);

I'm thinking to run that function in a separate thread when a push button is pressed and update a progress bar on my Swift UI based on the value of progress variable.

Can you please help me? What could be the best solution? Should I use a separate thread for the function or for the progress bar UI update?

Willeke
  • 14,578
  • 4
  • 19
  • 47
AirMonk
  • 43
  • 3
  • 1
    I think passing an `int *` won't work. Every time your C function makes progress and updates the integer pointer, there has to be something "on the other side" to notice that change. That means you'll need another thread to repeatedly poll and check the value. That could be made to work, but I think you'll have an all-around easier time by instead accepting a callback function pointer as a parameter, and calling it everytime progress is made. Your caller code can then provide a function which updates the progress bar UI in response to every callback invocation. – Alexander Feb 15 '21 at 13:46
  • Thanks for your response, I've just add a thread that just update the progress bar with the value of the int * passed to the C function and it seems to works but really don't like to have an infinite loop that just update the progress bar all time even if not necessary. Unfortunately don't know how to implement some kind of observer in Swift. – AirMonk Feb 19 '21 at 16:12

1 Answers1

0

As you saw, using an int * is rather inefficient. That's because the assignments done to that value can't trigger any other code to run (there's no equivalent to a didSet observer in C). So you're forced to make a separate thread whose purpose is to poll the int value periodically, which is super wasteful.

To fix this, you'll need to use a callback. These are easy in Swift, because closures are first class citizens that you can trivially create and pass around. In C, there's no such thing. The closest thing is a function pointer, but those are static, and lack the contextual storage that a closure can provide. This means that you can't pass Swift instance methods, or closures as c function pointers. You need a closure that's marked @convention(c), which prohibits it from capturing any state (including the self in a method).

But there's a workaround. To understand it, first you should understand the typical callback pattern used in C.

In C it's typical for a function that takes a function pointer (as a callback) to also have a second untyped (void *) "context" parameter. The idea is that you can manually pass in your own context, of whatever type, and it'll be stored alongside the function pointer. When it's time to trigger the callback, the C function will call your function pointer, passing in the context you gave it. Inside your callback function (the one whose pointer you passed as a callback), you have access to this void *context, which you can then cast to your appropriate type and access whatever you need.

This context param comes by many names, such as context, ctx, userdata, userinfo, (and underscored, camelcase variants), etc.

If you update your C function to something like:

typedef void (*ProgressChangedCallback)(const int newProgress, void *userinfo);

void doSomething(ProgressChangedCallback callback, void *userinfo) {
   // when the progress updates:
   callback(newProgress, userinfo);
}

In Swift, you can take advantage of this userinfo param to pass in the context you need to implement your call back. There's two popular ways to do it:

  1. You could use the userinfo param to smuggle in self, and then call methods on self with full access to the instance variables of the object.
  2. You could use the userinfo param to smuggle in a proper Swift closure. The closure itself can capture state, including self or whatever else you might need.

It would look something like this:


// ProgressChangedCallback would have type `@convention(c) (Int, UnsafeMutableRawPointer) -> Void`

class MyClass {
    var someInstanceState = 0
    
    func someFunctionThatHasAccessToInstanceState() {
        defer { someInstanceState += 1 }
        print(someInstanceState)
        // 5. This can update your progress bar UI or whatever.
    }
    
    func registerCallbackForDoSomething() {
        let myProgressChangedCallback: ProgressChangedCallback = { newProgress, userInfo in
            // 3. Unpack `userinfo` to get access to our instance again
            let instance = Unmanaged<MyClass>.fromOpaque(userInfo).takeUnretainedValue()
            
            // 4. Do whatever we might need with the instance
            instance.someFunctionThatHasAccessToInstanceState()
        }
        
        // 1. Retain `self`, and get an opaque pointer to it
        let retainedPointerToSelf = UnsafeMutableRawPointer(Unmanaged.passRetained(self).toOpaque())
        
        // 2. Register our callback, and pass the pointer to self as `userInfo`
        doSomething(myProgressChangedCallback, retainedPointerToSelf)
    }
}
Further reading
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Many thanks for the great explanation, I think this is the way to go! I've tried to put your code inside my MacOS App in the ViewController class so that when I push the button I can call registerCallbackForDoSomething but I'm getting an error on this line: let instance = Unmanaged.fromOpaque(userInfo).takeUnretainedValue(). The error report: Value of optional type 'UnsafeMutableRawPointer?' must be unwrapped to a value of type 'UnsafeMutableRawPointer'. Can you help me? – AirMonk Feb 22 '21 at 06:53
  • That’s just a basic optionality error, there’s thousands of questions on this site that can help you for it already. In this case I think it’s probably safe to force unwrap it – Alexander Feb 22 '21 at 13:42
  • Thank you so much Alexander for the great explanation. Your solution works fine! – AirMonk Feb 23 '21 at 13:37