Code Monkey home page Code Monkey logo

gi-crystal's Introduction

Build Status

GI Crystal

GI Crystal is a binding generator used to generate Crystal bindings for GObject based libraries using GObject Introspection.

If you are looking for GTK4 bindings for Crystal, go to GTK4

I wrote this while studying GObject Introspection to contribute with crystal-gobject but at some point I decided to take a different approach on how to generate the bindings, so I started this.

Besides the binding generator this shard provides bindings for GLib, GObject and Gio libraries.

Installation

You are probably looking for the GTK4 shard, not this one, since this shard is only useful if you are creating a binding for a GObject based library.

  1. Add the dependency to your shard.yml:

    developer_dependencies:
      gtk:
        github: hugopl/gi-crystal
  2. Run shards install

  3. Run ./bin/gi-crystal to generate the bindings.

Usage

Bindings are specified in binding.yml files. When you run the generator it will scan all binding.yml files under the project directory and generate the bindings at lib/gi-crystal/src/auto/.

The generator is compiled in a post-install task and can be found at bin/gi-crystal after you run shards install.

See https://github.com/hugopl/gtk4.cr for an example of how to use it.

If you want to use just GLib, GObject or Gio bindings do:

require "gi-crystal/glib"    # Just GLib bindings
require "gi-crystal/gobject" # GLib and GObject bindings
require "gi-crystal/gio"     # GLib, GObject and Gio bindings

Memory Management ❤️‍🔥️

Crystal is a garbage collected language, you create objects and have faith that the GC will free them at some point in time, while on the other hand GLib uses reference count, the clash of these two approaches of how to deal with memory management can't end up in something beautiful without corner cases, etc... but we try our best to reduce the mess.

The basic rules are:

  • All objects (except enums, flags and unions) are created in the heap (including non GObject C Structs).
  • Boxed structs (except GValue) are always allocated by GLib but owned by Crystal wrappers.
  • If the struct is passed from C to Crystal with "transfer none", the struct is copied anyway to ensure that every Crystal object wrapper always points to valid memory. On "transfer full" no copy is needed.
  • All Crystal GObject wrappers have just a pointer to the C object (always allocated by GLib) and always hold a reference during their lifetime.

If you don't know what means Transfer full, Transfer none and few other terms about GOBject introspection, is worth to read the docs.

Debugging

To help debug memory issues you can compile your code with -Ddebugmemory, this will print the object address and reference counter to STDOUT when any wrapper object finalize method is called.

How GObject is mapped to Crystal world

Despite of being written in a language that doesn't have object oriented features, GObject is an object oriented library by design so many things maps easily to OO languages. However each language has its way of doing things and some adaptation is always needed to have a better blending and let the bindings feels more native to the language.

Class names

Class names do not have the module prefix, i.e. GFile from GLib module is mapped to GLib::File, GtkLabel is be mapped to Gtk::Label, where GLib and Gtk are modules.

Interfaces

GObject interfaces are mapped to Crystal modules + a dummy class that only implements this module, used when there's some function returning the interface.

Down Casts

If the object was created by Crystal code you can cast it like you do with any Crystal object instance, using .as? and .as.

If the object was created by C code, e.g. Gtk::Builder where you get everything as a GObject::Object instance, Crystal type system doesn't knows the exact type of the object in GObject type system so you need to cast it using ClassName.cast(instance) or ClassName.cast?(instance). .cast throws a TypeCastError if the cast can't be made while .cast? just returns nil.

  builder = Gtk::Builder.new_from_string("...") # Returns a Gtk::Object
  label = Gtk::Label.cast(builder["label"])

Signal Connections

Suppose you want to connect the Gtk::Widget focus signal, the C signature is:

gboolean
user_function (GtkWidget       *widget,
               GtkDirectionType direction,
               gpointer         user_data)

The user_data parameter is used internally by bindings to pass closure data, so forget about it.

All signals are translated to a method named #{signal_name}_signal, that returns the signal object, the _signal suffix exists to solve name conflicts like Gtk::Window destroy method and destroy signal.

So there are 3 ways to connect this signal to a callback:

def slot_with_sender(widget, direction)
  # ...
