/*
 * Copyright 2015 Brockmann Consult GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * This file was modified by Deephaven Data Labs.
 *
 */

#include <jni.h>
#include <Python.h>
#include "frameobject.h"

#include "jpy_module.h"
#include "jpy_diag.h"
#include "jpy_jtype.h"
#include "jpy_jobj.h"
#include "jpy_conv.h"

#include "org_jpy_PyLib.h"
#include "org_jpy_PyLib_Diag.h"

// Note: Native org.jpy.PyLib function definition headers in this file are formatted according to the header
// generated by javah. This makes it easier to follow up changes in the header.

PyObject* PyLib_GetAttributeObject(JNIEnv* jenv, PyObject* pyValue, jstring jName);
PyObject* PyLib_CallAndReturnObject(JNIEnv *jenv, PyObject* pyValue, jboolean isMethodCall, jstring jName, jint argCount, jobjectArray jArgs, jobjectArray jParamClasses);
void PyLib_HandlePythonException(JNIEnv* jenv);
void PyLib_ThrowOOM(JNIEnv* jenv);
void PyLib_ThrowFNFE(JNIEnv* jenv, const char *file);
void PyLib_ThrowUOE(JNIEnv* jenv, const char *message);
void PyLib_ThrowRTE(JNIEnv* jenv, const char *message);
void PyLib_RedirectStdOut(void);
int copyPythonDictToJavaMap(JNIEnv *jenv, PyObject *pyDict, jobject jMap);

static PyThreadState *_save = NULL;

static int python_traceback_report(PyObject *tb, char **buf, int* bufLen);

//#define JPy_JNI_DEBUG 1
#define JPy_JNI_DEBUG 0

// Make sure the following contants are same as in enum org.jpy.PyInputMode
#define JPy_IM_STATEMENT  256
#define JPy_IM_SCRIPT     257
#define JPy_IM_EXPRESSION 258

#if PY_VERSION_HEX >= 0x030D0000
#define JPy_Py_IsFinalizing Py_IsFinalizing
#else
#define JPy_Py_IsFinalizing _Py_IsFinalizing
#endif

// Checking if Python is in the middle of finalization to make sure that we do not attempt to interact with the Python
// runtime while it is shutting down. This helps prevent undefined behavior, such as thread hanging/crashing in
// PyGILState_Ensure() as described in https://docs.python.org/3.14/c-api/init.html#c.PyGILState_Ensure. Note that this
// approach has some limitations:
// 1. it doesn't completely prevent the race condition from happening(TOCTOU), but it mitigates the risk significantly.
// 2. it adds some overhead to calling Python from Java, but this is usually negligible compared to the cost of acquiring
// and releasing the GIL itself.
#define JPy_BEGIN_GIL_STATE(retval) {                     \
    if (JPy_Py_IsFinalizing() != 0) {                                        \
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Thread attempted to call into Python during interpreter shutdown. Raising exception.\n"); \
        (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, "Thread attempted to call into Python during interpreter shutdown"); \
        return (retval);                                                     \
    }                                                                        \
    PyGILState_STATE gilState = PyGILState_Ensure();

#define JPy_BEGIN_GIL_STATE_VOID() {                      \
    if (JPy_Py_IsFinalizing() != 0) {                                        \
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Thread attempted to call into Python during interpreter shutdown. Raising exception.\n"); \
        (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, "Thread attempted to call into Python during interpreter shutdown"); \
        return;                                                              \
    }                                                                        \
    PyGILState_STATE gilState = PyGILState_Ensure();

#define JPy_END_GIL_STATE    PyGILState_Release(gilState); }


/**
 * Called if the JVM loads this module.
 * Will only called if this module's code is linked into a shared library and loaded by a Java VM.
 */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved)
{
    if (JPy_JNI_DEBUG) printf("JNI_OnLoad: enter: jvm=%p, JPy_JVM=%p, JPy_MustDestroyJVM=%d, Py_IsInitialized()=%d\n",
                              jvm, JPy_JVM, JPy_MustDestroyJVM, Py_IsInitialized());

    if (JPy_JVM == NULL) {
        JPy_JVM = jvm;
        JPy_MustDestroyJVM = JNI_FALSE;
    } else if (JPy_JVM == jvm) {
        if (JPy_JNI_DEBUG) printf("JNI_OnLoad: warning: same JVM already running\n");
    } else {
        if (JPy_JNI_DEBUG) printf("JNI_OnLoad: warning: different JVM already running (expect weird things!)\n");
    }

    if (JPy_JNI_DEBUG) printf("JNI_OnLoad: exit: jvm=%p, JPy_JVM=%p, JPy_MustDestroyJVM=%d, Py_IsInitialized()=%d\n",
                              jvm, JPy_JVM, JPy_MustDestroyJVM, Py_IsInitialized());

    if (JPy_JNI_DEBUG) fflush(stdout);

    return JPY_JNI_VERSION;
}


/**
 * Called if the JVM unloads this module.
 * Will only called if this module's code is linked into a shared library and loaded by a Java VM.
 */
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* jvm, void* reserved)
{
    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "JNI_OnUnload: enter: jvm=%p, JPy_JVM=%p, JPy_MustDestroyJVM=%d, Py_IsInitialized()=%d\n",
                   jvm, JPy_JVM, JPy_MustDestroyJVM, Py_IsInitialized());

    Py_Finalize();

    if (!JPy_MustDestroyJVM) {
        JPy_ClearGlobalVars(JPy_GetJNIEnv());
        JPy_JVM = NULL;
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "JNI_OnUnload: exit: jvm=%p, JPy_JVM=%p, JPy_MustDestroyJVM=%d, Py_IsInitialized()=%d\n",
                   jvm, JPy_JVM, JPy_MustDestroyJVM, Py_IsInitialized());
}



/*
 * Class:     org_jpy_PyLib
 * Method:    isPythonRunning
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_isPythonRunning
  (JNIEnv* jenv, jclass jLibClass)
{
    int init;
    init = Py_IsInitialized();
    return init && JPy_Module != NULL;
}

#define  MAX_PYTHON_HOME   256
wchar_t staticPythonHome[MAX_PYTHON_HOME];

/*
 * Class:     org_jpy_PyLib
 * Method:    setPythonHome
 * Signature: (Ljava/lang/String;)Z
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_setPythonHome
  (JNIEnv* jenv, jclass jLibClass, jstring jPythonHome)
{
    const wchar_t* pythonHome = NULL;

    const char *nonWidePythonHome = NULL;
    jboolean result = JNI_FALSE;
    nonWidePythonHome = (*jenv)->GetStringUTFChars(jenv, jPythonHome, NULL);

    if (nonWidePythonHome != NULL) {
        pythonHome = Py_DecodeLocale(nonWidePythonHome, NULL);
        if (pythonHome != NULL) {
            if (wcslen(pythonHome) < MAX_PYTHON_HOME) {
                 wcscpy(staticPythonHome, pythonHome);
                 result = JNI_TRUE;
            }
            else {
                PyMem_RawFree(pythonHome);
            }
        }

        if (result) {
            Py_SetPythonHome(staticPythonHome);
            PyMem_RawFree(pythonHome);
        }

        (*jenv)->ReleaseStringUTFChars(jenv, jPythonHome, nonWidePythonHome);
    }

    return result;
}

#define  MAX_PROGRAM_NAME   256
wchar_t staticProgramName[MAX_PROGRAM_NAME];

/*
 * Class:     org_jpy_PyLib
 * Method:    setProgramName
 * Signature: (Ljava/lang/String;)Z
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_setProgramName
  (JNIEnv* jenv, jclass jLibClass, jstring jProgramName)
{
    const wchar_t* programName = NULL;

    const char *nonWideProgramName = NULL;
    jboolean result = JNI_FALSE;
    nonWideProgramName = (*jenv)->GetStringUTFChars(jenv, jProgramName, NULL);

    if (nonWideProgramName != NULL) {
        programName = Py_DecodeLocale(nonWideProgramName, NULL);
        if (programName != NULL) {
            if (wcslen(programName) < MAX_PROGRAM_NAME) {
                 wcscpy(staticProgramName, programName);
                 result = JNI_TRUE;
            }
            else {
                PyMem_RawFree(programName);
            }

        }

        if (result) {
            Py_SetProgramName(staticProgramName);

            PyMem_RawFree(programName);
        }

        (*jenv)->ReleaseStringUTFChars(jenv, jProgramName, nonWideProgramName);
    }

    return result;
}

/*
 * Class:     org_jpy_PyLib
 * Method:    startPython0
 * Signature: ([Ljava/lang/String;)Z
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_startPython0
  (JNIEnv* jenv, jclass jLibClass, jobjectArray jPathArray)
{
    int pyInit = Py_IsInitialized();

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_startPython: entered: jenv=%p, pyInit=%d, JPy_Module=%p\n", jenv, pyInit, JPy_Module);

    if (!pyInit) {
        Py_Initialize();
        // See https://github.com/bcdev/jpy/issues/81
        PySys_SetArgvEx(0, NULL, 0);
        PyLib_RedirectStdOut();
        pyInit = Py_IsInitialized(); // todo: assert pyInit == 1?

        PyEval_InitThreads();
        _save = PyEval_SaveThread(); // todo: assert not null
    }

    if (pyInit) {

        if (JPy_DiagFlags != 0) {
            printf("PyLib_startPython: global Python interpreter information:\n");
            printf("  Py_GetProgramName()     = \"%ls\"\n", Py_GetProgramName());
            printf("  Py_GetPrefix()          = \"%ls\"\n", Py_GetPrefix());
            printf("  Py_GetExecPrefix()      = \"%ls\"\n", Py_GetExecPrefix());
            printf("  Py_GetProgramFullPath() = \"%ls\"\n", Py_GetProgramFullPath());
            printf("  Py_GetPath()            = \"%ls\"\n", Py_GetPath());
            printf("  Py_GetPythonHome()      = \"%ls\"\n", Py_GetPythonHome());
            printf("  Py_GetVersion()         = \"%s\"\n", Py_GetVersion());
            printf("  Py_GetPlatform()        = \"%s\"\n", Py_GetPlatform());
            printf("  Py_GetCompiler()        = \"%s\"\n", Py_GetCompiler());
            printf("  Py_GetBuildInfo()       = \"%s\"\n", Py_GetBuildInfo());
        }

        // If we've got jPathArray, add all entries to Python's "sys.path"
        //
        if (jPathArray != NULL) {
            PyObject* pyPathList;
            PyObject* pyPath;
            jstring jPath;
            jsize i, pathCount;

            pathCount = (*jenv)->GetArrayLength(jenv, jPathArray);
            //printf(">> pathCount=%d\n", pathCount);
            if (pathCount > 0) {

                JPy_BEGIN_GIL_STATE(JNI_FALSE)

                pyPathList = PySys_GetObject("path");
                //printf(">> pyPathList=%p, len=%ld\n", pyPathList, PyList_Size(pyPathList));
                if (pyPathList != NULL) {
                    JPy_INCREF(pyPathList);
                    for (i = pathCount - 1; i >= 0; i--) {
                        jPath = (*jenv)->GetObjectArrayElement(jenv, jPathArray, i);
                        //printf(">> i=%d, jPath=%p\n", i, jPath);
                        if (jPath != NULL) {
                            pyPath = JPy_FromJString(jenv, jPath);
                            //printf(">> i=%d, pyPath=%p\n", i, pyPath);
                            if (pyPath != NULL) {
                                PyList_Insert(pyPathList, 0, pyPath);
                            }
                        }
                    }
                    JPy_DECREF(pyPathList);
                }
                //printf(">> pyPathList=%p, len=%ld\n", pyPathList, PyList_Size(pyPathList));
                //printf(">> pyPathList=%p, len=%ld (check)\n", PySys_GetObject("path"), PyList_Size(PySys_GetObject("path")));

                JPy_END_GIL_STATE
            }
        }

        // if the global JPy_Module is NULL, then the 'jpy' extension module has not been imported yet.
        //
        if (JPy_Module == NULL) {
            PyObject* pyModule;

            JPy_BEGIN_GIL_STATE(JNI_FALSE)

            // We import 'jpy' so that Python can call our PyInit_jpy() which sets up a number of
            // required global variables (including JPy_Module, see above).
            //
            pyModule = PyImport_ImportModule("jpy");
            //printf(">> pyModule=%p\n", pyModule);
            if (pyModule == NULL) {
                JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_startPython: failed to import module 'jpy'\n");
                if (JPy_DiagFlags != 0 && PyErr_Occurred()) {
                    PyErr_Print();
                }
                PyLib_HandlePythonException(jenv);
            }

            JPy_END_GIL_STATE
        }
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_startPython: exiting: jenv=%p, pyInit=%d, JPy_Module=%p\n", jenv, pyInit, JPy_Module);

    //printf(">> JPy_Module=%p\n", JPy_Module);

    if (!pyInit) {
        (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, "Failed to initialize Python interpreter.");
        return JNI_FALSE;
    }
    if (JPy_Module == NULL) {
        (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, "Failed to initialize Python 'jpy' module.");
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

/*
 * Class:     org_jpy_PyLib
 * Method:    stopPython0
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_stopPython0
  (JNIEnv* jenv, jclass jLibClass)
{
    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_stopPython: entered: JPy_Module=%p\n", JPy_Module);

    if (Py_IsInitialized()) {
        // Cleanup the JPY stateful structures and shut down the interpreter.

        JPy_BEGIN_GIL_STATE_VOID()

        JPy_free();

        JPy_END_GIL_STATE

        PyEval_RestoreThread(_save);
        _save = NULL;
        Py_Finalize();
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_stopPython: exiting: JPy_Module=%p\n", JPy_Module);
}


/*
 * Class:     org_jpy_PyLib
 * Method:    getPythonVersion
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_org_jpy_PyLib_getPythonVersion
  (JNIEnv* jenv, jclass jLibClass)
{
    const char* version;

    version = Py_GetVersion();
    if (version == NULL) {
        return NULL;
    }

    return (*jenv)->NewStringUTF(jenv, version);
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    execScript
 * Signature: (Ljava/lang/String;)J
 */
