Skip to content

Commit 48d730f

Browse files
tkfstevengj
authored andcommitted
Finalize Python runtime while shutting down Julia (#603)
* Avoid segfault during exit process closes JuliaPy/pyjulia#208 * Support Julia 0.6 * Revert back pydecref_ return type * shorten code slightly
1 parent 31296cd commit 48d730f

File tree

3 files changed

+56
-1
lines changed

3 files changed

+56
-1
lines changed

src/PyCall.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ it is equivalent to a `PyNULL()` object.
9999
ispynull(o::PyObject) = o.o == PyPtr_NULL
100100

101101
function pydecref_(o::Union{PyPtr,PyObject})
102-
ccall(@pysym(:Py_DecRef), Cvoid, (PyPtr,), o)
102+
_finalized[] || ccall(@pysym(:Py_DecRef), Cvoid, (PyPtr,), o)
103103
return o
104104
end
105105

src/pyinit.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ end
6363

6464
#########################################################################
6565

66+
const _finalized = Ref(false)
67+
# This flag is set via `Py_AtExit` to avoid calling `pydecref_` after
68+
# Python is finalized.
69+
70+
function _set_finalized()
71+
# This function MUST NOT invoke any Python APIs.
72+
# https://docs.python.org/3/c-api/sys.html#c.Py_AtExit
73+
_finalized[] = true
74+
return nothing
75+
end
76+
77+
function Py_Finalize()
78+
ccall(@pysym(:Py_Finalize), Cvoid, ())
79+
end
80+
6681
function __init__()
6782
# sanity check: in Pkg for Julia 0.7+, the location of Conda can change
6883
# if e.g. you checkout Conda master, and we'll need to re-build PyCall
@@ -155,4 +170,25 @@ function __init__()
155170
end
156171
end
157172
end
173+
174+
# Configure finalization steps.
175+
#
176+
# * In julia/PyCall, `julia` needs to call `Py_Finalize` to
177+
# finalize Python runtime to invoke Python functions registered
178+
# in Python's exit hook. This is done by Julia's `atexit` exit
179+
# hook.
180+
#
181+
# * In PyJulia, `python` needs to call `jl_atexit_hook` in its
182+
# exit hook instead.
183+
#
184+
# In both cases, it is important to not invoke GC of the finalized
185+
# runtime. This is ensured by:
186+
@pycheckz ccall((@pysym :Py_AtExit), Cint, (Ptr{Cvoid},),
187+
@cfunction(_set_finalized, Cvoid, ()))
188+
if !already_inited
189+
# Once `_set_finalized` is successfully registered to
190+
# `Py_AtExit`, it is safe to call `Py_Finalize` during
191+
# finalization of this Julia process.
192+
atexit(Py_Finalize)
193+
end
158194
end

test/runtests.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,4 +687,23 @@ def try_call(f):
687687
pybuiltin("Exception"))
688688
end
689689

690+
@testset "atexit" begin
691+
if VERSION < v"0.7-"
692+
setup = ""
693+
else
694+
setup = Base.load_path_setup_code()
695+
end
696+
script = """
697+
$setup
698+
699+
using PyCall
700+
701+
pyimport("atexit")[:register]() do
702+
println("atexit called")
703+
end
704+
"""
705+
out = read(`$(Base.julia_cmd()) -e $script`, String)
706+
@test occursin("atexit called", out)
707+
end
708+
690709
include("test_pyfncall.jl")

0 commit comments

Comments
 (0)