WeakRef can not ref a module, what can i do?

My code doesn’t work because WeakRef can not ref the module HookProcessTerminate. Is there any good way to solve this problem?

code

  module Internal
    module HookInitialize
      abstract def after_initialize

      macro included
        macro finished
      \{% for method in @type.methods %}
        \{% if method.name == "initialize" %}
          def initialize(\{{ method.args.join(",").id }})
            previous_def
            after_initialize
          end
        \{% end %}
      \{% end %}
    end
      end
    end

    module HookProcessTerminate
      include HookInitialize

      @@hooks : Array(WeakRef(HookProcessTerminate)) = [] of WeakRef(HookProcessTerminate)

      abstract def on_process_terminate

      def after_initialize
        @@hooks << WeakRef.new(self)
      end

      def self.hook
        Process.on_terminate do |reason|
          case reason
          when .interrupted?
            puts "Signal : interrupted"
          when .terminal_disconnected?
            puts "Signal : terminal disconnected"
          when .session_ended?
            puts "Signal : session ended"
          end

          @@hooks.each &.value.try &.on_process_terminate

          Process.exit
        end
      end
    end
  end

error

In /usr/share/crystal/src/weak_ref.cr:35:5

 35 | @target.as(T?)
      ^
Error: can't cast Pointer(Void) to (Internal::HookProcessTerminate | Nil)

not use WeakRef, use custom WeakRef something like this?

struct MyWeakRef
  @value : Void*
  @typ : Class
end

Another question is, if the pointer is updated when GC is performing memory folding, will WeakRef also be invalid in this case (but the referenced object is actually alive)?

Could you switch to using structs to store the callbacks and then just don’t use WeakRef?

EDIT: Or why do you want to use WeakRef at all? You’re storing exit handlers which will be referenced the entire lifetime of the program, just to exit out of the program entirely which would free all memory related to said program.

the doc says:

Process.exit does not trigger at_exit handlers, nor does external process termination (see Process.on_terminate for handling that).

Does this mean that ctrl+c cannot trigger it?

Ah you’re too fast, updated my comment after reading the docs for that method again :see_no_evil:.

1 Like

Using callback still keep a strong ref to target, maybe i need a unregister method to remove it from @@hooks when target is finished.
I feel that using WeakRef is more convenient but unfortunatly not supported in crystal

But that doesn’t answer my question. If the entire program is going to exit after these handlers run, the OS will reclaim the memory used by the program when that happens. I.e. I’m pretty sure the GC will never even have a chance to reclaim it itself even if you were using WeakRef.

My scenario is this:

There are many tasks, each task registers a process terminate handle after initialization to save the task progress when the program exits abnormally.
If the task has been completed before, there is no need to execute this handle (I assume it may be GCed if WeakRef is used)

My English is not very good, I may not be able to describe my problem well and answer your questions

Gotcha, is the program a long running program? Or does it exit gracefully once the task is completed?

a long running program

1 Like

In that case, assuming the types that are stored in the array are actually classes (if they’re structs idt it would matter anyway since they wouldn’t be allocated by the GC), I think you could get away with just clearing the array.

For example:

class Foo
  def finalize
    puts "Going away"
  end
end

arr = Array(Foo).new 100
arr << Foo.new
arr << Foo.new
arr << Foo.new

arr.clear
GC.collect

Because the array is the only thing holding onto the references of Foo, clearing it and forcing a collection will result in Going away being printed three times as each of the three objects are collected.

1 Like

Yes, WeakRef should not be able to achieve what I want, even if I use a custom WeakRef (because of the uncertainty of GC) so I may choose to use storage strong reference and manually remove it from the array after the task is completed.
Thank you for your patient answer ;-)

UPDATE:

Simpler code might do better

 module Internal
    module HookProcessTerminate
      @@hooks = [] of HookProcessTerminate
      @@hooks_lock = Mutex.new

      abstract def on_process_terminate

      def self.register(item : HookProcessTerminate)
        @@hooks_lock.synchronize { @@hooks << item }
      end

      def self.unregister(item : HookProcessTerminate)
        @@hooks_lock.synchronize { @@hooks.delete(item) }
      end

      def self.hook
        Process.on_terminate do |reason|
          case reason
          when .interrupted?
            puts "Signal : interrupted"
          when .terminal_disconnected?
            puts "Signal : terminal disconnected"
          when .session_ended?
            puts "Signal : session ended"
          end

          @@hooks.each &.on_process_terminate

          Process.exit
        end
      end
    end
  end

Just for mention, the initial version of this code work well on Crystal 1.14.0 no any error, so way WeakRef ref on a module works now?

How did you test it? The code is just an excerpt not a full reproduction. Without any driver code it’s a no-op and compiles to an empty program.

Just add a require "weak_ref" at the beginning of code and include it into a new class, then crystal run it.

code
require "weak_ref"

module Internal
  module HookInitialize
    abstract def after_initialize

    macro included
      macro finished
      \{% for method in @type.methods %}
        \{% if method.name == "initialize" %}
          def initialize(\{{ method.args.join(",").id }})
            previous_def
            after_initialize
          end
        \{% end %}
      \{% end %}
    end
    end
  end

  module HookProcessTerminate
    include HookInitialize

    @@hooks : Array(WeakRef(HookProcessTerminate)) = [] of WeakRef(HookProcessTerminate)

    abstract def on_process_terminate

    def after_initialize
      @@hooks << WeakRef.new(self)
    end

    def self.hook
      Process.on_terminate do |reason|
        case reason
        when .interrupted?
          puts "Signal : interrupted"
        when .terminal_disconnected?
          puts "Signal : terminal disconnected"
        when .session_ended?
          puts "Signal : session ended"
        end

        @@hooks.each &.value.try &.on_process_terminate

        Process.exit
      end
    end
  end
end

class A
  include Internal::HookProcessTerminate

  def on_process_terminate
  end

  def self.hooks
    @@hooks
  end
end

p! typeof(A.hooks) # typeof(A.hooks) # => Array(WeakRef(Internal::HookProcessTerminate))

Without any driver code it’s a no-op and compiles to an empty program.

the doc said:

WARNING: The referenced object cannot be a module.

So, I thought it should give a compile-time when run it.