Python/Code

From kJams Wiki
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
}