JNIEXPORT jint JNICALL Java_org_jpy_PyLib_execScript
  (JNIEnv* jenv, jclass jLibClass, jstring jScript)
{
    const char* scriptChars;
    int retCode = -1;

    JPy_BEGIN_GIL_STATE(-1)

    scriptChars = (*jenv)->GetStringUTFChars(jenv, jScript, NULL);
    if (scriptChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_execScript: script='%s'\n", scriptChars);
    retCode = PyRun_SimpleString(scriptChars);
    if (retCode < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_execScript: error: PyRun_SimpleString(\"%s\") returned %d\n", scriptChars, retCode);
        // Note that we cannot retrieve last Python exception after a calling PyRun_SimpleString, see documentation of PyRun_SimpleString.
    }

error:
    if (scriptChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jScript, scriptChars);
    }

    JPy_END_GIL_STATE

    return retCode;
}


PyObject* PyLib_ConvertJavaToPythonObject(JNIEnv* jenv, jobject jObject)
{
    JPy_JType* type;
    if (jObject == NULL) {
      return JPy_FROM_JNULL();
    }
    type = JType_GetTypeForObject(jenv, jObject, JNI_FALSE);
    return JType_ConvertJavaToPythonObject(jenv, type, jObject);
}

int PyLib_ConvertPythonToJavaObject(JNIEnv* jenv, PyObject* pyObject, jobject* jObject)
{
    return JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, pyObject, jObject, JNI_FALSE);
}

void dumpDict(const char* dictName, PyObject* dict)
{
    Py_ssize_t size;
    PyObject *key = NULL, *value = NULL;
    Py_ssize_t pos = 0;
    Py_ssize_t i = 0;

    if (!PyDict_Check(dict)) {
        printf(">> dumpDict: %s is not a dictionary!\n", dictName);
        return;
    }

    size = PyDict_Size(dict);
    printf(">> dumpDict: %s.size = %ld\n", dictName, size);
#if PY_VERSION_HEX >= 0x030D0000 // >=3.13
    // PyDict_Next is not thread-safe, so we need to protect it with a critical section
    // https://docs.python.org/3/howto/free-threading-extensions.html#pydict-next
    Py_BEGIN_CRITICAL_SECTION(dict);
#endif
    while (PyDict_Next(dict, &pos, &key, &value)) {
        const char* name;
        name = JPy_AS_UTF8(key);
        printf(">> dumpDict: %s[%ld].name = '%s'\n", dictName, i, name);
        i++;
    }
#if PY_VERSION_HEX >= 0x030D0000 // >=3.13
    Py_END_CRITICAL_SECTION();
#endif
}

/**
 * Get the globals from the __main__ module.
 */
PyObject *getMainGlobals() {
    PyObject* pyMainModule;
    PyObject* pyGlobals;

    pyMainModule = PyImport_AddModule("__main__"); // borrowed ref

    if (pyMainModule == NULL) {
        return NULL;
    }

    pyGlobals = PyModule_GetDict(pyMainModule); // borrowed ref
    JPy_XINCREF(pyGlobals);

    return pyGlobals;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_getMainGlobals
        (JNIEnv *jenv, jclass libClass) {
    jobject objectRef = NULL;
    PyObject *globals;

    JPy_BEGIN_GIL_STATE(NULL)

    globals = getMainGlobals(); // new ref
    if (globals == NULL) {
        goto error;
    }

    if (JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, globals, &objectRef, JNI_FALSE) < 0) {
        objectRef = NULL;
        goto error;
    }

error:
    JPy_XDECREF(globals);
    JPy_END_GIL_STATE

    return objectRef;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_getCurrentGlobals
        (JNIEnv *jenv, jclass libClass) {
    jobject objectRef = NULL;
    PyObject *globals;

    JPy_BEGIN_GIL_STATE(NULL)

#if PY_VERSION_HEX < 0x030D0000 // < 3.13
    globals = PyEval_GetGlobals(); // borrowed ref
    JPy_XINCREF(globals);
#else
    // See https://peps.python.org/pep-0667 for the change in Python 3.13
    globals = PyEval_GetFrameGlobals(); // new ref
#endif

    if (globals == NULL) {
        goto error;
    }

    if (JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, globals, &objectRef, JNI_FALSE) < 0) {
        objectRef = NULL;
        goto error;
    }

error:
    JPy_XDECREF(globals);
    JPy_END_GIL_STATE

    return objectRef;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_getCurrentLocals
        (JNIEnv *jenv, jclass libClass) {
    jobject objectRef = NULL;
    PyObject *locals;

    JPy_BEGIN_GIL_STATE(NULL)

#if PY_VERSION_HEX < 0x030D0000 // < 3.13
    locals = PyEval_GetLocals(); // borrowed ref
    JPy_XINCREF(locals);
#else
    // See https://peps.python.org/pep-0667 for the change in Python 3.13
    locals = PyEval_GetFrameLocals(); // new ref
#endif

    if (locals == NULL) {
        goto error;
    }

    if (JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, locals, &objectRef, JNI_FALSE) < 0) {
        objectRef = NULL;
        goto error;
    }

error:
    JPy_XDECREF(locals);
    JPy_END_GIL_STATE

    return objectRef;
}


JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_copyDict
        (JNIEnv *jenv, jclass libClass, jlong pyPointer) {
    jobject objectRef = NULL;
    PyObject* copy = NULL;
    PyObject* src = (PyObject*)pyPointer;

    JPy_BEGIN_GIL_STATE(NULL)

    if (!PyDict_Check(src)) {
        PyLib_ThrowUOE(jenv, "Not a dictionary!");
        goto error;
    }

    copy = PyDict_Copy(src);
    if (JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, copy, &objectRef, JNI_FALSE) < 0) {
        objectRef = NULL;
        goto error;
    }

