From f2a71a796403b44465e6591544748da7581deccf Mon Sep 17 00:00:00 2001 From: Lars Wikman Date: Sat, 10 Oct 2026 09:30:44 +0200 Subject: [PATCH] Attempted fix for pythonx reference issue --- c_src/pythonx.cpp | 9 ++++++++- test/pythonx_test.exs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/c_src/pythonx.cpp b/c_src/pythonx.cpp index 8ff48b5..a0589a9 100644 --- a/c_src/pythonx.cpp +++ b/c_src/pythonx.cpp @@ -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) @@ -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), diff --git a/test/pythonx_test.exs b/test/pythonx_test.exs index 796f50c..d1df7e1 100644 --- a/test/pythonx_test.exs +++ b/test/pythonx_test.exs @@ -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