Hi On Tue, Aug 5, 2025 at 5:33 PM startergo via <qemu-devel@nongnu.org> wrote: > > Implement bidirectional clipboard integration between QEMU and host > system when using the SDL display backend. This allows seamless > copy-paste operations between the guest and host environments. > > Features: > - Bidirectional clipboard sync (guest ↔ host) > - Async clipboard request handling to prevent blocking > - Self-update detection to avoid clipboard manager conflicts > - Configurable via --enable-sdl-clipboard build option > - Text-only clipboard support (following existing QEMU patterns) > > The implementation follows the same patterns used by the existing > GTK and VNC clipboard implementations, integrating with QEMU's > clipboard subsystem through QemuClipboardPeer. > > Tested on macOS with successful build and runtime clipboard > functionality verification. > > Co-authored-by: Kamay Xutax <ad...@xutaxkamay.com> > Signed-off-by: startergo <starte...@protonmail.com>
Thanks for sending a patch that can be applied with git am ! Next time, make sure it passes checkpatch too: https://www.qemu.org/docs/master/devel/submitting-a-patch.html#writing-your-patches > --- > include/ui/sdl2.h | 12 ++++ > meson.build | 3 + > meson_options.txt | 2 + > ui/meson.build | 3 + > ui/sdl2-clipboard.c | 154 ++++++++++++++++++++++++++++++++++++++++++++ > ui/sdl2.c | 9 +++ > 6 files changed, 183 insertions(+) > create mode 100644 ui/sdl2-clipboard.c > > diff --git a/include/ui/sdl2.h b/include/ui/sdl2.h > index dbe6e3d973..0cadbe8c1c 100644 > --- a/include/ui/sdl2.h > +++ b/include/ui/sdl2.h > @@ -21,6 +21,10 @@ > # include <SDL_image.h> > #endif > > +#ifdef CONFIG_SDL_CLIPBOARD > +#include "ui/clipboard.h" > +#endif > + > #include "ui/kbd-state.h" > #ifdef CONFIG_OPENGL > # include "ui/egl-helpers.h" > @@ -45,6 +49,9 @@ struct sdl2_console { > bool gui_keysym; > SDL_GLContext winctx; > QKbdState *kbd; > +#ifdef CONFIG_SDL_CLIPBOARD > + QemuClipboardPeer cbpeer; > +#endif > #ifdef CONFIG_OPENGL > QemuGLShader *gls; > egl_fb guest_fb; > @@ -97,4 +104,9 @@ void sdl2_gl_scanout_texture(DisplayChangeListener *dcl, > void sdl2_gl_scanout_flush(DisplayChangeListener *dcl, > uint32_t x, uint32_t y, uint32_t w, uint32_t h); > > +#ifdef CONFIG_SDL_CLIPBOARD > +void sdl2_clipboard_init(struct sdl2_console *scon); > +void sdl2_clipboard_handle_request(struct sdl2_console *scon); > +#endif > + > #endif /* SDL2_H */ > diff --git a/meson.build b/meson.build > index 41f68d3806..4a37df9669 100644 > --- a/meson.build > +++ b/meson.build > @@ -1596,6 +1596,8 @@ else > sdl_image = not_found > endif > > +have_sdl_clipboard = sdl.found() and get_option('sdl_clipboard') you should handle the option the same way as gtk_clipboard: fail if requested but sdl not enabled, make it a feature, disabled by default ? etc > + > rbd = not_found > if not get_option('rbd').auto() or have_block > librados = cc.find_library('rados', required: get_option('rbd')) > @@ -2511,6 +2513,7 @@ config_host_data.set('CONFIG_RELOCATABLE', > get_option('relocatable')) > config_host_data.set('CONFIG_SAFESTACK', get_option('safe_stack')) > config_host_data.set('CONFIG_SDL', sdl.found()) > config_host_data.set('CONFIG_SDL_IMAGE', sdl_image.found()) > +config_host_data.set('CONFIG_SDL_CLIPBOARD', have_sdl_clipboard) > config_host_data.set('CONFIG_SECCOMP', seccomp.found()) > if seccomp.found() > config_host_data.set('CONFIG_SECCOMP_SYSRAWRC', seccomp_has_sysrawrc) > diff --git a/meson_options.txt b/meson_options.txt > index 59d973bca0..be2cba3a30 100644 > --- a/meson_options.txt > +++ b/meson_options.txt > @@ -212,6 +212,8 @@ option('sdl', type : 'feature', value : 'auto', > description: 'SDL user interface') > option('sdl_image', type : 'feature', value : 'auto', > description: 'SDL Image support for icons') > +option('sdl_clipboard', type : 'boolean', value : true, > + description: 'SDL clipboard support') > option('seccomp', type : 'feature', value : 'auto', > description: 'seccomp support') > option('smartcard', type : 'feature', value : 'auto', > diff --git a/ui/meson.build b/ui/meson.build > index 35fb04cadf..6d1bf3477e 100644 > --- a/ui/meson.build > +++ b/ui/meson.build > @@ -126,6 +126,9 @@ if sdl.found() > 'sdl2-input.c', > 'sdl2.c', > )) > + if have_sdl_clipboard > + sdl_ss.add(files('sdl2-clipboard.c')) > + endif > sdl_ss.add(when: opengl, if_true: files('sdl2-gl.c')) > sdl_ss.add(when: x11, if_true: files('x_keymap.c')) > ui_modules += {'sdl' : sdl_ss} > diff --git a/ui/sdl2-clipboard.c b/ui/sdl2-clipboard.c > new file mode 100644 > index 0000000000..e50ff11d5a > --- /dev/null > +++ b/ui/sdl2-clipboard.c > @@ -0,0 +1,154 @@ > +/* > + * SDL UI -- clipboard support (improved async version) > + * drop the "(improved async version)" > + * Copyright (C) 2023 Kamay Xutax <ad...@xutaxkamay.com> > + * Copyright (C) 2025 startergo <starte...@protonmail.com> > + * > + * SPDX-License-Identifier: GPL-2.0-or-later > + */ > + > +#include "qemu/osdep.h" > +#include "ui/console.h" > +#include "ui/clipboard.h" > +#include "ui/sdl2.h" > +#include "qemu/log.h" > + > +#ifdef CONFIG_SDL_CLIPBOARD this condition is unnecessary if the unit is already filtered out by meson > + > +/* Track pending clipboard requests to handle async data */ > +typedef struct { > + struct sdl2_console *scon; > + QemuClipboardInfo *info; > + QemuClipboardType type; > +} SDLClipboardRequest; > + > +static SDLClipboardRequest *pending_request = NULL; > + > +static void sdl2_clipboard_clear_pending(void) > +{ > + if (pending_request) { > + if (pending_request->info) { > + qemu_clipboard_info_unref(pending_request->info); > + } > + g_free(pending_request); > + pending_request = NULL; or g_clear_pointer(&pending_request, g_free) > + } > +} > + > +static void sdl2_clipboard_notify(Notifier *notifier, void *data) > +{ > + QemuClipboardNotify *notify = data; > + struct sdl2_console *scon = > + container_of(notifier, struct sdl2_console, cbpeer.notifier); > + bool self_update = notify->info->owner == &scon->cbpeer; > + const char *text_data; > + size_t text_size; > + > + switch (notify->type) { > + case QEMU_CLIPBOARD_UPDATE_INFO: > + { > + /* Skip self-updates to avoid clipboard manager conflicts */ > + if (self_update) { > + return; > + } > + > + if (!notify->info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) { > + return; > + } > + > + /* Check if this is completion of our pending request */ > + if (pending_request && pending_request->info == notify->info && > + pending_request->type == QEMU_CLIPBOARD_TYPE_TEXT) { > + sdl2_clipboard_clear_pending(); > + } > + > + /* Check if data is available, request asynchronously if not */ > + if (!notify->info->types[QEMU_CLIPBOARD_TYPE_TEXT].data) { > + if (!pending_request) { > + pending_request = g_new0(SDLClipboardRequest, 1); > + pending_request->scon = scon; > + pending_request->info = > qemu_clipboard_info_ref(notify->info); > + pending_request->type = QEMU_CLIPBOARD_TYPE_TEXT; > + qemu_clipboard_request(notify->info, > QEMU_CLIPBOARD_TYPE_TEXT); There is a risk of requesting clipboard text, getting nothing back, and going in a loop. It should handle that. > + } > + return; > + } > + > + /* Process available data */ > + text_size = notify->info->types[QEMU_CLIPBOARD_TYPE_TEXT].size; > + if (text_size == 0) { > + return; > + } > + > + text_data = (const char > *)notify->info->types[QEMU_CLIPBOARD_TYPE_TEXT].data; > + > + /* Ensure null termination for SDL clipboard */ > + g_autofree char *text = g_strndup(text_data, text_size); > + if (text && text[0] != '\0') { > + SDL_SetClipboardText(text); > + } else if (!text) { At this point text_data != NULL and text_size != 0, in this case g_strndup() will never return NULL. If OOM, the process will abort() before that. > + qemu_log_mask(LOG_GUEST_ERROR, > + "SDL clipboard: Failed to allocate memory for > clipboard text\n"); you will drop this then > + } > + break; > + } > + case QEMU_CLIPBOARD_RESET_SERIAL: > + sdl2_clipboard_clear_pending(); you can ignore this, just like gtk-clipboard > + break; > + } > +} > + > +static void sdl2_clipboard_request(QemuClipboardInfo *info, > + QemuClipboardType type) > +{ > + g_autofree char *text = NULL; > + > + if (type != QEMU_CLIPBOARD_TYPE_TEXT) { > + return; > + } > + > + text = SDL_GetClipboardText(); > + if (!text) { SDL_GetClipboardText doc says: Returns the clipboard text on success or an empty string on failure..Caller must call SDL_free() on the returned pointer when done with it (even if there was an error). if (!text || !text[0]) > + qemu_log_mask(LOG_GUEST_ERROR, > + "SDL clipboard: Failed to get clipboard text: %s\n", > + SDL_GetError()); use warn_report() && call SDL_free(text) > + return; > + } > + > + qemu_clipboard_set_data(info->owner, info, type, > + strlen(text), text, true); > +} > + > +void sdl2_clipboard_init(struct sdl2_console *scon) > +{ > + scon->cbpeer.name = "sdl2-clipboard"; > + scon->cbpeer.notifier.notify = sdl2_clipboard_notify; > + scon->cbpeer.request = sdl2_clipboard_request; > + > + qemu_clipboard_peer_register(&scon->cbpeer); > +} > + > +void sdl2_clipboard_handle_request(struct sdl2_console *scon) The function should be renamed, see below. > +{ > + g_autofree char *text = NULL; > + QemuClipboardInfo *info; > + > + text = SDL_GetClipboardText(); instead of requesting the content immediately here, we should wait for an actual guest/peer request. This will remove the duplication of code to actually get the content. > + if (!text) { > + qemu_log_mask(LOG_GUEST_ERROR, > + "SDL clipboard: Failed to get clipboard text: %s\n", > + SDL_GetError()); > + return; > + } > + > + if (text[0] == '\0') { > + return; /* Ignore empty clipboard */ > + } > + > + info = qemu_clipboard_info_new(&scon->cbpeer, > QEMU_CLIPBOARD_SELECTION_CLIPBOARD); > + qemu_clipboard_set_data(&scon->cbpeer, info, QEMU_CLIPBOARD_TYPE_TEXT, > + strlen(text), text, true); This will simply be qemu_clipboard_update(info) > + qemu_clipboard_info_unref(info); > +} > + > +#endif /* CONFIG_SDL_CLIPBOARD */ > diff --git a/ui/sdl2.c b/ui/sdl2.c > index cda4293a53..00a17b68a7 100644 > --- a/ui/sdl2.c > +++ b/ui/sdl2.c > @@ -691,6 +691,11 @@ void sdl2_poll_events(struct sdl2_console *scon) > case SDL_WINDOWEVENT: > handle_windowevent(ev); > break; > +#ifdef CONFIG_SDL_CLIPBOARD > + case SDL_CLIPBOARDUPDATE: > + sdl2_clipboard_handle_request(scon); It's not a request, it's an update. Please rename the function accordingly. > + break; > +#endif > default: > break; > } > @@ -901,6 +906,10 @@ static void sdl2_display_init(DisplayState *ds, > DisplayOptions *o) > } > register_displaychangelistener(&sdl2_console[i].dcl); > > +#ifdef CONFIG_SDL_CLIPBOARD > + sdl2_clipboard_init(&sdl2_console[i]); > +#endif > + > #if defined(SDL_VIDEO_DRIVER_WINDOWS) || defined(SDL_VIDEO_DRIVER_X11) > if (SDL_GetWindowWMInfo(sdl2_console[i].real_window, &info)) { > #if defined(SDL_VIDEO_DRIVER_WINDOWS) > -- > 2.50.1 > > -- Marc-André Lureau