Finalizer only works with mutable structs?

The finalizer function has the following doc string:

finalizer(f, x)

Register a function f(x) to be called when there are no program-accessible references to x, and return x. The type of x must be a mutable struct, otherwise the behavior of this function is unpredictable.

My question are:

  1. Why does it work with mutable objects only?

  2. How do we register a finalizer for immutable objects?

1 Like

Because an immutable object doesn’t have lifetime. Semantically, all the objects that has the same “value” are indistinguishable from each other so unless the compiler can proof that a certain value will never ever appear again in the program, it’s never dead and the finalizer must never be called.

It’s impossible.

5 Likes

I must be missing something fundamental here. I thought all objects have a lifetime – it can be collected by the garbage collector when it’s no longer being referenced. Is that not true?

More practically, if I create an immutable object that contains an external resource (e.g. an opened file handle) then what can I do to guarantee that the resource is released during GC?

1 Like

No, immutable structs have value semantics, and have no more of a lifetime than, say, an Int. As for the resource question, the answer would be, “you can’t” … as far as I know, the only way would be to make your struct mutable, so it will actually have an identity (and therefore a lifetime), not just a value.

1 Like

(Not saying that they can’t be implemented by heap allocation, but … that’s a different matter.)

I found that I also need to use mutable struct whenever I want to to use pointer_to_objref with an object (because this is passed through a callback function of a C library).

One option is to add a mutable field, e,g a Ref;
or a pointer Ptr then attach the finalizer to that.

The fact that you have something to do in the finalizer suggest you might already have such a field.
Since finalizers are most useful for cleaning up e.g open handles of versious kinds.
But sometimes you don’t because somethings things are weird.

1 Like

It is useful to think about three kinds of objects: Bitstype, immutable non-bitstype and mutable. Then there are two storage modalities: Heap-allocated objects with type header, and deconstructed. Deconstructed objects have no well-defined memory layout (e.g. splattered over multiple registers), but the compiler/runtime fakes (“architects”) the expected layout whenever you look, similar to how the superscalar CPU invents a plausible state it could have been, whenever you look.

@yuyichao talks about “julia architecture”, where immutable objects indeed have no lifetime or address. In “julia uarch”, many immutable non-bitstypes are indeed heap-allocated objects with proper header and lifetime. We want to keep open the possibility of non-breaking compiler improvements that remove allocations for many such objects, which is why we don’t allow pointer_from_objref or finalizers.

3 Likes

That’s not true. All ALLOCATIONS have a lifetime. That allocation may or may not have anthing to do with the object. It might not happen at the same time the object is created in the code (it may not even happen) and may or may not end at the same time as the object. This is especially true for immutables and could also be true for mutables.

Pass the object using Any or a correct Ref argument type directly. You almost never want to call pointer_to_objref yourself. (If you are calling it, you are likely doing it wrong for mutable types as well)

Depending on what you mean here.

  1. Ptr isnt mutable and Ref is an abstract type (neither mutable or immutable).
  2. If you mean that adding finalizer to a field of an object for tracking the lifetime of the parent object, then it won’t work. If you make sure that the thing you finalize is limited to that mutable field though it’ll of course work.
1 Like

I think I usually say semantics and implementation… =P.

Anyway, that’s pretty accurate…

2 Likes

I guess i should have used a semi-colon.

Sure, Ptr isn’t mutable but finalizers alway work on pointers. (I am surprised thsis isn’t in the docstring).
When I said Ref I technically ment RefValue, but since that isn’t exported, and ref is the exported supertyope that constructs RefValues I thought that would be understandable.
I guess Base.RefValue might be clearer, so I guess I will say th.

  1. If you mean that adding finalizer to a field of an object for tracking the lifetime of the parent object, then it won’t work. If you make sure that the thing you finalize is limited to that mutable field though it’ll of course work.

Yes, either a a dummy object just for purpose tracking this,
(or something that cleansup only itself, like what handles should do)

struct Foo
  ...
  _dummy = Base.RefValue{Nothing}
  function Foo(...)
      ...
     dummy = Ref(nothing)
     finalizers(dummy) do
        ...
     end
         
     return new(..., dummy)
  end
end
1 Like

Stack allocated immutables do have a “lifetime” in the sense that they live until the precise moment they fall out of scope. I often find myself wanting to define such C++ style finalisers on C api resource handlers for C++ style resource management.

Am I correct that the only available open for this type of management at the moment is a do block?

1 Like

What do you mean? You can’t register a finalizer on a Ptr.

What I was talking about is that you need to make sure your callback do not clean up the anything other than dummy, which seems to be nothing. Doing this while trying to clean up the actuall Foo object is wrong.

All variables have a scope. That has nothing to do with the lifetime of the variables they refer to. This scope is indeed independent of the type of the object (since it’s not even a property of the object). There’s no scope callback in the language.

do blocks does not provide any features that you couldn’t do without it. The only way to do this is the finally block, which has a pretty high overhead.

3 Likes

Would that make any difference if I then go on to unsafe_pointer_to_objref on the result?
I would like to learn more about the “right way” for my use case, where the C library will not look at the data at all, and simply provide the pointer back to the callback function as void*.
But I fear this it off-topic here.

I think scope callbacks would be great, for the same reason c++ destructors are. However they’d also need to be triggered on variable reassignment in Julia (which I think isn’t the case by default in C++, though you can overload reassignment to destruct first).

I’m aware do blocks are just syntactic sugar, my challenge is more providing a safe API for users to interact with something like Vulkan that is a bit forgiving if they don’t explicitly call destructors on a bunch of objects. Do block is the best API we have for that currently, an even better API would be an automatic destructor.

No. The point is that if you are calling it manually and not doing anything else, the compiler can just give you a garbage pointer since your object might be unused as far as the compiler can tell.

As I said, you can just use the ccall argument conversion.

1 Like

So your point is about GC-safety?
Since the object needs to survive beyond the ccall to submit_callback_function, I’m already storing a reference separately. It’s just that I can’t access that reference from the callback function.

Triggering on scope exit is OK but it needs a better implementation than finalizers (we actually have that in C…) Triggering on assignment could be pretty bad since it can mess up a lot of the existing macros/lowering that generally assume assigning to temperary variables are insignificant.

Yes.

If you are sure the lifetime of that storage is longer than the duration you are going to use it then it’s fine. To be fully generic and support immutable type though, you could still use the ccall argument conversion for Ref{Any} instead.

1 Like

Thanks for clearing that up!