end
# Connect to a slot with all arguments
widget.focus_signal.connect(->slot_with_sender(Gtk::Widget, Gtk::Direction)

def slot_without_sender(direction)
  # ...
end
# Connect to a slot without the sender
widget.focus_signal.connect(->slot_without_sender(Gtk::Direction)

# Connect to a block (always without sender parameter)
widget.focus_signal.connect do |direction|
  # ...
end

If the signal requires a slot that returns nothing, a slot that returns nothing (Nil) must be used, this is a limitation of the current implementation that will probably change in the future to just ignore the return value on those slots.

After signals

Use the after keyword argument:

# Connect to a slot without the sender
widget.focus_signal.connect(->slot_without_sender(Gtk::Direction), after: true)

# Connect to a block (always without sender parameter)
widget.focus_signal.connect(after: true) do |direction|
  # ...
end

Signals with details

# To connect the equivalent in C to "notify::my_property" do
widget.notify_signal["my_property"].connect do
  # ...
end

Disconnecting signals

When you connect a signal it returns a GObject::SignalConnection object, call the disconnect method on it and it's done.

⚠️ Objects with signals connections will never be garbage collected, so remember to disconnect all signals from your object if you want to really free up that beloved memory.

GValue

When returned by methods or as signal parameters they are represented by GObject::Value class, however if a method accepts a GValue as parameter you can pass any supported value. I.e. you can pass e.g. a plain Int32 to a method that in C expects a GValue.

GObject inheritance

You can inherit GObjects, when you do so a new type is registered in GObject type system. Crystal objects that inherit GObjects must always have a reference in Crystal world, otherwise they will be collected by the GC.

Trying to cast a GObject that was already collected by Crystal GC will result in a GICrystal::ObjectCollectedError exception.

Crystal objects that inherit GObject returns the same object reference on casts, i.e. no memory allocation is done. For more examples see the inheritance tests.

Declaring GObject signals

You can declare signals in your GObject::Object derived class using the signal macro, e.g.:

class Foo < GObject::Object
  signal my_signal_without_args
  signal my_signal(number : Int32, some_float : Float32)
end

# Using the signal
foo = Foo.new
foo.my_signal_without_args_signal.connect { puts "Got signal!" }
foo.my_signal_signal.connect { |a, b| puts "Got signal with #{a} and #{b}!" }

# emitting signals
foo.my_signal_without_args_signal.emit
foo.my_signal_signal.emit(42, 3.14)

⚠️ Meanwhile signals only support parameters of Integer, Float, String and Boolean types.

Also note that String parameters will be copied for each signal receiver, this is because the String goes to C, then back to Crystal as a const char* pointer. This may change in the future.

Declaring GObject properties

GObject Properties are declared using the GObject::Property annotation on the instance variable.

Virtual Methods

Virtual methods must have the GObject::Virtual annotation, currently only virtual methods from interfaces are supported.

class Widget0 < Gtk::Widget
  # GObject virtual method name is guessed from Crystal method name, that can start with `do_`.
  @[GObject::Virtual]
  def do_snapshot(snapshot : Gtk::Snapshot)
  end
emd

class Widget1 < Gtk::Widget
  # If the `do_` prefix annoyes you, just use the same GObject virtual method name.
  @[GObject::Virtual]
  def snapshot(snapshot : Gtk::Snapshot)
  end
end

class Widget2 < Gtk::Widget
  # Or you can use whatever name and inform the GObject virtual method name in the annotation.
  @[GObject::Virtual(name: "snapshot")]
  def heyho(snapshot : Gtk::Snapshot)
  end
end

If for some reason (peformance or GICrystal bugs 🙊️) you don't want wrappers, you can create an unsafe virtual method:

class Widget3 < Gtk::Widget
  @[GObject::Virtual(unsafe: true)]
  def snapshot(snapshot : Pointer(Void))
    # User is responsible for memory management here, like in C.
  end
end

GLib GError

GI-Crystal translates all GLib errors to different exceptions.

Example: G_FILE_ERROR_EXIST is a GLib error from domain FILE_ERROR with the code name EXIST, GICrystal translates this in these the following exception classes:

module GLib
  class GLibError < RuntimeError
  end

  class FileError < GLibError
    class Exist < FileError
      def code : Int32
        # ...
      end
    end
    # ...
  end
end

So if you want to rescue from this specific error you must rescue e : GLib::FileError::Exist, if you want to rescue from any error in this domain you must rescue e : GLib::FileError, and finally if you want to rescue from any GLib errors you do rescue e : GLib::GLibError.

Raw C Structs

At binding.yml file you can define the strategy used to bind the structs, if set to autoit will behave like lsited bellow:

  • If the struct have no pointer attributes it's mapped to a Crystal struct with the same memory layout of the C struct (stack_struct binding strategy).
  • If the struct have pointer attributes it's mapped to a Crystal class with the same memory layout of the C struct, so a finalize method can be implemented to free the resources. Not that no setters are generated to pointer attributes, since we can't guess how this memory must be handled (heap_struct binding strategy).
  • If the struct is a opaque pointer it's mapped to a Crystal class with a pointer to the C object, it's assumed that the object is a GObject Box, so the g_boxed_* family of functions are used to handle the memory (heap_wrapper_struct binding strategy).

Contributing

See HACKING.md for details about how the generator works.

  1. Fork it (https://github.com/hugopl/gi-crystal/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

gi-crystal's People

Contributors

bigboybarney avatar blobcodes avatar charitybell avatar geopjr avatar hugopl avatar mamantoha avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

gi-crystal's Issues

Setting a GObject property to nil causes a runtime warning

The following code:

it "can be GObjects" do
  subject = Test::Subject.new
  subject.gobj.should eq(nil)

  value = Test::Subject.new
  subject.gobj = value
  value.ref_count.should eq(2)
  subject.gobj.not_nil!.to_unsafe.should eq(value.to_unsafe)

  subject.gobj = nil
  value.ref_count.should eq(1)
  subject.gobj.should eq(nil)
end

There's no way to ignore constants from being generated

GLib (and other modules) has a lot of constants with no meaning for a Crystal bindings like:

  • C_STD_VERSION
  • E
  • GNUC_FUNCTION
  • GNUC_PRETTY_FUNCTION
  • *_FORMAT
  • *_MODIFIER
  • A lot of math constants and other things that already exists in Crystal stdlib

So would be nice to have a way to ignore them on binding.yml file.

Maybe this could be simple like:

ignore_constants:
- FOO
- BAR

Because if someone needs to modify a constant it's a matter of ignore the original one and create another one in a included Crystal file using the require_before/require_after.

Support GC resistant GObject subclasses

When passing a instance of a object that inherits GObject to a function that takes the ownershipt of this object, if the only Crystal instance dies, the GC collects the memory, so later when you retrieve the object and try to reding it to a Crystal object an exception is thrown (or a crash happen somewhere).

Among many awesome patches, @BlobCodes fixed this issue at #64, however the approach used IMO changes the common way of doing things in Crystal too much, so I choose to not merge it until we find a better solution.

To make things clear, here's a spec test that reproduce the issue.

require "./spec_helper"

private class GCResistantObj < GObject::Object
  property moto : String

  def initialize(@moto)
    super()
  end
end

@[NoInline]
def feed_gc : Test::Subject
  subject = Test::Subject.new
  gc_resistance = GCResistantObj.new("viva la resistance!")
  subject.gobj = gc_resistance
  subject
end

describe "GC friendness" do
  it "does not collect objects that goes to C world" do

    subject = feed_gc
    GC.collect
    # Nowadays this raises GICrystal::ObjectCollectedError, because the object was collected.
    hey = GCResistantObj.cast(subject.gobj)
    # Nowadays this fails with TypeCastError because it's accessing memory garbage collected
    # hey = subject.gobj.as(GCResistantObj)
    hey.moto.should eq("viva la resistance!")
  end
end

Copy this under spec/, run make tests, then later ./bin/spec spec/gc_spec.cr -Ddebugmemory to better see things happening.

`OUT` / `INOUT` arguments in vfuncs not typed as pointers

Example definition of Gtk::Widget._register_unsafe_do_measure

    private macro _register_unsafe_do_measure
      private def self._vfunc_unsafe_measure(%this : Pointer(Void), lib_orientation :  UInt32, lib_for_size :  Int32, lib_minimum :  Int32, lib_natural :  Int32, lib_minimum_baseline :  Int32, lib_natural_baseline :  Int32, ) : Void
# @orientation: 
# @for_size: 
# @minimum: (out) (transfer full) (optional) 
# @natural: (out) (transfer full) (optional) 
# @minimum_baseline: (out) (transfer full) (optional) 
# @natural_baseline: (out) (transfer full) (optional) 

        %gc_collected = !LibGObject.g_object_get_qdata(%this, GICrystal::GC_COLLECTED_QDATA_KEY).null?
        %instance = LibGObject.g_object_get_qdata(%this, GICrystal::INSTANCE_QDATA_KEY)
        raise GICrystal::ObjectCollectedError.new if %gc_collected || %instance.null?

        %instance.as(self).unsafe_do_measure(lib_orientation, lib_for_size, lib_minimum, lib_natural, lib_minimum_baseline, lib_natural_baseline)
      end

      def self._class_init(type_struct : Pointer(LibGObject::TypeClass), user_data : Pointer(Void)) : Nil
        vfunc_ptr = (type_struct.as(Pointer(Void)) + 232).as(Pointer(Pointer(Void)))
        vfunc_ptr.value = (->_vfunc_unsafe_measure(Pointer(Void), UInt32, Int32, Int32, Int32, Int32, Int32)).pointer
        previous_def
      end
    end

The last four arguments have to be pointers because their ArgInfo#direction are OUT. This is currently not accounted for.

`GLib::SList` of type `Gio::File` raises

Example:

require "gtk4"

app = Gtk::Application.new("hello.example.com", Gio::ApplicationFlags::None)

app.activate_signal.connect do
  window = Gtk::ApplicationWindow.new(app)
  window.title = "DnD test"
  window.set_default_size(200, 200)

  button = Gtk::Label.new("Drag-n-Drop a file here")
  window.child = button

  controller = Gtk::DropTarget.new(Gdk::FileList.g_type, Gdk::DragAction::Copy)
  controller.drop_signal.connect(->dnd_drop(GObject::Value, Float64, Float64))
  window.add_controller(controller)

  window.present
end

def dnd_drop(value, x, y)
  files = Gdk::FileList.new(value.to_unsafe, GICrystal::Transfer::Full).files
  p files
  p files.size
  p files[0]

  true
end

exit(app.run)

raises (with error-trace)

In examples/hello_world.cr:15:34

 15 | controller.drop_signal.connect(->dnd_drop(GObject::Value, Float64, Float64))
                                     ^-------
Error: instantiating 'dnd_drop(GObject::Value, Float64, Float64)'


In examples/hello_world.cr:25:10

 25 | p files[0]
             ^
Error: instantiating 'GLib::SList(Gio::File)#[](Int32)'


In lib/gi-crystal/src/bindings/g_lib/slist.cr:25:11

 25 | self[n]?.not_nil!
          ^
Error: instantiating 'GLib::SList(Gio::File)#[]?(Int32)'


In lib/gi-crystal/src/bindings/g_lib/slist.cr:32:7

 32 | to_crystal(value)
      ^---------
Error: instantiating 'to_crystal(Pointer(LibGLib::SList))'


In lib/gi-crystal/src/bindings/g_lib/slist.cr:38:7

 38 | {% if T == String %}
      ^
Error: expanding macro


There was a problem expanding macro 'macro_140293467917264'

Called macro defined in lib/gi-crystal/src/bindings/g_lib/slist.cr:38:7

 38 | {% if T == String %}

Which expanded to:

 > 1 | 
 > 2 |         T.new(data)
 > 3 |       
Error: undefined method 'new' for Gio::File:Module

Gio::File:Module trace:

  macro macro_140293467917264 (in /tmp/gtk4.cr/lib/gi-crystal/src/bindings/g_lib/slist.cr:38):2

            T.new(data)

Edit:
Working dnd example with #72

require "gtk4"

app = Gtk::Application.new("hello.example.com", Gio::ApplicationFlags::None)

app.activate_signal.connect do
  window = Gtk::ApplicationWindow.new(app)
  window.title = "DnD test"
  window.set_default_size(200, 200)

  button = Gtk::Label.new("Drag-n-Drop a file here")
  window.child = button

  controller = Gtk::DropTarget.new(Gdk::FileList.g_type, Gdk::DragAction::Copy)
  controller.enter_signal.connect(->dnd_enter(Float64, Float64))
  controller.drop_signal.connect(->dnd_drop(GObject::Value, Float64, Float64))
  button.add_controller(controller)

  window.present
end

def dnd_enter(x, y)
  puts "entered"
  Gdk::DragAction::Copy
end

def dnd_drop(value, x, y)
  object_ptr = LibGObject.g_value_get_boxed(value.to_unsafe)
  files = Gdk::FileList.new(object_ptr, GICrystal::Transfer::Full).files
  p files
  files.each do |x|
    puts x.uri
  end

  true
end

exit(app.run)

Some strutcs use ref count and need to be handled in a special way

Some structs like Pango::FontMetrics uses ref count, so when trying to use the bindings a double free happens.

Code above should trigger the issue

Gtk::widget.new.pango_context.metrics(nil, nil)

Probably binding.yml need some entry to inform that for some structs it must not generate:

  • @data instance variable.
  • initialize(pointer : Pointer(Void), transfer : GICrystal::Transfer) constructor.
  • self.new(...)
  • to_unsafe method.

So this can be created by hand.

Better handling shards dependencies

Looking at https://github.com/GeopJr/libadwaita.cr usage of GI-Crystal I found that it works like a fork of https://github.com/hugopl/gtk4.cr, at first it seems weird... but after trying to fix I found the reason.

In the ideal world libadwaitashard.yml would have:

dependencies
  gtk4:
    github: hugopl/gtk4
    version: ...

And no gtk4 shard files would needed to be replicated in libadwaita repo, so it would just generate the extra modules that libadwaita depends without going through any modules that were already generated by gtk4 shard.

The easier way to fix that is removing the GTK4 files from the repo, adding it as a dependency on shard.yml and modify the post install to: postinstall: ../../bin/gi-crystal Adw 1 -o src/auto --config-paths=.:../gi-crystal:../gtk4. However this would make GTK4 bindings to be generated twice... under lib/gtk4/src and under src/auto/ (libadwaita generate the files in src/auto), so I thought about add a flag to gi-crystal, something like --don-t-generate=gtk4, so libadwaita postinstall could be: postinstall: ../../bin/gi-crystal Adw 1 -o src/auto --don-t-generate=gtk4 --config-paths=.:../gi-crystal:../gtk4

But these config paths, etc... are all error prone, and I believe this must be solved automatically by GI-Crystal, I just didn't stop yet to think about how to do it...

So... the issue:

Make easier the life of people doing bindings for libraries that depends on other libraries that already have bindings using GI-Crystal.

Enforce type safety on vfunc return types.

I'm aware this is a problem for Boolean type, but probably is for other types as well.

If you implement a vfunc that returns Bool, but return a Int32 instead it's going to compile, but not with the Cish expected results, since 0 is true in Crystal.

The issue is caused because when fixing #95 the generated code just does:

result ? 1 : 0

Works nice for booleans, but if result is almost anything else it will always send 1 to C, that means true.

So I think the vfunc.ecr tempalte must render a simple macro to check if the return type is the desired one, like

{% raise "VFunc blablabla must return Bool but is returning Int32" if somecheck %}

Easy to fix in the current code base, but would be nice to think this with #45 in mind.

Virtual functions that return nullable objects fail to compile

Gio::ListModel has a virtual function called get_item that returns an GObject::Object or nil, however if the code returns nil it fails to compile:

There was a problem expanding macro '_register_get_item_vfunc'

Code in macro 'method_added'

 5 | _register_get_item_vfunc(get_item)
     ^
Called macro defined in lib/gi-crystal/src/auto/gio-2.0/list_model.cr:72:13

 72 | private macro _register_get_item_vfunc(impl_method_name)

Which expanded to:

 > 10 | 
 > 11 |         __temp_70 = __temp_69.as(self).get_item(position)
 > 12 |         __temp_70.to_unsafe
                          ^--------
Error: undefined method 'to_unsafe' for Nil (compile-time type is (Doc | Nil))

In my code, Doc was a class that inherits GObject::Object.

Boolean return type for vfuncs are not converted

Similar to #34 , but this time for booleans.

How to reproduce

Try to implement a virtual function that returns a boolean.

Expected Behavior

You return a Crystal Boolean and it works.

What happens

Crystal code expect a Int32.

Inheriting from a non-user class which inherits from a class implementing interfaces re-includes that interface

I recently wanted to create a widget inheriting from Adw::Bin. It itself is a subclass of Gtk::Widget, which implements the three interfaces: Gtk::Accessible, Gtk::Buildable and Gtk::ConstraintTarget.

When subclassing it, the following is printed to STDOUT:

(crystal-run-mangaba.tmp:37321): GLib-GObject-WARNING **: 16:07:17.256: cannot add interface type 'GtkConstraintTarget' to type 'Mangaba-UI-Description', since type 'Mangaba-UI-Description' already conforms to interface

(crystal-run-mangaba.tmp:37321): GLib-GObject-WARNING **: 16:07:17.256: cannot add interface type 'GtkBuildable' to type 'Mangaba-UI-Description', since type 'Mangaba-UI-Description' already conforms to interface

(crystal-run-mangaba.tmp:37321): GLib-GObject-WARNING **: 16:07:17.256: cannot add interface type 'GtkAccessible' to type 'Mangaba-UI-Description', since type 'Mangaba-UI-Description' already conforms to interface

(crystal-run-mangaba.tmp:37321): GLib-GObject-WARNING **: 16:07:17.256: cannot add interface type 'GtkConstraintTarget' to type 'Mangaba-UI-Description', since type 'Mangaba-UI-Description' already conforms to interface

(crystal-run-mangaba.tmp:37321): GLib-GObject-WARNING **: 16:07:17.256: cannot add interface type 'GtkBuildable' to type 'Mangaba-UI-Description', since type 'Mangaba-UI-Description' already conforms to interface

(crystal-run-mangaba.tmp:37321): GLib-GObject-WARNING **: 16:07:17.256: cannot add interface type 'GtkAccessible' to type 'Mangaba-UI-Description', since type 'Mangaba-UI-Description' already conforms to interface

As you can see, each of these three interfaces gets implemented three times.

We can either refactor the interface inheriting code or create a class variable (ex. @@_implemented_gtk_buildable) which we use to only implement it once.

Virtual functions does not obey return value ownership transfer

Found this while trying to create a Gio::ListModel, the get_item virtual func returns an GObject, but with transfer full, this makes the C code steal the reference owned by the Crystal object, then at some time a nice crash will happen.

To fix that the vfunc machinery needs to increase the reference of the returned object before returning it to C world.

Interface casts crashes

Interface cast was implemented like normal object casts, but they aren't. However this way it works in some circustances.

In current test suit, expanding the C macros we get the GObject implementations:

static inline gboolean TEST_IS_IFACE (gpointer ptr) {
  return {
    GTypeInstance *__inst = (GTypeInstance*) (ptr);
    GType __t = (test_iface_get_type ());
    gboolean __r;
    if (!__inst)
      __r = (0);
    else if (__inst->g_class && __inst->g_class->g_type == __t)
      __r = (!(0));
    else
      __r = g_type_check_instance_is_a (__inst, __t);
    __r;
  };
}

static inline TestIfaceInterface * TEST_IFACE_GET_IFACE (gpointer ptr) {
  return (TestIfaceInterface*) g_type_interface_peek(((GTypeInstance*) ptr)->g_class, test_iface_get_type());
}

While in the bindings this is done as:

# Cast a `GObject::Object` to this type, returns nil if cast can't be made.
def self.cast?(obj : GObject::Object) : self?
  return if LibGObject.g_type_check_instance_is_a(obj, g_type).zero?

  instance = GICrystal.instance_pointer(obj)
  # This should never happen with GC resistant objects
  raise GICrystal::ObjectCollectedError.new if instance.null?

  instance.as(self)
end

i.e. we have only the instance cast implementation 😁

[Feature Request] Unsafe vfunc implementations

I think it would be useful be be able to directly implement vfuncs.
This could be used to pass values to other GObjects which don't need to be directly accessed inside crystal (to avoid mallocs) or to bypass bugs in gi-crystal.
I think something like def do_(vfunc)! or def unsafe_do_(vfunc) would be good.

Something like this:

class TestObject
  def do_example_vfunc!(some_struct : LibGObject::SomeStruct, some_object : Void*)
  end
end

It would be quite easy to implement, but I'd first like to know whether this is something worthwhile or not.

[Request] non-void signals

Could the signal macro support non-void signals?

Use case:
I'm emitting a singal from a child widget to call another function from the parent and want it to return a bool if it succeeded or not

(exact use case: A widget has a button and a clicked signal. The parent connects to the signal that provides the hash function to copy in string (e.g. "MD5"), the parent then copies it to clipboard and should return a bool to the widget if it succeeded or not so the button displays the correct feedback icon (:heavy_check_mark: or :x:))

`Gdk::FileList.new_from_array` expects `(Pointer(LibGio::File))`, not `Pointer(Pointer(Void))`

Example:

require "gtk4"

app = Gtk::Application.new("hello.example.com", Gio::ApplicationFlags::None)

app.activate_signal.connect do
  window = Gtk::ApplicationWindow.new(app)
  window.title = "test"
  window.set_default_size(200, 200)

  Gdk::FileList.new_from_array([
    Gio::File.new_for_path("/tmp/gtk4.cr/shard.yml"),
  ])

  window.present
end

exit(app.run)

raises:

In lib/gi-crystal/src/auto/gdk-4.0/file_list.cr:45:53

 45 | _retval = LibGdk.gdk_file_list_new_from_array(files, n_files)
                                                    ^----
Error: argument 'files' of 'LibGdk#gdk_file_list_new_from_array' must be Pointer(Pointer(LibGio::File)), not Pointer(Pointer(Void))

Type error with BoxHelper & `Gtk::Switch#state_set_signal`

Example app with a switch:

require "gtk4"

app = Gtk::Application.new("hello.example.com", Gio::ApplicationFlags::None)

app.activate_signal.connect do
  window = Gtk::ApplicationWindow.new(app)
  window.set_default_size(200, 200)

  switch = Gtk::Switch.new
  switch.state_set_signal.connect do |foo|
    puts foo

    true
  end

  window.child = switch
  window.present
end

exit(app.run)

results in (with --error-trace):

In example.cr:10:27

 10 | switch.state_set_signal.connect do |foo|
                              ^------
Error: instantiating 'Gtk::Switch::StateSetSignal#connect()'


In lib/gi-crystal/src/auto/gtk-4.0/switch.cr:396:9

 396 | connect(block, after: after)
       ^------
Error: instantiating 'connect(Proc(Bool, Bool))'


In lib/gi-crystal/src/auto/gtk-4.0/switch.cr:403:51

 403 | ::Box(Proc(Bool, Bool)).unbox(_lib_box).call(state)
                                               ^---
Error: no overload matches 'Proc(Bool, Bool)#call' with type Int32

Overloads are:
 - Proc(*T, R)#call(*args : *T)

[meta] Apply to GNOME Circle

This is more of a discussion.

If you believe gtk4.cr+gi-crystal are "production-ready", would you like to submit them to Circle?

In my opinion, it could promote Crystal for building GTK apps!

My main concerns on the application form are:

  • Project license: gi-crystal is BSD-3-Clause while gtk.cr is MIT, not sure which one should be listed
  • Please list some examples of where the project is used: I know of Tijolo and Collision, I believe they are enough but if you know anything else, add it!

I don't think an org is required but I am willing to transfer over libadwaita.cr, gettext.cr, non-blocking-spawn and/or any other gtk related lib or script I have so there's a "central authority" (+ so releases don't lag behind).

Constant treated as type

I came across the following error while calling Gio::Resource#enumerate_children:

In lib/gi-crystal/src/auto/gio-2.0/resource.cr:201:60

 201 | def enumerate_children(path : ::String, lookup_flags : Gio::ResourceLookupFlags) : Enumerable(::String)
                                                              ^-----------------------
Error: Gio::ResourceLookupFlags is not a type, it's a constant

From a quick look, it happens on src/generator/helpers.cr#L162-L165 but I'm unsure how to distinguish interface from constant (if that's even what I'm supposed to do).

Corrent type should be UInt32 (or should Gio::ResourceLookupFlags be an enum with just one item? (https://docs.gtk.org/gio/flags.ResourceLookupFlags.html))

(I don't use this or the others that are affected (#info, #lookup_data, #open_stream) so priority is low 😅)

nullable transfer full params doesn't check for null before increase object reference.

Following code:

/**
 * test_subject_nullable_transfer_full_param:
 * @gobj: (transfer full) (nullable):
 */
void test_subject_nullable_transfer_full_param(GObject* gobj);

Generates:

    def self.nullable_transfer_full_param(gobj : GObject::Object?) : Nil
      # test_subject_nullable_transfer_full_param: (None)
      # @gobj: (transfer full) (nullable)
      # Returns: (transfer none)

      # Generator::NullableArrayPlan
      gobj = if gobj.nil?
               Pointer(Void).null
             else
               gobj.to_unsafe
             end
      # Generator::TransferFullArgPlan
      LibGObject.g_object_ref_sink(gobj)
      # C call
      LibTest.test_subject_nullable_transfer_full_param(gobj)

      # Return value handling
    end

LibGObject.g_object_ref_sink(gobj) must be uder a if obj, otherwise NULL is passed to g_object_ref_sink causing some warnings.

Error: undefined local variable or method 'start' for Generator::ModuleGen

Hi!

echo 'name: testi
version: 0.0.1
dependencies:
  gtk4:
    github: hugopl/gtk4.cr' > shard.yml
shards install --error-trace

->

Resolving dependencies
Fetching https://github.com/hugopl/gtk4.cr.git
Fetching https://github.com/hugopl/gi-crystal.git
Fetching https://github.com/hugopl/version_from_shard.git
Installing version_from_shard (1.2.5)
Installing gi-crystal (0.14.0)
Postinstall of gi-crystal: shards build
Failed postinstall of gi-crystal on shards build:
Resolving dependencies
Fetching https://github.com/hugopl/version_from_shard.git
Using version_from_shard (1.2.5)
Writing shard.lock
Building: gi-crystal
Error target gi-crystal failed to compile:
Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'macro_139794426358304'

Code in macro 'embed'

 1 | {{ run("ecr/process", "ecr/module.ecr", "io") }}
     ^
Called macro defined in macro 'embed'

 1 | {{ run("ecr/process", "ecr/module.ecr", "io") }}

Which expanded to:

 > 1 | self  # => "\n" + "require \"../"
 > 2 | start # => 1
       ^----
Error: undefined local variable or method 'start' for Generator::ModuleGen

Honestly I have no idea what is going on all of a sudden, it used to be fine before. It doesn't seem you changed anything, and it's broken with previous commits too.

I also got this error

Error target gi-crystal failed to compile:
Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'declare'

Code in src/generator/main.cr:10:1

 10 | VersionFromShard.declare
      ^
Called macro defined in lib/version_from_shard/src/version_from_shard.cr:2:3

 2 | macro declare(dir = "")

Which expanded to:

 > 1 | self  # => "0.4.1-54-ga5ade1f"
       ^---
Error: there's no self in this scope

but when I tried to narrow it down on a reproduction case, I got the first one... it somehow suddenly stopped working without me upgrading anything (also not Crystal itself). Do you have any idea what's going on? Other dependencies install fine, so I assume it's somehow related to this one.

WebKit2 library define a None enum != 0 and fail to compile

Generating bindings for WebKit2 module you get this error when trying to require the module:

In lib/gi-crystal/src/auto/web_kit2-5.0/web_kit2.cr:503:5

 503 | None          =  2
       ^---
Error: flags enum can't redefine None member to non-0

Crystal language doesn't allow a flag to define None as non-zero, but WebKit2 define this:

  # Flags
  @[Flags]
  enum EditorTypingAttributes : UInt32
    None          =  2
    Bold          =  4
    Italic        =  8
    Underline     = 16
    Strikethrough = 32

    # Returns the type id (GType) registered in GLib type system.
    def self.g_type : UInt64
      LibWebKit2.webkit_editor_typing_attributes_get_type
    end
  end

So, some sort of translation must be done when instances of this type crosses Crystal-C boundary. However what if the C enum is like this:

enum Foo {
  None = 4,
  Troll = 0
};

:trollface:

g_unichar must be Crystal Char, not UInt32

Gtk::TextIter have some methods that returns gunichar, the generator is translating them to UInt32 instead of Char.

    def char : UInt32
      # gtk_text_iter_get_char: (Method)
      # Returns: (transfer none)

      # C call
      _retval = LibGtk.gtk_text_iter_get_char(to_unsafe)

      # Return value handling
      _retval
    end

Bad code generated for fundamental types not in GObject module

It's not possible to instantiate GSK render nodes:

In lib/libadwaita/lib/gi-crystal/src/auto/gsk-4.0/render_node.cr:34:18

 34 | LibGObject.gsk_render_node_ref(self) if transfer.none?
                 ^------------------
Error: undefined fun 'gsk_render_node_ref' for LibGObject

GSK objects also ref other things that must be only in GObject types.

Add CI using actions

This is something that I can do but I'm too lazy to do and prefer spend my time writing code instead YAML files, however if a good soul with free time submit a MR I would appreciate that 😁

Error: undefined constant GLib::Error

Hi!

First up: I've been forking gtk4 into gtk3: https://github.com/phil294/gtk3.cr. Reason: I dislike most decisions behind Gtk 4 and the general direction of the framework. Breaking changes should be a last resort and removing functionality is a clear red flag for me... anyway, when I was using jhass' https://github.com/jhass/crystal-gobject, I kept running into rare GC-related bugs regardless of what I did. With this new approach however, I have been able mitigate all of them, I think. This binding generator is fantastic and I want to thank you very much for it! Migrating was a breeze, the syntax is just as clear (if not better in parts) and my subjective feeling is that the resulting bindings are of overall higher quality and less bugs. The manual ./bin/gi-crystal caching step also improves compilation speed.

If you want to support Gtk 3 yourself some day I'll be glad to close mine in favor of yours, but until then, I just made my changes public, and they have been fairly minimal.

I have encountered just one single problem that looks to me like it is to be blamed on gi-crystal itself:

Some GLib imports are missing. This happens in two places:

First:

In lib/gi-crystal/src/auto/xlib-2.0/xlib.cr:21:21

 21 | class XlibError < GLib::Error
                        ^----------
Error: undefined constant GLib::Error

Did you mean 'Xlib'?

I work around this right now by manually adding

require "../g_lib-2.0/g_lib"

to the top of lib/gtk4/lib/gi-crystal/src/auto/xlib-2.0/xlib.cr.

Second:

In lib/gi-crystal/src/auto/gtk-3.0/gtk.cr:866:53

 866 | alias RcPropertyParser = Proc(GObject::ParamSpec, GLib::String, GObject::Value, Bool)
                                                         ^-----------
Error: undefined constant GLib::String

I work around this by just replacing all Glib::String with ::String in lib/gtk4/lib/gi-crystal/src/auto/gtk-3.0/gtk.cr.

transfer full interface parameters don't get their reference increased.

Gtk::SingleSelection class has this generated constructor:

    def self.new(model : Gio::ListModel?) : self
      # gtk_single_selection_new: (Constructor)
      # @model: (transfer full) (nullable)
      # Returns: (transfer full)

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

      # C call
      _retval = LibGtk.gtk_single_selection_new(model)

      # Return value handling

      Gtk::SingleSelection.new(_retval, GICrystal::Transfer::Full)
    end

model is an interface, but for some reason ArgStrategy clas schoose NullableArrayPlan for it and generated wrong code.

correct code must use the TransferFullArgPlan and increase the object reference.

Add suppport for @[Raises] annotation on functions that may execute callbacks

Functions that may execute callbacks implemented in Crystal must have the @[Raises] annotation in the lib declaration to let everything behave well[1].

As there's no way to infer what methods can trigger callbacks, this information need to be in the bindings.yml file, like:

execute_callback:
  - g_main_loop_run

execute_callback seems not a good name, but is what come to my mind now 😅

[1] https://crystal-lang.org/reference/1.3/syntax_and_semantics/c_bindings/callbacks.html#raises-annotation

Error compiling harf_buzz - can't use variable name 'extents' inside assignment to variable 'extents'

Trying to compile rtfm:

  margret@nobody-t14:~/dev/crystal/rtfm main@c360719 (dirty)
  [0] $ make

shards install
Resolving dependencies
Fetching https://github.com/hugopl/fzy.git
Fetching https://github.com/geopjr/libadwaita.cr.git
Fetching https://github.com/crystal-lang/crystal-sqlite3.git
Fetching https://github.com/hugopl/gtk4.cr.git
Fetching https://github.com/hugopl/gi-crystal.git
Fetching https://github.com/hugopl/pango.cr.git
Fetching https://github.com/hugopl/harfbuzz.cr.git
Fetching https://github.com/hugopl/gio.cr.git
Fetching https://github.com/crystal-lang/crystal-db.git
Installing fzy (0.5.5)
Installing gi-crystal (0.16.0 at 9ebef99)
Postinstall of gi-crystal: shards build
Installing harfbuzz (0.1.0)
Installing gio (0.1.0)
Installing pango (0.2.0)
Installing gtk4 (0.14.0 at 83c6921)
Installing libadwaita (1.0.0 at 23ce21d)
Installing db (0.12.0)
Installing sqlite3 (0.20.0)
./bin/gi-crystal
info - Starting at 2023-07-16 22:30:21 -04:00, project dir: /home/margret/dev/crystal/rtfm
info - Gi-Crystal version 0.16.0, built with Crystal 1.8.2.
info - Generating bindings at /home/margret/dev/crystal/rtfm/lib/gi-crystal/src/auto
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/pango/src/bindings/pango/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/gio/src/bindings/gio/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/harfbuzz/src/bindings/harfbuzz/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/gtk4/src/bindings/gtk/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/gtk4/src/bindings/gdk/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/gtk4/src/bindings/gsk/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/gi-crystal/src/bindings/g_lib/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/gi-crystal/src/bindings/g_object/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/lib/libadwaita/src/bindings/binding.yml
info - Using binding config at /home/margret/dev/crystal/rtfm/src/bindings/webkit2/binding.yml
info - Pango - No binding config found for cairo-1.0.
info - HarfBuzz - No binding config found for freetype2-2.0.
warn - HarfBuzz - Interface constant not supported.
warn - g_cancellable_connect - Callback without user_data!
warn - Gio::ActionEntry padding field - Unknown conversion to crystal for fixed size array.
warn - Gio::DBusInterfaceVTable padding field - Unknown conversion to crystal for fixed size array.
warn - Gio::DBusSubtreeVTable padding field - Unknown conversion to crystal for fixed size array.
warn - Boxed not working for enums
info - gtk_image_new_from_pixbuf - No binding config found for GdkPixbuf-2.0.
info - gtk_snapshot_append_cairo - No binding config found for Graphene-1.0.
warn - Gtk::BuildableParser padding field - Unknown conversion to crystal for fixed size array.
warn - gsk_border_node_get_widths - Unknown conversion to crystal for fixed size array.
info - Gdk - No binding config found for PangoCairo-1.0.
warn - Gdk - SeatCapabilities::All (0xf) doesn't have all possible bits set (0x1f).
warn - Gdk::TimeCoord axes field - Unknown conversion to crystal for fixed size array.
info - GdkPixbuf - No binding config found for GModule-2.0.
warn - gdk_pixbuf_get_options - Unknown conversion to crystal for GHash
info - WebKit - No binding config found for Soup-3.0.
info - WebKit - No binding config found for JavaScriptCore-6.0.
warn - soup_form_decode - Unknown conversion to crystal for GHash
warn - soup_form_decode_multipart - Unknown conversion to crystal for GHash
warn - soup_header_parse_param_list - Unknown conversion to crystal for GHash
warn - soup_header_parse_param_list_strict - Unknown conversion to crystal for GHash
warn - soup_header_parse_semi_param_list - Unknown conversion to crystal for GHash
warn - soup_header_parse_semi_param_list_strict - Unknown conversion to crystal for GHash
warn - soup_server_add_early_handler - Unknown conversion to crystal for GHash
warn - soup_server_add_handler - Unknown conversion to crystal for GHash
warn - Generator::VFuncGen - Unknown conversion to crystal for GHash
warn - jsc_class_add_property - Callback without user_data!
warn - jsc_value_object_define_property_accessor - Callback without user_data!
syntax error in './harf_buzz-0.0/harf_buzz.cr:7056:14': can't use variable name 'extents' inside assignment to variable 'extents'
fatal - Error formating generated files at '/home/margret/dev/crystal/rtfm/lib/gi-crystal/src/auto'.
make: *** [Makefile:8: rtfm] Error 1

Lines in question (gi-crystal/src/auto/harf_buzz-0.0/harf_buzz.cr):

def self.ot_layout_get_font_extents(font : HarfBuzz::FontT,direction : HarfBuzz::DirectionT,script_tag : UInt32,language_tag : UInt32,) : HarfBuzz::FontExtentsT
  # hb_ot_layout_get_font_extents: (None)
# @font: 
# @direction: 
# @script_tag: 
# @language_tag: 
# @extents: (out) (nullable) (caller-allocates) 
# Returns: (transfer none) 

  
  # Generator::NullableArrayPlan
extents = if extents.nil?
Void.null
else
extents.to_unsafe
end
# Generator::CallerAllocatesPlan
extents=HarfBuzz::FontExtentsT.new
  # C call
  _retval = LibHarfBuzz.hb_ot_layout_get_font_extents(font, direction, script_tag, language_tag, extents)


  
  # Return value handling
  
  extents

end

Crystal version:

$ crystal --version

Crystal 1.8.2 (2023-05-11)

LLVM: 15.0.7
Default target: x86_64-pc-linux-gnu

Wrong generated signature for some `Harfbuzz` functions

Hello, thank you for this useful project.

I've pulled the new gtk4 version 0.13.0 and tried to compile helloworld demo after updating the generated binding against local libraries, but compiler gave some errors in some of the generated functions in harfbuzz library version 4.4.1 like

Dependencies are satisfied
Building: hello_gtk
Error target hello_gtk failed to compile:
Showing last frame. Use --error-trace for full trace.

In lib/gtk4/lib/gi-crystal/src/auto/harf_buzz-0.0/lib_harf_buzz.cr:443:35

 443 | fun hb_ft_font_create(ft_face : Void, destroy : Void*) : Pointer(Void)
                                       ^---
Error: can't use Void as parameter type

changing all Void reference to Void* abolished the error.

here are all functions

fun hb_ft_face_create(ft_face : Void, destroy : Void*) : Pointer(Void)
fun hb_ft_face_create_cached(ft_face : Void) : Pointer(Void)
fun hb_ft_face_create_referenced(ft_face : Void) : Pointer(Void)
fun hb_ft_font_create(ft_face : Void, destroy : Void*) : Pointer(Void)
fun hb_ft_font_create_referenced(ft_face : Void) : Pointer(Void)

thanks

Subclass type name contains invalid characters

While testing out GObject subclasses, I rather quickly got this error:

(process:29558): GLib-GObject-WARNING **: 15:40:37.670: type name 'Test::Object' contains invalid characters

(process:29558): GLib-CRITICAL **: 15:40:37.670: g_once_init_leave: assertion 'result != 0' failed

(process:29558): GLib-GObject-CRITICAL **: 15:40:37.670: g_object_newv: assertion 'G_TYPE_IS_OBJECT (object_type)' failed

(process:29558): GLib-GObject-CRITICAL **: 15:40:37.670: g_object_is_floating: assertion 'G_IS_OBJECT (object)' failed

(process:29558): GLib-GObject-CRITICAL **: 15:40:37.670: g_object_set_qdata: assertion 'G_IS_OBJECT (object)' failed

Code:

require "gtk4"

class Test::Object < GObject::Object
end

Test::Object.new

I assume colons are not allowed in GObject type names and need to be filtered out.

Cannot implement vfunc `Gtk::Widget#do_measure`

Trying to implement the vfunc measure results in a compile-time error:

Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro '_register_do_measure'

Code in macro 'method_added'

 3 | _register_do_measure
     ^
Called macro defined in lib/libadwaita/lib/gi-crystal/src/auto/gtk-4.0/widget.cr:5053:13

 5053 | private macro _register_do_measure

Which expanded to:

 > 20 |         raise GICrystal::ObjectCollectedError.new if __temp_46 || __temp_47.null?
 > 21 | 
 > 22 |         __temp_47.as(self).do_measure(orientation, for_size, minimum, natural, minimum_baseline, natural_baseline)
                                                                     ^------
Error: undefined local variable or method 'minimum' for Mangaba::UI::Description.class

I think this is because the last four arguments are of type Int32* as four Int32 should be returned using pointers.

Fail to compile with newer libraries.

Error:

In src/auto/g_object-2.0/g_object.cr:376:72

 376 | alias TypeValueCollectFunc = Proc(GObject::Value, UInt32, Enumerable(GObject::TypeCValue), UInt32, ::String)
                                                                            ^------------------
Error: undefined constant GObject::TypeCValue

Library versions:

gobject-introspection 1.78.1-1
glib2 2.78.0-2

`Gsk::RoundedRect#init` corrupts memory

The following code (just example code to create Gsk::RoundedRect):

require "gtk4"

loop do
  w = 100
  h = 100

  bounds = Graphene::Rect.new
  bounds.init(0.0f32, 0.0f32, w.to_f32, h.to_f32)

  corner = Graphene::Size.new
  corner.init(12.0f32, 12.0f32)

  rounded_rect = Gsk::RoundedRect.new
  rounded_rect.init(bounds, corner, corner, corner, corner)
end

Results in memory issues, depending on whether the GC is used or not.

If BDWGC is used:

[blobcodes@toolbox mangaba]$ crystal run src/test2.cr
Invalid memory access (signal 11) at address 0x0
[0x481b76] *Exception::CallStack::print_backtrace:Nil +118 in /var/home/blobcodes/.cache/crystal/crystal-run-test2.tmp
[0x470f7a] ~procProc(Int32, Pointer(LibC::SiginfoT), Pointer(Void), Nil) +330 in /var/home/blobcodes/.cache/crystal/crystal-run-test2.tmp
[0x7f3623488ac0] ?? +139870496918208 in /lib64/libc.so.6
Invalid memory access (signal 7) at address 0x0
[0x481b76] *Exception::CallStack::print_backtrace:Nil +118 in /var/home/blobcodes/.cache/crystal/crystal-run-test2.tmp
[0x470f7a] ~procProc(Int32, Pointer(LibC::SiginfoT), Pointer(Void), Nil) +330 in /var/home/blobcodes/.cache/crystal/crystal-run-test2.tmp
[0x7f3623488ac0] ?? +139870496918208 in /lib64/libc.so.6
Program received and didn't handle signal BUS (7)

..and if no GC is used:

⬢[blobcodes@toolbox mangaba]$ crystal run src/test2.cr -Dgc_none
malloc(): corrupted top size
Program received and didn't handle signal IOT (6)

While using this inside a vfunc, I also received this error:

corrupted size vs. prev_size
Program received and didn't handle signal IOT (6)

I tested on both release v0.11 and master, the behaviour doesn't change.

I think this may have something to do with RoundedRect having a fixed-size array in its struct:

  struct RoundedRect # 48 bytes long
    bounds : LibGraphene::Rect
    corner : Void[4]
  end

However: puts sizeof(Void[4]) # => 4

I think this must be Void*[4] instead. Maybe this happened because not enough space has been allocated.

EDIT: the correct awnser would have been LibGraphene::Size[4]

Issues with Proc & GObject::Callback

Hi, I'm having some issues with JavaScriptCore bindings (https://github.com/GeopJr/JavaScriptCore.cr):

  • Proc

For example, JavaScriptCore::ClassVTable#get_property= accepts JavaScriptCore::ClassGetPropertyFunction (which is an alias for Proc(JavaScriptCore::Class, JavaScriptCore::Context, Pointer(Void)?, JavaScriptCore::Value)) generates:

def get_property=(value : JavaScriptCore::ClassGetPropertyFunction)
      _var = (to_unsafe + 0).as(Pointer(Void*))
      _var.copy_from(value.to_unsafe, sizeof(LibJavaScriptCore::ClassVTable))
      value
end

but Proc#to_unsafe is not a method.

  • GObject::Callback

Almost all callbacks are of type GObject::Callback which I'm unsure how to work with - even after some manual editing, I couldn't get past Error: argument 'getter' of 'LibJavaScriptCore#jsc_class_add_property' must be Pointer(Void) or invalid memory access.

Example code for the first issue:

require "javascriptcore"

JavaScriptCore::ClassVTable.new(
    get_property: JavaScriptCore::ClassGetPropertyFunction.new { |x, y, _| JavaScriptCore::Value.new_null(y) }
)

I understand if this is out of the scope of gi-crystal and I'll probably end up writing bindings manually if closed (as the api is not that big) 😅

Also surprised getter and setter work as method parameters

Support to declare GObject properties

This is meant to be a discussion about the implementation of user declared GObject properties.

IMO it must be as transparent as possible, i.e. a property/setter/getter macro on GObject, overwriting the original Crystal Object macros.

class Foo < GObject::Object
  # Creates a read/write GObject property
  property foo : Int32
  # Creates a read only GObject property
  getter bar : Int32
  # Creates a write only GObject property
  setter moo : Int32 
  # Prop with blurb
  property hey : String, blurb: "I'm the property blurb"
  # Prop with cusom nick
  property nicked : String, nick: "Nickey"
end

The nick can be the name.titleize, or custom. Blurb could be default to name.titleize too

These macros would create the variables on Crystal world, so people could use @foo to read foo property without overhead.

A possible implementation for this would be the macro generate something like

@[GObject::Property(blurb: "Prop blurb"]
@foo : String

So a finalize macro would catch all instance variables with this annotation and generate the set_property/get_property methods plus the class_init that register the properties.

I didn't test this yet, but if it works we could use the same schema to vfuncs, to avoid the bug that forbid a method starting with do_ to be declared if no vfuncs with that name exists, so instead vfuncs can just be:

@[GObject::VFunc]
def do_whatever

# or

@[GObject::VFunc(name: :whatever)]
def whatever_name

But this is another subject 😁

Back to properties... other options for properties can also be added as extra parameters in the macro, e.g.:

property foo : Int32, min: 0, max: 32

Return types from vfuncs are not converted

(yes this is my fault)

Currently, return values from vfuncs implemented by GObject subclasses are not converted.
For example, if I want to implement the vfunc get_request_mode, I would normally assume I have to return a Gtk::SizeRequestMode. However, the current implementation wants me to return a UInt32.

I think we can copy the code used for conversion to c-types from the signal implementation (which is what I did for the vfunc parameters as well).

syntax error generating bindings?

Very first time I try to use this, I may well be doing something wrong.

fish❯ crystal init app cr_gtk4
    create  /.mnt/data/experiments/cr_gtk4/.gitignore
    create  /.mnt/data/experiments/cr_gtk4/.editorconfig
    create  /.mnt/data/experiments/cr_gtk4/LICENSE
    create  /.mnt/data/experiments/cr_gtk4/README.md
    create  /.mnt/data/experiments/cr_gtk4/shard.yml
    create  /.mnt/data/experiments/cr_gtk4/src/cr_gtk4.cr
    create  /.mnt/data/experiments/cr_gtk4/spec/spec_helper.cr
    create  /.mnt/data/experiments/cr_gtk4/spec/cr_gtk4_spec.cr
Initialized empty Git repository in /.mnt/data/experiments/cr_gtk4/.git/

...

fish❯ cat shard.yml
name: cr_gtk4
version: 0.1.0

authors:
  - your-name-here <your-email-here>

targets:
  cr_gtk4:
    main: src/cr_gtk4.cr

dependencies:
  gtk4:
    github: hugopl/gtk4.cr

crystal: 1.7.3

license: MIT
fish❯ shards install
Resolving dependencies
Fetching https://github.com/hugopl/gtk4.cr.git
Fetching https://github.com/hugopl/gi-crystal.git
Fetching https://github.com/hugopl/version_from_shard.git
Installing version_from_shard (1.2.5)
Installing gi-crystal (0.14.0)
Postinstall of gi-crystal: shards build
Installing gtk4 (0.12.0)
Writing shard.lock
fish❯ ./bin/gi-crystal
info - Starting at 2023-03-30 11:39:02 +02:00, project dir: /.mnt/data/experiments/cr_gtk4
info - Generating bindings at /.mnt/data/experiments/cr_gtk4/lib/gi-crystal/src/auto
info - Using binding config at /.mnt/data/experiments/cr_gtk4/lib/gi-crystal/src/bindings/g_object/binding.yml
info - Using binding config at /.mnt/data/experiments/cr_gtk4/lib/gi-crystal/src/bindings/g_lib/binding.yml
info - Using binding config at /.mnt/data/experiments/cr_gtk4/lib/gtk4/src/bindings/gio/binding.yml
info - Using binding config at /.mnt/data/experiments/cr_gtk4/lib/gtk4/src/bindings/harfbuzz/binding.yml
info - Using binding config at /.mnt/data/experiments/cr_gtk4/lib/gtk4/src/bindings/gsk/binding.yml
info - Using binding config at /.mnt/data/experiments/cr_gtk4/lib/gtk4/src/bindings/gtk/binding.yml
info - Using binding config at /.mnt/data/experiments/cr_gtk4/lib/gtk4/src/bindings/gdk/binding.yml
info - Using binding config at /.mnt/data/experiments/cr_gtk4/lib/gtk4/src/bindings/pango/binding.yml
warn - g_cancellable_connect - Callback without user_data!
warn - Gio::ActionEntry padding field - Unknown conversion to crystal for fixed size array.
warn - Gio::DBusInterfaceVTable padding field - Unknown conversion to crystal for fixed size array.
warn - Gio::DBusSubtreeVTable padding field - Unknown conversion to crystal for fixed size array.
info - HarfBuzz - No binding config found for freetype2-2.0.
warn - HarfBuzz - Interface constant not supported.
info - Gsk - No binding config found for Graphene-1.0.
warn - gsk_border_node_get_widths - Unknown conversion to crystal for fixed size array.
info - gsk_render_node_draw - No binding config found for cairo-1.0.
info - Gdk - No binding config found for PangoCairo-1.0.
info - Gdk - No binding config found for GdkPixbuf-2.0.
warn - Gdk - SeatCapabilities::All (0xf) doesn't have all possible bits set (0x1f).
warn - Gdk::TimeCoord axes field - Unknown conversion to crystal for fixed size array.
info - GdkPixbuf - No binding config found for GModule-2.0.
warn - gdk_pixbuf_get_options - Unknown conversion to crystal for GHash
warn - Boxed not working for enums
warn - Gtk::BuildableParser padding field - Unknown conversion to crystal for fixed size array.
syntax error in './harf_buzz-0.0/harf_buzz.cr:3371:15': can't use variable name 'unicodes' inside assignment to variable 'unicodes'
fatal - Error formating generated files at '/.mnt/data/experiments/cr_gtk4/lib/gi-crystal/src/auto'.

The syntax error code is this (in ./lib/gi-crystal/src/auto/harf_buzz-0.0/harf_buzz.cr):

3361   │ def self.face_collect_nominal_glyph_mapping(face : HarfBuzz::FaceT,) : HarfBuzz::MapT
3362   │   # hb_face_collect_nominal_glyph_mapping: (None)
3363   │ # @face:
3364   │ # @mapping: (out) (caller-allocates)
3365   │ # @unicodes: (out) (nullable) (caller-allocates)
3366   │ # Returns: (transfer none)
3367   │
3368   │
3369   │   # Generator::CallerAllocatesPlan
3370   │ mapping=HarfBuzz::MapT.new# Generator::NullableArrayPlan
3371   │ unicodes = if unicodes.nil?
3372   │ Void.null
3373   │ else
3374   │ unicodes.to_unsafe
3375   │ end
3376   │ # Generator::CallerAllocatesPlan
3377   │ unicodes=HarfBuzz::SetT.new
3378   │   # C call
3379   │   LibHarfBuzz.hb_face_collect_nominal_glyph_mapping(face, mapping, unicodes)
3380   │
3381   │
3382   │
3383   │   # Return value handling
3384   │
3385   │   mapping
3386   │
3387   │ end

but it looks correct to me, and I could not reproduce the syntax error with similar code by itself.
Let me know if I should provide more info or do some tests, it's all a bit over my head but I want this to work and I have time on my hands.

edit: looks like it may be related to #53

Error when using multiple gi-crystal shards

When requiring both libadwaita and gtk4 this happens:

In lib/libshumate/lib/gi-crystal/src/auto/g_lib-2.0/lib_g_lib.cr:5:8

 5 | type AsciiType = UInt32
          ^
Error: AsciiType is already defined

This also happened when I tried to generate my own bindings for libshumate. I'm not sure if this is a bug or if I'm just using the generator wrong. Is there something I need to do to make sure I'm not importing 2 copies of the base gobject stuff?

`intern` returned as `GObject::ParamSpec`s `object.type_init`

The following code:

class Test < GObject::ParamSpec
end

Causes a linker error:

/usr/bin/ld: T-est.o: in function `g_type':
/var/mnt/media/Projects/Other/mangaba/lib/gtk4/lib/gi-crystal/src/auto/g_object-2.0/param_spec.cr:73: undefined reference to `intern'
collect2: error: ld returned 1 exit status
Error: execution of command failed with code: 1: `cc "${@}" -o /var/home/blobcodes/.cache/crystal/crystal-run-test2.tmp  -rdynamic -L/usr/lib64/crystal -lgtk-4 -lgio-2.0 -lgobject-2.0 -lglib-2.0  -lgobject-2.0 -lglib-2.0  -lglib-2.0  -lgobject-2.0 -lglib-2.0  -lpcre -lm -lgc -lpthread -levent  -lrt -lpthread -ldl`

This is because object.type_init returns the string "internal" for GObject::ParamSpec, leading to this code:

    # Returns the type id (GType) registered in GLib type system.
    def self.g_type : UInt64
      LibGObject.intern
    end

AFAICT this seems to be a special value, in which case there should just be a compiler error.
I have no real use-case where this matters but this issue could show errors in the cooperation with gobject-introspection.

Generated code expects incorrect type for Gio::AsyncReadyCallback

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.

Getters & Setters broke after the revert

Hi, I was working on libadwaita bindings and was using gi-crystal of 5a3ca15.
After updating to b696535 getters and setters of Adw broke.

Eg. in Adw::ApplicationWindow, the following got added:

def content=(value : Widget?) : Widget?
      unsafe_value = value

      LibGObject.g_object_set(self, "content", unsafe_value, Pointer(Void).null)
      value
    end

    def content : Widget?
      # Returns: None

      value = uninitialized Pointer(Void)
      LibGObject.g_object_get(self, "content", pointerof(value), Pointer(Void).null)
      Gtk::Widget.new(value, GICrystal::Transfer::None) unless value.null?
    end

but when running one of the examples (or specs) I get the following error:

In src/auto/adw-1/application_window.cr:360:26

 360 | def content=(value : Widget?) : Widget?
                            ^-----
Error: undefined constant Widget

In this case/libadwaita's chaning all instances of Widget to Gtk::Widget fixes it. It's not really an issue for libadwaita.cr since gi-crystal is locked to 5a3ca15 and the bindings are not being generated on demand, but thought I should create this issue just in case.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.