jhass / crystal-gobject

gobject-introspection for Crystal
BSD 3-Clause "New" or "Revised" License
127 stars 13 forks source link

Using GAction with Gtk Application #26

Closed thiagojedi closed 4 years ago

thiagojedi commented 4 years ago

Hey!

I'm trying to use this lib to create a simple Gtk application, but I'm having issues to setup the menu.

Here's the gist: https://gist.github.com/thiagoabreu/39858db3345904249505a41ccf408bd8

The problem I have is with this line:

action_entries = [
      LibGio::ActionEntry.new(name: "quit", activate: ->{ puts "quit" }), # works
      LibGio::ActionEntry.new(name: "quit", activate: ->{ self.quit }), # doesn't work
    ]
    add_action_entries action_entries, action_entries.size, nil

The second entry raises an Error: Error: can't set closure as C struct member

Is there something I'm doing wrong?

jhass commented 4 years ago

Yeah, you have to jump through some hoops to be able to pass closured data.

A block comes in two forms in Crystal, closured (referencing stack elements from its definition context) and unclosured. Your first example is unclosured and thus just a static function pointer. The second example is closured and has two pointers, the function pointer and a pointer to a blob that basically contains the data it references. Now passing that to C you can only pass one pointer, the function one, so it would break once you actually invoke that function pointer given it doesn't get invoked with the second pointer for the closure data. Crystal prevents you from doing this mistake by refusing to just strip the closure data pointer.

Many GObject libraries offer function variants that help with this sort of calling convention by allowing to pass a "user data" object alongside the callback. In those cases we can do something like the following: https://github.com/jhass/crystal-gobject/blob/master/src/notify/notification.cr#L67-L81, here I wrap the closure block into a Box so I can get a simple pointer to the user data. Then I keep a reference to that pointer in a global singleton so it won't become garbage collected and pass the pointer as user data. Then as function I have something that doesn't reference any outside context, so doesn't require a closure itself, that takes the user data, dereferences and unwraps it and just calls the resulting proc object. This API even has a helpful callback for when the the registered callback is removed so we can free the user data (in this remove our reference so the GC can collect it).

Other functions are even API compatible to how the closure data is passed into the function of a closure block, so we can just pass the closure data pointer as user data, keep a reference to it, strip the closure data from our original block and pass its function pointer as callback: https://github.com/jhass/crystal-gobject/blob/master/src/g_object/object.cr#L14-L19

Unfortunately the ActionEntry APIs seem to support neither of the approaches in a straight forward way. I see some reference to user_data in the signature description at https://developer.gnome.org/gio/stable/GActionMap.html#GActionEntry, but I guess it's the user_data that's passed to https://developer.gnome.org/gio/stable/GActionMap.html#g-action-map-add-action-entries so common to all entries in the map. If we abstract away over add_action_entries to take a list of a custom struct akin to record ActionEntry(name : String, callback : ->) instead, we could build the action entries in there all pointing to a static function, ship the actual callbacks in the user data and then dispatch based on the signal name, I guess. I don't fully understand the GSimpleAction API yet. This would disallow multiple action entries with the same name, but I hope that's illegal anyway.

Contributions to make this possible or simpler are always welcome :)

thiagojedi commented 4 years ago

Ok. Got it to work, but not with the ActionMap#add_action_entries.

Here's how it ended:

def setup_actions
    quitAction = Gio::SimpleAction.new("quit", nil)
    quitAction.on_activate { self.quit }
    add_action(quitAction)
  end

The Crystal compiler raises an error in the SimpleAction#on_activate so I had to patch it.

If you'd like, I can create a PR with the fix, and my simple application as an example.

jhass commented 4 years ago

Yes, please :)