Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
5 changes: 3 additions & 2 deletions ecr/object.ecr
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ module <%= namespace_name %>
end
<% elsif class_struct %>
# :nodoc:
def self._register_derived_type(class_name : String, class_init, instance_init)
def self._register_derived_type(class_name : String, class_init, instance_init, flags : GObject::TypeFlags = GObject::TypeFlags::None)
LibGObject.g_type_register_static_simple(g_type, class_name,
sizeof(<%= to_lib_type(class_struct) %>), class_init,
sizeof(<%= to_lib_type(object) %>), instance_init, 0)
sizeof(<%= to_lib_type(object) %>), instance_init,
flags)
end
<% end %>

Expand Down
61 changes: 61 additions & 0 deletions spec/c_born_crystal_objects_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
require "./spec_helper"

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

def initialize
super
end
end

private class InheritedUserObj < UserObj
@[GObject::Property]
property crystal_prop2 = ""

def initialize
super
end
end

private abstract class AbstractUserObj < GObject::Object
end

private class NonAbstractUserObj < AbstractUserObj
end

private class NonDefaultCtorObj < GObject::Object
def initialize(int)
super()
end
end

describe "Crystal GObjects" do
it "can born in C land" do
ptr = LibGObject.g_object_new(UserObj.g_type, "crystal_prop1", "value", Pointer(Void).null)
user_obj = UserObj.new(ptr, :none)
user_obj.crystal_prop1.should eq("value")
user_obj.crystal_attr.should eq(42)
user_obj.ref_count.should eq(1)
end

it "works with types hierarchy" do
ptr = LibGObject.g_object_new(InheritedUserObj.g_type, "crystal_prop1", "value1", "crystal_prop2", "value2", Pointer(Void).null)
user_obj = UserObj.new(ptr, :none)
user_obj.crystal_prop1.should eq("value1")
inherited_obj = InheritedUserObj.cast?(user_obj)
inherited_obj.should_not eq(nil)
inherited_obj.not_nil!.crystal_prop2.should eq("value2")
end

it "works with abstract classes in hierarchy" do
NonAbstractUserObj.new
end

it "doesn't require classes to have a default cosntructor" do
NonDefaultCtorObj.new(42)
# The line bellow must issue an GLib error and abort.
# LibGObject.g_object_new_with_properties(NonDefaultCtorObj.g_type, 0, Pointer(Pointer(UInt8)).null, Pointer(LibGObject::Value).null)
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
2 changes: 2 additions & 0 deletions src/bindings/g_lib/lib_g_lib.cr
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ lib LibGLib
# To work with old and newer GLibs, these functiosn are lib-ignored and added here manually.
fun g_once_init_enter(location : Pointer(Void)) : LibC::Int
fun g_once_init_leave(location : Pointer(Void), result : UInt64) : Void

fun g_log(log_domain : LibC::Char*, log_level : Int32, format : LibC::Char*, ...)
end
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
78 changes: 69 additions & 9 deletions src/bindings/g_object/object.cr
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ module GObject

class Object
macro inherited
# :nodoc
def self._create_obj_through_default_constructor : Pointer(Void)
LibGLib.g_log("GICrystal", 4,
{{ "Tried to create an instance of #{@type} from C, but #{@type} doesn't have a default constructor." }})
Pointer(Void).null
end

{% unless @type.annotation(GICrystal::GeneratedWrapper) %}
macro method_added(method)
{% verbatim do %}
Expand All @@ -71,6 +78,13 @@ module GObject
{% end %}
_register_{{ vfunc_name.id }}_vfunc({{ method.name }})
{% end %}

{% if method.name == "initialize" && (method.args.empty? || method.args.all?(&.default_value)) %}
# :nodoc
def self._create_obj_through_default_constructor : Pointer(Void)
{{ @type }}.new.as(Void*)
end
{% end %}
{% end %}
end

Expand All @@ -86,9 +100,17 @@ module GObject
if LibGLib.g_once_init_enter(pointerof(@@_g_type)) != 0
g_type = {{ @type.superclass.id }}._register_derived_type("{{ @type.name.gsub(/::/, "-") }}",
->_class_init(Pointer(LibGObject::TypeClass), Pointer(Void)),
->_instance_init(Pointer(LibGObject::TypeInstance), Pointer(LibGObject::TypeClass)))

