Finalizer bug? Or am I dumb?

I have a julia object that contains some data fields that are initialized inside a ccall, so I am trying to write a finalizer to allow C to free the allocated memory, but I’m getting some weird behavior that I think might be a bug.

Here’s a MWE:

               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.8.5 (2023-01-08)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |


julia> mutable struct Foo
           bar
       end

julia> finalizer(Foo) do x
           x.bar
           return x
       end
Foo

julia> y = Foo(1)
Foo(1)

julia> exit()
error in running finalizer: ErrorException("type DataType has no field bar")

So it seems like julia is forgetting what Foo is before calling the finalizer for Foo. If I try to assert x::Foo in the finalizer, I get this really wacky error:

julia> finalizer(Foo) do x
           x::Foo
           return x
       end
Foo

julia> y = Foo(3); exit()
error in running finalizer: TypeError(func=:typeassert, context="", expected=Main.Foo, got=Main.Foo)

“error: expected Foo, got Foo…” seems like this isn’t working right, unless I have missed something critical about finalizers.

1 Like

You’re registering the finalizer on the type Foo, not an instance of the type y = Foo(1).

See the docstring:

help?> finalizer
search: finalizer UndefInitializer finalize

  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.

  f must not cause a task switch, which excludes most I/O operations such as println. Using the @async macro (to defer context switching to outside of the finalizer) or ccall to directly invoke IO
  functions in C may be helpful for debugging purposes.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  finalizer(my_mutable_struct) do x
      @async println("Finalizing $x.")
  end
  
  finalizer(my_mutable_struct) do x
      ccall(:jl_safe_printf, Cvoid, (Cstring, Cstring), "Finalizing %s.", repr(x))
  end

  A finalizer may be registered at object construction. In the following example note that we implicitly rely on the finalizer returning the newly created mutable struct x.

  Example
  ≡≡≡≡≡≡≡≡≡

  mutable struct MyMutableStruct
      bar
      function MyMutableStruct(bar)
          x = new(bar)
          f(t) = @async println("Finalizing $t.")
          finalizer(f, x)
      end
  end

Your example would be

julia> mutable struct Foo
           bar
           function Foo(bar)
               y = new(bar)
               finalizer(y) do x
                   @show x.bar
                   return x
               end
               return y
           end
       end

julia> y = Foo(1)
Foo(1)

julia> finalize(y)
x.bar = 1
2 Likes

Thanks for this. I read the documentation, but I guess I was confused.
Edit: Good thing I allowed for myself being dumb in the title of this post :slight_smile:

2 Likes

No such thing as a dumb question. Im sure this question and answer will help other people in the future.

4 Likes