Multiple macro inherited

Hi,

I’m having problems with inherited macro, does it runs just once for a type hierarchy?

I want to define an inherited macro in 2 classes in the hierarchy and have them run in the third class. To avoid the macro to run in the first 2 I use a annotation.

Example, C inherits B that inherits A, B and A has a macro inherited, When C in born to the compiler I was expecting the inherited macro defined in A and then the inherited macro defined in B to run, but just the macro from A really generates code. Weird is that if I introduce some syntax error in the code generated by the inherited macro defined in type B the compiler detects it, so it seems the macro is being executed but generating nothing? Is this a compiler bug or am I missing something?

annotation Skip
end

@[Skip]
class A
  macro inherited
    {% unless @type.annotation(Skip) %}
    def foo
       puts "A"
    end
    {% end %}
  end
end

@[Skip]
class B < A
  macro inherited
    {% unless @type.annotation(Skip) %}
    def foo
      previous_def
      puts "B"
    end
    {% end %}
  end
end

class C < B
end

C.new.foo

I was expecting this to print:

A
B

But it just prints

A

Why I want this? I have a type annotation defined in one shard that generated some stuff, but in another shard I want to extend this annotation with more parameters that add more stuff in a series of functions defined with previous_def calls. yes, GTK stuff :grin:

In your example, A’s inherited macro is called for B which is skipped, then B’s inherited macro is called for C which defines B#foo on C, then A’s inherited macro is called again on C which defines A#foo on C. This final definition overrides the one made by B which is why “A” is printed.

Thanks, you are right, if I add a previous_def call to foo definition in A inherited macro it works…

so the issue is just the ordering with the macros are run in this code. Thanks!

1 Like

But now I have a new problem… I want be able to write

class D < A
end

D.new.foo

and let it print A, but if I do this (with the previous code fixed) I get an error because there will be no previous definition of foo

I believe you would need a separate annotation to apply to the subclasses. Out of curiosity, why are you using macros for inheritance control? If D defines its own foo method it would be separate to A#foo unless the signatures are identical, in which case D#foo overrides the former.

Maybe it’s a a lot of detail if you aren’t used to GObject type system, but here it goes anyway:

I’m writing a binding generator for GObject based libraries and for GTK4 itself, GObject has properties, nowadays you can create a Crystal object that inherits a GObject and use an annotation to let it register this property on GObject.

class Foo < GObject::Object
  @[GObject::Property]
  property my_prop : Int32 = 0
end

You can pass the object above to a C function call and it will recognize the my_prop property.

How it works

In GObject C world this is done at type registration time (class_init functions), in the bindings when a type is registered in the GObject type system a class method named _class_init is called and scan the object instance variables with the Property annotation to do the right C function calls to register the properties in the object.

So far so good, but in GTK there’s a function named gtk_widget_class_add_binding_action, this function adds a gtk_widget_class_install_property_action that creates a GTK action for a GObject property, so when the action is triggered in the application the property value changes.

The problem is that this C function also needs to be called in the _class_init function, it’s in another shard and the worst… I would like to implement this as a parameter anotation in the same Property annotation, but this extra parameter would be handled by the GTK4 shard, not by the gi-crystal shard that has the GLib and GObject bindings.

I wanted this code:

# GObject::Object is part of Gtk::Widget hierarchy
class Foo < Gtk::Widget
  @[GObject::Property(action: "my_action")
  property bar : Int32 = 0
end

to generate

class Foo
    # generated by an inherited macro on GObject::Object
  def _class_init(...)
    # register the property, code created by a macro in GObject
  end

  # generated by an inherited macro on Gtk::Widget
  def _class_init(...)
    LibGtk.gtk_widget_class_install_property_action(...)
    previous_def
  end
end

GObject::Object uses a lot the previous_def to create a chain of _class_init function calls, because different features of GObject library need to call functions at class initialization time (a.k.a. class_init).

For example, when people do:

class Bar < Gtk::Widget
  include Gtk::Editable
end

this also creates a _class_init implementation telling GObject type system that this types implements this module, such implementation have a previous_def call, so all _class_init are called in the case of a type implement multiple interface or simple has multiple properties. The same for GObject virtual functions implementation in the bindings.

If everything gets implemented in GObject::Object all works, but I’ll be messing up hard with shard boundaries and this isn’t an option.

Anyway, seems a nice problem to solve.

Ah, the classes generated by the generator, i.e. GObject::Object and Gtk::Widget have an annotation, the equivalent of the Skip annotation in the code snippet, so I can do all this machinery only for user types, not for types generated by the binding generator.

A previous_def? macro would solve the problem

Since it doesn’t exist I solved the issue with a bit of runtime code, creating a simple linked list with the Procs that must be called.

annotation Skip
end

annotation Property
end

class ClassInitChain
  property class_init : Proc(Nil)?
  property next : ClassInitChain?

  def initialize(@class_init = nil, @next = nil)
  end

  def prepend(class_init : Proc(Nil))
    if @class_init.nil?
      @class_init = class_init
    else
      new_next = ClassInitChain.new(@class_init, @next)
      @class_init = class_init
      @next = new_next
    end
  end

  def call
    @class_init.try(&.call)
    next_init = @next
    while next_init
      next_init.class_init.try(&.call)
      next_init = next_init.next
    end
  end
end

@[Skip]
class Obj
  macro property(var)
    @[Property]
    @{{ var }}
  end

  def self.prepend_class_init(init : Proc(Nil))
    @@class_init_chain.try(&.prepend(init))
  end

  macro inherited
    {% unless @type.annotation(Skip) %}
    puts "Obj#inherited macro on #{{{ @type.name }}}"

    def self.%obj_init
      {% verbatim do %}
      puts {{ "Obj#inherited: " + (@type.name + ".class_init").stringify }}
      print "properties: "
      puts {{ @type.instance_vars.select(&.annotation(Property)).stringify }}
      {% end %}

    end

    @@class_init_chain : ClassInitChain? = ClassInitChain.new
    prepend_class_init(->%obj_init)

    def self.class_init
      @@class_init_chain.try(&.call)
      @@class_init_chain = nil # free some bytes
    end
    {% end %}
  end
end

@[Skip]
class Widget < Obj
  macro inherited
    {% unless @type.annotation(Skip) %}
    puts "Widget#inherited macro on #{{{ @type.name }}}"

    def self.%widget_init
      puts {{ "Widget#inherited: " + (@type.name + ".class_init").stringify }}
    end
    prepend_class_init(->self.%widget_init)

    {% end %}
  end
end

class UserWidget < Widget
  property foo : Int32 = 0
end

class UserObj < Obj
  property bar : Int32 = 0
end

puts "\n --- RUN -- \n\n"

puts "--- UserObj.class_init"
UserObj.class_init
puts "--- UserWidget.class_init"
UserWidget.class_init

With prints:

Widget#inherited macro on UserWidget
Obj#inherited macro on UserWidget
Obj#inherited macro on UserObj

 --- RUN -- 

--- UserObj.class_init
Obj#inherited: UserObj.class_init
properties: [bar]
--- UserWidget.class_init
Obj#inherited: UserWidget.class_init
properties: [foo]
Widget#inherited: UserWidget.class_init

The extra price is just few memory allocations that can be freed after the type is initialized.

Thanks everyone.

1 Like