Python/Code
Jump to navigation
Jump to search
// CPython.cpp #include "kVersion.h" #if kUsePython #if __WIN32__ #include "python.h" #else #include <Python/Python.h> #endif #endif #include "stdafx.h" #include "CApp.h" #include "CPython.h" bool CPython_UsePython() { return kUsePython; } #if kUsePython #include "CTaskMgr.h" #define kEmbeddedModuleName "kjams" #define kCustomPrint "custom_print" #define kCustomErr "custom_err" #define kCommand "do_command" #define kStringCommand "do_command_str" #define kDoMenuCommand "do_menu_command" #define kDoMenuName "do_menu_name" #define kStartupScriptName "startup.py" #define kUserAbortStr "User Abort" /**********************************************/ class ScEnsureGIL { PyGILState_STATE i_state; public: ScEnsureGIL() : i_state(PyGILState_Ensure()) { } ~ScEnsureGIL() { PyGILState_Release(i_state); } }; class ScReleaseGIL { public: PyThreadState *i_stateP; ScReleaseGIL() : i_stateP(PyEval_SaveThread()) { } ~ScReleaseGIL() { PyEval_RestoreThread(i_stateP); } }; class ScPyObject { PyObject *i_objP; public: ScPyObject(PyObject *objP) : i_objP(objP) {} ~ScPyObject() { Py_DECREF(i_objP); } operator PyObject*() { return i_objP; } }; /**********************************************/ class CPython; class CT_RunScript; typedef std::vector<CT_RunScript *> ScriptVec; /* this thread runs in the background, serving as the "main event loop" for all python scripts it runs an "idle" on each script every 1/4 second to look for user-aborted threads, and if found, causes an exception to be thrown within that thread */ class CPython_RunLoop : public CT_Preemptive { public: CPython *thiz; CMutex_bool i_abortB; CMutexT<ScriptVec *> i_scriptVecP; bool i_continueB; CPython_RunLoop(CPython *in_thiz); ~CPython_RunLoop(); virtual OSStatus operator()(OSStatus err); void RunScript(const char *unf8NameZ, const char *utf8ScriptZ); void AddScript(CT_RunScript *scriptP); void RemoveScript(CT_RunScript *scriptP); void IdleScripts(); void AllowPendingCalls(); size_t CountScripts(); }; /* this is the main Python object the app uses to communicate with the python runloop above */ class CPython { friend class CPython_RunLoop; static CPython *s_pythonP; CMutexT<bool> i_inittedB; std::string i_appName; CMutexT<CPython_RunLoop*> i_runLoopP; public: CPython(const char *appNameZ); ~CPython(); static CPython* Get(const char *appNameZ = NULL); /****************************************************/ void Startup(); void Test(); }; //static CPython* CPython::s_pythonP = NULL; /*********************************************************************/ struct ThisRec { CT_RunScript *i_scriptP; ThisRec(CT_RunScript *scriptP) : i_scriptP(scriptP) {} }; boost::thread_specific_ptr<ThisRec> g_threadP; /* this is a script-running thread if any "error" statements are "printed", they are gathered up into a single string and presented to the user as a dialog */ class CT_RunScript : public CT_Preemptive { public: SuperString i_name; SuperString i_script; CPython_RunLoop *i_runLoopP; long i_thread_id; class CShowErrorTimer; CMutexT<CShowErrorTimer *> i_errTimerP; /***************************** this gathers all errors printed out: if it gets some error text, it waits a half second. if more error prints come in within that time, they are appended when the timer expires, all the error messages gathered are shown to the user in a dialog */ class CShowErrorTimer : public CT_Timer { friend class CT_RunScript; SuperString i_errStr; CMutex_bool i_abortB; CT_RunScript *i_scriptP; bool i_doneB; public: CShowErrorTimer(CT_RunScript *scriptP) : i_doneB(false), i_scriptP(scriptP), CT_Timer(NO_KILL "CShowErrorTimer", kEventDurationSecond / 2) { call(); } ~CShowErrorTimer() { i_scriptP->i_errTimerP.Set(NULL); } virtual OSStatus operator()() { CCritical sc(&i_scriptP->i_errTimerP); if (!i_doneB) { i_doneB = true; if (!i_errStr.Contains(kUserAbortStr)) { if (i_errStr.GetIndCharR() == '\r') { i_errStr.pop_back(); } i_errStr.Replace("<string>", i_scriptP->i_name); PostAlert("Python Error:", i_errStr.utf8Z()); } } return threadTimerTerminate; } void append(const char *utf8Z) { if (!i_doneB) { i_errStr.append(utf8Z); prime(); // tickle the timer } } }; /*****************************/ CPW_TaskRec *i_taskRecP; CPW_ProgData i_progData; CT_RunScript( CPython_RunLoop *runLoopP, const SuperString& name, const SuperString& script ) : i_runLoopP(runLoopP), i_name(name), i_script(script) { SuperString verb1("Python: "); verb1.append(name); i_taskRecP = gApp->NewTask(verb1.ref(), NULL); i_runLoopP->AddScript(this); call(NO_KILL "CPython::CT_RunScript"); } ~CT_RunScript() { CF_ASSERT(i_errTimerP.Get() == NULL); i_taskRecP->Delete(); i_runLoopP->RemoveScript(this); } // called from CPython_RunLoop thread, NOT from "this" thread void Idle() { OSStatus err = noErr; ERR(i_runLoopP->i_abortB.Get()); ERR(i_taskRecP->MT_UpdateData(&i_progData)); // an error here means the user has aborted the script if (err) { err = noErr; ERR_XTE_START { ScEnsureGIL sc; ScPyObject exceptionP(PyString_FromString(kUserAbortStr)); int countI(PyThreadState_SetAsyncExc(i_thread_id, exceptionP)); // during shut down it is reasonable that countI may be 0 // but it should never be greater than 1 CF_ASSERT(countI == 0 || countI == 1); } ERR_XTE_END; if (err) { ReportErr("Python: Exception when attempting to kill thread", err); } } } virtual OSStatus operator()(OSStatus err) { SetThis(this); { ScEnsureGIL sc; // gather my thread ID so i can be killed later if // the user hits cancel on my thread i_thread_id = PyThreadState_Get()->thread_id; ERR(PyRun_SimpleString(i_script.utf8Z())); } // now wait until errors have already been shown, if any while (i_errTimerP.Get()) { IdleDuration(0.1f, kDurationForever_Idle); } return err; } /***************************/ // extensions to python for use within scripts static CT_RunScript* GetThis() { CT_RunScript *thiz = NULL; ThisRec *thisRecP = g_threadP.get(); if (thisRecP) { thiz = thisRecP->i_scriptP; } return thiz; } static void SetThis(CT_RunScript *scriptP) { g_threadP.reset(new ThisRec(scriptP)); } static PyObject* emb_print_err(PyObject *self, PyObject *args) { return GetThis()->print_err(args); } PyObject* print_err(PyObject *args) { PyObject *resultObjP = NULL; const char *utf8_strZ = NULL; if (PyArg_ParseTuple(args, "s", &utf8_strZ)) { CCritical sc(&i_errTimerP); CShowErrorTimer *errTimerP(i_errTimerP.Get()); if (errTimerP == NULL) { errTimerP = new CShowErrorTimer(this); i_errTimerP.Set(errTimerP); } errTimerP->append(utf8_strZ); resultObjP = Py_None; Py_INCREF(resultObjP); } return resultObjP; } /***************************/ static PyObject* emb_print(PyObject *self, PyObject *args) { return GetThis()->print(args); } PyObject* print(PyObject *args) { PyObject *resultObjP = NULL; const char *utf8_strZ = NULL; if (PyArg_ParseTuple(args, "s", &utf8_strZ)) { Log(utf8_strZ, false); resultObjP = Py_None; Py_INCREF(resultObjP); } return resultObjP; } /***************************/ static PyObject* emb_do_command(PyObject *self, PyObject *args) { return GetThis()->do_command(args); } PyObject* do_command(PyObject *args) { PyObject *resultObjP = NULL; int commandID = kScriptCommand_NONE; if (PyArg_ParseTuple(args, "i", &commandID)) { double resultF = Scripting_Command((SInt32)commandID); resultObjP = PyFloat_FromDouble(resultF); } return resultObjP; } /***************************/ static PyObject* emb_do_command_str(PyObject *self, PyObject *args) { return GetThis()->do_command_str(args); } PyObject* do_command_str(PyObject *args) { PyObject *resultObjP = NULL; int commandID = kScriptCommand_NONE; if (PyArg_ParseTuple(args, "i", &commandID)) { SuperString resultStr(Scripting_CommandStr((SInt32)commandID), true); resultObjP = PyString_FromString(resultStr.utf8Z()); } return resultObjP; } /***************************/ static PyObject* emb_do_menu_command(PyObject *self, PyObject *args) { return GetThis()->do_menu_command(args); } PyObject* do_menu_command(PyObject *args) { PyObject *resultObjP = NULL; short menuI = 0; short menu_itemI = 0; short sub_menu_itemI = 0; if (PyArg_ParseTuple(args, "hh|h", &menuI, &menu_itemI, &sub_menu_itemI)) { SInt16Vec intVec; intVec.push_back(menuI); intVec.push_back(menu_itemI); if (sub_menu_itemI) { intVec.push_back(sub_menu_itemI); } DoMenuCommand(intVec); resultObjP = Py_None; Py_INCREF(resultObjP); } return resultObjP; } /***************************/ static PyObject* emb_do_menu_name(PyObject *self, PyObject *args) { return GetThis()->do_menu_name(args); } PyObject* do_menu_name(PyObject *args) { PyObject *resultObjP = NULL; const char *menuZ = NULL; const char *menu_itemZ = NULL; const char *sub_menu_itemZ = NULL; if (PyArg_ParseTuple(args, "ss|s", &menuZ, &menu_itemZ, &sub_menu_itemZ)) { SStringVec stringVec; stringVec.push_back(menuZ); stringVec.push_back(menu_itemZ); if (sub_menu_itemZ) { stringVec.push_back(sub_menu_itemZ); } DoMenuCommand(stringVec); resultObjP = Py_None; Py_INCREF(resultObjP); } return resultObjP; } }; /*****************************************************/ static const PyMethodDef EmbMethods[] = { {kCustomPrint, CT_RunScript::emb_print, METH_VARARGS, "Calls custom print function."}, {kCustomErr, CT_RunScript::emb_print_err, METH_VARARGS, "Calls custom error function."}, {kCommand, CT_RunScript::emb_do_command, METH_VARARGS, "calls scripting command (float result)."}, {kStringCommand, CT_RunScript::emb_do_command_str, METH_VARARGS, "calls scripting command (string result)."}, {kDoMenuCommand, CT_RunScript::emb_do_menu_command, METH_VARARGS, "calls menu command by index"}, {kDoMenuName, CT_RunScript::emb_do_menu_name, METH_VARARGS, "calls menu command by name"}, {NULL, NULL, 0, NULL} }; static const char *s_RedirectPrint = "import " kEmbeddedModuleName "\n" "import sys\n" "\n" "class CustomPrintClass:\n" " def write(self, stuff):\n" " " kEmbeddedModuleName "." kCustomPrint "(stuff)\n" "class CustomErrClass:\n" " def write(self, stuff):\n" " " kEmbeddedModuleName "." kCustomErr "(stuff)\n" "sys.stdout = CustomPrintClass()\n" "sys.stderr = CustomErrClass()\n"; static const char *s_PrintTime = "import time\n" "print 'Today is', time.ctime(time.time())\n"; static const char *s_AllowPendingCalls = "pass\n"; /***************************************************************** on Windows, if you bundle python27.dll with your app, it will launch but crash on systems that do NOT have python actually installed. so get around that here, and bail gracefully. at that point you could instruct the user to go install ActivePython 2.7 for x86 (32bit) from here: http://www.activestate.com/activepython/downloads */ static bool PythonExists() { bool existsB = true; #if OPT_WINOS { CFileRef system32(CFileRef::kFolder_SYSTEM); // CSIDL_SYSTEMX86 existsB = system32.Descend("python27.dll") == noErr; } #endif return existsB; } CPython_RunLoop::CPython_RunLoop(CPython *in_thiz) : thiz(in_thiz), i_continueB(PythonExists()) { i_scriptVecP.Set(new ScriptVec()); call(NO_KILL "CPython_RunLoop"); } CPython_RunLoop::~CPython_RunLoop() { CCritical sc(&i_scriptVecP); ScriptVec *vecP(i_scriptVecP.Get()); CF_ASSERT(vecP); CF_ASSERT(vecP->empty()); delete vecP; i_scriptVecP.Set(NULL); } OSStatus CPython_RunLoop::operator()(OSStatus err) { XTE_START { if (i_continueB) { Log("Python: about to set program name"); Py_SetProgramName(const_cast<char *>(thiz->i_appName.c_str())); Log("Python: about to init"); Py_Initialize(); { Log("Python: about to create " kEmbeddedModuleName " module"); PyObject *myModuleP = Py_InitModule( kEmbeddedModuleName, const_cast<PyMethodDef*>(EmbMethods)); ETX(myModuleP == NULL); // the owner of myModuleP is now the python interpreter // it will auto-decref during Py_Finalize() } // redirect stdout and stderr to my own logging functions Log("Python: about to run log redirect script"); ETX(PyRun_SimpleString(s_RedirectPrint)); PyEval_InitThreads(); thiz->i_inittedB.Set(true); } } XTE_END; LogYesOrNo("Python Initted", thiz->i_inittedB.Get()); if (thiz->i_inittedB.Get()) { bool abortB = false; bool doneB = false; { ScReleaseGIL sc; /* run the "loop" that will handle killing of any scripts canceled by the user */ do { // IdleScripts will kill any scripts that the user has hit cancel on IdleScripts(); // if you want to use Py_AddPendingCall() to send a message to THIS // thread, then you'd need to uncomment this line: // AllowPendingCalls(); if (!abortB) { // this gets set when quitting the app abortB = i_abortB.Get(); } if (abortB) { doneB = CountScripts() == 0; } if (!doneB) { IdleDuration(kQuarterSecond); } } while (!doneB); } Py_Finalize(); thiz->i_inittedB.Set(false); } thiz->i_runLoopP.Set(NULL); return err; } void CPython_RunLoop::AllowPendingCalls() { ScEnsureGIL sc; if (PyRun_SimpleString(s_AllowPendingCalls) != 0) { PostAlert("Python: s_AllowPendingCalls failed"); i_abortB.Set(true); } } void CPython_RunLoop::RunScript(const char *unf8NameZ, const char *utf8ScriptZ) { new CT_RunScript(this, unf8NameZ, utf8ScriptZ); } void CPython_RunLoop::AddScript(CT_RunScript *scriptP) { CCritical sc(&i_scriptVecP); CF_ASSERT(i_scriptVecP.Get()); i_scriptVecP.Get()->push_back(scriptP); } void CPython_RunLoop::RemoveScript(CT_RunScript *scriptP) { CCritical sc(&i_scriptVecP); CF_ASSERT(i_scriptVecP.Get()); ScriptVec& scriptVec(*i_scriptVecP.Get()); ScriptVec::iterator it(std::find(scriptVec.begin(), scriptVec.end(), scriptP)); CF_ASSERT(it != scriptVec.end()); if (it != scriptVec.end()) { scriptVec.erase(it); } } void CPython_RunLoop::IdleScripts() { ScriptVec iter_scriptVec; { CCritical sc(&i_scriptVecP); CF_ASSERT(i_scriptVecP.Get()); ScriptVec& orig_scriptVec(*i_scriptVecP.Get()); // make a copy to iterate over, cuz during the iterate // we may actually delete the current script iter_scriptVec = orig_scriptVec; } BOOST_FOREACH(CT_RunScript *scriptP, iter_scriptVec) { { CCritical sc(&i_scriptVecP); CF_ASSERT(i_scriptVecP.Get()); ScriptVec& scriptVec(*i_scriptVecP.Get()); ScriptVec::iterator it(std::find(scriptVec.begin(), scriptVec.end(), scriptP)); // we still have to check to see if this script still exists before calling it if (it != scriptVec.end()) { scriptP->Idle(); } } } } size_t CPython_RunLoop::CountScripts() { CCritical sc(&i_scriptVecP); CF_ASSERT(i_scriptVecP.Get()); return i_scriptVecP.Get()->size(); } /**************************************************************/ CPython::CPython(const char *appNameZ) : i_appName(appNameZ) { i_runLoopP.Set(new CPython_RunLoop(this)); } CPython::~CPython() { s_pythonP = NULL; { CCritical sc(&i_runLoopP); CPython_RunLoop *runLoopP(i_runLoopP.Get()); if (runLoopP) { runLoopP->i_abortB.Set(true); } } while (i_runLoopP.Get()) { IdleDuration(0.1f, kDurationForever_Idle); } } // static CPython* CPython::Get(const char *appNameZ) { if (s_pythonP == NULL) { CF_ASSERT(appNameZ); s_pythonP = new CPython(appNameZ); } return s_pythonP; } /****************************************************/ void CPython::Startup() { if (i_inittedB.Get()) XTE_START { CharVec charVec; CFileRef pythonRef(kFolder_KJAMS); ETX(pythonRef.Descend("Python/" kStartupScriptName)); pythonRef.Load(&charVec); charVec.push_back(0); i_runLoopP.Get()->RunScript(kStartupScriptName, &charVec[0]); } XTE_END; } void CPython::Test() { if (i_inittedB.Get()) { i_runLoopP.Get()->RunScript("print_time.py", s_PrintTime); } } /*****************************************************************/ #endif // kUsePython // the entire public interface is here // called on startup to init OSStatus CPython_PreAlloc(const char *utf8Z) { OSStatus err = noErr; #if kUsePython if (CPython::Get(utf8Z) == NULL) { ERR(tsmUnsupScriptLanguageErr); } #endif return err; } // called on shutdown void CPython_PostDispose() { #if kUsePython CPython *pyP(CPython::Get()); if (pyP) { delete pyP; } #endif } // very simple unit test void CPython_Test() { #if kUsePython CPython *pyP(CPython::Get()); if (pyP) { pyP->Test(); } #endif } // called when startup is complete void CPython_Startup() { #if kUsePython CPython *pyP(CPython::Get()); if (pyP) { pyP->Startup(); } #endif }