error:
    JPy_XDECREF(copy);
    JPy_END_GIL_STATE

    return objectRef;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_newDict
        (JNIEnv *jenv, jclass libClass) {
    jobject objectRef = NULL;
    PyObject *dict;

    JPy_BEGIN_GIL_STATE(NULL)

    dict = PyDict_New();

    if (JType_ConvertPythonToJavaObject(jenv, JPy_JPyObject, dict, &objectRef, JNI_FALSE) < 0) {
        objectRef = NULL;
        goto error;
    }

error:
    JPy_XDECREF(dict);
    JPy_END_GIL_STATE

    return objectRef;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_pyDictKeys
        (JNIEnv *jenv, jclass libClass, jlong pyDict) {
    jobject result = NULL;
    PyObject* keys = NULL;
    PyObject* src = (PyObject*)pyDict;

    JPy_BEGIN_GIL_STATE(NULL)

    if (!PyDict_Check(src)) {
        PyLib_ThrowUOE(jenv, "Not a dictionary!");
        goto error;
    }

    keys = PyDict_Keys(src);
    if (JType_CreateJavaPyObject(jenv, JPy_JPyObject, keys, &result) < 0) {
        result = NULL;
        goto error;
    }

error:
    JPy_XDECREF(keys);
    JPy_END_GIL_STATE
    return result;
}

JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_pyDictValues
        (JNIEnv *jenv, jclass libClass, jlong pyDict) {
    jobject result = NULL;
    PyObject* values = NULL;
    PyObject* src = (PyObject*)pyDict;

    JPy_BEGIN_GIL_STATE(NULL)

    if (!PyDict_Check(src)) {
        PyLib_ThrowUOE(jenv, "Not a dictionary!");
        goto error;
    }

    values = PyDict_Values(src);
    if (JType_CreateJavaPyObject(jenv, JPy_JPyObject, values, &result) < 0) {
        result = NULL;
        goto error;
    }

error:
    JPy_XDECREF(values);
    JPy_END_GIL_STATE
    return result;
}

JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyDictContains
        (JNIEnv *jenv, jclass libClass, jlong pyDict, jobject jKey, jclass jKeyClass) {
    PyObject* src = (PyObject*)pyDict;
    int result = 0;
    JPy_JType* keyType;
    PyObject* pyKey;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (!PyDict_Check(src)) {
        result = -1;
        PyLib_ThrowUOE(jenv, "Not a dictionary!");
        goto error;
    }

    if (jKeyClass != NULL) {
        keyType = JType_GetType(jenv, jKeyClass, JNI_FALSE);
        if (keyType == NULL) {
            result = -1;
            JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_pyDictContains: failed to retrieve type\n");
            PyLib_HandlePythonException(jenv);
            goto error;
        }
        pyKey = JPy_FromJObjectWithType(jenv, jKey, keyType);
    } else {
        pyKey = JPy_FromJObject(jenv, jKey);
    }

    result = PyDict_Contains(src, pyKey);
    if (result < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_pyDictContains: PyDict_Contains error\n");
        PyLib_HandlePythonException(jenv);
        goto error;
    }

error:
    JPy_END_GIL_STATE
    return result == 1 ? JNI_TRUE : JNI_FALSE;
}

/**
 * Copies a Java Map<String, Object> into a new Python dictionary.
 */
PyObject *copyJavaStringObjectMapToPyDict(JNIEnv *jenv, jobject jMap) {
    PyObject* result = NULL;
    jobject entrySet, iterator, mapEntry;
    jboolean hasNext;

    result = PyDict_New();
    if (result == NULL) {
        return result;
    }

    entrySet = (*jenv)->CallObjectMethod(jenv, jMap, JPy_Map_entrySet_MID);
    if (entrySet == NULL) {
        goto error;
    }

    iterator = (*jenv)->CallObjectMethod(jenv, entrySet, JPy_Set_Iterator_MID);
    if (iterator == NULL) {
        goto error;
    }

    hasNext = (*jenv)->CallBooleanMethod(jenv, iterator, JPy_Iterator_hasNext_MID);

    while (hasNext) {
        jobject key, value;
        char const *keyChars;
        PyObject *pyKey;
        PyObject *pyValue;
        JPy_JType* type;

        mapEntry = (*jenv)->CallObjectMethod(jenv, iterator, JPy_Iterator_next_MID);
        if (mapEntry == NULL) {
            goto error;
        }

        key = (*jenv)->CallObjectMethod(jenv, mapEntry, JPy_Map_Entry_getKey_MID);
        if (key == NULL) {
            goto error;
        }

        // we require string keys
        if (!(*jenv)->IsInstanceOf(jenv, key, JPy_String_JClass)) {
            goto error;
        }
        keyChars = (*jenv)->GetStringUTFChars(jenv, (jstring)key, NULL);
        if (keyChars == NULL) {
            goto error;
        }

        pyKey = JPy_FROM_CSTR(keyChars);
        (*jenv)->ReleaseStringUTFChars(jenv, (jstring)key, keyChars);

        value = (*jenv)->CallObjectMethod(jenv, mapEntry, JPy_Map_Entry_getValue_MID);

        type = JType_GetTypeForObject(jenv, value, JNI_FALSE);
        pyValue = JType_ConvertJavaToPythonObject(jenv, type, value);

        PyDict_SetItem(result, pyKey, pyValue);

        hasNext = (*jenv)->CallBooleanMethod(jenv, iterator, JPy_Iterator_hasNext_MID);
    }

    return result;

error:
    JPy_XDECREF(result);
    return NULL;
}

int copyPythonDictToJavaMap(JNIEnv *jenv, PyObject *pyDict, jobject jMap) {
    PyObject *pyKey, *pyValue;
    Py_ssize_t pos = 0;
    Py_ssize_t dictSize;
    jobject *jValues = NULL;
    jobject *jKeys = NULL;
    int ii;
    jboolean exceptionAlready = JNI_FALSE;
    jthrowable savedException = NULL;
    int retcode = -1;

    if (!PyDict_Check(pyDict)) {
        PyLib_ThrowUOE(jenv, "PyObject is not a dictionary!");
        return -1;
    }

    dictSize = PyDict_Size(pyDict);

    jKeys = malloc(dictSize * sizeof(jobject));
    jValues = malloc(dictSize * sizeof(jobject));
    if (jKeys == NULL || jValues == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }

    exceptionAlready = (*jenv)->ExceptionCheck(jenv);
    if (exceptionAlready) {
        // save the exception away, because otherwise the conversion methods might spuriously fail
        savedException = (*jenv)->ExceptionOccurred(jenv);
        (*jenv)->ExceptionClear(jenv);
    }

    // first convert everything
    ii = 0;
    while (PyDict_Next(pyDict, &pos, &pyKey, &pyValue)) {
        if (JPy_AsJObjectWithClass(jenv, pyKey, &(jKeys[ii]), JPy_String_JClass) < 0) {
            // an error occurred
            goto error;
        }
        if (JPy_AsJObject(jenv, pyValue, &(jValues[ii]), JNI_TRUE) < 0) {
            // an error occurred
            goto error;
        }
        ii++;
    }

    // now that we've converted, clear out the map and repopulate it
    (*jenv)->CallVoidMethod(jenv, jMap, JPy_Map_clear_MID);
    for (ii = 0; ii < dictSize; ++ii) {
        // since the map is cleared, we want to plow through all of the put operations
        (*jenv)->CallObjectMethod(jenv, jMap, JPy_Map_put_MID, jKeys[ii], jValues[ii]);
    }
    // and we are successful!
    retcode = 0;

error:
    if (exceptionAlready) {
        // restore our original exception
        (*jenv)->Throw(jenv, savedException);
    }

    free(jKeys);
    free(jValues);
    return retcode;
}

typedef PyObject * (*DoRun)(const void *,int,PyObject*,PyObject*);

/**
 * After setting up the globals and locals, calls the provided function.
 *
 * jStart must be JPy_IM_STATEMENT, JPy_IM_SCRIPT, JPy_IM_EXPRESSION; matching what you are trying to
 * run.  If you use a statement or expression instead of a script; some of your code may be ignored.
 *
 * If jGlobals is not specified, then the main module globals are used.
 *
 * If jLocals is not specified, then the globals are used.
 *
 * jGlobals and jLocals may be a PyObject, in which case they are used without translation.  Otherwise,
 * they must be a map from String to Object, and will be copied to a new python dictionary.  After execution
 * completes the dictionary entries will be copied back.
 *
 */
jlong executeInternal(JNIEnv* jenv, jclass jLibClass, jint jStart, jobject jGlobals, jobject jLocals, DoRun runFunction, void *runArg) {
    PyObject *pyReturnValue;
    PyObject *pyGlobals;
    PyObject *pyLocals;
    int start;
    jboolean decGlobals, decLocals, copyGlobals, copyLocals;

    JPy_BEGIN_GIL_STATE(0)

    decGlobals = decLocals = JNI_FALSE;
    copyGlobals = copyLocals = JNI_FALSE;
    pyGlobals = NULL;
    pyLocals = NULL;
    pyReturnValue = NULL;

    if (jGlobals == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using main globals\n");
        pyGlobals = getMainGlobals(); // new ref
        decGlobals = JNI_TRUE;
        if (pyGlobals == NULL) {
            PyLib_HandlePythonException(jenv);
            goto error;
        }
    } else if ((*jenv)->IsInstanceOf(jenv, jGlobals, JPy_PyObject_JClass)) {
        // if we are an instance of PyObject, just use the object
        pyGlobals = (PyObject *)((*jenv)->CallLongMethod(jenv, jGlobals, JPy_PyObject_GetPointer_MID));
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using PyObject globals\n");
    } else if ((*jenv)->IsInstanceOf(jenv, jGlobals, JPy_PyDictWrapper_JClass)) {
        // if we are an instance of a wrapped dictionary, just use the underlying dictionary
        pyGlobals = (PyObject *)((*jenv)->CallLongMethod(jenv, jGlobals, JPy_PyDictWrapper_GetPointer_MID));
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using PyDictWrapper globals\n");
    } else if ((*jenv)->IsInstanceOf(jenv, jGlobals, JPy_Map_JClass)) {
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using Java Map globals\n");
        // this is a java Map and we need to convert it
        pyGlobals = copyJavaStringObjectMapToPyDict(jenv, jGlobals);
        if (pyGlobals == NULL) {
            PyLib_ThrowRTE(jenv, "Could not convert globals from Java Map to Python dictionary");
            goto error;
        }
        copyGlobals = decGlobals = JNI_TRUE;
    } else {
        PyLib_ThrowUOE(jenv, "Unsupported globals type");
        goto error;
    }

    // if we have no locals specified, using globals for it matches the behavior of eval, "The expression argument is
    // parsed and evaluated as a Python expression (technically speaking, a condition list) using the globals and
    // locals dictionaries as global and local namespace. ... If the locals dictionary is omitted it defaults to the
    // globals dictionary. If both dictionaries are omitted, the expression is executed in the environment where eval()
    // is called. The return value is the result of the evaluated expression.
    if (jLocals == NULL) {
        pyLocals = pyGlobals;
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using globals for locals\n");
    } else if ((*jenv)->IsInstanceOf(jenv, jLocals, JPy_PyObject_JClass)) {
        // if we are an instance of PyObject, just use the object
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using PyObject locals\n");
        pyLocals = (PyObject *)((*jenv)->CallLongMethod(jenv, jLocals, JPy_PyObject_GetPointer_MID));
    } else if ((*jenv)->IsInstanceOf(jenv, jLocals, JPy_PyDictWrapper_JClass)) {
        // if we are an instance of a wrapped dictionary, just use the underlying dictionary
        pyLocals = (PyObject *)((*jenv)->CallLongMethod(jenv, jLocals, JPy_PyDictWrapper_GetPointer_MID));
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using PyDictWrapper locals\n");
    } else if ((*jenv)->IsInstanceOf(jenv, jLocals, JPy_Map_JClass)) {
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: using Java Map locals\n");
        // this is a java Map and we need to convert it
        pyLocals = copyJavaStringObjectMapToPyDict(jenv, jLocals);
        if (pyLocals == NULL) {
            PyLib_ThrowRTE(jenv, "Could not convert locals from Java Map to Python dictionary");
            goto error;
        }
        copyLocals = decLocals = JNI_TRUE;
    } else {
        PyLib_ThrowUOE(jenv, "Unsupported locals type");
        goto error;
    }

    start = jStart == JPy_IM_STATEMENT ? Py_single_input :
            jStart == JPy_IM_SCRIPT ? Py_file_input :
            Py_eval_input;

    pyReturnValue = runFunction(runArg, start, pyGlobals, pyLocals);
    if (pyReturnValue == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: Handle Python Exception\n");
        PyLib_HandlePythonException(jenv);
        goto error;
    }

error:
    if (copyGlobals) {
        copyPythonDictToJavaMap(jenv, pyGlobals, jGlobals);
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: copied back Java global\n");
    }
    if (copyLocals) {
        copyPythonDictToJavaMap(jenv, pyLocals, jLocals);
        JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeInternal: copied back Java locals\n");
    }
    if (decGlobals) {
        JPy_XDECREF(pyGlobals);
    }
    if (decLocals) {
        JPy_XDECREF(pyLocals);
    }

    JPy_END_GIL_STATE

    return (jlong) pyReturnValue;
}

PyObject *pyRunStringWrapper(const char *code, int start, PyObject *globals, PyObject *locals) {
    PyObject *result = PyRun_String(code, start, globals, locals);
    return result;
}

/**
 * Calls PyRun_String under the covers to execute the string contents.
 *
 * jStart must be JPy_IM_STATEMENT, JPy_IM_SCRIPT, JPy_IM_EXPRESSION; matching what you are trying to
 * run.  If you use a statement or expression instead of a script; some of your code may be ignored.
 *
 * If jGlobals is not specified, then the main module globals are used.
 *
 * If jLocals is not specified, then the globals are used.
 *
 * jGlobals and jLocals may be a PyObject, in which case they are used without translation.  Otherwise,
 * they must be a map from String to Object, and will be copied to a new python dictionary.  After execution
 * completes the dictionary entries will be copied back.
 */
JNIEXPORT
jlong JNICALL Java_org_jpy_PyLib_executeCode
        (JNIEnv* jenv, jclass jLibClass, jstring jCode, jint jStart, jobject jGlobals, jobject jLocals) {
    const char *codeChars;
    jlong result;

    codeChars = (*jenv)->GetStringUTFChars(jenv, jCode, NULL);
    if (codeChars == NULL) {
        PyLib_ThrowOOM(jenv);
        return 0;
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_executeCode: code='%s'\n", codeChars);

    result = executeInternal(jenv, jLibClass, jStart, jGlobals, jLocals, (DoRun)pyRunStringWrapper, codeChars);

    if (codeChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jCode, codeChars);
    }

    return result;
}

typedef struct {
    FILE *fp;
    const char *filechars;
} RunFileArgs;

PyObject *pyRunFileWrapper(RunFileArgs *args, int start, PyObject *globals, PyObject *locals) {
    return PyRun_File(args->fp, args->filechars, start, globals, locals);
}

/**
 * Calls PyRun_Script under the covers to execute the script contents.
 *
 * jStart must be JPy_IM_STATEMENT, JPy_IM_SCRIPT, JPy_IM_EXPRESSION; matching what you are trying to
 * run.  If you use a statement or expression instead of a script; some of your code may be ignored.
 *
 * If jGlobals is not specified, then the main module globals are used.
 *
 * If jLocals is not specified, then the globals are used.
 *
 * jGlobals and jLocals may be a PyObject, in which case they are used without translation.  Otherwise,
 * they must be a map from String to Object, and will be copied to a new python dictionary.  After execution
 * completes the dictionary entries will be copied back.
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_executeScript
        (JNIEnv* jenv, jclass jLibClass, jstring jFile, jint jStart, jobject jGlobals, jobject jLocals) {
    RunFileArgs runFileArgs;
    jlong result = 0;

    runFileArgs.fp = NULL;
    runFileArgs.filechars = NULL;

    runFileArgs.filechars = (*jenv)->GetStringUTFChars(jenv, jFile, NULL);
    if (runFileArgs.filechars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }

    runFileArgs.fp = fopen(runFileArgs.filechars, "r");
    if (!runFileArgs.fp) {
        PyLib_ThrowFNFE(jenv, runFileArgs.filechars);
        goto error;
    }

    result = executeInternal(jenv, jLibClass, jStart, jGlobals, jLocals, (DoRun)pyRunFileWrapper, &runFileArgs);

error:
    if (runFileArgs.filechars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jFile, runFileArgs.filechars);
    }
    if (runFileArgs.fp != NULL) {
        fclose(runFileArgs.fp);
    }
    return result;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    incRef
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_incRef
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    Py_ssize_t refCount;

    pyObject = (PyObject*) objId;

    if (Py_IsInitialized()) {
        JPy_BEGIN_GIL_STATE_VOID()

        refCount = Py_REFCNT(pyObject);
        JPy_DIAG_PRINT(JPy_DIAG_F_MEM, "Java_org_jpy_PyLib_incRef: pyObject=%p, refCount=%d, type='%s'\n", pyObject, refCount, Py_TYPE(pyObject)->tp_name);
        JPy_INCREF(pyObject);

        JPy_END_GIL_STATE
    } else {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_incRef: error: no interpreter: pyObject=%p\n", pyObject);
    }
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    decRef
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_decRef
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    Py_ssize_t refCount;

    pyObject = (PyObject*) objId;

    if (Py_IsInitialized()) {
        JPy_BEGIN_GIL_STATE_VOID()

        refCount = Py_REFCNT(pyObject);
        if (refCount <= 0) {
            JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_decRef: error: refCount <= 0: pyObject=%p, refCount=%d\n", pyObject, refCount);
        } else {
            JPy_DIAG_PRINT(JPy_DIAG_F_MEM, "Java_org_jpy_PyLib_decRef: pyObject=%p, refCount=%d, type='%s'\n", pyObject, refCount, Py_TYPE(pyObject)->tp_name);
            JPy_DECREF(pyObject);
        }

        JPy_END_GIL_STATE
    } else {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_decRef: error: no interpreter: pyObject=%p\n", pyObject);
    }
}

JNIEXPORT void JNICALL Java_org_jpy_PyLib_decRefs
  (JNIEnv* jenv, jclass jLibClass, jlongArray objIds, jsize len)
{
    PyObject* pyObject;
    Py_ssize_t refCount;
    jsize i;

    jboolean isCopy;
    jlong* buf;

    if (Py_IsInitialized()) {
        JPy_BEGIN_GIL_STATE_VOID()

        // Note: it *may* be desirable to instead force a local copy using GetLongArrayRegion, TBD.
        // It is *not* a good idea to use a critical array here, as JPy_DECREF may trigger the python
        // object destructor, which can run arbitrary code.
        buf = (*jenv)->GetLongArrayElements(jenv, objIds, &isCopy);
        for (i = 0; i < len; i++) {
            pyObject = (PyObject*) buf[i];
            refCount = Py_REFCNT(pyObject);
            if (refCount <= 0) {
                JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_decRefs: error: refCount <= 0: pyObject=%p, refCount=%d\n", pyObject, refCount);
            } else {
                JPy_DIAG_PRINT(JPy_DIAG_F_MEM, "Java_org_jpy_PyLib_decRefs: pyObject=%p, refCount=%d, type='%s'\n", pyObject, refCount, Py_TYPE(pyObject)->tp_name);
                JPy_DECREF(pyObject);
            }
        }
        (*jenv)->ReleaseLongArrayElements(jenv, objIds, buf, JNI_ABORT);
        JPy_END_GIL_STATE
    } else {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_decRefs: error: no interpreter\n");
    }
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    getIntValue
 * Signature: (J)I
 */
JNIEXPORT jint JNICALL Java_org_jpy_PyLib_getIntValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jint value;

    JPy_BEGIN_GIL_STATE(-1)

    pyObject = (PyObject*) objId;
    value = (jint) JPy_AS_CLONG(pyObject);
    // Note: we are not handling the overflow case, but there might be a need to have getIntValueSafe() in the future
    if (value == -1 && PyErr_Occurred()) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getIntValue: error: failed to convert Python object to Java int\n");
        PyLib_HandlePythonException(jenv);
    }

    JPy_END_GIL_STATE

    return value;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getLongValue
 * Signature: (J)J
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_getLongValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jlong value;

    JPy_BEGIN_GIL_STATE(-1)

    pyObject = (PyObject*) objId;
    value = JPy_AS_CLONG(pyObject);
    if (value == -1 && PyErr_Occurred()) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getLongValue: error: failed to convert Python object to Java long\n");
        PyLib_HandlePythonException(jenv);
    }
    JPy_END_GIL_STATE

    return value;
}