->_instance_init(Pointer(LibGObject::TypeInstance), Pointer(LibGObject::TypeClass)),
{% if @type.abstract? %}
GObject::TypeFlags::Abstract,
{% end %}
)
LibGLib.g_once_init_leave(pointerof(@@_g_type), g_type)

{% unless @type.abstract? %}
ctor = ->_create_obj_through_default_constructor
LibGObject.g_type_set_qdata(g_type, GICrystal::INSTANCE_USERTYPE_FACTORY, ctor.pointer)
{% end %}
self._install_ifaces
end

Expand Down Expand Up @@ -128,6 +150,32 @@ module GObject
{% end %}
end

# :nodoc:
#
# GObject instance initialization, creates the Crystal instance if there's no one created yet.
def self._instance_init(instance : Pointer(LibGObject::TypeInstance), type : Pointer(LibGObject::TypeClass)) : Nil
# Return if the Crystal instance is already set up.
crystal_instance = LibGObject.g_object_get_qdata(instance, GICrystal::INSTANCE_QDATA_KEY)
return if crystal_instance

# Check if this was called from a Crystal constructor
crystal_instance = GICrystal.crystal_object_being_created
# If not, this comes from a C call, so a Crystal instance needs to be created, however
{% unless @type.abstract? %}
crystal_instance ||= GICrystal.create_user_type_from_c_instance(instance, type)
{% end %}

# Now we have a Crystal object instance, let's set it up:
# - Set the INSTANCE_QDATA_KEY, so if someone read a property the get_property callback can
# know what's the Crystal instance.
# - Set the Crystal instance @pointer variable, so Crystal code can run without a dangling pointer.
if crystal_instance
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
end
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 +470,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 +496,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.

hugopl marked this conversation as resolved.
Show resolved Hide resolved
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 +647,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 +666,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
36 changes: 35 additions & 1 deletion src/gi-crystal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ module GICrystal
# See `declare_new_method`.
INSTANCE_FACTORY = LibGLib.g_quark_from_static_string("gi-crystal::factory")

# GICrystal stores the type constructor pointer as a qdata in GType, so when C code request
# the creation of a Type, the instance_init of the base type can create the right Crystal
# instance.
#
# `INSTANCE_FACTORY` OTOH stores the constructor that receives a pointer and the transfer mode,
# it's used when C code returns an base type but we need to create a wrapper for the right type.
INSTANCE_USERTYPE_FACTORY = LibGLib.g_quark_from_static_string("gi-crystal::ut-factory")

# Raised when trying to cast an object that was already collected by GC.
class ObjectCollectedError < RuntimeError
end
Expand Down Expand Up @@ -119,6 +127,32 @@ 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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could cause the application to crash as ThreadLocal values aren't tracked by the GC

Also, having a global value for constructing objects seems very hacky.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is very hacky :-P.

BTW I didn't know that about thread local variables in Crystal, thanks.

class_property g_object_being_created : Pointer(Void) = Pointer(Void).null

@[ThreadLocal]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

class_property object_g_type_being_created : UInt64 = 0

# 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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

class_property crystal_object_being_created : Pointer(Void) = Pointer(Void).null

# This is used on `_instance_init` functions of user defined GObjects to create Crystal instances when the
# object was created from C code, i.e. `g_object_new`.
def create_user_type_from_c_instance(instance : Pointer(LibGObject::TypeInstance), type : Pointer(LibGObject::TypeClass)) : Pointer(Void)
ctor_ptr = LibGObject.g_type_get_qdata(type.value.g_type, GICrystal::INSTANCE_USERTYPE_FACTORY)
return Pointer(Void).null unless ctor_ptr

# Set the g_object_being_created, so the Crystal code wont call g_object_new again.
GICrystal.g_object_being_created = instance.as(Void*)
Proc(Void*).new(ctor_ptr, Pointer(Void).null).call
ensure
GICrystal.g_object_being_created = Pointer(Void).null
end

# 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 All @@ -142,7 +176,7 @@ module GICrystal
ctor_ptr = LibGObject.g_type_get_qdata(instance_g_type, GICrystal::INSTANCE_FACTORY)
if ctor_ptr
ctor = Proc(Void*, GICrystal::Transfer, {{ type }}).new(ctor_ptr, Pointer(Void).null)
return ctor.call(pointer, transfer)
return ctor.call(pointer, transfer).as({{ type }})
end
end

Expand Down