diff --git a/ext/pycall/libpython.c b/ext/pycall/libpython.c index 48ca1d2..e0c8245 100644 --- a/ext/pycall/libpython.c +++ b/ext/pycall/libpython.c @@ -89,6 +89,7 @@ pycall_init_libpython_api_table(VALUE libpython_handle) INIT_API_TABLE_ENTRY(PyUnicode_Type, required); INIT_API_TABLE_ENTRY(Py_InitializeEx, required); + INIT_API_TABLE_ENTRY(Py_FinalizeEx, required); INIT_API_TABLE_ENTRY(Py_IsInitialized, required); INIT_API_TABLE_ENTRY(Py_GetVersion, required); diff --git a/ext/pycall/pycall.c b/ext/pycall/pycall.c index 2c7d56e..6b38d71 100644 --- a/ext/pycall/pycall.c +++ b/ext/pycall/pycall.c @@ -681,6 +681,15 @@ pycall_libpython_api_PyList_GetItem(VALUE mod, VALUE pyptr, VALUE idx) return pycall_pyptr_new(pyobj_item); } +static VALUE +pycall_libpython_api_Py_FinalizeEx(VALUE mod) +{ + assert(Py_API(Py_IsInitialized())); + + return Py_API(Py_FinalizeEx)(); +} + + /* ==== PyCall::Helpers ==== */ static VALUE @@ -2372,6 +2381,7 @@ Init_pycall(void) rb_define_module_function(mAPI, "PyObject_Dir", pycall_libpython_api_PyObject_Dir, 1); rb_define_module_function(mAPI, "PyList_Size", pycall_libpython_api_PyList_Size, 1); rb_define_module_function(mAPI, "PyList_GetItem", pycall_libpython_api_PyList_GetItem, 2); + rb_define_module_function(mAPI, "Py_FinalizeEx", pycall_libpython_api_Py_FinalizeEx, 0); /* PyCall::LibPython::Helpers */ diff --git a/ext/pycall/pycall_internal.h b/ext/pycall/pycall_internal.h index 4a7a06c..f1d2b63 100644 --- a/ext/pycall/pycall_internal.h +++ b/ext/pycall/pycall_internal.h @@ -565,6 +565,7 @@ typedef struct { PyObject *PyExc_TypeError; void (* Py_InitializeEx)(int); + int (* Py_FinalizeEx)(void); int (* Py_IsInitialized)(); char const * (* Py_GetVersion)(); diff --git a/lib/pycall/init.rb b/lib/pycall/init.rb index e14a107..c735fb6 100644 --- a/lib/pycall/init.rb +++ b/lib/pycall/init.rb @@ -45,6 +45,17 @@ class << LibPython require 'pycall/slice' const_set(:PYTHON_VERSION, LibPython::PYTHON_VERSION) const_set(:PYTHON_DESCRIPTION, LibPython::PYTHON_DESCRIPTION) + + @init_thread_id = Thread.current.object_id + at_exit do + # Finalize is only safe to call if we're on the same thread that initialized PyCall + finalize if Thread.current.object_id == @init_thread_id + end + true end + + def self.finalize + LibPython::API.Py_FinalizeEx() + end end diff --git a/pycall_hangs_main_thread.rb b/pycall_hangs_main_thread.rb new file mode 100755 index 0000000..ffac8aa --- /dev/null +++ b/pycall_hangs_main_thread.rb @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +side_thread = Thread.new do + require 'pycall' + PyCall.import_module('sys') + PyCall.import_module('pandas') + + PyCall.finalize # if this line is commented out, the process will hang on exit + puts "side thread: exiting" +end +side_thread.join + +puts "main thread: exiting" +#=> process exits! \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..9420e38 --- /dev/null +++ b/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -ex + +bundle exec ruby -I ext/pycall pycall_hangs_main_thread.rb 2> /dev/null \ No newline at end of file