This is an old question but I thought I'd go ahead and add a better way of doing this:
Firstly, the problem with Bertrand's Method 1 & 2 is that it's not advisable to do long running threads on the UI thread, even if one calls gtk_main_iteration()
to service pending events as can be seen one is then roped into corner cases involving close and delete events, and further it can possibly cause stack overflow if one does this from too many widgets which all do it with long running work to do, and it just seems like a fragile band-aid solution to be calling gtk_events_pending()
and gtk_main_iteration()
, it may work for shorter operations where one needs to keep the UI alive while doing something real quick, but for like a long running network operation this doesn't seem like a good design pattern, it'd be much better to put that into it's own thread separate from the UI completely.
Now, if one wishes to update the UI from such a long running thread, such as doing several network transfers and reporting the status then one could use pipes for interthread communication. The problem with using mutexes like in Bertrand's method 3 is that acquiring the lock can be slow and can block if the long running thread already has the lock acquired, especially the way Bertrand loops back to debutLoop
, this causes the UI thread to stall waiting for the compute thread, which is unacceptable.
However, using pipes, one can communicate with the UI thread in a non-blocking manner.
Essentially one creates a non-blocking pipe from a FIFO file at the beginning of the program and then one can use gdk_threads_add_idle
to create a sentinel thread for receiving messages from a thread in the background, this sentinel function can even exist the entire lifetime of the program if for instance one has timer threads which check a URL often to update a UI element with it's result from an HTTP transaction.
For example:
/* MyRealTimeIP, an example of GTK UI update thread interthread communication
*
* Copyright (C) 2023 Michael Motes
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#include <gtk/gtk.h>
#include <fcntl.h>
#include <pthread.h>
#include <curl/curl.h>
#include <sys/stat.h>
#include <stdlib.h>
#define APP_NAME "MyRealTimeIP"
#define MY_IP_LABEL "My IP: "
#define UPDATE_TIME_LABEL "Updated at: "
#define FIFO_FILE "/tmp/" APP_NAME "_Pipe"
#define IP_CHECK_URL "https://domains.google.com/checkip"
#define NETWORK_ERROR_TIMEOUT 1 //one second cooldown in case of curl error
#define PIPE_TIMEOUT 50000000 //50ms timeout between pipe checks to lower CPU usage
#define IP_CHECK_URL "https://domains.google.com/checkip"
#define UNEXPECTED_ERROR (-1)
#define MEMORY_ERROR "\nMemory allocation failed.\n"
#define MEMCHECK(x) \
if((x)== NULL){ \
fprintf(stderr, MEMORY_ERROR); \
exit(UNEXPECTED_ERROR); \
}
#define TIME_FMT_LEN 45
#define CURRENT_TIME_STR(timeStr){ \
struct timespec rt_clock = {}; \
clock_gettime(CLOCK_REALTIME,&rt_clock); \
time_t raw_time; \
struct tm *time_info; \
time(&raw_time); \
time_info = localtime(&raw_time); \
/*If this is ever not true it means the
hour changed between clock_gettime call
and localtime call, so I update the values
unless it would roll back the day, in that case
I just roll forward nanoseconds to 0.*/ \
if(time_info->tm_hour - (daylight ? 1 : 0) \
+ timezone/3600 != \
(int)((rt_clock.tv_sec / 3600)\
% 24))\
{ \
if(time_info->tm_hour == 0) { \
rt_clock.tv_nsec = 0; \
}else{ \
time_info->tm_hour = \
(int)((rt_clock.tv_sec / 3600)\
% 24);\
time_info->tm_sec = \
(int)(rt_clock.tv_sec % 60);\
time_info->tm_min = \
(int)((rt_clock.tv_sec / 60)\
% 60);\
} \
} else { \
time_info->tm_sec = \
(int)(rt_clock.tv_sec % 60); \
time_info->tm_min = \
(int)((rt_clock.tv_sec / 60) \
% 60); \
} \
\
timeStr = malloc(TIME_FMT_LEN); \
snprintf(timeStr,TIME_FMT_LEN, \
"%04d-%02d-%02d %02d:%02d:%02d.%03d", \
time_info->tm_year + 1900, \
time_info->tm_mon + 1, \
time_info->tm_mday, \
time_info->tm_hour, \
time_info->tm_min, \
time_info->tm_sec, \
(int)(rt_clock.tv_nsec/1000000)); \
}
#pragma region IO_Macros
#define READ_BUF_SET_BYTES(fd, buffer, numb, bytesRead){ \
ssize_t rb = bytesRead; \
ssize_t nb; \
while (rb < numb) { \
nb = read(fd,(char*)&buffer + rb,numb - rb); \
if(nb<=0) \
break; \
rb += nb; \
} \
bytesRead = rb; \
}
#define READ_BUF(fd, buffer, numb) { \
ssize_t bytesRead = 0; \
READ_BUF_SET_BYTES(fd, buffer, numb, bytesRead)\
}
#define WRITE_BUF(fd, buf, sz){ \
size_t nb = 0; \
size_t wb = 0; \
while (nb < sz){ \
wb = write(fd, &buf + nb, sz-nb); \
if(wb == EOF) break; \
nb += wb; \
} \
}
#pragma endregion
GtkWidget *my_IP_Label;
GtkWidget *updatedTimeLabel;
static int interthread_pipe;
enum pipeCmd {
SET_IP_LABEL,
SET_UPDATED_TIME_LABEL,
IDLE
};
typedef struct {
size_t size;
char *str;
} curl_ret_data;
static void fifo_write(enum pipeCmd newUIcmd);
static void fifo_write_ip(char *newIP_Str);
static void fifo_write_update_time(char *newUpdateTimeStr);
static gboolean ui_update_thread(gpointer unused);
static void *ui_update_restart_thread(void *);
static size_t curl_write_data(void *in, size_t size, size_t nmemb, curl_ret_data *data_out);
static void *checkIP_thread(void *);
int main(int argc, char *argv[]) {
mkfifo(FIFO_FILE, 0777);
interthread_pipe = open(FIFO_FILE, O_RDWR | O_NONBLOCK);
gtk_init(&argc, &argv);
GtkWidget *appWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW (appWindow), APP_NAME);
gtk_widget_set_size_request(appWindow, 333, 206);
GtkBox *vbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
my_IP_Label = gtk_label_new(MY_IP_LABEL "Not updated yet.");
updatedTimeLabel = gtk_label_new(UPDATE_TIME_LABEL "Not updated yet.");
gtk_box_pack_start(vbox, my_IP_Label, TRUE, FALSE, 0);
gtk_box_pack_start(vbox, updatedTimeLabel, TRUE, FALSE, 0);
gtk_container_add(GTK_CONTAINER (appWindow), GTK_WIDGET(vbox));
gtk_widget_show_all(appWindow);
g_signal_connect (G_OBJECT(appWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
pthread_t checkIP_thread_pid;
if (pthread_create(&checkIP_thread_pid, NULL, &checkIP_thread, NULL) != 0)
return UNEXPECTED_ERROR;
gdk_threads_add_idle(ui_update_thread, NULL);
gtk_main();
pthread_cancel(checkIP_thread_pid);
pthread_join(checkIP_thread_pid, NULL);
return 0;
}
size_t curl_write_data(void *in, size_t size, size_t nmemb, curl_ret_data *data_out) {
size_t index = data_out->size;
size_t n = (size * nmemb);
char *temp;
data_out->size += (size * nmemb);
temp = realloc(data_out->str, data_out->size + 1);
MEMCHECK(temp)
data_out->str = temp;
memcpy((data_out->str + index), in, n);
data_out->str[data_out->size] = '\0';
return size * nmemb;
}
_Noreturn void *checkIP_thread(void *unused) {
sleep(2); //not needed, just for example purposes to show initial screen first
while (1) {
CURL *curl;
CURLcode res;
curl_ret_data data = {};
while (data.str == NULL) {
curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, IP_CHECK_URL);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_data);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
res = curl_easy_perform(curl);
if (res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
if (data.str != NULL) {
free(data.str);
data.str = NULL;
}
sleep(NETWORK_ERROR_TIMEOUT);
}
curl_easy_cleanup(curl);
}
}
int newIP_StrSz = strlen(MY_IP_LABEL) + data.size + 1;
char *newIP_Str = calloc(1, newIP_StrSz);
snprintf(newIP_Str, newIP_StrSz, MY_IP_LABEL " %s", data.str);
fifo_write_ip(newIP_Str);
char *timeStr;
CURRENT_TIME_STR(timeStr)
int newUpdateTimeStrSz = strlen(UPDATE_TIME_LABEL) + TIME_FMT_LEN + 1;
char *newUpdateTimeStr = calloc(1, newUpdateTimeStrSz);
snprintf(newUpdateTimeStr, newUpdateTimeStrSz, UPDATE_TIME_LABEL " %s", timeStr);
free(timeStr);
fifo_write_update_time(newUpdateTimeStr);
sleep(5);
}
}
static void fifo_write(enum pipeCmd newUIcmd) {
WRITE_BUF(interthread_pipe, newUIcmd, sizeof(newUIcmd))
}
static void fifo_write_ip(char *newIP_Str) {
fifo_write(SET_IP_LABEL);
WRITE_BUF(interthread_pipe, newIP_Str, sizeof(newIP_Str))
}
static void fifo_write_update_time(char *newUpdateTimeStr) {
fifo_write(SET_UPDATED_TIME_LABEL);
WRITE_BUF(interthread_pipe, newUpdateTimeStr, sizeof(newUpdateTimeStr))
}
gboolean ui_update_thread(gpointer unused) {
enum pipeCmd pipeBuffer = IDLE;
READ_BUF(interthread_pipe, pipeBuffer, sizeof(pipeBuffer))
switch (pipeBuffer) {
case SET_IP_LABEL: {
char *newIP_Str = NULL;
int bytesRead = 0;
while (bytesRead != sizeof(newIP_Str)) {
READ_BUF_SET_BYTES(interthread_pipe, newIP_Str, sizeof(newIP_Str) - bytesRead, bytesRead)
}
gtk_label_set_text(GTK_LABEL(my_IP_Label), newIP_Str);
free(newIP_Str);
break;
}
case SET_UPDATED_TIME_LABEL: {
char *newUpdateTimeStr = NULL;
int bytesRead = 0;
while (bytesRead != sizeof(newUpdateTimeStr)) {
READ_BUF_SET_BYTES(interthread_pipe, newUpdateTimeStr, sizeof(newUpdateTimeStr) - bytesRead, bytesRead)
}
gtk_label_set_text(GTK_LABEL(updatedTimeLabel), newUpdateTimeStr);
free(newUpdateTimeStr);
break;
}
case IDLE:
break;
}
//Return false to detach update ui thread, reattach it after a timeout so CPU doesn't spin unnecessarily.
pthread_t _unused;
if (pthread_create(&_unused, NULL, ui_update_restart_thread, NULL))
exit(UNEXPECTED_ERROR);
return FALSE;
}
static void *ui_update_restart_thread(void *unused) {
struct timespec delay = {0, PIPE_TIMEOUT};
nanosleep(&delay, NULL);
gdk_threads_add_idle(ui_update_thread, NULL);
return NULL;
}