How to pass GtkWidget to task?

, ,

Hi. I want to get selected item from GtkDropDown in task run from separate thread, but from what I saw during my debugging attempts it appears that my dropdown is not passed correctly.
How should I pass widgets to tasks so I can operate on them?

GtkWidget *receivedDataView;
GTask *task;
GCancellable *cancellable;
GObject *object;
GtkWidget *itemsDropdown;

int main (int argc, char *argv[]) {
  //starting application
  return startApp("exe.test", G_APPLICATION_DEFAULT_FLAGS, activateApp, argc, argv);
}
int startApp(char *id, GApplicationFlags flags, void (*activateFunc), int argc, char *argv[]) {
    GtkApplication *app;
    int stat;

    app=gtk_application_new(id, flags);
    g_signal_connect(app, "activate", G_CALLBACK(activateFunc), NULL);
    stat=g_application_run(G_APPLICATION(app), argc, argv);
    g_object_unref(app);

    return stat;
}
static void activateApp (GApplication *app) {
  
  ...

  //TASK
  object=g_object_new(G_TYPE_OBJECT, NULL);
  cancellable=g_cancellable_new();
  task=g_task_new(object, cancellable, (void *)cb, itemsDropdown);

  g_task_set_task_data(task, NULL, cbDestroy);
  g_task_run_in_thread(task, (void *)cb);

  g_object_unref(cancellable);
  g_object_unref(object);
  g_object_unref(task);
  //TASK END

  gtk_window_present(GTK_WINDOW(win));
}
void cb(GTask *task, gpointer source_object, gpointer itemsDropdown, GCancellable *cancellable) {
	do {
		static char buf[2]="1";
		GtkWidget *dropdown=itemsDropdown;

		char *selectedItemAsString=g_idle_add_once((void *)getStringFromDropdown, dropdown);
		g_usleep(1);
		static void *data[2];
			  data[0]=receivedDataView;
			  data[1]=selectedItemAsString; //if I put buf here the instruction below works fine
		g_idle_add_once((void *)setText, data);
		g_usleep(1);
	}
	while(!g_cancellable_is_cancelled(cancellable));
}
const char* getStringFromDropdown(GtkWidget *dropdown) {
    GObject *itemFromDropdown=gtk_drop_down_get_selected_item(GTK_DROP_DOWN(dropdown));
    const char *obtainedString=gtk_string_object_get_string(GTK_STRING_OBJECT(itemFromDropdown));

    return obtainedString;
}
void setText(void* data[]) {
    setTextInTextView(data[0], data[1]);
}
void setTextInTextView(GtkWidget *textView, char *text) {
    GtkTextBuffer *infoBuffer=gtk_text_buffer_new(NULL);
    gtk_text_buffer_set_text(GTK_TEXT_BUFFER(infoBuffer), text, strlen(text));
    gtk_text_view_set_buffer(GTK_TEXT_VIEW(textView), infoBuffer);
}
void cbDestroy() {
    //nothing to free(?)
}

Hi,

char *selectedItemAsString=g_idle_add_once((void *)getStringFromDropdown, dropdown);

Can’t work, g_idle_add_once returns a GLib event ID, it doesn’t make any sense to try converting is to a string…
(by the way, this function schedules an asynchronous call, so by definition you can’t use it in a synchronous way and directly get a return from the callback, that would block the tread)

What you can do is:

data[1] = itemsDropdown;

/* setText is called from the mainloop context, so it's safe to query dropdown items from here */
void setText(void* data[]) {
    GObject *itemFromDropdown=gtk_drop_down_get_selected_item(GTK_DROP_DOWN(data[1]));
    const char *obtainedString=gtk_string_object_get_string(GTK_STRING_OBJECT(itemFromDropdown));

    setTextInTextView(data[0], obtainedString);
}
		static void *data[2];
			  data[0]=receivedDataView;
			  data[1]=selectedItemAsString; //if I put buf here the instruction below works fine
		g_idle_add_once((void *)setText, data);

That looks dangerous…data is allocated on the stack, you may read an old value from the callback, or a use-after-free in the last loop when the thread exits.

You should allocate data with malloc() or similar, then free() it from setText after you’re done with it.

What if I get rid of the static keyword? Will it become safer?

Thank you. I’ll try to implement it next week and see if it works. Also I don’t know how I didn’t see that I have been assigning the return value of g_idle_add_once to string variable. It is an obvious mistake.

From a memory allocation point of view, yes, but in case of high load, if new data arrives before the mainloop had time to call setText , then you’ll override the previous data values and process it twice because g_idle_add scheduled the callback twice…

For this kind of things, I personally use queues to send data from one thread to another. I mostly code in Python, so use the builtin module Queue for that. In C, you can probably use GAsyncQueue, but I never tested.

So I made some changes in the code:

//function prototype
void cb(void *passedDropdown);

//global variables
GtkWidget *itemsDropdown;
GtkWidget *receivedDataView;

//main program
static void activateApp (GApplication *app)  {
	
	...
	
  //TASK TEST
	  object=g_object_new(G_TYPE_OBJECT, NULL);
	  cancellable=g_cancellable_new();
	  task=g_task_new(object, cancellable, (void *)cb, itemsDropdown);

	  g_task_set_task_data(task, itemsDropdown, cb);
	  g_task_run_in_thread(task, (void *)cb);

	  g_object_unref(cancellable);
	  g_object_unref(object);
	  g_object_unref(task);
  //TASK TEST END
  
  gtk_window_present(GTK_WINDOW(win));
}
void cb(void *passedDropdown) {
    do {
        void *data[2];
          data[0]=receivedDataView;
          data[1]=itemsDropdown; //replacing 'itemsDropdown' with 'passedDropdown' makes the program crash
        getStringFromDropdownAndSetText(data);
		g_usleep(1);
    } while(!g_cancellable_is_cancelled(cancellable));
}
void getStringFromDropdownAndSetText(void *data[]) {
    GObject *itemFromDropdown=gtk_drop_down_get_selected_item(GTK_DROP_DOWN(data[1]));
    const char *obtainedString=gtk_string_object_get_string(GTK_STRING_OBJECT(itemFromDropdown));

    setTextInTextView(data[0], obtainedString);
}



When program is coded this way it works and string in TextView widget is being set properly, but when I try to set task data in activateApp function and use the passed dropdown in cb function instead of refering to the global variable, the program crashes.
What am I doing wrong?



EDIT I think I found the solution. I changed the cb function once again and used the task’s data (i.e. itemsDropdown) which I did set in activateApp function:

void cb(GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) {
    do {
        void *data[2];
          data[0]=receivedDataView;
          data[1]=task_data;
        //getStringFromDropdownAndSetText(data); //this is NOT a proper way to modify
                                                 //GTK widgets from inside of threads.
                                                 //Use function below instead:
        g_idle_add_once((void *)getStringFromDropdownAndSetText, data);
        g_usleep(1); //if program crashes, try to use delays. They might help.
    } while(!g_cancellable_is_cancelled(cancellable));
}

Have you removed the g_idle_add_once()?

The last code is not safe because a gtk API gtk_drop_down_get_selected_item is called from the thread context.

Good point, I forgot about this detail. I’ll edit my last comment so the code presented there is fixed.

1 Like