hugopl / gi-crystal

Tool to generate Crystal bindings for gobject-based libraries (i.e. GTK)
BSD 3-Clause "New" or "Revised" License
44 stars 4 forks source link

Generated code expects incorrect type for Gio::AsyncReadyCallback #83

Open wildeyedskies opened 1 year ago

wildeyedskies commented 1 year ago

If you try to compile this branch of my project, you will get the following error.

https://gitlab.gnome.org/wildeyedskies/wince/-/tree/fix-geoclue-init

In lib/gi-crystal/src/auto/geoclue-2.0/simple.cr:264:76

 264 | LibGeoclue.gclue_simple_new(desktop_id, accuracy_level, cancellable, callback, user_data)
                                                                            ^-------
Error: argument 'callback' of 'LibGeoclue#gclue_simple_new' must be Pointer(Void), not Proc((GObject::Object | Nil), Gio::AsyncResult, Nil)

This results from the generated code function requiring a Gio::AsyncReadyCallback, but the C call expects a pointer for the callback.

However, if you change the C call to instead take the pointer of the proc, you get a error, so I'm not entirely sure what's going on here.

hugopl commented 1 year ago

However, if you change the C call to instead take the pointer of the proc, you get a error, so I'm not entirely sure what's going on here.

Here is the explanation why your attempt to fix didn't work.

skinnyjames commented 9 months ago

Here is the explanation why your attempt to fix didn't work.

I don't think boxing is the reason for this. The signature for Gio.AsyncReadyCallback is actually not a Pointer(Void), so I'm pretty sure the libgen isn't working correctly.

I ran into this while trying to use Gio.Task which generates mismatched bindings between LibGio and Gio.

lib LIbGio
  # generates the alias, but doesn't use it.
  alias AsyncReadyCallback = Pointer(LibGObject::Object), Pointer(LibGio::AsyncResult), Pointer(Void) -> Void

  fun g_task_new(source_object : Pointer(Void), cancellable : Pointer(Void), callback : Void*, callback_data : Pointer(Void)) : Pointer(Void)
end

module Gio
    def self.new(source_object : GObject::Object?, cancellable : Gio::Cancellable?, callback : Gio::AsyncReadyCallback?, callback_data : Pointer(Void)?) : self
      # g_task_new: (Constructor)
      # @source_object: (nullable)
      # @cancellable: (nullable)
      # @callback: (nullable)
      # @callback_data: (nullable)
      # Returns: (transfer full)

      # Generator::NullableArrayPlan
      source_object = if source_object.nil?
                        Pointer(Void).null
                      else
                        source_object.to_unsafe
                      end
      # Generator::NullableArrayPlan
      cancellable = if cancellable.nil?
                      Pointer(Void).null
                    else
                      cancellable.to_unsafe
                    end
      # Generator::NullableArrayPlan
      callback_data = if callback_data.nil?
                        Pointer(Void).null
                      else
                        callback_data.to_unsafe
                      end

      # C call
      _retval = LibGio.g_task_new(source_object, cancellable, callback, callback_data)

      # Return value handling
      Gio::Task.new(_retval, GICrystal::Transfer::Full)
    end
end

There's also a clear problem with trying to call #to_unsafe on a callback_data, which is a Pointer(Void), but that's another issue.

Unfortunately, I'm not sure there are any workarounds that I can find.

Monkey patching isn't possible as you can't monkey patch C lib bindings. Trying to override the binding.yml to use lib_ignore for Gio also proves to be pretty difficult, as it can't conflict with Gio's already present binding.yml

Trying to use shard.override.yml to use a separate gio.crwith a differen't binding.yml also ignores the binding.yml in gio.cr.

info - Pango - No binding config found for Gio-2.0.

Putting binding.yml really anywhere in the project directory causes compile errors for the project like:

Error: can't find file '../../../../../src/bindings/g_lib/error.cr' relative to '/Users/skinnyjames/dsrc/big_editor/lib/gi-crystal/src/auto/g_lib-2.0/g_lib.cr'bi

So it seems that binding.yml is coupled with the lib repo, and the lib repo can't be swapped out.

While the lib is cool, I can't seem to make an app do moderate computation without blocking the UI thread. Definitely open to suggestions. It'd be really great if Lib<Gio/Gtk/whatever> generations were decoupled from their corresponding Gio/Gtk/whatever abstractions, which make it a lot more complicated to work with, especially when there are bugs.

hugopl commented 9 months ago

The Lib* objects must be coupled only with other Lib* form C libraries it depends, any other coupling must be interpreted as a bug. I mean, must be possible to monkey patch things to fix the gi-crystal bugs.

Callback need more <3 for sure. On top of my head it needs:

It's possible to mimic a async API in libtest to be able to fix and test these issues in gi-crystal.

hugopl commented 7 months ago

I'm trying to use Gio::Subprocess and this issue is now blocking me too 😬.

The problem is that the method doesn't have a DestroyNotify parameter, so GICrystal doesn't recognize it as a callback.

Anyway... I also need to think a better way to map these GObject assync API to Crystal... probably:

proc = Gio::Subprocess.new(argv: ["ls", "-l"], flags: :none)
proc.wait_async(cancelable) do |result|
  ...
end

This would cause GI-Crystal to call g_subprocess_wait_async with GAsyncReadyCallback being a wrapper function that would call the user callback then de-register itself from ClosureManager.

hugopl commented 3 weeks ago

Here´s a monkey patch I did to be able to use GtkSource::FileSaver#save_async from GTKSourceView.

module GtkSource
  class FileSaver
    def save_async(io_priority : GLib::Priority, cancellable : Gio::Cancellable? = nil, &callback : Gio::AsyncReadyCallback) : Nil
      c_callback = Pointer(Void).null
      if callback
        c_callback = ->(gobj : Void*, result : Void*, box : Void*) {
          unboxed_callback = Box(Gio::AsyncReadyCallback).unbox(box)
          unboxed_callback.call(GObject::Object.new(gobj, :none), Gio::AbstractAsyncResult.new(result, :none))

          GICrystal::ClosureDataManager.deregister(box)
          nil
        }.pointer
      end

      # C call
      box = Box.box(callback)
      GICrystal::ClosureDataManager.register(box)
      LibGtkSource.gtk_source_file_saver_save_async(to_unsafe, io_priority, cancellable, Pointer(Void).null, Pointer(Void).null, Pointer(Void).null, c_callback, box)
    end
  end
end

I have plans to let the generator generate something similar to this, but passing to the block a generated type base don what the *_finish methods returns, e.g. in this case it would be GtkSource::FileSaver::SaveAsyncResult, with the method value returning what the gtksource_file_saver_save_finish would return, and more methods for any out parama of the *_finish functions.

In case of error, any of these methods would just thrown the corresponding exception.

As usual... I'm not sure when I'll implement this, but at least it's all clear how to do it.