diff --git a/src/PyCall.jl b/src/PyCall.jl index 8e5ccc46..46ee4643 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -99,7 +99,7 @@ it is equivalent to a `PyNULL()` object. ispynull(o::PyObject) = o.o == PyPtr_NULL function pydecref_(o::Union{PyPtr,PyObject}) - ccall(@pysym(:Py_DecRef), Cvoid, (PyPtr,), o) + _finalized[] || ccall(@pysym(:Py_DecRef), Cvoid, (PyPtr,), o) return o end diff --git a/src/pyinit.jl b/src/pyinit.jl index 7515b12c..d3ec8409 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -62,6 +62,21 @@ end ######################################################################### +const _finalized = Ref(false) +# This flag is set via `Py_AtExit` to avoid calling `pydecref_` after +# Python is finalized. + +function _set_finalized() + # This function MUST NOT invoke any Python APIs. + # https://docs.python.org/3/c-api/sys.html#c.Py_AtExit + _finalized[] = true + return nothing +end + +function Py_Finalize() + ccall(@pysym(:Py_Finalize), Cvoid, ()) +end + function __init__() # sanity check: in Pkg for Julia 0.7+, the location of Conda can change # if e.g. you checkout Conda master, and we'll need to re-build PyCall @@ -154,4 +169,25 @@ function __init__() end end end + + # Configure finalization steps. + # + # * In julia/PyCall, `julia` needs to call `Py_Finalize` to + # finalize Python runtime to invoke Python functions registered + # in Python's exit hook. This is done by Julia's `atexit` exit + # hook. + # + # * In PyJulia, `python` needs to call `jl_atexit_hook` in its + # exit hook instead. + # + # In both cases, it is important to not invoke GC of the finalized + # runtime. This is ensured by: + @pycheckz ccall((@pysym :Py_AtExit), Cint, (Ptr{Cvoid},), + @cfunction(_set_finalized, Cvoid, ())) + if !already_inited + # Once `_set_finalized` is successfully registered to + # `Py_AtExit`, it is safe to call `Py_Finalize` during + # finalization of this Julia process. + atexit(Py_Finalize) + end end diff --git a/test/runtests.jl b/test/runtests.jl index a1951214..a9d34404 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -666,4 +666,23 @@ def try_call(f): pybuiltin("Exception")) end +@testset "atexit" begin + if VERSION < v"0.7-" + setup = "" + else + setup = Base.load_path_setup_code() + end + script = """ + $setup + + using PyCall + + pyimport("atexit")[:register]() do + println("atexit called") + end + """ + out = read(`$(Base.julia_cmd()) -e $script`, String) + @test occursin("atexit called", out) +end + include("test_pyfncall.jl")