/**
 * Used to convert a python object into it's corresponding boolean.  If the PyObject is not a boolean;
 * then return false.
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_getBooleanValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jboolean value;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    pyObject = (PyObject*) objId;
    if (PyBool_Check(pyObject)) {
        value = (pyObject == Py_True) ? JNI_TRUE : JNI_FALSE;
    } else {
        value = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return value;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getDoubleValue
 * Signature: (J)D
 */
JNIEXPORT jdouble JNICALL Java_org_jpy_PyLib_getDoubleValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jdouble value;

    JPy_BEGIN_GIL_STATE(-1.0)

    pyObject = (PyObject*) objId;
    value = (jdouble) PyFloat_AsDouble(pyObject);
    if (value == -1.0 && PyErr_Occurred()) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getDoubleValue: error: failed to convert Python object to Java double\n");
        PyLib_HandlePythonException(jenv);
    }

    JPy_END_GIL_STATE

    return value;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getStringValue
 * Signature: (J)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_org_jpy_PyLib_getStringValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jstring jString;

    JPy_BEGIN_GIL_STATE(NULL)

    pyObject = (PyObject*) objId;

    if (JPy_AsJString(jenv, pyObject, &jString) < 0) {
        jString = NULL;
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getStringValue: error: failed to convert Python object to Java String\n");
        PyLib_HandlePythonException(jenv);
    }

    JPy_END_GIL_STATE

    return jString;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getObjectValue
 * Signature: (J)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_getObjectValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jobject jObject;

    JPy_BEGIN_GIL_STATE(NULL)

    pyObject = (PyObject*) objId;

    if (JObj_Check(pyObject)) {
        jObject = ((JPy_JObj*) pyObject)->objectRef;
    } else {
        if (JPy_AsJObject(jenv, pyObject, &jObject, JNI_FALSE) < 0) {
            jObject = NULL;
            JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getObjectValue: error: failed to convert Python object to Java Object\n");
            PyLib_HandlePythonException(jenv);
        }
    }

    JPy_END_GIL_STATE

    return jObject;
}

