Skip to content

Commit be43093

Browse files
committed
use weakrefs to prevent Julia from garbage-collecting arrays that Python needs; conversions of nothing/None; beginnings of callback support
1 parent fe78e3d commit be43093

File tree

5 files changed

+136
-18
lines changed

5 files changed

+136
-18
lines changed

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,7 @@ interface to provide a data pointer and shape information.)
111111

112112
Conversely, when passing arrays *to* Python, Julia `Array` types are
113113
converted to `PyObject` types *without* making a copy via NumPy,
114-
e.g. when passed as `pycall` arguments. **Warning:** If Python creates
115-
a new reference to an `Array` object and returns it from `pycall`, you
116-
*must* ensure that the original `Array` object still exists (i.e., is not
117-
garbage collected) as long as any such "hidden" Python references
118-
exist.
114+
e.g. when passed as `pycall` arguments.
119115

120116
#### PyDict
121117

src/PyCall.jl

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,18 @@ end
3535

3636
type PyObject
3737
o::PyPtr # the actual PyObject*
38-
39-
# For copy-free wrapping of arrays, the PyObject may reference a
40-
# pointer to Julia array data. In this case, we must keep a
41-
# reference to the Julia object inside the PyObject so that the
42-
# former is not garbage collected before the latter.
43-
keep::Any
44-
45-
function PyObject(o::PyPtr, keep::Any)
46-
po = new(o, keep)
38+
function PyObject(o::PyPtr)
39+
po = new(o)
4740
finalizer(po, pydecref)
4841
return po
4942
end
5043
end
5144

52-
PyObject(o::PyPtr) = PyObject(o, nothing) # no Julia object to keep
53-
5445
function pydecref(o::PyObject)
5546
if initialized::Bool # don't decref after pyfinalize!
5647
ccall(pyfunc(:Py_DecRef), Void, (PyPtr,), o.o)
5748
end
5849
o.o = C_NULL
59-
o.keep = nothing
6050
o
6151
end
6252

@@ -99,6 +89,10 @@ MethodWrapperType = PyObject(C_NULL)
9989
# special function type used in NumPy and SciPy (if available)
10090
ufuncType = PyObject(C_NULL)
10191

92+
# cache Python None and PyNoneType
93+
pynothing = PyObject(C_NULL)
94+
PyNoneType = PyObject(C_NULL)
95+
10296
# Py_SetProgramName needs its argument to persist as long as Python does
10397
pyprogramname = bytestring("")
10498

@@ -116,6 +110,8 @@ function pyinitialize(libpy::Ptr{Void})
116110
global MethodType
117111
global MethodWrapperType
118112
global ufuncType
113+
global pynothing
114+
global PyNoneType
119115
if !initialized::Bool
120116
if finalized::Bool
121117
# From the Py_Finalize documentation:
@@ -147,6 +143,8 @@ function pyinitialize(libpy::Ptr{Void})
147143
catch
148144
ufuncType = PyObject(C_NULL) # NumPy not available
149145
end
146+
pynothing = pybuiltin("None")
147+
PyNoneType = types["NoneType"]
150148
end
151149
return
152150
end
@@ -185,14 +183,20 @@ function pyfinalize()
185183
global TypeType
186184
global MethodType
187185
global MethodWrapperType
186+
global ufuncType
187+
global pynothing
188+
global PyNoneType
188189
if initialized::Bool
189-
npyfinalize()
190190
pydecref(ufuncType)
191+
npyfinalize()
192+
pydecref(PyNoneType)
193+
pydecref(pynothing)
191194
pydecref(BuiltinFunctionType::PyObject)
192195
pydecref(TypeType::PyObject)
193196
pydecref(MethodType::PyObject)
194197
pydecref(MethodWrapperType::PyObject)
195198
pydecref(inspect::PyObject)
199+
pygc_finalize()
196200
gc() # collect/decref any remaining PyObjects
197201
ccall(pyfunc(:Py_Finalize), Void, ())
198202
dlclose(libpython::Ptr{Void})
@@ -285,6 +289,16 @@ end
285289

286290
#########################################################################
287291

292+
include("gc.jl")
293+
294+
# make a PyObject that embeds a reference to keep, to prevent Julia
295+
# from garbage-collecting keep until o is finalized.
296+
PyObject(o::PyPtr, keep::Any) = pyembed(PyObject(o), keep)
297+
298+
#########################################################################
299+
300+
include("callback.jl")
301+
288302
include("conversions.jl")
289303

290304
#########################################################################

