Python: Difference between revisions

From kJams Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
Line 106: Line 106:
void RemoveScript(CT_RunScript *scriptP);
void RemoveScript(CT_RunScript *scriptP);
void IdleScripts();
void IdleScripts();
void AllowPendingCalls();
size_t CountScripts();
size_t CountScripts();
};
};
Line 551: Line 552:
// if you want to use Py_AddPendingCall() to send a message to THIS
// if you want to use Py_AddPendingCall() to send a message to THIS
// thread, then you'd need to uncomment this line
// thread, then you'd need to uncomment this line:
#if 0
// AllowPendingCalls();
if (PyRun_SimpleString(s_AllowPendingCalls) != 0) {
PostAlert("Python: s_AllowPendingCalls failed");
i_abortB.Set();
}
#endif


if (!abortB) {
if (!abortB) {
Line 591: Line 587:
return err;
return err;
}
void CPython_RunLoop::AllowPendingCalls()
{
ScEnsureGIL sc;
if (PyRun_SimpleString(s_AllowPendingCalls) != 0) {
PostAlert("Python: s_AllowPendingCalls failed");
i_abortB.Set();
}
}
}



Revision as of 20:07, 30 July 2013

//	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;
	
	CPython_RunLoop(CPython *in_thiz);
	
	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);

			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();
			}
		}
	};
	/*****************************/
	
	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;
		static SuperString	s_err_str;
		
		if (PyArg_ParseTuple(args, "s", &utf8_strZ)) {
			CCritical		sc(&i_errTimerP);
			
			if (i_errTimerP.Get() == NULL) {
				i_errTimerP.Set(new CShowErrorTimer(this));
			}
			
			i_errTimerP.Get()->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";

/*****************************************************************/
CPython_RunLoop::CPython_RunLoop(CPython *in_thiz) : 
	thiz(in_thiz)
{
	i_scriptVecP.Set(new ScriptVec());
	call(NO_KILL "CPython_RunLoop");
}

OSStatus	CPython_RunLoop::operator()(OSStatus err)
{
	XTE_START {
		bool	continueB = true;

		#if OPT_WINOS
		{
			//	don't crash if python is not installed, simply bail
			HMODULE		pythonH = LoadLibrary(L"pywintypes27");
			
			continueB = pythonH != NULL;
		}
		#endif
		
		if (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);
	
	{
		ScriptVec	*vecP = i_scriptVecP.Get();
		
		CF_ASSERT(vecP);
		CF_ASSERT(vecP->empty());
		delete vecP;
	}
	
	i_scriptVecP.Set(NULL);
	
	return err;
}

void		CPython_RunLoop::AllowPendingCalls()
{
	ScEnsureGIL		sc;

	if (PyRun_SimpleString(s_AllowPendingCalls) != 0) {
		PostAlert("Python: s_AllowPendingCalls failed");
		i_abortB.Set();
	}
}

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);

	i_scriptVecP.Get()->push_back(scriptP);
}

void		CPython_RunLoop::RemoveScript(CT_RunScript *scriptP)
{
	CCritical			sc(&i_scriptVecP);
	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);
		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);
			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);

	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
}