hugopl / gi-crystal

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

Let Crystal user objects be created from g_gobject_new calls. #153

Open hugopl opened 2 months ago

hugopl commented 2 months ago

This is a breaking change, since it now requires all GObject subclasses to have a constructor without arguments.

What changed:

There are 2 thread local variables to inform if the object is being created in C land or Crystal land, we store the objects instance pointers there.

When the object is created in C land, the Crystal instance is created on GObject instance_init method.

When the object is created in Crystal land, the GObject instance_init method creates no Crystal instance.

This also fixes the use case of tryign to use a Crystal defined GObject property before the Wrapper gets fully initialized.

Fixes https://github.com/hugopl/gtk4.cr/issues/69

hugopl commented 2 months ago

Weird... the tests don't fail on me locally.

hugopl commented 2 months ago

Tests now pass... but I noticed something that may be a compiler bug, if I change the class on c_born_crystal_objects_spec.cr to

private class UserObj < GObject::Object
  @[GObject::Property]
  property crystal_prop = ""
  getter crystal_attr : Int32 = 42

  def initialize(@hey = "")
    super()
  end
end

It doesn't initialize crystal_attr to 42 and the test fail.

hugopl commented 2 months ago

@BlobCodes , I remember you wanted this feature years ago, and it was only possible due to your work on toggle refs. If you have time, at your own pace, could you take a look on this and give me some feedback? Thanks!

hugopl commented 2 months ago

This MR fails to compile with abstract classes, mainly because GICrystal doesn't set any flags in the generated C Type if the Crystal class is abstract.

hugopl commented 2 months ago

Hmmm, CI was failing with old GObject libs, after updating the CI things start passing.

With the current patch things works as expected, but there's the breaking change that requires Crystal GObjects to define initialize(), after few minutes porting Tijolo to use this patch I saw how annoying is this, so I'll try to avoid this breaking change, i.e. If the type doesn't have a default constructor it cannot be created from C.

The challenge will be able to provide a good compiler error message in these cases.

hugopl commented 2 months ago

I believe now it's ready to be merged. And it's no more a breaking change, since it doesn't require the Crystal object to have a default constructor. However I'll only merge this after I start using Tijolo with this for few days.

BlobCodes commented 2 months ago

@BlobCodes , I remember you wanted this feature years ago, and it was only possible due to your work on toggle refs. If you have time, at your own pace, could you take a look on this and give me some feedback? Thanks!

It seems like you are initializing the entire object inside the _instance_init hook, which has a few disadvantages.

There are multiple steps to the initialization of a GObject:

For example, if our GObject is a templated Gtk::Widget, the template is initialized during Gtk::Widgets constructed vfunc. This means that there is currently a big behavioral difference between objects created from C or from Crystal: Objects created from Crystal already have their template initialized, while Objects created from C aren't allowed to query for template objects yet.


I think we should do the following to better match GObjects behaviour:

hugopl commented 2 months ago

When I started with the idea of always initialize the Crystal instance in the instance_init, but I guess I was not able to make custom constructors work, now I see that I can just use the method_added macro we already have and add a self.new everytime I see a def initialize.

I felt something was wrong and I was unsure of how many time my code was setting the qdata depending on the 3 use cases of object construction.

Because I forgot that I would write a new method, I had only the self.new() available, so the use case Foo.new(prop: value) always have the Crystal instance first... anyway, writing a new method for each initialize found on that macro will reduce this to the base 1 use case.

Thanks for the feed back! I'll re-work on this patch and ping you when there's something worth to double check.

hugopl commented 1 month ago

I think we should do the following to better match GObjects behaviour:

  • Create a protected def initialize in GObject, which calls the previous constructed vfunc This would allow a super() call to initialize the inherited GObject, matching normal crystal behaviour
  • Automatically map any initialize params to CONSTRUCT_ONLY GObject parameters

I don't like this, IMO it's ok to have constructor params that has nothing to do with GObject properties. I also don't think is useful to support construct only Crystal gobject properties, but I may be wrong.

  • Only initialize memory of crystal object in _instance_init, call initialize inside vfunc constructed

This is the problem, I want to allow the user to write any possible constructor, so on constructed vfunc I would need to known what Crystal constructor was called, so I could do the correct initialize call. IIRC this is why the Crystal instance is created in more than one place, something I agree is bad... maybe I can fix that creating closures that call the correct initialize func, but it would involve the use of hacky thread local variables again.

  • Always create crystal GObjects through C, instead of having two separate flows

Maybe my requirements for the desired API are too high.