/**
 * Returns true if this object can be converted from a Python object into a Java object (or primitive);
 * if this returns false, when you fetch an Object from Python it will be a PyObject wrapper.
 *
 * objId is a pointer to a PyObject.
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_isConvertible
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    pyObject = (PyObject*) objId;

    result = pyObject == Py_None || JObj_Check(pyObject) || PyBool_Check(pyObject) ||
                 JPy_IS_CLONG(pyObject) || PyFloat_Check(pyObject) || JPy_IS_STR(pyObject) ? JNI_TRUE : JNI_FALSE;


    JPy_END_GIL_STATE

    return result;
}

/**
 * Gets the Python type object of specified objId.
 *
 * objId is a pointer to a PyObject.
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_getType
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;

    JPy_BEGIN_GIL_STATE(0)

    pyObject = ((PyObject*) objId)->ob_type;
    JPy_INCREF(pyObject);

    JPy_END_GIL_STATE

    return (jlong)pyObject;
}

/**
 * Evaluate PyDict_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the PyLib class object
 * @param objId a pointer to a python object
 * @return true if objId is a python dictionary
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyDictCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyDict_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyList_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the PyLib class object
 * @param objId a pointer to a python object
 * @return true if objId is a python list
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyListCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyList_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyBool_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python boolean
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyBoolCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyBool_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Check equality against Py_None and a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a None
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyNoneCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (Py_None == (((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyInt_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python int
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyIntCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    int check;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    check = JPy_IS_CLONG(((PyObject*) objId));

    JPy_END_GIL_STATE

    return check ? (jboolean)JNI_TRUE : (jboolean)JNI_FALSE;
}

/**
 * Evaluate PyLong_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python long
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyLongCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyLong_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyFloat_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python float
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyFloatCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyFloat_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyString_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python String
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyStringCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (JPy_IS_STR(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyCallable_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python callable
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyCallableCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyCallable_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyFunction_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python function
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyFunctionCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyFunction_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyModule_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python module
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyModuleCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyModule_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Evaluate PyTuple_Check against a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass
 * @param objId a pointer to a python object
 * @return true if objId is a python tuple
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_pyTupleCheck
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    jboolean result;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    if (PyTuple_Check(((PyObject*) objId))) {
        result = JNI_TRUE;
    } else {
        result = JNI_FALSE;
    }

    JPy_END_GIL_STATE

    return result;
}

/**
 * Runs the str function on a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the class object for PyLib
 * @param objId a pointer to a python object
 * @return the Python toString of this object
 */
JNIEXPORT jstring JNICALL Java_org_jpy_PyLib_str
  (JNIEnv* jenv, jclass jLibClass, jlong objId) {
    PyObject *pyObject;
    jobject jObject;
    PyObject *pyStr;

    JPy_BEGIN_GIL_STATE(NULL)

    pyObject = (PyObject *) objId;

    pyStr = PyObject_Str(pyObject);
    if (pyStr != NULL) {
        jObject = (*jenv)->NewStringUTF(jenv, JPy_AS_UTF8(pyStr));
        JPy_DECREF(pyStr);
    } else {
        jObject = NULL;
        PyLib_HandlePythonException(jenv);
    }

    JPy_END_GIL_STATE

    return jObject;
}


/**
 * Runs the repr function on a Python object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the class object for PyLib
 * @param objId a pointer to a python object
 * @return the Python representation string of this object
 */
JNIEXPORT jstring JNICALL Java_org_jpy_PyLib_repr
  (JNIEnv* jenv, jclass jLibClass, jlong objId) {
    PyObject *pyObject;
    jobject jObject;
    PyObject *pyStr;

    JPy_BEGIN_GIL_STATE(NULL)

    pyObject = (PyObject *) objId;

    pyStr = PyObject_Repr(pyObject);
    if (pyStr) {
        jObject = (*jenv)->NewStringUTF(jenv, JPy_AS_UTF8(pyStr));
        JPy_DECREF(pyStr);
    } else {
        jObject = NULL;
    }


    JPy_END_GIL_STATE

    return jObject;
}

/*
 * Compute and return the hash value of an object o. On failure, return -1.
 * This is the equivalent of the Python expression hash(o).
 *
 * @param jenv JNI environment.
 * @param jLibClass the class object for PyLib
 * @param objId a pointer to a python object
 * @return the hash code, -1 on failure
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_hash
  (JNIEnv* jenv, jclass jLibClass, jlong objId)
{
    PyObject* pyObject;
    long hash;

    JPy_BEGIN_GIL_STATE(-1)

    pyObject = (PyObject *) objId;

    if ((hash = PyObject_Hash(pyObject)) == -1) {
        PyLib_HandlePythonException(jenv);
    }

    JPy_END_GIL_STATE

    return (jlong)hash;
}

JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_eq
  (JNIEnv* jenv, jclass jLibClass, jlong objId1, jobject other)
{
    PyObject* pyObject1;
    PyObject* pyObject2;
    PyObject* eq;
    jboolean result = JNI_FALSE;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    pyObject1 = (PyObject *) objId1;
    pyObject2 = PyLib_ConvertJavaToPythonObject(jenv, other);

    // Note: there are subtle differences between PyObject_RichCompare and PyObject_RichCompareBool
    // that make PyObject_RichCompareBool unacceptable to use here.
    //
    // Specifically, PyObject_RichCompareBool says that it will return True for the same object
    // https://docs.python.org/2/c-api/object.html#c.PyObject_RichCompareBool
    // We *DON'T* want that, we want to delegate to the implementations __eq__ function instead.
    // i.e.
    /*
    >>> class AlwaysFalse:
    ...     def __eq__(self, other):
    ...             return False
    ...
    >>> a == a
    False
    */

    eq = PyObject_RichCompare(pyObject1, pyObject2, Py_EQ);
    JPy_DECREF(pyObject2);

    if (eq == NULL) {
        PyLib_HandlePythonException(jenv);
    } else if (PyBool_Check(eq)) {
      result = (eq == Py_True) ? JNI_TRUE : JNI_FALSE;
      JPy_DECREF(eq);
    } else {
      int val = PyObject_IsTrue(eq);
      JPy_DECREF(eq);
      if (val == -1) {
          PyLib_HandlePythonException(jenv);
      } else {
          result = val ? JNI_TRUE : JNI_FALSE;
      }
    }

    JPy_END_GIL_STATE

    return result;
}

// TODO: this implementation *should* use JType_CreateJavaArray instead...
/*
 * Class:     org_jpy_PyLib
 * Method:    getObjectArrayValue
 * Signature: (JLjava/lang/Class;)[Ljava/lang/Object;
 */
