A C function pointer cannot be formed from a local function that captures context

Hi there,
I am sorry if it is not a right place to ask. I try to call a C-style API, CGEventTap to be specific from Swift. And I have defined my EventManager class. The first problem I had was that when my function was a member of the class, I couldn't pass a pointer to it as a callback to the CGEventTap. So I have defined my function inside an initialiser to fix the issue and it worked. However I had to add some more logic to the program and something as simple as an if statement broke it. Here's the code

Logger.eventLogger.info("Przechwyciłem zdarzenie")
			if isMouseLocked {
				return nil
			}
			return Unmanaged.passRetained(event)
		}

If I remove the if statement it works. One question is how to fix it and the other one, more important is WHY this happens?

WHY this happens

C language doesn't have closures - the function cannot use anything that isn't explicitly passed to it as a parameter. That means you cannot use stuff from the outside, like isMouseLocked or even self

how to fix it

the tapCreate function has a userInfo parameter. Put everything you need somewhere behind a pointer, pass that pointer to the tapCreate, and then it will show up as the last parameter to your callback. Then inside the callback you can cast it to the apropriate type, and use it

It is unsafe and annoying, but that's everyday life for C programmers

4 Likes

You might find this answer useful.

1 Like

I actually need o pass one variable to userInfo. Is it necessary to make a separate structure for this bool?

I hope I am not too stupid for it but how does t work then? Does it have something to do with the fact, that parameters are placed on the function’s stack and when I marshal it to C-style callback nothing else can be read? I am fascinated by this.

Closures that capture local context aren't just function pointers (i.e. a memory address pointing to the first instruction of a block of compiled code). They're really more like objects.

Consider this code:

let localVariable1 = 5
let localVariable2 = "Hello"

callAClosure { [localVariable1, localVariable2] in
  print("The local variables are: \(localVariable1), \(localVariable2)")
}

...

func callAClosure(_ closure: () -> Void) {
  closure()
}

The closure passed to callAClosure is sort of like an instance of an anonymous type created on the fly by the compiler with some sort of _call or _invoke method:

protocol Callable {
  func _call()
}

...

let localVariable1 = 5
let localVariable2 = "Hello"

struct ClosureObject_vbxgxgedg: Callable { // Some random compiler-generated unique type name
  let localVariable1: Int
  let localVariable2: String

  func _call() {
    print("The local variables are: \(localVariable1), \(localVariable2)")
  }
}

let closure = ClosureObject_vbxgxgedg(
  localVariable1: localVariable1,
  localVariable2: localVariable2
)

callAClosure(closure)

func callAClosure<C: Callable>(_ closure: C) {
  closure._call()
}

Now imagine all that has to happen for the call to callAClosure to work. ClosureObject_vbxgxgedg is a struct, a value type, it has type info (which includes a function pointer for _call), some size in memory (enough to hold those two capture variables), and it is copied when it is passed around as a function parameter. So when you call callAClosure, the compiled code has to check the size of this struct, allocate enough memory to hold it, and perform a copy (the type itself will supply the copy operation) to initialize this memory. It will also need to deinitialize (destroy) it later. Function pointers for these copy and destroy operations probably live in the type info alongside the function pointer for _call.

If callAClosure stored the closure somewhere, the storage would have to be capable of holding any concrete type that conforms to the Callable protocol. That's some sort of existential box that probably sticks the actual value in a pointer, along with a pointer to the type info structure that can be followed to find the copy and destroy function pointers for that type, and a witness table holding the function pointer for that type's _call function. Copying this existential box involves making a new box, and doing a deep copy of the value pointed to by using the type's copy function (found by following the box's type info pointer).

This is all essential because different closures have different amounts of captured state. You have to be sure you copy all of it (will be different amounts of memory for each closure), and the values being copied may have non-trivial copy operations themselves. Of course you have to copy the captured variables if you store the closure because the variables now have to outlive the local scope they were originally created in.

This is all very different than if the closure were just a function pointer, i.e. UnsafeRawPointer. That's just a simple integer (of whatever size the platform uses for pointers), it's trivially copyable (just copy the bits), and there's no issue of polymorphism (no different "types" of pointers, they're all just pointers and all work identically).

C only understands the latter. A function pointer in C is just a pointer, basically just a specially typed integer holding the address. There's nowhere to fit captured local state into a pointer. The memory is fully occupied by the address of the first instruction, and there's no extra space to stick captured values. Even if there was space, C would have no idea how to correctly copy that extra stuff if it needed to save it somewhere.

Thus, C APIs that use callbacks basically implement a closure-like capability, but the users have to work with it more explicitly. They give you that arbitrary "bag of whatever" to pass along with the function pointer. But you (as usual) need to manage the memory of it (decide when to malloc it and when to free it), and you need to deal with discovering the actual type of that bag and reinterpreting it accordingly (luckily you probably know statically and don't need to implement any kind of runtime polymorphism, which Swift has to do to solve the general case).

Perhaps what's surprising is that you can ever pass Swift "blocks" to C code. If you make sure not to capture anything, then Swift can compile the block to an anonymous global function, and that is just a function pointer.

Note: in the example I explicitly captured the two local variables, because implicit capture is more complicated (it's more like capturing the getters and setters for the local variables, which allows you to mutate those local variables directly).

2 Likes

Thank you for the great reply. I will read and understand.

1 Like