In situations similar-but-different to this, I also have found that the best thing to use for “a callback” is a very short stub routine ... written in baggage-free “C.” All that the callback routine does, really, is to store relevant status information and send some kind of a signal. High-level languages such as Perl usually carry a lot of baggage around with them ... but a callback does not really need to have one.
Mutual-exclusion, of course, is quite a necessity ... but in the callback routine you use the same low-level OS primitives that you know the high-level language will also respect and use. (You aren’t using the language’s constructs, but you make certain that the two pieces of code will interlock as they should.)
I have also learned, again quite painfully, that whenever an API provides for a “handle” (as they, of course, always do...), that handle ought to be a random number that can be safely looked-up in a hash owned by you. In other words, it should never be “a pointer.” Aside from the fact that we are now routinely dealing with 64-bit addresses (and 32-bit legacy integer handles), a pointer can’t be verified ... so it is a “memory stomp” waiting to happen. Random handles, on the other hand, can be looked-up and thereby verified... and if they are no longer present in the hash structure (for whatever reason), we have “a reportable bug-condition” (probably), “but not a stomp.”
The callback logic, then, might go something like this:
Grab a mutual-exclusion object (using a low-level OS call).
Look up the handle that is provided. If it cannot be found, release the mutex and exit silently.
Store the information into the data-structure thus located. This data structure should be a buffer that the high-level language side already carved out for you. Check it for bogosity...
Signal an event that the main logic might be waiting for. Unlock the mutex and exit.