Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion c_src/pythonx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,9 @@ pythonx_handle_io_write = ctypes.CFUNCTYPE(
None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool
)(pythonx_handle_io_write_ptr)

pythonx_handle_send_tagged_object = ctypes.CFUNCTYPE(
# PYFUNCTYPE keeps the GIL held during the call, which the handler needs
# to take a reference to the object.
pythonx_handle_send_tagged_object = ctypes.PYFUNCTYPE(
None, ctypes.c_char_p, ctypes.c_char_p, ctypes.py_object, ctypes.c_char_p
)(pythonx_handle_send_tagged_object_ptr)

Expand Down Expand Up @@ -1638,6 +1640,11 @@ pythonx_handle_send_tagged_object(const char *pid_bytes, const char *tag,
auto pid = ErlNifPid{};
std::memcpy(&pid, pid_bytes, sizeof(ErlNifPid));

// The object is a borrowed reference, while the resource releases a
// reference once garbage collected, so we take one for it. The caller
// holds the GIL (see the PYFUNCTYPE prototype).
pythonx::python::Py_IncRef(py_object);

auto msg = fine::encode(
env, std::make_tuple(
fine::Atom(tag),
Expand Down
30 changes: 30 additions & 0 deletions test/pythonx_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,36 @@ defmodule PythonxTest do
assert_receive {:message_from_python, %Pythonx.Object{} = object}
assert repr(object) == "('hello', 1)"
end

test "pythonx.send_tagged_object takes a reference for the sent object" do
# Like any %Pythonx.Object{}, the sent one holds a reference, which
# its resource releases once it's garbage collected. Without taking
# one, that release frees the object while Python still uses it.
#
# Both counts are taken here, while the message is in the mailbox.
# Sending leaves reference cycles behind (from inspect.stack()) that
# hold on to the object, so those are collected first.
{result, _globals} =
Pythonx.eval(
"""
import gc
import sys
import pythonx

obj = [1, 2, 3]
before = sys.getrefcount(obj)
pythonx.send_tagged_object(pid, "message_from_python", obj)
gc.collect()
(before, sys.getrefcount(obj))
""",
%{"pid" => self()}
)

assert_receive {:message_from_python, %Pythonx.Object{}}

{before, after_send} = Pythonx.decode(result)
assert after_send == before + 1
end
end

describe "remote evaluation" do
Expand Down