See More

/* indent-tabs-mode: nil -*- */ /* qore Python module Copyright (C) 2020 - 2026 Qore Technologies, s.r.o. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "python-module.h" #include "QoreThreadAttachHelper.h" #include "QoreLoader.h" #include "JavaLoader.h" #include "QoreMetaPathFinder.h" #include "QorePythonProgram.h" #include "PythonQoreCallable.h" #include QorePythonProgram* qore_python_pgm = nullptr; PyDoc_STRVAR(module_doc, "This module provides dynamic access to Qore APIs."); static int slot_qoreloader_exec(PyObject* m); static PyObject* qoreloader_load_java(PyObject* self, PyObject* args); static PyObject* qoreloader_issue_module_cmd(PyObject* self, PyObject* args); static PyMethodDef qoreloader_methods[] = { {"load_java", qoreloader_load_java, METH_VARARGS, "Import the given Java path to the parent Qore program if " \ "possible; args: import_str (ex: 'org.qore.lang.restclient.*')"}, {"issue_module_cmd", qoreloader_issue_module_cmd, METH_VARARGS, "Issue the given module command on the given " \ "module; args: module_name, cmd"}, {nullptr, nullptr, 0, nullptr}, }; bool qore_needs_shutdown = false; thread_local QoreThreadAttacher qoreThreadAttacher; PyTypeObject PythonQoreObjectBase_Type = { PyVarObject_HEAD_INIT(nullptr, 0) #if !defined(__clang__) && __GNUC__ < 8 // g++ 5.4.0 does not accept the short-form initialization below :( "PythonQoreObjectBase", // tp_name 0, // tp_basicsize 0, // tp_itemsize nullptr, // tp_dealloc 0, // tp_vectorcall_offset/ 0, // tp_getattr 0, // tp_setattr 0, // tp_as_async nullptr, // tp_repr 0, // tp_as_number 0, // tp_as_sequence 0, // tp_as_mapping 0, // tp_hash 0, // tp_call 0, // tp_str 0, // tp_getattro 0, // tp_setattro 0, // tp_as_buffer Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // tp_flags "base class for Python objects based on Qore classes", // tp_doc 0, // tp_traverse 0, // tp_clear 0, // tp_richcompare 0, // tp_weaklistoffset 0, // tp_iter 0, // tp_iternext 0, // tp_methods 0, // tp_members 0, // tp_getset 0, // tp_base 0, // tp_dict 0, // tp_descr_get 0, // tp_descr_set 0, // tp_dictoffset 0, // tp_init 0, // tp_alloc 0, // tp_new #else .tp_name = "PythonQoreObjectBase", .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = "base class for Python objects based on Qore classes", #endif }; static int init_count = 0; void qoreloader_free(void* obj) { if (!--init_count) { printd(5, "qoreloader_free() obj: %p qore_needs_shutdown: %d\n", obj, qore_needs_shutdown); if (qore_python_pgm) { qore_python_pgm->doDeref(); qore_python_pgm = nullptr; } QoreMetaPathFinder::del(); QoreLoader::del(); if (qore_needs_shutdown) { qore_cleanup(); } } } static struct PyModuleDef_Slot qoreloader_slots[] = { {Py_mod_exec, reinterpret_cast(slot_qoreloader_exec)}, #if PY_VERSION_HEX >= 0x030C0000 // Python 3.12+ sub-interpreter support (PEP 684) {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, #endif #if PY_VERSION_HEX >= 0x030D0000 // Python 3.13+ free-threading support (PEP 703) {Py_mod_gil, Py_MOD_GIL_NOT_USED}, #endif {0, nullptr}, }; static struct PyModuleDef qoreloadermodule = { PyModuleDef_HEAD_INIT, "qoreloader", // m_name module_doc, // m_doc 0, // m_size qoreloader_methods, // m_methods qoreloader_slots, // m_slots nullptr, // m_traverse nullptr, // m_clear qoreloader_free, // m_free }; typedef std::set qpy_pgm_set_t; static qpy_pgm_set_t qpy_pgm_set; QoreThreadLock qpy_lock; // Global set to track ALL QorePythonProgram pointers for validity checking static qpy_pgm_set_t qpy_global_set; static QoreThreadLock qpy_global_lock; // Counter for how many sub-interpreters have been destroyed // Used to detect when destructor calls might be unsafe static std::atomic qpy_destroyed_count{0}; void qpy_global_register(QorePythonProgram* p) { AutoLocker al(qpy_global_lock); qpy_global_set.insert(p); } void qpy_global_deregister(QorePythonProgram* p) { AutoLocker al(qpy_global_lock); qpy_global_set.erase(p); // NOTE: We don't increment destroyed count here - it's incremented when the // interpreter is actually destroyed in qpy_interpreter_destroyed() } void qpy_interpreter_destroyed() { ++qpy_destroyed_count; } bool qpy_is_valid(QorePythonProgram* p) { AutoLocker al(qpy_global_lock); return qpy_global_set.find(p) != qpy_global_set.end(); } int qpy_get_destroyed_count() { return qpy_destroyed_count.load(); } bool qpy_register(QorePythonProgram* p) { if (!qore_needs_shutdown) { return false; } printd(5, "qpy_register() p: %p\n", p); AutoLocker al(qpy_lock); assert(qpy_pgm_set.find(p) == qpy_pgm_set.end()); qpy_pgm_set.insert(p); return true; } void qpy_deregister(QorePythonProgram* p) { if (!qore_needs_shutdown) { return; } printd(5, "qpy_deregister() p: %p\n", p, qore_needs_shutdown); AutoLocker al(qpy_lock); qpy_pgm_set_t::iterator i = qpy_pgm_set.find(p); if (i != qpy_pgm_set.end()) { qpy_pgm_set.erase(i); } } static PyObject* qoreloader_atexit(PyObject* self, PyObject* args) { #ifndef Py_GIL_DISABLED PyGILState_STATE gstate = PyGILState_Ensure(); #endif if (!qpy_pgm_set.empty()) { assert(mainThreadState); ExceptionSink xsink; for (auto& i : qpy_pgm_set) { //printd(5, "qoreloader_atexit() p: %p\n", i); i->py_destructor(&xsink); } } qore_needs_shutdown = false; qore_cleanup(); assert(mainThreadState); _QORE_PYTHREAD_STATE_SWAP(mainThreadState); Py_INCREF(Py_None); #ifndef Py_GIL_DISABLED PyGILState_Release(gstate); #endif return Py_None; } static PyMethodDef atexit_md = { "qoreloader_atexit", qoreloader_atexit, METH_NOARGS, "qoreloader cleanup function", }; static int slot_qoreloader_exec(PyObject *m) { ++init_count; printd(5, "slot_qoreloader_exec() ic: %d\n", init_count); if (init_count == 1) { // initialize qore library if necessary if (!q_libqore_initalized()) { printd(5, "PyInit_qoreloader() initializing Qore library\n"); qore_init(QL_MIT); qore_needs_shutdown = true; printd(5, "PyInit_qoreloader() Qore library initialized\n"); // ensure that the Qore python module is registered as a Qore module ExceptionSink xsink; MM.runTimeLoadModule(&xsink, "python", nullptr, python_qore_module_desc); if (xsink) { return -1; } } init_global_qore_python_pgm(); if (PyType_Ready(&PythonQoreObjectBase_Type) < 0) { return -1; } if (PyType_Ready(&PythonQoreCallable_Type) < 0) { return -1; } if (QoreLoader::init()) { return -1; } if (JavaLoader::init()) { return -1; } if (QoreMetaPathFinder::init()) { return -1; } if (qore_needs_shutdown) { // set cleanup function call QorePythonReferenceHolder atexit(PyImport_ImportModule("atexit")); if (!*atexit) { printd(5, "slot_qoreloader_exec() ERROR: no atexit module\n"); return -1; } if (!PyObject_HasAttrString(*atexit, "register")) { printd(5, "slot_qoreloader_exec() ERROR: no atexit.register() method\n"); return -1; } QorePythonReferenceHolder register_func(PyObject_GetAttrString(*atexit, "register")); if (!register_func || !PyCallable_Check(*register_func)) { printd(5, "slot_qoreloader_exec() ERROR: atexit.register() is not callable\n"); return -1; } // call atexit.register() QorePythonReferenceHolder args(PyTuple_New(1)); QorePythonReferenceHolder func(PyCFunction_New(&atexit_md, nullptr)); PyTuple_SET_ITEM(*args, 0, func.release()); QorePythonReferenceHolder rv_ignored(PyEval_CallObject(*register_func, *args)); } } QoreMetaPathFinder::setupModules(); return 0; } int init_global_qore_python_pgm() { if (!qore_python_pgm) { // save and restore the Python thread state while initializing the Qore python module PythonThreadStateHelper ptsh; QoreThreadAttachHelper attach_helper; attach_helper.attach(); qore_python_pgm = new QorePythonProgram; } return 0; } PyMODINIT_FUNC PyInit_qoreloader() { // first check if the runtime library differs from the dynamically-linked one if (!Py_TYPE(PyExc_RuntimeError)) { fprintf(stderr, "ERROR: the Python runtime library is different than the dynamically linked one; it's not " \ "possible to raise a Python exception in this case without a crash; aborting\n"); return nullptr; } const char* ver = Py_GetVersion(); if (!ver) { fprintf(stderr, "cannot determine Python version; no value returned from Py_GetVersion()'\n"); return nullptr; } const char* p = strchr(ver, '.'); if (!p || p == ver) { fprintf(stderr, "cannot determine Python major version from '%s'\n", ver); return nullptr; } int major = atoi(ver); int minor = atoi(p + 1); if (major != PY_MAJOR_VERSION || minor != PY_MINOR_VERSION) { fprintf(stderr, "cannot load the qoreloader module; compiled with '%d.%d.%d'; runtime version is '%s'\n", PY_MAJOR_VERSION, PY_MINOR_VERSION, PY_MICRO_VERSION, ver); return nullptr; } return PyModuleDef_Init(&qoreloadermodule); } int load_jni_module(QorePythonProgram* qore_python_pgm) { #ifdef Py_GIL_DISABLED // JNI is not compatible with Python free-threading mode due to mimalloc heap conflicts // JNI creates threads that don't have proper Python mimalloc heap initialization PyErr_SetString(PyExc_RuntimeError, "JNI/Java integration is not supported with Python free-threading (PEP 703) builds; " "use a GIL-enabled Python build for Java integration"); return -1; #else static bool jni_loaded = false; if (!jni_loaded) { QoreProgram* qpgm = qore_python_pgm->getQoreProgram(); ExceptionSink xsink; if (ModuleManager::runTimeLoadModule("jni", qpgm, &xsink)) { qore_python_pgm->raisePythonException(xsink); return -1; } printd(5, "load_jni_module() 'jni' module loaded\n"); jni_loaded = true; } return 0; #endif } int do_jni_module_import(QorePythonProgram* qore_python_pgm, const char* name_str) { typedef int (*jni_module_import_t)(ExceptionSink* xsink, QoreProgram* pgm, const char* import); static jni_module_import_t jni_module_import = nullptr; if (!jni_module_import) { if (load_jni_module(qore_python_pgm)) { return -1; } jni_module_import = (jni_module_import_t)dlsym(RTLD_DEFAULT, "jni_module_import"); if (!jni_module_import) { PyErr_SetString(PyExc_ValueError, "cannot find required symbol 'jni_module_import'"); return -1; } } { QoreString path(name_str); if (path.size() > 2 && (path[-2] == '.' && path[-1] == '*')) { QoreStringMaker desc("'%s': wildcard imports are not currently supported", name_str); PyErr_SetString(PyExc_ValueError, desc.c_str()); return -1; } } QoreProgram* qpgm = qore_python_pgm->getQoreProgram(); ExceptionSink xsink; // set program context for initialization QoreProgramContextHelper pgm_ctx(qpgm); if (jni_module_import(&xsink, qpgm, name_str)) { assert(xsink); qore_python_pgm->raisePythonException(xsink); return -1; } return 0; } PyObject* qoreloader_load_java(PyObject* self, PyObject* args) { if (!PyTuple_Check(args) || (PyTuple_Size(args) != 1)) { PyErr_SetString(PyExc_ValueError, "single string argument required for 'qoreloader.load_java()'"); return nullptr; } PyObject* name = PyTuple_GetItem(args, 0); if (!PyUnicode_Check(name)) { QoreStringMaker desc("single string string argument required for 'qoreloader.load_java(); got type '%s' " \ "instead", Py_TYPE(name)->tp_name); PyErr_SetString(PyExc_ValueError, desc.c_str()); return nullptr; } const char* name_str = PyUnicode_AsUTF8(name); QorePythonProgram* qore_python_pgm = QorePythonProgram::getContext(); if (do_jni_module_import(qore_python_pgm, name_str)) { return nullptr; } //printd(5, "qoreloader_load_java() jni module loaded: import '%s' pgm: %p\n", name_str, qpgm); Py_INCREF(Py_None); return Py_None; } PyObject* qoreloader_issue_module_cmd(PyObject* self, PyObject* args) { if (!PyTuple_Check(args) || (PyTuple_Size(args) != 2)) { PyErr_SetString(PyExc_ValueError, "two string arguments required for 'qoreloader.issue_module_cmd()'"); return nullptr; } PyObject* module = PyTuple_GetItem(args, 0); if (!PyUnicode_Check(module)) { QoreStringMaker desc("first agument must be a string when calling 'qoreloader.issue_module_cmd(); got type " \ "'%s' instead", Py_TYPE(module)->tp_name); PyErr_SetString(PyExc_ValueError, desc.c_str()); return nullptr; } PyObject* cmd = PyTuple_GetItem(args, 1); if (!PyUnicode_Check(cmd)) { QoreStringMaker desc("second agument must be a string when calling 'qoreloader.issue_module_cmd(); got type " \ "'%s' instead", Py_TYPE(cmd)->tp_name); PyErr_SetString(PyExc_ValueError, desc.c_str()); return nullptr; } const char* module_str = PyUnicode_AsUTF8(module); const char* cmd_str = PyUnicode_AsUTF8(cmd); ExceptionSink xsink; QorePythonProgram* qore_python_pgm = QorePythonProgram::getContext(); QoreProgram* qpgm = qore_python_pgm->getQoreProgram(); if (qpgm->issueModuleCmd(module_str, cmd_str, &xsink)) { assert(xsink); qore_python_pgm->raisePythonException(xsink); return nullptr; } assert(!xsink); Py_INCREF(Py_None); return Py_None; }