From 14b81ef139d852c09762127adb29e0f129766615 Mon Sep 17 00:00:00 2001 From: Hugo Parente Lima Date: Sun, 23 Jun 2024 23:33:21 -0300 Subject: [PATCH] Let Crystal user objects be created from g_gobject_new calls. 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 --- ecr/gobject_constructor.ecr | 4 +-- spec/c_born_crystal_objects_spec.cr | 21 ++++++++++++ spec/gc_spec.cr | 5 +++ spec/inheritance_spec.cr | 9 +++++ src/bindings/g_object/binding.yml | 1 + src/bindings/g_object/object.cr | 52 +++++++++++++++++++++++++---- src/generator/method_gen.cr | 3 +- src/gi-crystal.cr | 9 +++++ 8 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 spec/c_born_crystal_objects_spec.cr diff --git a/ecr/gobject_constructor.ecr b/ecr/gobject_constructor.ecr index 80d8e02..7ce15e6 100644 --- a/ecr/gobject_constructor.ecr +++ b/ecr/gobject_constructor.ecr @@ -11,12 +11,12 @@ def initialize(<%= gobject_constructor_parameter_declaration %>) end <% end %> + GICrystal.crystal_object_being_created = self.as(Void*) ptr = LibGObject.g_object_new_with_properties(self.class.g_type, _n, _names, _values) + LibGObject.<%= object.qdata_set_func %>(ptr, GICrystal::INSTANCE_QDATA_KEY, Pointer(Void).new(object_id)) super(ptr, :full) _n.times do |i| LibGObject.g_value_unset(_values.to_unsafe + i) end - - LibGObject.<%= object.qdata_set_func %>(@pointer, GICrystal::INSTANCE_QDATA_KEY, Pointer(Void).new(object_id)) end diff --git a/spec/c_born_crystal_objects_spec.cr b/spec/c_born_crystal_objects_spec.cr new file mode 100644 index 0000000..e8a6ae6 --- /dev/null +++ b/spec/c_born_crystal_objects_spec.cr @@ -0,0 +1,21 @@ +require "./spec_helper" + +private class UserObj < GObject::Object + @[GObject::Property] + property crystal_prop = "" + getter crystal_attr : Int32 = 42 + + def initialize(@hey = "") + super() + end +end + +describe "Crystal GObjects" do + it "can born in C land" do + ptr = LibGObject.g_object_new(UserObj.g_type, "crystal_prop", "value", Pointer(Void).null) + user_obj = UserObj.new(ptr, :none) + user_obj.crystal_prop.should eq("value") + user_obj.crystal_attr.should eq(42) + user_obj.ref_count.should eq(1) + end +end diff --git a/spec/gc_spec.cr b/spec/gc_spec.cr index 8117c7d..c6d3fd6 100644 --- a/spec/gc_spec.cr +++ b/spec/gc_spec.cr @@ -3,6 +3,11 @@ require "./spec_helper" private class GCResistantObj < GObject::Object property moto : String + def initialize + super + @moto = "" + end + def initialize(@moto) super() end diff --git a/spec/inheritance_spec.cr b/spec/inheritance_spec.cr index 64e047d..096eab5 100644 --- a/spec/inheritance_spec.cr +++ b/spec/inheritance_spec.cr @@ -10,9 +10,18 @@ private class UserObjectWithCtor < GObject::Object def initialize(@string : String) super() end + + def initialize + super + @string = "" + end end private class UserSubject < Test::Subject + def initialize + super + end + def initialize(string : String) super(string: string) end diff --git a/src/bindings/g_object/binding.yml b/src/bindings/g_object/binding.yml index 37ab978..8c8b24a 100644 --- a/src/bindings/g_object/binding.yml +++ b/src/bindings/g_object/binding.yml @@ -279,3 +279,4 @@ types: execute_callback: - g_signal_emitv - g_closure_invoke + - g_object_newv diff --git a/src/bindings/g_object/object.cr b/src/bindings/g_object/object.cr index 4a47838..794cca1 100644 --- a/src/bindings/g_object/object.cr +++ b/src/bindings/g_object/object.cr @@ -128,6 +128,32 @@ module GObject {% end %} end + # :nodoc: + # + # GObject instance initialization, this can be called when a GObject is created from Crystal `MyObj.new` or by + # C `g_object_new(MyObject.g_type)`, in both cases some tasks are always done here: + # + # - INSTANCE_QDATA_KEY is always set here, so Crystal properties can be fetched. + # - Floating refs are sank + # + # So, if the object is created from C + # + # - Set `GICrystal.g_object_being_created` with the C object instance pointer. + # - Call the Crystal object constructor, that will look the flag set above and use it instead of call `g_object_new`. + # + # If the object is created from Crystal + # + # - Set `GICrystal.crystal_object_being_created` with the Crystal object instance pointer. + # - Use the Crystal object instead of calling the Crystal object constructor. + def self._instance_init(instance : Pointer(LibGObject::TypeInstance), type : Pointer(LibGObject::TypeClass)) : Nil + GICrystal.g_object_being_created = instance.as(Void*) + crystal_instance = GICrystal.crystal_object_being_created || {{ @type }}.new.as(Void*) + crystal_instance.as(GObject::Object)._gobj_pointer = instance.as(Void*) + LibGObject.g_object_set_qdata(instance, GICrystal::INSTANCE_QDATA_KEY, crystal_instance) + GICrystal.crystal_object_being_created = Pointer(Void).null + GICrystal.g_object_being_created = Pointer(Void).null + end + # :nodoc: def self._g_toggle_notify(object : Void*, _gobject : Void*, is_last_ref : Int32) : Nil return if object.null? @@ -422,10 +448,6 @@ module GObject {% end %} end - # :nodoc: - def self._instance_init(instance : Pointer(LibGObject::TypeInstance), type : Pointer(LibGObject::TypeClass)) : Nil - end - # :nodoc: def self._install_ifaces {% verbatim do %} @@ -452,7 +474,6 @@ module GObject # This specific implementation turns a normal reference into a toggle reference. private def _after_init : Nil # Set toggle ref to protect the crystal object from the garbage collector while in C. - self.class._g_toggle_notify(self.as(Void*), @pointer, 0) LibGObject.g_object_add_toggle_ref(@pointer, G_TOGGLE_NOTIFY__, self.as(Void*)) LibGObject.g_object_unref(@pointer) @@ -604,9 +625,16 @@ module GObject end def initialize - @pointer = LibGObject.g_object_newv(self.class.g_type, 0, Pointer(LibGObject::Parameter).null) + GICrystal.crystal_object_being_created = Pointer(Void).new(object_id) + + g_object = GICrystal.g_object_being_created + @pointer = g_object || LibGObject.g_object_newv(self.class.g_type, 0, Pointer(LibGObject::Parameter).null) + GICrystal.g_object_being_created = Pointer(Void).null + + # If object is created by C, the qdata was already set. + LibGObject.g_object_set_qdata(self, GICrystal::INSTANCE_QDATA_KEY, self.as(Void*)) unless g_object LibGObject.g_object_ref_sink(self) if LibGObject.g_object_is_floating(self) == 1 - LibGObject.g_object_set_qdata(self, GICrystal::INSTANCE_QDATA_KEY, Pointer(Void).new(object_id)) + self._after_init end @@ -616,6 +644,16 @@ module GObject self._after_init end + # :nodoc: + # Set the internal GObject pointer. + # + # When creating crystal objects using property constructors that set Crystal properties we must + # set the @pointer in the GObject instance_init method, because at this point the Crystal instance + # was already created but is inside a call of `g_object_new_with_properties` and would only set the + # @pointer after it returns, however the @pointer is needed to write the properties. + def _gobj_pointer=(@pointer) + end + # Returns GObject reference counter. def ref_count : UInt32 to_unsafe.as(Pointer(LibGObject::Object)).value.ref_count diff --git a/src/generator/method_gen.cr b/src/generator/method_gen.cr index 833d721..1fc8b25 100644 --- a/src/generator/method_gen.cr +++ b/src/generator/method_gen.cr @@ -87,7 +87,8 @@ module Generator private def method_return_type_declaration : String if @method.flags.constructor? - return @method.may_return_null? ? ": self?" : ": self" + type = to_crystal_type(object.as(RegisteredTypeInfo)) + return @method.may_return_null? ? ": #{type}?" : ": #{type}" end return_type = method_return_type diff --git a/src/gi-crystal.cr b/src/gi-crystal.cr index 3d85d06..967d820 100644 --- a/src/gi-crystal.cr +++ b/src/gi-crystal.cr @@ -119,6 +119,15 @@ module GICrystal end end + # When creating an user defined GObject from C, the GObject instance is stored here, so the Crystal + # constructor uses it instead of call `g_object_new` + @[ThreadLocal] + class_property g_object_being_created : Pointer(Void) = Pointer(Void).null + # When creating an user defined GObject from Crystal, the Crystal instance is stored here, so the + # GObject `instance_init` doesn't instantiate another Crystal object. + @[ThreadLocal] + class_property crystal_object_being_created : Pointer(Void) = Pointer(Void).null + # This declare the `new` method on a instance of type *type*, *qdata_get_func* (g_object_get_qdata) is used # to fetch a possible already existing Crystal object. #