Skip to content

Commit

Permalink
Let Crystal user objects be created from g_gobject_new calls.
Browse files Browse the repository at this point in the history
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 hugopl/gtk4.cr#69
  • Loading branch information
hugopl committed Jun 24, 2024
1 parent 9d9aced commit 14b81ef
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 10 deletions.
4 changes: 2 additions & 2 deletions ecr/gobject_constructor.ecr
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 21 additions & 0 deletions spec/c_born_crystal_objects_spec.cr
Original file line number Diff line number Diff line change
@@ -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)

Check failure on line 18 in spec/c_born_crystal_objects_spec.cr

View workflow job for this annotation

GitHub Actions / tests

got: 0
user_obj.ref_count.should eq(1)
end
end
5 changes: 5 additions & 0 deletions spec/gc_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions spec/inheritance_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/bindings/g_object/binding.yml
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,4 @@ types:
execute_callback:
- g_signal_emitv
- g_closure_invoke
- g_object_newv
52 changes: 45 additions & 7 deletions src/bindings/g_object/object.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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 %}
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/generator/method_gen.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/gi-crystal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down

0 comments on commit 14b81ef

Please sign in to comment.