Search code examples
macosqtpastenspasteboardcfrunloop

In Macos, how to Move pasteboard `promiseKeeper` Function to a Specific Child Thread to Avoid Main Thread Blocking?


Questions: I am currently using the QT 15.15.2

  1. I am currently using the <ApplicationServices/ApplicationServices.h> library. Based on my understanding:

    • I can use PasteboardSetPromiseKeeper to set a promise keeper function for clipboard handling (which I believe is a callback function for rendering clipboard data).
    • I can use PasteboardPutItemFlavor to place empty data.
    • When the user presses Ctrl+V, it should trigger the PromiseKeeper event.
  2. However, I am currently facing an issue where the PromiseKeeper function is running on my main thread, causing my UI to freeze and certain control flows to hang.

  3. I have already created a subthread using QThread, and both PasteboardCreate and PasteboardSetPromiseKeeper are running within the subthread.

Partial pseudocode

// in child thread context
OSStatus err = PasteboardCreate(kPasteboardClipboard, &m_pasteboard);
err = PasteboardSetPromiseKeeper(m_pasteboard, promiseKeeper, this);

OSStatus MacClipboard::promiseKeeper(PasteboardRef pasteboard, PasteboardItemID pasteboardItemID, CFStringRef uti, void *_macClipboard)
{
    qDebug() << "promiseKeeper Thread id=" << QThread::currentThreadId();
    
    // In this case, I need to access the data on the network in another thread.

    macClipboard->_promisMutex.lock();
    macClipboard->_createPromise(pasteboardItemID, formatName);
    bool timeouted = !(macClipboard->_promisWaitCondition.wait(&macClipboard->_promisMutex, 5000));
    macClipboard->_promisMutex.unlock();

    const CFDataRef cfData = CFDataCreate(nullptr, (UInt8*)macClipboard->promiseData()->constData(), macClipboard->promiseData()->size());
    resultStatus = PasteboardPutItemFlavor(pasteboard, pasteboardItemID, uti, cfData, kPasteboardFlavorNoFlags);

    return noErr;
}

  1. I have already tried nesting multiple layers of threads, but it had no effect. The promiseKeeper function still runs on the main thread.

  2. I suspect it may be related to the event loop of the macOS system. After referring to the QT source code, it seems that I need to create a "run loop context" in the context of the subthread. However, I'm not very clear on its usage, so I haven't tried it yet.

Did I misunderstand? I don't need to block promiseKeeper? Then how can I achieve the goal of retrieving data from the network and placing it into the clipboard when the user presses CTRL+V?


Solution

  • It's very rare for Apple APIs to offer you a choice of which thread to get called on. In most cases, you either always get called on the main thread, or (particularly for asynchronous APIs with a completion callback) you might get called back on whatever thread you started from.

    And sometimes (as with Pasteboard Manager) this isn't documented and you are not really guaranteed any particular choice of thread—the Pasteboard Manager does not guarantee it will call you on the main thread, so don't rely on that, but neither does it guarantee that it won't call you on the main thread, so don't rely on that, either.

    Basically, you have to assume that this callback will block the main thread. Moreover, there is no way to get Pasteboard Manager to call you on a different thread, nor is there any way to say “call me back later”, nor “I've started getting that ready and will let you know when it is”.

    (Part of this is an artifact of how pasting works in the receiving application: the receiving application simply tells the pasteboard API—whichever one it's using; there are two, down from three in the 32-bit era—to give it whatever's on the pasteboard. That's a synchronous API; it assumes the data is already available, and if it isn't, blocks until it is, even if that means going through a promise keeper, pasteboard filter, or both. So yeah, it's normal for pasting to hang in these sorts of situations—just ask anyone who's copied something from their iPad and pasted it on their Mac.)


    Pasteboard promises, particularly to modern eyes, might look like they solve two different needs:

    • the need to promise something that would be expensive/slow to create or retrieve, in the hope that the promise might never get called in
    • the need to asynchronously begin producing or procuring data when a promise is called in, and deliver the data when it is ready

    But that's not correct. Pasteboard promises are really designed for the former only. You offer a promise in the hope that it won't get called, but if it is called, you're expected to service the request synchronously—blocking the whole time.

    (The normal use case is providing files, particularly for drag-and-drop; that isn't free, but, especially on a modern system, it's not nearly as slow as a network access. Pasting things from the network is not unheard of—see the above reference to Universal Clipboard—but it is relatively unusual and certainly not what this API is designed for.)

    What I would recommend is to make the promise, then start your threaded download eagerly (that is, right when you make the promise), in the hope that if and when your promise keeper gets called, the download will have finished, or at least be closer to the finish line.

    This implies, of course, that you'll need some way to expire and release/delete the downloaded data if the promise never gets called. What that looks like will depend on where you're downloading it to (memory vs. storage), how big it is, how time-sensitive it is, etc. Those are questions you'll need to answer for yourself.

    It also suggests that you might want to consider a caching system so that things only need to be downloaded once unless expired.

    You might even consider building an asset library UI of some sort that makes explicit the division between “things that need to be downloaded” and “things that can be pasted”: a user would go to your asset library and download something, and then be able to copy and paste it. I generally prefer to hide these sorts of implementation artifacts, but in this case, it's better than hanging.


    TL;DR: Start the download when you create the promise. If and when your promise gets called, hopefully you're either ready to deliver on it or at least won't have to block as long. Blocking is, unfortunately, expected in this case.