JNIEXPORT jobjectArray JNICALL Java_org_jpy_PyLib_getObjectArrayValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jclass itemClassRef)
{
    PyObject* pyObject;
    jobject jObject;

    JPy_BEGIN_GIL_STATE(NULL)

    pyObject = (PyObject*) objId;

    if (pyObject == Py_None) {
        jObject = NULL;
    } else if (JObj_Check(pyObject)) {
        jObject = ((JPy_JObj*) pyObject)->objectRef;
    } else if (PySequence_Check(pyObject)) {
        PyObject* pyItem;
        jobject jItem;
        jint i, length;

        length = PySequence_Length(pyObject);

        jObject = (*jenv)->NewObjectArray(jenv, length, itemClassRef, NULL);

        for (i = 0; i < length; i++) {
            pyItem = PySequence_GetItem(pyObject, i);
            if (pyItem == NULL) {
                JPy_DELETE_LOCAL_REF(jObject);
                goto error;
            }
            if (JPy_AsJObject(jenv, pyItem, &jItem, JNI_FALSE) < 0) {
                JPy_DELETE_LOCAL_REF(jObject);
                JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getObjectArrayValue: error: failed to convert Python item to Java Object\n");
                PyLib_HandlePythonException(jenv);
                goto error;
            }
            JPy_XDECREF(pyItem);
            (*jenv)->SetObjectArrayElement(jenv, jObject, i, jItem);
            if ((*jenv)->ExceptionCheck(jenv)) {
                JPy_DELETE_LOCAL_REF(jObject);
                goto error;
            }
        }
    } else {
        jObject = NULL;
        (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, "python object cannot be converted to Object[]");
    }

error:
    JPy_END_GIL_STATE

    return jObject;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    getModule
 * Signature: (Ljava/lang/String;)J
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_importModule
  (JNIEnv* jenv, jclass jLibClass, jstring jName)
{
    PyObject* pyName;
    PyObject* pyModule = NULL;
    const char* nameChars;

    JPy_BEGIN_GIL_STATE(0)

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_importModule: name='%s'\n", nameChars);
    /* Note: pyName is a new reference */
    pyName = JPy_FROM_CSTR(nameChars);
    /* Note: pyModule is a new reference */
    pyModule = PyImport_Import(pyName);
    if (pyModule == NULL) {
        PyLib_HandlePythonException(jenv);
    }
    JPy_XDECREF(pyName);

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }

    JPy_END_GIL_STATE

    return (jlong) pyModule;
}




/*
 * Class:     org_jpy_python_PyLib
 * Method:    getAttributeValue
 * Signature: (JLjava/lang/String;)J
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_getAttributeObject
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName)
{
    PyObject* pyObject;
    PyObject* pyValue;

    JPy_BEGIN_GIL_STATE(0)

    pyObject = (PyObject*) objId;
    pyValue = PyLib_GetAttributeObject(jenv, pyObject, jName);

    JPy_END_GIL_STATE

    return (jlong) pyValue;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    getAttributeValue
 * Signature: (JLjava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_getAttributeValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName, jclass jValueClass)
{
    PyObject* pyObject;
    PyObject* pyValue;
    jobject jReturnValue;

    JPy_BEGIN_GIL_STATE(NULL)

    pyObject = (PyObject*) objId;

    pyValue = PyLib_GetAttributeObject(jenv, pyObject, jName);
    if (pyValue == NULL) {
        jReturnValue = NULL;
        goto error;
    }

    if (JPy_AsJObjectWithClass(jenv, pyValue, &jReturnValue, jValueClass) < 0) {
        jReturnValue = NULL;
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_getAttributeValue: error: failed to convert attribute value\n");
        PyLib_HandlePythonException(jenv);
    }

error:
    JPy_XDECREF(pyValue);
    JPy_END_GIL_STATE

    return jReturnValue;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    setAttributeValue
 * Signature: (JLjava/lang/String;J)V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_setAttributeValue
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName, jobject jValue, jclass jValueClass)
{
    PyObject* pyObject;
    const char* nameChars;
    PyObject* pyValue;
    JPy_JType* valueType;

    JPy_BEGIN_GIL_STATE_VOID()

    pyObject = (PyObject*) objId;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_setAttributeValue: objId=%p, name='%s', jValue=%p, jValueClass=%p\n", pyObject, nameChars, jValue, jValueClass);

    if (jValueClass != NULL) {
        valueType = JType_GetType(jenv, jValueClass, JNI_FALSE);
    } else {
        valueType = NULL;
    }

    if (valueType != NULL) {
        pyValue = JPy_FromJObjectWithType(jenv, jValue, valueType);
    } else {
        pyValue = JPy_FromJObject(jenv, jValue);
    }

    if (pyValue == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_setAttributeValue: error: attribute '%s': Java object not convertible\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

    // PyObject_SetAttrString will increase ref count
    if (PyObject_SetAttrString(pyObject, nameChars, pyValue) < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_setAttributeValue: error: PyObject_SetAttrString failed on attribute '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }

    JPy_XDECREF(pyValue);

    JPy_END_GIL_STATE
}

/**
 * Deletes an attribute from an object.
 *
 * @param jenv JNI environment.
 * @param jLibClass the PyLib class object
 * @param objId a pointer to a python object
 * @param jName the java string naming the attribute to delete
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_delAttribute
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName)
{
    PyObject* pyObject;
    const char* nameChars;

    JPy_BEGIN_GIL_STATE_VOID()

    pyObject = (PyObject*) objId;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_delAttribute: objId=%p, name='%s'\n", pyObject, nameChars);

    if (PyObject_DelAttrString(pyObject, nameChars) < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_delAttribute: error: PyObject_DelAttrString failed on attribute '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }


    JPy_END_GIL_STATE
}

/*
 * Checks for an attribute's existence.
 *
 * @param jenv JNI environment.
 * @param jLibClass the PyLib class object
 * @param objId a pointer to a python object
 * @param jName the java string naming the attribute to delete
 *
 * @return true if the attribute exists on this object
 */

JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_hasAttribute
  (JNIEnv* jenv, jclass jLibClass, jlong objId, jstring jName)
{
    PyObject* pyObject;
    const char* nameChars;
    jboolean result = JNI_FALSE;

    JPy_BEGIN_GIL_STATE(JNI_FALSE)

    pyObject = (PyObject*) objId;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "Java_org_jpy_PyLib_delAttribute: objId=%p, name='%s'\n", pyObject, nameChars);

    result = PyObject_HasAttrString(pyObject, nameChars) ? JNI_TRUE : JNI_FALSE;

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }

    JPy_END_GIL_STATE

    return result;
}

/*
 * Class:     org_jpy_PyLib
 * Method:    hasGil
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_org_jpy_PyLib_hasGil
  (JNIEnv* jenv, jclass jLibClass)
{
    jboolean result;

    // Note: we *don't* need the GIL to inquire if we have the GIL, that would be silly.
    result = PyGILState_Check() ? JNI_TRUE : JNI_FALSE;
    return result;
}

/*
 * Class:     org_jpy_PyLib
 * Method:    ensureGil
 * Signature: (Ljava/util/function/Supplier;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_ensureGil
  (JNIEnv* jenv, jclass jLibClass, jobject supplier)
{
    jobject result;
    JPy_BEGIN_GIL_STATE(NULL)
    result = (*jenv)->CallObjectMethod(jenv, supplier, JPy_Supplier_get_MID);
    JPy_END_GIL_STATE
    return result;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    call
 * Signature: (JZLjava/lang/String;I[Ljava/lang/Object;[Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/Object;
 */
JNIEXPORT jlong JNICALL Java_org_jpy_PyLib_callAndReturnObject
  (JNIEnv *jenv, jclass jLibClass, jlong objId, jboolean isMethodCall, jstring jName, jint argCount, jobjectArray jArgs, jobjectArray jParamClasses)
{
    PyObject* pyObject;
    PyObject* pyReturnValue;

    JPy_BEGIN_GIL_STATE(0)

    pyObject = (PyObject*) objId;
    pyReturnValue = PyLib_CallAndReturnObject(jenv, pyObject, isMethodCall, jName, argCount, jArgs, jParamClasses);

    JPy_END_GIL_STATE

    return (jlong) pyReturnValue;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    callAndReturnValue
 * Signature: (JZLjava/lang/String;I[Ljava/lang/Object;[Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_jpy_PyLib_callAndReturnValue
  (JNIEnv *jenv, jclass jLibClass, jlong objId, jboolean isMethodCall, jstring jName, jint argCount, jobjectArray jArgs, jobjectArray jParamClasses, jclass jReturnClass)
{
    PyObject* pyObject = (PyObject*) objId;
    PyObject* pyReturnValue;
    jobject jReturnValue = NULL;

    JPy_BEGIN_GIL_STATE(NULL)

    pyReturnValue = PyLib_CallAndReturnObject(jenv, pyObject, isMethodCall, jName, argCount, jArgs, jParamClasses);
    if (pyReturnValue == NULL) {
        goto error;
    }

    if (JPy_AsJObjectWithClass(jenv, pyReturnValue, &jReturnValue, jReturnClass) < 0) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "Java_org_jpy_PyLib_callAndReturnValue: error: failed to convert attribute value\n");
        PyLib_HandlePythonException(jenv);
        jReturnValue = NULL;
        goto error;
    }

error:
    JPy_XDECREF(pyReturnValue);
    JPy_END_GIL_STATE

    return jReturnValue;
}


/*
 * Class:     org_jpy_python_PyLib
 * Method:    getDiagFlags
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_org_jpy_PyLib_00024Diag_getFlags
  (JNIEnv *jenv, jclass classRef)
{
    return JPy_DiagFlags;
}

/*
 * Class:     org_jpy_python_PyLib
 * Method:    setDiagFlags
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_org_jpy_PyLib_00024Diag_setFlags
  (JNIEnv *jenv, jclass classRef, jint flags)
{
    JPy_DiagFlags = flags;
}

////////////////////////////////////////////////////////////////////////////////////
// Helpers that also throw Java exceptions


PyObject* PyLib_GetAttributeObject(JNIEnv* jenv, PyObject* pyObject, jstring jName)
{
    PyObject* pyValue = NULL;
    const char* nameChars;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }
    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "PyLib_GetAttributeObject: objId=%p, name='%s'\n", pyObject, nameChars);
    /* Note: pyValue is a new reference */
    pyValue = PyObject_GetAttrString(pyObject, nameChars);
    if (pyValue == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_GetAttributeObject: error: attribute not found '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
    }
error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }
    return pyValue;
}

