From 6b4601cee6c4ee22372b43c28c97d5030e7e2ada 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 --- spec/c_born_crystal_objects_spec.cr | 26 +++++++++++++++++++ spec/gc_spec.cr | 5 ++++ spec/inheritance_spec.cr | 9 +++++++ src/bindings/g_object/binding.yml | 1 + src/bindings/g_object/object.cr | 39 +++++++++++++++++++++++------ src/generator/method_gen.cr | 3 ++- src/gi-crystal.cr | 9 +++++++ 7 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 spec/c_born_crystal_objects_spec.cr diff --git a/spec/c_born_crystal_objects_spec.cr b/spec/c_born_crystal_objects_spec.cr new file mode 100644 index 0000000..9f68e70 --- /dev/null +++ b/spec/c_born_crystal_objects_spec.cr @@ -0,0 +1,26 @@ +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 + puts "\n\n\n" + 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) + end + + it "can born in Crystal land" do + puts "\n\n\n" + user_obj = UserObj.new + 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..527b970 100644 --- a/src/bindings/g_object/object.cr +++ b/src/bindings/g_object/object.cr @@ -128,6 +128,30 @@ 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*) + 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 +446,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 +472,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 +623,15 @@ 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 + + LibGObject.g_object_set_qdata(self, GICrystal::INSTANCE_QDATA_KEY, self.as(Void*)) 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 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. #