Eager finalization insertion location question

If I modify the example given here https://github.com/JuliaLang/julia/pull/46651#issuecomment-1253471390 to

init_finalization_count!() = FINALIZATION_COUNT[] = 0
get_finalization_count() = FINALIZATION_COUNT[]
@noinline add_finalization_count!(x) = FINALIZATION_COUNT[] += x
@noinline Base.@assume_effects :nothrow safeprint(io::IO, x...) = (@nospecialize; print(io, x...))
@test Core.Compiler.is_finalizer_inlineable(Base.infer_effects(add_finalization_count!, (Int,)))

mutable struct DoAllocWithFieldInter
function register_finalizer!(obj::DoAllocWithFieldInter)
    finalizer(obj) do this

function cfg_finalization6(io)
    for i = -999:1000
        o = DoAllocWithFieldInter(0)
        safeprint(io, o.x, '\n')

Let’s look at the ir code to see where the finalizer has been inserted

julia> ir = Base.code_ircode(cfg_finalization6, (IO,); optimize_until="SROA") |> only |> first
2  1 ─       goto #7 if not true        β”‚   
   2 β”„ %21 = Ο† (#1 => -999, #6 => %14)::Int64
   β”‚         nothing::Tuple{Int64, Int64}   
   β”‚   %3  = %21::Int64                 β”‚   
4  β”‚   %4  = %new(Base.RefValue{Int64}, 0)::Base.RefValue{Int64}
5  β”‚         nothing::Nothing           β”‚β•»   register_finalizer!
6  β”‚   %6  = Base.getfield(%4, :x)::Int64β•»   getproperty
   β”‚   %23 = Base.getfield(%4, :x)::Int64   
   β”‚         invoke Main.add_finalization_count!(%23::Int64)::Int64
   β”‚         invoke Main.safeprint(_2::IO, %6::Any, '\n'::Vararg{Any})::Any
13 β”‚   %8  = (%3 === 1000)::Bool        β”‚β•»β•·  iterate
   └──       goto #4 if not %8          β”‚β”‚  
   3 ─       goto #5                    β”‚β”‚  
   4 ─ %11 = Base.add_int(%3, 1)::Int64 β”‚β”‚β•»   +
   β”‚         nothing::Tuple{Int64, Int64}β•»   iterate
   └──       goto #5                    β”‚β”‚  
   5 β”„ %14 = Ο† (#4 => %11)::Int64       β”‚   
   β”‚   %22 = Ο† (#3 => true, #4 => false)::Bool
   β”‚         nothing::Union{Nothing, Tuple{Int64, Int64}}
   β”‚   %16 = %22::Bool                  β”‚   
   β”‚   %17 = Base.not_int(%16)::Bool    β”‚   
   └──       goto #7 if not %17         β”‚   
   6 ─       goto #2                    β”‚   
   7 β”„       return nothing 

It’s been inserted after the last getfield call but before the safeprint. Is this intended as I want to instead free a pointer in the finalizer and so cannot have any uses of it after the finalizer, e.g.

mutable struct UniquePointer{T} <: Ref{T}
    function UniquePointer(ptr::Ptr{T}) where T
        self = new{T}(ptr)
        finalizer(free, self)
Base.@assume_effects :nothrow :notaskstate free(ptr) = Libc.free(ptr)
free(self::UniquePointer) = free(self.ptr)

I see in the SROA pass that the finalizer only seems to track uses for getfield, setfield, isdefined and ccalls. Would changing it to also track uses of its fields be desired.

Yes, that is intended - the integer stored in your field is an isbits value, meaning it’s identity is defined by its bitpattern (the same is true for an explicitly stored Ptr). The only thing the finalizer itself cares about is whether the mutable struct itself is no longer needed - which is the case after the field has been accessed and its value has been retrieved.

It is your responsibility to make sure the object referred to by the pointer is kept alive while its memory is being accessed through the pointer, via GC.@preserve or otherwise keeping a live reference around. Eagerly freeing the pointer once the struct it’s contained in is no longer referenced may lead to use after free.

1 Like