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

Reply via email to