{"id":28727441,"url":"https://github.com/rylern/spicegtk-tutorial","last_synced_at":"2025-06-15T14:40:56.834Z","repository":{"id":71612552,"uuid":"401066658","full_name":"Rylern/SpiceGTK-tutorial","owner":"Rylern","description":"Tutorial for creating a Virtual Machine Viewer on Fedora with Spice-GTK","archived":false,"fork":false,"pushed_at":"2024-04-18T09:57:01.000Z","size":12,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-04-18T11:41:27.051Z","etag":null,"topics":["c","fedora","gtk","spice","spice-gtk","tutorial","viewer","vm","vm-viewer"],"latest_commit_sha":null,"homepage":"https://dev.to/rylern/create-a-virtual-machine-viewer-with-spice-gtk-38me","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Rylern.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-08-29T14:46:48.000Z","updated_at":"2024-02-25T13:17:33.000Z","dependencies_parsed_at":"2023-05-12T21:15:17.776Z","dependency_job_id":null,"html_url":"https://github.com/Rylern/SpiceGTK-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Rylern/SpiceGTK-tutorial","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rylern%2FSpiceGTK-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rylern%2FSpiceGTK-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rylern%2FSpiceGTK-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rylern%2FSpiceGTK-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Rylern","download_url":"https://codeload.github.com/Rylern/SpiceGTK-tutorial/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rylern%2FSpiceGTK-tutorial/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259991540,"owners_count":22942653,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","fedora","gtk","spice","spice-gtk","tutorial","viewer","vm","vm-viewer"],"created_at":"2025-06-15T14:40:55.470Z","updated_at":"2025-06-15T14:40:56.811Z","avatar_url":"https://github.com/Rylern.png","language":"C","readme":"# Create a Virtual Machine viewer with Spice-GTK\n\n\n\nOne way to work with Linux virtual machines (VM) is to use [Libvirt](https://libvirt.org/) to manage the VM, and [Spice](https://www.spice-space.org/) to interact with the VM. The [Libvirt API](https://libvirt.org/html/index.html) and the [Libvirt Application Development Guide](https://libvirt.gitlab.io/libvirt-appdev-guide-python/) show how to create an application with Libvirt. Such guide not being present for Spice, I will show in this tutorial how to create a GTK application in C for interacting and controlling a VM.\n\nTo follow this tutorial, you will need:\n\n* The Fedora distribution (it should work on other distro but you will have to change the `dnf` commands).\n* Some basic knowledge in C and GTK.\n\n## 1. Creating a VM\n\nThe first step is to create a VM running a Spice server. I recommend using the [virt-manager](https://virt-manager.org/) software. Plenty of tutorials showing how to install virt-manager and create a VM are available on the Web, so I will not detail this part.\n\nBy default, a Spice server is used when creating a VM with virt-manager, but you can check it by opening the VM, going to the `Details` view and on the `Display Spice` tab. Here, you should choose the `Type` as `Spice server`. If the VM is running, note the port used as we will need it later (by default, it should be 5900).\n\n## 2. Installing dependencies\n\nTo develop this application, we need both the development files of GTK3 (GTK4 is not supported by Spice-GTK) and Spice-GTK.\n\nGTK3 can be installed with:\n\n```shell\nsudo dnf install gtk3-devel\n```\n\nAnd Spice-GTK:\n\n```shell\nsudo dnf builddep spice-gtk\nwget https://www.spice-space.org/download/gtk/spice-gtk-0.39.tar.xz\ntar -xf spice-gtk-0.39.tar.xz\ncd spice-gtk-0.39/\nmeson builddir \u0026\u0026 cd builddir\nmeson compile\nsudo meson install\n```\n\n## 3. Creating the application\n\nLet's now create the application. The final source code is available on [Github](https://github.com/Rylern/SpiceGTK-tutorial).\n\nWe can begin with a basic window and include GTK and Spice-GTK to make sure everything is installed properly:\n\n```c\n#include \u003cgtk/gtk.h\u003e\n#include \u003cspice-client-gtk.h\u003e\n\nvoid activate (GtkApplication *app, gpointer user_data) {\n\tGtkWidget* window = gtk_application_window_new(app);\n\tgtk_window_set_title(GTK_WINDOW(window), \"VM Viewer\");\n\tgtk_window_set_default_size(GTK_WINDOW (window), 800, 600);\n\tgtk_widget_show_all(window);\n}\n\nint main (int argc, char **argv) {\n\tGtkApplication *app;\n\tint status;\n\n\tapp = gtk_application_new(\"org.gtk.example\", G_APPLICATION_FLAGS_NONE);\n\tg_signal_connect(app, \"activate\", G_CALLBACK(activate), NULL);\n\tstatus = g_application_run(G_APPLICATION(app), argc, argv);\n\tg_object_unref(app);\n\n\treturn status;\n}\n```\n\nTo compile, we need both `gtk+-3.0` and `spice-client-gtk-3.0`:\n\n```shell\ngcc main.c `pkg-config --cflags gtk+-3.0 spice-client-gtk-3.0` `pkg-config --libs gtk+-3.0 spice-client-gtk-3.0`\n```\n\nWe can now start using the Spice library. The API reference is available [here](https://www.spice-space.org/spice-gtk.html).\n\nThe first thing to do is to create a Spice session, pass the connection information to it and connecting to the VM:\n\n```c\n#include \u003cgtk/gtk.h\u003e\n#include \u003cspice-client-gtk.h\u003e\n\nSpiceSession* spiceSession;\n\nvoid onClose() {\n    // Close the Spice session\n\tspice_session_disconnect(spiceSession);\n}\n\nvoid activate (GtkApplication *app, gpointer user_data) {\n\tGtkWidget *window = gtk_application_window_new(app);\n\tgtk_window_set_title(GTK_WINDOW(window), \"VM Viewer\");\n\tgtk_window_set_default_size(GTK_WINDOW (window), 800, 600);\n\tg_signal_connect(G_OBJECT(window), \"destroy\", onClose, NULL);\n\tgtk_widget_show_all(window);\n\t\n    // Create a Spice session\n\tspiceSession = spice_session_new();\n    \n    // Pass the URI of the VM to the session\n\tGValue uri = G_VALUE_INIT;\n\tg_value_init(\u0026uri, G_TYPE_STRING);\n\tg_value_set_static_string(\u0026uri, \"spice://localhost?port=5900\");\n\tg_object_set_property(G_OBJECT(spiceSession), \"uri\", \u0026uri);\n\tg_value_unset(\u0026uri);\n    \n    // Connect to the VM\n\tspice_session_connect(spiceSession);\n}\n\nint main (int argc, char **argv) {\n\tGtkApplication *app;\n\tint status;\n\n\tapp = gtk_application_new(\"org.gtk.example\", G_APPLICATION_FLAGS_NONE);\n\tg_signal_connect(app, \"activate\", G_CALLBACK(activate), NULL);\n\tstatus = g_application_run(G_APPLICATION(app), argc, argv);\n\tg_object_unref(app);\n\n\treturn status;\n}\n```\n\nThe URI is of the form `spice://hostname?port=XXXX`. Here, we connect to a local machine but it could also be a distant machine (if the firewall of the distant host permits it). The port can be found by opening the VM with Virt-manager, going to the `Details` view and on the `Display Spice` tab. By default, it should be 5900. It is also possible to access the port with Libvirt. The session must be closed before leaving the application, which is done here by the `onClose()` function connected to the `destroy` signal of the window.\n\nWhen a spice session is connecting, \"channels\" are created. There is one channel for each type of interaction, for example:\n\n* The Main channel, which handles communication initialization (channels list), migrations, mouse modes, multimedia time, and agent communication.\n* The Display channel, which renders the remote display.\n* The USB Redirection channel, which can redirect USB devices from the host to the VM.\n* The Cursor channel, which updates the cursor shape and position.\n* The Inputs channel, which control the server mouse and keyboard.\n* And more described in the [API reference](https://www.spice-space.org/spice-gtk.html).\n\nThe first channel we are going to use is the Main channel, because it can tell us whether the connection to the VM was a success or not. To do this, we have to connect the session to the `channel-new` signal. This signal will emit each time a channel is created. Once the Main channel is created, we have to connect it to the `channel-event` signal. This signal will indicate whether the session successfully connected to the VM.\n\n```c\n#include \u003cgtk/gtk.h\u003e\n#include \u003cspice-client-gtk.h\u003e\n\nGtkWidget *window;\nSpiceSession* spiceSession;\n\nvoid onClose() {\n\tspice_session_disconnect(spiceSession);\n}\n\n// Called when an event happens to the Main channel\nvoid channelEvent(SpiceChannel *channel, SpiceChannelEvent event, gpointer user_data) {\n    // If there is a connection error, we end the application\n\tif (event == SPICE_CHANNEL_ERROR_CONNECT) {\n\t\tprintf(\"SPICE_CHANNEL_ERROR_CONNECT\\n\");\n\t\tg_signal_emit_by_name(window, \"destroy\");\n\t}\n}\n\n// Called when a new channel is created\nvoid newChannel(SpiceSession *session, SpiceChannel *channel, gpointer user_data) {\n   \t// Get the channel type\n\tgint channelType; \n\tg_object_get(channel, \"channel-type\", \u0026channelType, NULL);\n\tprintf(\"%d: %s\\n\", channelType, spice_channel_type_to_string(channelType));\n\t\n    // Type 1 corresponds to the Main channel\n\tif (channelType == 1) {\n        // Connect the Main channel to the channel-event signal\n\t\tg_signal_connect(G_OBJECT(channel), \"channel-event\", G_CALLBACK(channelEvent), NULL);\n\t}\n}\n\nvoid activate (GtkApplication *app, gpointer user_data) {\n\twindow = gtk_application_window_new(app);\n\tgtk_window_set_title(GTK_WINDOW(window), \"VM Viewer\");\n\tgtk_window_set_default_size(GTK_WINDOW (window), 800, 600);\n\tg_signal_connect(G_OBJECT(window), \"destroy\", onClose, NULL);\n\tgtk_widget_show_all(window);\n\t\n\tspiceSession = spice_session_new();\n\t\n    // Connect the session to the channel-new signal\n\tg_signal_connect(G_OBJECT(spiceSession), \"channel-new\", G_CALLBACK(newChannel), NULL);\n\t\n\tGValue uri = G_VALUE_INIT;\n\tg_value_init(\u0026uri, G_TYPE_STRING);\n\tg_value_set_static_string(\u0026uri, \"spice://localhost?port=5900\");\n\tg_object_set_property(G_OBJECT(spiceSession), \"uri\", \u0026uri);\n\tg_value_unset(\u0026uri);\n\tspice_session_connect(spiceSession);\n}\n\nint main (int argc, char **argv) {\n\tGtkApplication *app;\n\tint status;\n\n\tapp = gtk_application_new(\"org.gtk.example\", G_APPLICATION_FLAGS_NONE);\n\tg_signal_connect(app, \"activate\", G_CALLBACK(activate), NULL);\n\tstatus = g_application_run(G_APPLICATION(app), argc, argv);\n\tg_object_unref(app);\n\n\treturn status;\n}\n```\n\nThe `newChannel()` function is called each time a channel is created. We can get the type of the channel (main, display, USB redirection...) by looking at its `channel-type` property. This property is an enum (so a number), but we can get its corresponding string with the `spice_channel_type_to_string()` function. For example, the enum `1` corresponds to the Main channel.\n\nThe `channelEvent()` function is called each time the state of the main channel changes. We use it here to detect any connection error.\n\nWe can now display the screen of the VM. We will use a GTK display widget provided by Spice. It requires the Display channel, so it will be created in the `newChannel()` function:\n\n```c\n#include \u003cgtk/gtk.h\u003e\n#include \u003cspice-client-gtk.h\u003e\n\nGtkWidget *window;\nSpiceSession* spiceSession;\n\nvoid onClose() {\n\tspice_session_disconnect(spiceSession);\n}\n\nvoid channelEvent(SpiceChannel *channel, SpiceChannelEvent event, gpointer user_data) {\n\tif (event == SPICE_CHANNEL_ERROR_CONNECT) {\n\t\tprintf(\"SPICE_CHANNEL_ERROR_CONNECT\\n\");\n\t\tg_signal_emit_by_name(window, \"destroy\");\n\t}\n}\n\nvoid newChannel(SpiceSession *session, SpiceChannel *channel, gpointer user_data) {\n\tgint channelType; \n\tg_object_get(channel, \"channel-type\", \u0026channelType, NULL);\n\tprintf(\"%d: %s\\n\", channelType, spice_channel_type_to_string(channelType));\n\t\n\tif (channelType == 1) {\n\t\tg_signal_connect(G_OBJECT(channel), \"channel-event\", G_CALLBACK(channelEvent), NULL);\n\t}\n    // Type 2 corresponds to the Display channel\n    else if (channelType == 2) {\n        // Get the channel ID of the Display channel\n\t\tgint channelId; \n\t\tg_object_get(channel, \"channel-id\", \u0026channelId, NULL);\n        \n        // Create the display widget\n\t\tSpiceDisplay* spiceDisplay = spice_display_new(session, channelId);\n\n        // Add the display widget to the main window \n\t\tgtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(spiceDisplay));\n\t\tgtk_widget_show_all(window);\n\t}\n}\n\nvoid activate (GtkApplication *app, gpointer user_data) {\n\twindow = gtk_application_window_new(app);\n\tgtk_window_set_title(GTK_WINDOW(window), \"VM Viewer\");\n\tgtk_window_set_default_size(GTK_WINDOW (window), 800, 600);\n\tg_signal_connect(G_OBJECT(window), \"destroy\", onClose, NULL);\n\t\n\tspiceSession = spice_session_new();\n\t\n\tg_signal_connect(G_OBJECT(spiceSession), \"channel-new\", G_CALLBACK(newChannel), NULL);\n\t\n\tGValue uri = G_VALUE_INIT;\n\tg_value_init(\u0026uri, G_TYPE_STRING);\n\tg_value_set_static_string(\u0026uri, \"spice://localhost?port=5900\");\n\tg_object_set_property(G_OBJECT(spiceSession), \"uri\", \u0026uri);\n\tg_value_unset(\u0026uri);\n\tspice_session_connect(spiceSession);\n}\n\nint main (int argc, char **argv) {\n\tGtkApplication *app;\n\tint status;\n\n\tapp = gtk_application_new(\"org.gtk.example\", G_APPLICATION_FLAGS_NONE);\n\tg_signal_connect(app, \"activate\", G_CALLBACK(activate), NULL);\n\tstatus = g_application_run(G_APPLICATION(app), argc, argv);\n\tg_object_unref(app);\n\n\treturn status;\n}\n```\n\nTo create the display widget, we need the `channel-id` property of the Display channel.\n\nThe Inputs channel will automatically redirects the mouse and keyboard to the VM, and the Cursor channel will automatically updates the cursor shape and position, so we have now a fully working viewer.\n\nFinally, I will show how to redirect USB devices to the VM. Spice provides a widget for this, the Spice USB device selection widget. I will put it inside a new window and display it when the display widget is launched:\n\n```c\n#include \u003cgtk/gtk.h\u003e\n#include \u003cspice-client-gtk.h\u003e\n\nGtkWidget *window;\nSpiceSession* spiceSession;\n\nvoid onClose() {\n\tspice_session_disconnect(spiceSession);\n}\n\nvoid channelEvent(SpiceChannel *channel, SpiceChannelEvent event, gpointer user_data) {\n\tif (event == SPICE_CHANNEL_ERROR_CONNECT) {\n\t\tprintf(\"SPICE_CHANNEL_ERROR_CONNECT\\n\");\n\t\tg_signal_emit_by_name(window, \"destroy\");\n\t}\n}\n\nvoid newChannel(SpiceSession *session, SpiceChannel *channel, gpointer user_data) {\n\tgint channelType; \n\tg_object_get(channel, \"channel-type\", \u0026channelType, NULL);\n\tprintf(\"%d: %s\\n\", channelType, spice_channel_type_to_string(channelType));\n\t\n\tif (channelType == 1) {\n\t\tg_signal_connect(G_OBJECT(channel), \"channel-event\", G_CALLBACK(channelEvent), NULL);\n\t} else if (channelType == 2) {\n\t\tgint channelId; \n\t\tg_object_get(channel, \"channel-id\", \u0026channelId, NULL);\n\t\tSpiceDisplay* spiceDisplay = spice_display_new(session, channelId);\n\n\t\tgtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(spiceDisplay));\n\t\tgtk_widget_show_all(window);\n\t\t\n        // Create a window\n\t\tGtkWidget* USBWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);\n\t\tgtk_window_set_title(GTK_WINDOW(USBWindow), \"USB redirection\");\n        \n        // Put the USB widget inside the new window and display it\n\t\tGtkWidget* USBWidget = spice_usb_device_widget_new(session, \"%s %s %s at %d-%d\");\n\t\tgtk_container_add(GTK_CONTAINER(USBWindow), GTK_WIDGET(USBWidget));\n\t\tgtk_widget_show_all(USBWindow);\n\t}\n}\n\nvoid activate (GtkApplication *app, gpointer user_data) {\n\twindow = gtk_application_window_new(app);\n\tgtk_window_set_title(GTK_WINDOW(window), \"VM Viewer\");\n\tgtk_window_set_default_size(GTK_WINDOW (window), 800, 600);\n\tg_signal_connect(G_OBJECT(window), \"destroy\", onClose, NULL);\n\t\n\tspiceSession = spice_session_new();\n\t\n\tg_signal_connect(G_OBJECT(spiceSession), \"channel-new\", G_CALLBACK(newChannel), NULL);\n\t\n\tGValue uri = G_VALUE_INIT;\n\tg_value_init(\u0026uri, G_TYPE_STRING);\n\tg_value_set_static_string(\u0026uri, \"spice://localhost?port=5900\");\n\tg_object_set_property(G_OBJECT(spiceSession), \"uri\", \u0026uri);\n\tg_value_unset(\u0026uri);\n\tspice_session_connect(spiceSession);\n}\n\nint main (int argc, char **argv) {\n\tGtkApplication *app;\n\tint status;\n\n\tapp = gtk_application_new(\"org.gtk.example\", G_APPLICATION_FLAGS_NONE);\n\tg_signal_connect(app, \"activate\", G_CALLBACK(activate), NULL);\n\tstatus = g_application_run(G_APPLICATION(app), argc, argv);\n\tg_object_unref(app);\n\n\treturn status;\n}\n```\n\nThe second argument of the `spice_usb_device_widget_new()` function is a string describing each device inside the Spice USB device selection widget, with the following format:\n\n* First `%s`: manufacturer.\n* Second `%s`: product.\n* Third `%s`: descriptor (a [vendor_id:product_id] string).\n* First `%d`: bus.\n* Second `%d`: address.\n\n## 4. Conclusion\n\nYou can now create a simple VM viewer with Spice and GTK. More advanced options are described in the [API reference](https://www.spice-space.org/spice-gtk.html). \n\nTo create an application able to fully manage and interact with virtual machines, I recommend using the [Libvirt API](https://libvirt.org/html/index.html) along with Spice-GTK.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frylern%2Fspicegtk-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frylern%2Fspicegtk-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frylern%2Fspicegtk-tutorial/lists"}