Skip to content

Threading contract ​

Which API may be called from which thread, and what the host guarantees.


APIAllowed threadHost guarantee
initialize / shutdown / action_*Any (often a worker thread)The main thread is not assumed; initialize of an external plugin runs on a background thread
Hotkey APIs such as register_plugin_hotkeyAnySwitches to the main thread internally before RegisterHotKey
feed_direct_caption / feed_captionAnyQueue / signal into the UI
translate / translate_batch / speak / stop_speakAnySwitches to a worker thread internally
emit_event / handle_host_eventAnyThread-safe
subscribe_event / unsubscribe_*AnyLocked internally
show_toast / show_error / set_window_excluded_from_captureAnySignal into the UI
Overlay APIs such as create_plugin_windowAnySwitches to the GUI thread internally and waits for the result
pick_file / pick_files / pick_folder / pick_save_fileAnySwitches to the GUI thread internally, and blocks until the user closes the dialog
The begin_progress familyAnyLocked; the pluginProgress signal goes into the UI
Config / KV / data directoryAnyThe host serialises writes
QML invokePluginActionMain threadSynchronous; use invokePluginActionAsync or run_in_background for time-consuming actions

Hard prohibitions ​

  • ❌ A plugin calling ctypes.RegisterHotKey itself — binding it to the wrong thread makes it fail silently. Always go through the host API.
  • ❌ Doing time-consuming network or inference work synchronously inside initialize / initialize (it slows down and can even block the caller); use self.run_in_background(...).
  • ❌ Calling a long-running plugin action synchronously in QML anywhere outside Component.onCompleted.

Always put time-consuming work in the background and send the result back to the UI with emit_event:

python
def action_ask(self, prompt="", **kwargs):
    self.run_in_background(self._do_work, prompt)

def _do_work(self, prompt):
    try:
        result = self._call_api(prompt)          # slow
    except Exception as exc:
        self.host.show_error(f"请求失败:{exc}", source=self.id)
        self.emit_event("stream_done", {"full_text": "", "error": str(exc)})
        return
    self.emit_event("stream_done", {"full_text": result})

Local-service plugins have one more point to watch: ensure_loaded() is allowed to block (the host already calls it from a background thread), but initialize() / read-only methods are not — see Local-service engine contract.


TransBox — empowering borderless cross-language communication for everyone