src/callback.jl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Passing Julia callback functions to Python routines.
2+
#
3+
# Note that this will typically involve two functions: the
4+
# desired Julia function/closure, and a top-level C-callable
5+
# wrapper function used with PyCFunction_NewEx -- the latter
6+
# is called from Python and calls the former as needed.
7+
8+
################################################################
9+
# mirror of Python API types and constants from methodobject.h
10+
11+
type PyMethodDef
12+
ml_name::Ptr{Uint8}
13+
ml_meth::Ptr{Void}
14+
ml_flags::Cint
15+
ml_doc::Ptr{Uint8} # may be NULL
16+
end
17+
18+
# A PyCFunction is a C function of the form
19+
# PyObject *func(PyObject *self, PyObject *args)
20+
# The first parameter is the "self" function for method, or
21+
# for module functions it is the module object. The second
22+
# parameter is either a tuple of args (for METH_VARARGS),
23+
# a single arg (for METH_O), or NULL (for METH_NOARGS). func
24+
# must return non-NULL (Py_None is okay) unless there was an
25+
# error, in which case an exception must have been set.
26+
27+
# ml_flags should be one of:
28+
const METH_VARARGS = 0x0001 # args are a tuple of arguments
29+
const METH_NOARGS = 0x0004 # no arguments (NULL argument pointer)
30+
const METH_O = 0x0008 # single argument (not wrapped in tuple)
31+
32+
################################################################
33+
34+
# Define a Python method/function object from f(PyPtr,PyPtr)::PyPtr.
35+
# Requires f to be a top-level function.
36+
function pymethod(f::Function, name::String, flags::Integer)
37+
# Python expects the PyMethodDef structure to be a *constant*,
38+
# and the strings pointed to therein must also be constants,
39+
# so we define anonymous globals to hold these
40+
def = gensym("PyMethodDef")
41+
defname = gensym("PyMethodDef_ml_name")
42+
@eval const $defname = bytestring($name)
43+
@eval const $def = PyMethodDef(convert(Ptr{Uint8}, $defname),
44+
$(cfunction(f, PyPtr, (PyPtr,PyPtr))),
45+
convert(Cint, $flags),
46+
convert(Ptr{Uint8}, C_NULL))
47+
PyObject(@pycheckn ccall(pyfunc(:PyCFunction_NewEx), PyPtr,
48+
(Ptr{PyMethodDef}, Ptr{Void}, Ptr{Void}),
49+
&eval(def), C_NULL, C_NULL))
50+
end
51+
52+
################################################################

src/conversions.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ PyObject(s::String) = PyObject(@pycheckn ccall(pyfunc(:PyString_FromString),
2525
PyPtr, (Ptr{Uint8},),
2626
bytestring(s)))
2727

28+
PyObject(n::Nothing) = pyincref(pynothing)
29+
2830
# conversions to Julia types from PyObject
2931

3032
convert{T<:Integer}(::Type{T}, po::PyObject) =
@@ -49,6 +51,8 @@ convert{T<:String}(::Type{T}, po::PyObject) =
4951
bytestring(@pycheck ccall(pyfunc(:PyString_AsString),
5052
Ptr{Uint8}, (PyPtr,), po))
5153

54+
convert(::Type{Nothing}, po::PyObject) = nothing
55+
5256
#########################################################################
5357
# for automatic conversions, I pass Vector{PyAny}, NTuple{PyAny}, etc.,
5458
# but since PyAny is an abstract type I need to convert this to Any
@@ -318,6 +322,8 @@ pystring_query(o::PyObject) = pyisinstance(o, :PyString_Type) ? String : None
318322

319323
pyfunction_query(o::PyObject) = pyisinstance(o, :PyFunction_Type) || pyisinstance(o, BuiltinFunctionType) || pyisinstance(o, ufuncType) || pyisinstance(o, TypeType) || pyisinstance(o, MethodType) || pyisinstance(o, MethodWrapperType) ? Function : None
320324

325+
pynone_query(o::PyObject) = pyisinstance(o, PyNoneType) ? Nothing : None
326+
321327
# we check for "items" attr since PyMapping_Check doesn't do this (it only
322328
# checks for __getitem__) and PyMapping_Check returns true for some
323329
# scipy scalar array members, grrr.
@@ -365,6 +371,7 @@ function pytype_query(o::PyObject, default::Type)
365371
@return_not_None pyfunction_query(o)
366372
@return_not_None pydict_query(o)
367373
@return_not_None pysequence_query(o)
374+
@return_not_None pynone_query(o)
368375
return default
369376
end
370377

src/gc.jl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Interactions of the Julia and Python garbage collection:
2+
#
3+
# * When we wrap a Python object in a Julia object jo (e.g. a PyDict),
4+
# we keep an explicit PyObject reference inside jo, whose finalizer
5+
# decrefs the Python object when it is called.
6+
#
7+
# * When we wrap a Julia object jo inside a Python object po
8+
# (e.g a numpy array), we add jo to the pycall_gc dictionary,
9+
# keyed by a weak reference to po. The Python weak reference
10+
# allows us to register a callback function that is called
11+
# when po is deallocated, and this callback function removes
12+
# jo from pycall_gc so that Julia can garbage-collect it.
13+
14+
pycall_gc = Dict{PyPtr,Any}()
15+
16+
function weakref_callback(callback::PyPtr, wo::PyPtr)
17+
global pycall_gc
18+
try
19+
delete!(pycall_gc::Dict{PyPtr,Any}, wo)
20+
# not sure what to do if there is an exception here
21+
finally
22+
ccall(pyfunc(:Py_DecRef), Void, (PyPtr,), wo)
23+
end
24+
pyincref(pynothing).o
25+
end
26+
27+
weakref_callback_obj = PyObject(C_NULL) # weakref_callback Python method
28+
29+
function pygc_finalize()
30+
global pycall_gc
31+
global weakref_callback_obj
32+
pydecref(weakref_callback_obj)
33+
pycall_gc::Dict{PyPtr,Any} = Dict{PyPtr,Any}()
34+
end
35+
36+
# "embed" a reference to jo in po, using the weak-reference mechanism
37+
function pyembed(po::PyObject, jo::Any)
38+
global pycall_gc
39+
global weakref_callback_obj
40+
if (weakref_callback_obj::PyObject).o == C_NULL
41+
weakref_callback_obj::PyObject = pymethod(weakref_callback,
42+
"weakref_callback",
43+
METH_O)
44+
end
45+
wo = @pycheckn ccall(pyfunc(:PyWeakref_NewRef), PyPtr, (PyPtr,PyPtr),
46+
po, weakref_callback_obj)
47+
(pycall_gc::Dict{PyPtr,Any})[wo] = jo
48+
return po
49+
end

0 commit comments

Comments
 (0)