/*
* @file opencog/cython/PythonLoader.cc
* @author Zhenhua Cai <czhedu@gmail.com>
* Ramin Barati <rekino@gmail.com>
* Keyvan Mir Mohammad Sadeghi <keyvan@opencog.org>
* Curtis Faith <curtis.m.faith@gmail.com>
* Alexey Potapov <alexey@singularitynet.io>
* @date 2011-09-20
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License v3 as
* published by the Free Software Foundation and including the exceptions
* at http://opencog.org/wiki/Licenses
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, write to:
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <vector>
#include <opencog/util/exceptions.h>
#include <opencog/util/Logger.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/eval/FrameStack.h>
#include "PythonEval.h"
#include "PyGILGuard.h"
// This is a header in the build directory, auto-gened by cython.
// It can only ever be included just once, over all c++ files.
// More than one inclusion leads to obscure runtime errors.
#include "opencog/atomspace_api.h"
#include <dlfcn.h>
using namespace opencog;
const int NO_SIGNAL_HANDLERS = 0;
// ============================================================
// Python system initialization
static bool already_initialized = false;
static bool initialized_outside_opencog = false;
static void *_dlso = nullptr;
void opencog::global_python_initialize()
{
// Don't initialize twice
if (already_initialized) return;
already_initialized = true;
logger().debug("[global_python_initialize] Start");
_dlso = dlopen(PYLIBNAME, RTLD_LAZY | RTLD_GLOBAL);
// We don't really know the gstate yet but we'll set
// it here to avoid compiler warnings below.
PyGILState_STATE gstate = PyGILState_UNLOCKED;
// Start up Python.
if (Py_IsInitialized())
{
// If we were already initialized then someone else did it.
initialized_outside_opencog = true;
// Just grab the GIL
gstate = PyGILState_Ensure();
}
else
{
// We are doing the initialization.
initialized_outside_opencog = false;
// Initialize Python (InitThreads grabs GIL implicitly)
Py_InitializeEx(NO_SIGNAL_HANDLERS);
// Python 3.9 came out in October 2020. Remove this in 2026/2027
#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 9
PyEval_InitThreads();
#endif
}
// Initialize the auto-generated Cython api.
import_opencog__atomspace();
// The import_opencog__atomspace() call above sets the
// py_atom() function pointer if the cython module load
// succeeded. But the function pointer will be NULL if the
// opencog.atomspace cython module failed to load. Avert
// a hard-to-debug crash on null-pointer-deref, and replace
// it by this hard-to-debug error message.
if (nullptr == py_atom) {
PyErr_Print();
logger().warn("PythonEval::%s Failed to load the "
"opencog.atomspace module", __FUNCTION__);
} else {
// Only create and set a default atomspace if there isn't one
// already in the frame stack (e.g., set by Scheme before Python
// was initialized). This allows type constructors to work without
// requiring explicit set_default_atomspace() calls, while also
// respecting atomspaces that were set up before Python init.
if (nullptr == get_frame()) {
AtomSpacePtr default_atomspace = createAtomSpace();
set_frame(default_atomspace);
logger().debug("[global_python_initialize] Created default atomspace");
} else {
logger().debug("[global_python_initialize] Using existing atomspace from frame stack");
}
}
// Release the GIL, otherwise the Python shell hangs on startup.
if (initialized_outside_opencog)
PyGILState_Release(gstate);
else
// Several websites suggest that `PyEval_ReleaseLock()`
// should be used here. However, this results in bug
// opencog/atomspace#671. A closer reading of the official
// python docs suggests that `PyEval_SaveThread` be used
// instead, and indeed ... that works! Woo hoo!
PyEval_SaveThread();
logger().debug("[global_python_initialize] Finish");
}
void opencog::global_python_finalize()
{
logger().debug("[global_python_finalize] Start");
if (!already_initialized)
return;
// Cleanup Python.
if (!initialized_outside_opencog)
{
PyGILState_Ensure(); // yes this is needed, see bug #671
Py_Finalize();
if (_dlso) dlclose(_dlso);
}
// No longer initialized.
already_initialized = false;
_dlso = nullptr;
logger().debug("[global_python_finalize] Finish");
}
void PythonEval::initialize_python_objects_and_imports(void)
{
// Grab the GIL
GILGuard gil;
// Get the __main__ module. NOTE: PyImport_AddModule returns
// a borrowed reference so we must promote it with an increment.
_pyRootModule = PyImport_AddModule("__main__");
Py_INCREF(_pyRootModule);
PyModule_AddStringConstant(_pyRootModule, "__file__", "");
logger().debug("PythonEval::%s Finished initialising python evaluator.",
__FUNCTION__);
}
// ====================================================
// Finding python functions
/**
* Get the Python function from a dotted name like "module.Class.method".
* Returns a new reference that the caller must DECREF.
*/
PyObject* PythonEval::get_function(const std::string& moduleFunction)
{
// Split into parts: "module.Class.method" -> ["module", "Class", "method"]
std::vector<std::string> parts;
size_t start = 0;
size_t dot = moduleFunction.find('.');
while (dot != std::string::npos)
{
parts.push_back(moduleFunction.substr(start, dot - start));
start = dot + 1;
dot = moduleFunction.find('.', start);
}
parts.push_back(moduleFunction.substr(start));
// Start with __main__ or import the first part as a module
PyObject* obj = nullptr;
size_t i = 0;
if (parts.size() > 1)
{
// Try to import first part as a module
obj = PyImport_ImportModule(parts[0].c_str());
if (obj) {
i = 1; // Successfully imported, continue from second part
} else {
// Not a module, clear error and try as __main__ attribute
PyErr_Clear();
obj = _pyRootModule;
Py_INCREF(obj);
i = 0;
}
}
else
{
// No dots, just a function name in __main__
obj = _pyRootModule;
Py_INCREF(obj);
}
// Walk the remaining dotted path
for (; i < parts.size(); i++)
{
PyObject* next = PyObject_GetAttrString(obj, parts[i].c_str());
Py_DECREF(obj);
if (!next)
{
PyErr_Clear();
throw RuntimeException(TRACE_INFO,
"Python object '%s' not found in '%s'!",
parts[i].c_str(), moduleFunction.c_str());
}
obj = next;
}
return obj; // Returns new reference
}
/**
* Call the user defined function with the arguments passed in the
* ListLink handle 'arguments'.
*
* On error throws an exception.
*/
ValuePtr PythonEval::call_user_function(const std::string& func,
const HandleSeq& args)
{
// Create the Python tuple for the function call with python
// atoms for each of the atoms in the link arguments.
size_t nargs = args.size();
PyObject* pyArguments = PyTuple_New(nargs);
for (size_t i=0; i<nargs; i++)
PyTuple_SetItem(pyArguments, i, py_atom(args[i]));
// Execute the user function and store its return value.
PyObject* pyValue = do_call_user_function(func, pyArguments);
// Get the C++ ValuePtr directly via the API.
ValuePtr vptr = py_value_ptr(pyValue);
Py_DECREF(pyValue);
// The pyx wrapper code will return a null ValuePtr if it
// stumbled on anything unexpected.
if (nullptr == vptr)
throw RuntimeException(TRACE_INFO,
"Python function '%s' did not return Atomese!",
func.c_str());
return vptr;
}
// =========== END OF FILE =========