PyObject* PyLib_FromJObjectForTuple(JNIEnv *jenv, jobject jArg, jclass jParamClass, char* nameChars, jint index) {
    JPy_JType* implicitParamType;
    JPy_JType* explicitParamType;
    PyObject* pyReturnValue;

    // if jArg is NULL, we don't care about the implicit / explicit types, we'll return None
    if (jArg == NULL) {
        return JPy_FROM_JNULL();
    }

    pyReturnValue = NULL;
    explicitParamType = NULL;
    implicitParamType = JType_GetTypeForObject(jenv, jArg, JNI_FALSE);
    if (implicitParamType == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_FromJObjectForTuple: error: callable '%s': argument %d: failed to retrieve implicit-type\n", nameChars, index);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

    if (jParamClass != NULL) {
        explicitParamType = JType_GetType(jenv, jParamClass, JNI_FALSE);
        if (explicitParamType == NULL) {
            JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_FromJObjectForTuple: error: callable '%s': argument %d: failed to retrieve explicit-type\n", nameChars, index);
            PyLib_HandlePythonException(jenv);
            goto error;
        }
        pyReturnValue = JPy_FromJObjectWithType(jenv, jArg, explicitParamType);
    } else {
        pyReturnValue = JPy_FromJObjectWithType(jenv, jArg, implicitParamType);
    }

error:
    JPy_XDECREF(explicitParamType);
    JPy_XDECREF(implicitParamType);
    return pyReturnValue;
}


PyObject* PyLib_CallAndReturnObject(JNIEnv *jenv, PyObject* pyObject, jboolean isMethodCall, jstring jName, jint argCount, jobjectArray jArgs, jobjectArray jParamClasses)
{
    PyObject* pyCallable = NULL;
    PyObject* pyArgs = NULL;
    PyObject* pyArg;
    PyObject* pyReturnValue = NULL;
    const char* nameChars;
    jint i;
    jobject jArg;
    jclass jParamClass = NULL;
    JPy_JType* explicitParamType = NULL;
    JPy_JType* jArgParamType;

    nameChars = (*jenv)->GetStringUTFChars(jenv, jName, NULL);
    if (nameChars == NULL) {
        PyLib_ThrowOOM(jenv);
        goto error;
    }

    JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "PyLib_CallAndReturnObject: objId=%p, isMethodCall=%d, name='%s', argCount=%d\n", pyObject, isMethodCall, nameChars, argCount);

    // Note: pyCallable is a new reference
    pyCallable = PyObject_GetAttrString(pyObject, nameChars);
    if (pyCallable == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: function or method not found: '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

    if (!PyCallable_Check(pyCallable)) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: object is not callable: '%s'\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

    pyArgs = PyTuple_New(argCount);
    for (i = 0; i < argCount; i++) {
        // get the implicit java type (the real type of the object)
        jArg = (*jenv)->GetObjectArrayElement(jenv, jArgs, i);
        if (jParamClasses != NULL) {
            jParamClass = (*jenv)->GetObjectArrayElement(jenv, jParamClasses, i);
        }
        pyArg = PyLib_FromJObjectForTuple(jenv, jArg, jParamClass, nameChars, i);
        if (jParamClass != NULL) {
            JPy_DELETE_LOCAL_REF(jParamClass);
        }
        JPy_DELETE_LOCAL_REF(jArg);
        if (pyArg == NULL) {
            JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: callable '%s': argument %d: failed to convert Java into Python object\n", nameChars, i);
            PyLib_HandlePythonException(jenv);
            goto error;
        }
        // pyArg reference stolen here
        PyTuple_SetItem(pyArgs, i, pyArg);
    }

    // Check why: for some reason, we don't need the following code to invoke object methods.
    /*
    if (isMethodCall) {
        PyObject* pyMethod;

        pyMethod = PyMethod_New(pyCallable, pyObject);
        if (pyMethod == NULL) {
            JPy_DIAG_PRINT(JPy_DIAG_F_EXEC, "PyLib_CallAndReturnObject: error: callable '%s': no memory\n", nameChars);
            PyLib_HandlePythonException(jenv);
            goto error;
        }
        JPy_DECREF(pyCallable);
        pyCallable = pyMethod;
    }
    */

    pyReturnValue = PyObject_CallObject(pyCallable, argCount > 0 ? pyArgs : NULL);
    if (pyReturnValue == NULL) {
        JPy_DIAG_PRINT(JPy_DIAG_F_ALL, "PyLib_CallAndReturnObject: error: callable '%s': call returned NULL\n", nameChars);
        PyLib_HandlePythonException(jenv);
        goto error;
    }

error:
    if (nameChars != NULL) {
        (*jenv)->ReleaseStringUTFChars(jenv, jName, nameChars);
    }
    JPy_XDECREF(pyCallable);
    JPy_XDECREF(pyArgs);

    return pyReturnValue;
}

char* PyLib_ObjToChars(PyObject* pyObj, PyObject** pyNewRef)
{
    char* chars = NULL;
    if (pyObj != NULL) {
        PyObject* pyObjStr = PyObject_Str(pyObj);
        if (pyObjStr != NULL) {
            PyObject* pyObjUtf8 = PyUnicode_AsEncodedString(pyObjStr, "utf-8", "replace");
            if (pyObjUtf8 != NULL) {
                chars = PyBytes_AsString(pyObjUtf8);
                *pyNewRef = pyObjUtf8;
            }
            JPy_XDECREF(pyObjStr);
        }
    }
    return chars;
}

#define JPY_NOT_AVAILABLE_MSG "<not available>"
#define JPY_NOT_AVAILABLE_MSG_LEN strlen(JPY_NOT_AVAILABLE_MSG)
#define JPY_ERR_BASE_MSG "Error in Python interpreter"
#define JPY_NO_INFO_MSG JPY_ERR_BASE_MSG ", no information available"
#define JPY_INFO_ALLOC_FAILED_MSG JPY_ERR_BASE_MSG ", failed to allocate information text"

void PyLib_HandlePythonException(JNIEnv* jenv)
{
    PyObject* pyType = NULL;
    PyObject* pyValue = NULL;
    PyTracebackObject* pyTraceback = NULL;

    PyObject* pyTypeUtf8 = NULL;
    PyObject* pyValueUtf8 = NULL;
    PyObject* pyLinenoUtf8 = NULL;
    PyObject* pyFilenameUtf8 = NULL;
    PyObject* pyNamespaceUtf8 = NULL;
    char* typeChars = NULL;
    char* valueChars = NULL;
    char* linenoChars = NULL;
    char* filenameChars = NULL;
    char* namespaceChars = NULL;

    jclass jExceptionClass;

    if (PyErr_Occurred() == NULL) {
        return;
    }

    PyErr_Fetch(&pyType, &pyValue, &pyTraceback);
    //printf("M1: pyType=%p, pyValue=%p, pyTraceback=%p\n", pyType, pyValue, pyTraceback);
    //printf("U1: pyType=%s, pyValue=%s, pyTraceback=%s\n", Py_TYPE(pyType)->tp_name, Py_TYPE(pyValue)->tp_name, pyTraceback != NULL ? Py_TYPE(pyTraceback)->tp_name : "?");
    PyErr_NormalizeException(&pyType, &pyValue, &pyTraceback);
    //printf("M2: pyType=%p, pyValue=%p, pyTraceback=%p\n", pyType, pyValue, pyTraceback);
    //printf("U2: pyType=%s, pyValue=%s, pyTraceback=%s\n", Py_TYPE(pyType)->tp_name, Py_TYPE(pyValue)->tp_name, pyTraceback != NULL ? Py_TYPE(pyTraceback)->tp_name : "?");

    typeChars = PyLib_ObjToChars(pyType, &pyTypeUtf8);
    valueChars = PyLib_ObjToChars(pyValue, &pyValueUtf8);
    if (PyObject_TypeCheck(pyValue, (PyTypeObject*) PyExc_KeyError)) {
        jExceptionClass = JPy_KeyError_JClass;
    } else if (PyObject_TypeCheck(pyValue, (PyTypeObject*) PyExc_StopIteration)) {
        jExceptionClass = JPy_StopIteration_JClass;
    } else {
        jExceptionClass = JPy_RuntimeException_JClass;
    }

    PyTracebackObject* topPyTraceback = pyTraceback;
    while (pyTraceback && pyTraceback->tb_next)
        pyTraceback = pyTraceback->tb_next;

    if (pyTraceback != NULL) {
        PyFrameObject* pyFrame = NULL;
        PyCodeObject* pyCode = NULL;
        linenoChars = PyLib_ObjToChars(PyObject_GetAttrString(pyTraceback, "tb_lineno"), &pyLinenoUtf8);
        pyFrame = PyObject_GetAttrString(pyTraceback, "tb_frame");
        if (pyFrame != NULL) {
            pyCode = PyFrame_GetCode(pyFrame);
            if (pyCode != NULL) {
                filenameChars = PyLib_ObjToChars(PyObject_GetAttrString(pyCode, "co_filename"), &pyFilenameUtf8);
                namespaceChars = PyLib_ObjToChars(PyObject_GetAttrString(pyCode, "co_name"), &pyNamespaceUtf8);
            }
        }
        JPy_XDECREF(pyCode);
        JPy_XDECREF(pyFrame);
    }

    //printf("U2: typeChars=%s, valueChars=%s, linenoChars=%s, filenameChars=%s, namespaceChars=%s\n",
    //       typeChars, valueChars, linenoChars, filenameChars, namespaceChars);

    pyTraceback = topPyTraceback;

    if (typeChars != NULL || valueChars != NULL
        || linenoChars != NULL || filenameChars != NULL || namespaceChars != NULL) {
        char* javaMessage;
        int bufLen = (typeChars != NULL ? strlen(typeChars) : JPY_NOT_AVAILABLE_MSG_LEN)
                               + (valueChars != NULL ? strlen(valueChars) : JPY_NOT_AVAILABLE_MSG_LEN)
                               + (linenoChars != NULL ? strlen(linenoChars) : JPY_NOT_AVAILABLE_MSG_LEN)
                               + (filenameChars != NULL ? strlen(filenameChars) : JPY_NOT_AVAILABLE_MSG_LEN)
                               + (namespaceChars != NULL ? strlen(namespaceChars) : JPY_NOT_AVAILABLE_MSG_LEN) + 1024;
        javaMessage = PyMem_New(char, bufLen);
        if (javaMessage != NULL) {
            sprintf(javaMessage,
                    JPY_ERR_BASE_MSG ":\n"
                    "Type: %s\n"
                    "Value: %s\n"
                    "Line: %s\n"
                    "Namespace: %s\n"
                    "File: %s",
                    typeChars != NULL ? typeChars : JPY_NOT_AVAILABLE_MSG,
                    valueChars != NULL ? valueChars : JPY_NOT_AVAILABLE_MSG,
                    linenoChars != NULL ? linenoChars : JPY_NOT_AVAILABLE_MSG,
                    namespaceChars != NULL ? namespaceChars : JPY_NOT_AVAILABLE_MSG,
                    filenameChars != NULL ? filenameChars : JPY_NOT_AVAILABLE_MSG);
            int err = python_traceback_report(pyTraceback, &javaMessage, &bufLen);
            if (err != 0) {
                (*jenv)->ThrowNew(jenv, jExceptionClass, JPY_INFO_ALLOC_FAILED_MSG);
            } else {
                (*jenv)->ThrowNew(jenv, jExceptionClass, javaMessage);
            }
            PyMem_Del(javaMessage);
        } else {
            (*jenv)->ThrowNew(jenv, jExceptionClass, JPY_INFO_ALLOC_FAILED_MSG);
        }
    } else {
        (*jenv)->ThrowNew(jenv, jExceptionClass, JPY_NO_INFO_MSG);
    }

    JPy_XDECREF(pyType);
    JPy_XDECREF(pyValue);
    JPy_XDECREF(pyTraceback);
    JPy_XDECREF(pyTypeUtf8);
    JPy_XDECREF(pyValueUtf8);
    JPy_XDECREF(pyLinenoUtf8);
    JPy_XDECREF(pyFilenameUtf8);
    JPy_XDECREF(pyNamespaceUtf8);

    PyErr_Clear();
}

/**
 * Throw an OutOfMemoryError.
 * @param jenv the jni environment
 */
void PyLib_ThrowOOM(JNIEnv* jenv) {
    (*jenv)->ThrowNew(jenv, JPy_OutOfMemoryError_JClass, "Out of memory");
}

/**
 * Throw a FileNotFoundException.
 * @param jenv the jni environment
 */
void PyLib_ThrowFNFE(JNIEnv* jenv, const char *file) {
    (*jenv)->ThrowNew(jenv, JPy_FileNotFoundException_JClass, file);
}

/**
 * Throw an UnsupportedOperationException.
 * @param jenv the jni environment
 * @param message the exception message
 */
void PyLib_ThrowUOE(JNIEnv* jenv, const char *message) {
    (*jenv)->ThrowNew(jenv, JPy_UnsupportedOperationException_JClass, message);
}

/**
 * Throw an UnsupportedOperationException.
 * @param jenv the jni environment
 * @param message the exception message
 */
void PyLib_ThrowRTE(JNIEnv* jenv, const char *message) {
    (*jenv)->ThrowNew(jenv, JPy_RuntimeException_JClass, message);
}

////////////////////////////////////////////////////////////////////////////////////////////////
// Redirect stdout

static PyObject* JPrint_write(PyObject* self, PyObject* args)
{
    if (stdout != NULL) {
        const char* text;
        if (!PyArg_ParseTuple(args, "s", &text)) {
            return NULL;
        }
        fprintf(stdout, "%s", text);
    }
    Py_RETURN_NONE;
}

static PyObject* JPrint_flush(PyObject* self, PyObject* args)
{
    if (stdout != NULL) {
        fflush(stdout);
    }
    Py_RETURN_NONE;
}

static PyMethodDef JPrint_Functions[] = {

    {"write",       (PyCFunction) JPrint_write, METH_VARARGS,
                    "Internal function. Used to print to stdout in embedded mode."},

    {"flush",       (PyCFunction) JPrint_flush, METH_VARARGS,
                    "Internal function. Used to flush to stdout in embedded mode."},

    {NULL, NULL, 0, NULL} /*Sentinel*/
};

#define JPY_STDOUT_MODULE_NAME "jpy_stdout"
#define JPY_STDOUT_MODULE_DOC  "Redirect 'stdout' to the console in embedded mode"

static struct PyModuleDef JPrint_ModuleDef =
{
    PyModuleDef_HEAD_INIT,
    JPY_STDOUT_MODULE_NAME, /* Name of the Python JPy_Module */
    JPY_STDOUT_MODULE_DOC,  /* Module documentation */
    -1,                 /* Size of per-interpreter state of the JPy_Module, or -1 if the JPy_Module keeps state in global variables. */
    JPrint_Functions,    /* Structure containing global jpy-functions */
    NULL,     // m_reload
    NULL,     // m_traverse
    NULL,     // m_clear
    NULL      // m_free
};

void PyLib_RedirectStdOut(void)
{
    PyObject* module;
    module = PyModule_Create(&JPrint_ModuleDef);
    PySys_SetObject("stdout", module);
    PySys_SetObject("stderr", module);
}


static const int PYLIB_RECURSIVE_CUTOFF = 3;
#define PyLib_TraceBack_LIMIT 1024

static PyObject *format_displayline(PyObject *filename, int lineno, PyObject *name)
{
    PyObject *line;
    PyObject *pyObjUtf8 = NULL;

    if (filename == NULL || name == NULL)
        return NULL;

    line = PyUnicode_FromFormat("  File \"%U\", line %d, in %U\n",
                                filename, lineno, name);
    if (line == NULL) {
        return NULL;
    }

    pyObjUtf8 = PyUnicode_AsEncodedString(line, "utf-8", "replace");
    JPy_DECREF(line);
    return pyObjUtf8;
}

static PyObject *format_line_repeated(long cnt)
{
    cnt -= PYLIB_RECURSIVE_CUTOFF;
    PyObject *line = PyUnicode_FromFormat(
        (cnt > 1)
          ? "  [Previous line repeated %ld more times]\n"
          : "  [Previous line repeated %ld more time]\n",
        cnt);
    if (line == NULL) {
        return NULL;
    }
    PyObject* pyObjUtf8 = PyUnicode_AsEncodedString(line, "utf-8", "replace");
    JPy_DECREF(line);
    return pyObjUtf8;
}

static int append_to_java_message(PyObject * pyObjUtf8, char **buf, int *bufLen ) {
    if (pyObjUtf8 == NULL)
        return 0;

    char *msg = PyBytes_AsString(pyObjUtf8);
    int msgLen = strlen(msg);

    if (strlen(*buf) + msgLen + 1 >= *bufLen) {
        char *newBuf = PyMem_New(char, *bufLen + 64 * msgLen);
        if (newBuf == NULL) {
            JPy_DECREF(pyObjUtf8);
            return -1;
        }

        newBuf[0]='\0';
        strcat(newBuf, *buf);
        PyMem_Del(*buf);
        *buf = newBuf;
        *bufLen = *bufLen + 64 * msgLen;
    }

    strcat(*buf, msg);
    JPy_DECREF(pyObjUtf8);
    return 0;
}

static int get_traceback_lineno(PyTracebackObject *tb) {
    PyObject* po_lineno = PyObject_GetAttrString((PyObject*)tb, "tb_lineno");
    int lineno = (int)PyLong_AsLong(po_lineno);
    JPy_DECREF(po_lineno);
    return lineno;
}

static int format_python_traceback(PyTracebackObject *tb, char **buf, int *bufLen)
{
    int err = 0;
    long limit = PyLib_TraceBack_LIMIT;
    PyObject *pyObjUtf8 = NULL;
    Py_ssize_t depth = 0;
    PyObject *last_file = NULL;
    int last_line = -1;
    PyObject *last_name = NULL;
    long cnt = 0;
    PyTracebackObject *tb1 = tb;

    while (tb1 != NULL) {
        depth++;
        tb1 = tb1->tb_next;
    }
    while (tb != NULL && depth > limit) {
        depth--;
        tb = tb->tb_next;
    }
    while (tb != NULL && err == 0) {
        PyCodeObject* co = PyFrame_GetCode(tb->tb_frame);
        int tb_lineno = get_traceback_lineno(tb);
        if (last_file == NULL ||
            co->co_filename != last_file ||
            last_line == -1 || tb_lineno != last_line ||
            last_name == NULL || co->co_name != last_name) {
            if (cnt >  PYLIB_RECURSIVE_CUTOFF) {
                pyObjUtf8 = format_line_repeated(cnt);
                err = append_to_java_message(pyObjUtf8, buf, bufLen);
                if (err != 0) {
                    JPy_DECREF(co);
                    return err;
                }
            }
            last_file = co->co_filename;
            last_line = tb_lineno;
            last_name = co->co_name;
            cnt = 0;
        }
        cnt++;
        if (err == 0 && cnt <= PYLIB_RECURSIVE_CUTOFF) {
            pyObjUtf8 = format_displayline( 
                                 co->co_filename,
                                 tb_lineno,
                                 co->co_name);
            err = append_to_java_message(pyObjUtf8, buf, bufLen);
            if (err != 0) {
                JPy_DECREF(co);
                return err;
            }
        }
        JPy_DECREF(co);
        tb = tb->tb_next;
    }
    if (err == 0 && cnt > PYLIB_RECURSIVE_CUTOFF) {
        PyObject *pyObjUtf8 = format_line_repeated(cnt);
        err = append_to_java_message(pyObjUtf8, buf, bufLen);
    }
    return err;
}

static int python_traceback_report(PyObject *tb, char **buf, int* bufLen)
{
    /* buf should be big enough for the message */
    strcat(*buf, "\nTraceback (most recent call last):\n");

    return format_python_traceback(tb, buf, bufLen);
}
