Wrapping C callbacks in Swift closures

So, I'm learning Swift trying to create a wrapper to Glfw

I'm stuck now on the callback part.

Glfw defines, for example, the following:

typedef void (* GLFWerrorfun)(int,const char*);

and I ported that to Swift as:

typealias ErrorFun = (Error, _ description: String) -> Void

where Error is an enum.

On C there is the following call:

GLFWAPI GLFWerrorfun glfwSetErrorCallback(GLFWerrorfun cbfun);

which I'm trying to safely wrapping in Swift as:

    func setErrorCallback(cbFun: ErrorFun) -> GLFWerrorfun? {
        glfwSetErrorCallback { err, desc in
            cbFun(Error(rawValue: err)!, String(utf8String: desc!)!)
        }
    }

but I keep getting:

error: a C function pointer cannot be formed from a closure that captures context

Why? How can I solve? (found a lot online, but I couldnt figure it out)

Where's cbFun declared? If it is a class method, it will implicitly capture self, which doesn't have C equivalent.

If that's the case, try moving cbFun outside of the class.

It's a parameter?

I'm not yet using the method in a real scenario, this is purely throw by having just the wrapper itself

Or did you mean setErrorCallback? Because that is indeed a class method..

Ps: I just tried to move it out, as a top function, but same error..

Oh opps, didn't see the parameter.

I see now. ErrorFun is a Swift function, (IIRC) put @convention(c) in front of the type:

typealias ErrorFun = @convention(c) (Error, _ description: String) -> Void

This is because, as I said earlier, normal Swift function can capture variable, but C function can not.

now I get also the following on top:

error: '(glfw.Error, String) -> Void' is not representable in Objective-C, so it cannot be used with '@convention(c)'

Correct. String does not have C equivalent. It contains a lot more than array of char. Instead, use UnsafePointer<CChar> which is what pointer to char is ported to. You can convert between them using init(utf8String:), and withCString, or their siblings if the string you pass is non-ascii.

typealias ErrorFun = @convention(c) (Error, UnsafePointer<CChar>) -> Void

func setErrorCallback(cbFun: ErrorFun) -> GLFWerrorfun? {
    glfwSetErrorCallback { err, desc in
        cbFun(Error(rawValue: err)!, String(utf8String: desc!)!)
    }
}

If Error doesn't have C equivalent, you'd need to do similar thing, but it seems you're porting that from C, so that should be fine.

Error is an enum, it still complain:

typealias ErrorFun = @convention(c)(Error, _ description: UnsafePointer<CChar>) -> Void

if I use an Int instead it works.. how can it be?

Hmm, if glfw.Error is ported from C, it should be able to do a round-trip. I don't know much about interop either. Guess we'll need to wait for someone more knowledgeable.

Because, in Swift, an enum is not an Int.

No, it's a Swift enum.. then it make sense..

Honestly I wouldn't have expected all these issue with C callbacks

There are a lot of mismatch between Swift and C. Whatever you end up doing, it'll likely reflect that.

  • Swift function is a pointer to function + capture list.
    C function is just a pointer (no capturing).
    • I think there's C lambda that let you capture, but if the in-memory layout doesn't match that of Swift, you'll still need to do conversion anyhow.
  • Swift String is a unicode-correct string that support both utf8 and utf16.
    C string is just an array of ascii characters.
    • There's also C wide char which I don't know what it does.
  • Most (all?) of Swift types engages in reference-counting. C doesn't have one. You'll need to drop-down into unsafe territory since there's nothing telling compiler if what goes into C is still in used.

AFAICT these mismatches is not easily synthesised since there's no one right way to do it.

Should there be some concrete improvements to be had, you can pitch some idea in this forum! (perhaps as a separate thread)

Because the C-function signature is assuming an int as the error parameter, and you're passing a Swift enum.

Hi @elect86, what a coincidence! I have spent some time on this problem last week : )

Have a look of this article: Swift callbacks, I solved my problem through this way(You need to make some minor changes to the code in the article, because of the API change)

And here are some other detailed information about this problem:
Swift and C functions
Unmanaged

Good luck!

1 Like

Hi @andyliu,

nice timing, so I can "exploit" you :stuck_out_tongue:

Based on this SO answer linked as source in your first link, I tried the following:

class glfw {
    func setErrorCallback(cbFun: ErrorFun) -> GLFWerrorfun? {
        // Void pointer to `self`:
        let observer = UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque())

        return glfwSetErrorCallback { err, desc in

                // Extract pointer to `self` from void pointer:
                let mySelf = Unmanaged<glfw>.fromOpaque(observer).takeUnretainedValue()
        }
    }
}

However as soon as I have myself I get the famous:

error: a C function pointer cannot be formed from a closure that captures context

What am I doing wrong?

class glfw {
    func setErrorCallback(cbFun: ErrorFun) -> GLFWerrorfun? {
        let observer = UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque())
        //  ^^^^^^^^ observer is defined here
        return glfwSetErrorCallback { err, desc in
        //                            ^^^^^^^^^ these are the *only* arguments that this C function takes, and observer is neither of them
                let mySelf = Unmanaged<glfw>.fromOpaque(observer).takeUnretainedValue()
        //                                              ^^^^^^^^ observer is used here
        }
    }
}

As you can see, inside the C function you're using something that wasn't passed in as a parameter. C functions cannot do that. It works in swift closures, because as @Lantua said, "swift function is a pointer to a function + capture list", so you can pass observer via the capture list, unlike C function that is "just a pointer (no capturing)".

In the stack overflow example, observer is passed as a parameter

1 Like

Roughly speaking, in that SO example a pointer to self is “tunneled” to the callback function via the observer parameter.

Your glfwSetErrorCallback() function does not have such a “user data parameter” which is passed on to the callback, so there is no (clean) way to get a pointer to self inside the callback. The callback is probably meant to be a global function.

1 Like

@elect86, as the error message said, you can not use any variable outside the closure. But you used observer in your closure.

In this piece of code, the observer is used to pass the class pointer. But you need to pass it to the C function first and then pass it back, then you can use it in the closure. :sweat_smile:

I'll show you a demo which I used in my project. The only difference is that the callback type is () -> Void

The swift code:

func getClassPtr<T: AnyObject>(_ obj: T) -> UnsafeMutableRawPointer {
    return UnsafeMutableRawPointer(Unmanaged.passUnretained(obj).toOpaque())
}

class Timer {
    var callback: (() -> Void)?

    func addCallback(_ closure: @escaping ()->Void) {
        self.callback = closure

        lowLevelAddCallback(getClassPtr(self)) { (classPtr)->Void in
            let mySelf = Unmanaged<Timer>.fromOpaque(classPtr!).takeUnretainedValue()
            //Then you can use mySelf to access class member
            mySelf.callback!()
        }
    }
}

The C code:

typedef struct {
	void *classPtr;
	void (*callback)(void *);
} CallbackWrapper;

CallbackWrapper callbackWrapper;


void lowLevelAddCallback(void *classPtr, void (*function)(void *))
{
	callbackWrapper.classPtr = classPtr;
	callbackWrapper.callback = function;
}

void lowLevelRunCallback()
{
    callbackWrapper.callback(callbackWrapper.classPtr);
}

It's really not easy to understand, I also spend much energy on this : )

1 Like

I knew this was gonna be a blood-bath.. :pensive:

@andyliu is your project open? Can I take a deeper look?

I will put my projects on GitHub in a few days. But I think it's more complicated than the demo I gave :sweat_smile:

@andyliu do you think that strategy could work also for static methods?

Because I'm using a class essentially just as a place holder