diff --git a/README.md b/README.md index ac1ef95..7c966a8 100755 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The `Function` class has, apart from being a callable, additional methods: - `set_max_stack_size` - `memory` – returns a dict with information about memory usage. - `add_callable` – adds a Python function and makes it callable from JS. +- `execute_pending_job` – executes a pending job (such as a async function or Promise). ## Documentation For full functionality, please see `test_quickjs.py` diff --git a/module.c b/module.c index 7990d65..0e3ba89 100644 --- a/module.c +++ b/module.c @@ -244,6 +244,37 @@ static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds) { return quickjs_to_python(self->context, value); } +// Converts the current Javascript exception to a Python exception via a C string. +static void quickjs_exception_to_python(JSContext *context) { + JSValue exception = JS_GetException(context); + const char *cstring = JS_ToCString(context, exception); + const char *stack_cstring = NULL; + if (!JS_IsNull(exception) && !JS_IsUndefined(exception)) { + JSValue stack = JS_GetPropertyStr(context, exception, "stack"); + if (!JS_IsException(stack)) { + stack_cstring = JS_ToCString(context, stack); + JS_FreeValue(context, stack); + } + } + if (cstring != NULL) { + const char *safe_stack_cstring = stack_cstring ? stack_cstring : ""; + if (strstr(cstring, "stack overflow") != NULL) { + PyErr_Format(StackOverflow, "%s\n%s", cstring, safe_stack_cstring); + } else { + PyErr_Format(JSException, "%s\n%s", cstring, safe_stack_cstring); + } + } else { + // This has been observed to happen when different threads have used the same QuickJS + // runtime, but not at the same time. + // Could potentially be another problem though, since JS_ToCString may return NULL. + PyErr_Format(JSException, + "(Failed obtaining QuickJS error string. Concurrency issue?)"); + } + JS_FreeCString(context, cstring); + JS_FreeCString(context, stack_cstring); + JS_FreeValue(context, exception); +} + // Converts a JSValue to a Python object. // // Takes ownership of the JSValue and will deallocate it (refcount reduced by 1). @@ -266,34 +297,7 @@ static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) { } else if (tag == JS_TAG_UNDEFINED) { return_value = Py_None; } else if (tag == JS_TAG_EXCEPTION) { - // We have a Javascript exception. We convert it to a Python exception via a C string. - JSValue exception = JS_GetException(context); - const char *cstring = JS_ToCString(context, exception); - const char *stack_cstring = NULL; - if (!JS_IsNull(exception) && !JS_IsUndefined(exception)) { - JSValue stack = JS_GetPropertyStr(context, exception, "stack"); - if (!JS_IsException(stack)) { - stack_cstring = JS_ToCString(context, stack); - JS_FreeValue(context, stack); - } - } - if (cstring != NULL) { - const char *safe_stack_cstring = stack_cstring ? stack_cstring : ""; - if (strstr(cstring, "stack overflow") != NULL) { - PyErr_Format(StackOverflow, "%s\n%s", cstring, safe_stack_cstring); - } else { - PyErr_Format(JSException, "%s\n%s", cstring, safe_stack_cstring); - } - } else { - // This has been observed to happen when different threads have used the same QuickJS - // runtime, but not at the same time. - // Could potentially be another problem though, since JS_ToCString may return NULL. - PyErr_Format(JSException, - "(Failed obtaining QuickJS error string. Concurrency issue?)"); - } - JS_FreeCString(context, cstring); - JS_FreeCString(context, stack_cstring); - JS_FreeValue(context, exception); + quickjs_exception_to_python(context); } else if (tag == JS_TAG_FLOAT64) { return_value = Py_BuildValue("d", JS_VALUE_GET_FLOAT64(value)); } else if (tag == JS_TAG_STRING) { @@ -390,6 +394,24 @@ static PyObject *context_module(ContextData *self, PyObject *args) { return context_eval_internal(self, args, JS_EVAL_TYPE_MODULE); } +// _quickjs.Context.execute_pending_job +// +// If there are pending jobs, executes one and returns True. Else returns False. +static PyObject *context_execute_pending_job(ContextData *self) { + prepare_call_js(self); + JSContext *ctx; + int ret = JS_ExecutePendingJob(self->runtime, &ctx); + end_call_js(self); + if (ret > 0) { + Py_RETURN_TRUE; + } else if (ret == 0) { + Py_RETURN_FALSE; + } else { + quickjs_exception_to_python(ctx); + return NULL; + } +} + // _quickjs.Context.parse_json // // Evaluates a Python string as JSON and returns the result as a Python object. Will @@ -645,6 +667,7 @@ static PyMethodDef context_methods[] = { (PyCFunction)context_module, METH_VARARGS, "Evaluates a Javascript string as a module."}, + {"execute_pending_job", (PyCFunction)context_execute_pending_job, METH_NOARGS, "Executes a pending job."}, {"parse_json", (PyCFunction)context_parse_json, METH_VARARGS, "Parses a JSON string."}, {"get", (PyCFunction)context_get, METH_VARARGS, "Gets a Javascript global variable."}, {"set", (PyCFunction)context_set, METH_VARARGS, "Sets a Javascript global variable."}, diff --git a/quickjs/__init__.py b/quickjs/__init__.py index dd80a5f..7216fa6 100755 --- a/quickjs/__init__.py +++ b/quickjs/__init__.py @@ -73,6 +73,10 @@ def gc(self): with self._lock: self._context.gc() + def execute_pending_job(self) -> bool: + with self._lock: + return self._context.execute_pending_job() + def _compile(self, name: str, code: str) -> Tuple[Context, Object]: context = Context() context.eval(code) diff --git a/test_quickjs.py b/test_quickjs.py index ed4166b..75b2a50 100644 --- a/test_quickjs.py +++ b/test_quickjs.py @@ -150,6 +150,14 @@ def test_json_error(self): with self.assertRaisesRegex(quickjs.JSException, "unexpected token"): self.context.parse_json("a b c") + def test_execute_pending_job(self): + self.context.eval("obj = {}") + self.assertEqual(self.context.execute_pending_job(), False) + self.context.eval("Promise.resolve().then(() => {obj.x = 1;})") + self.assertEqual(self.context.execute_pending_job(), True) + self.assertEqual(self.context.eval("obj.x"), 1) + self.assertEqual(self.context.execute_pending_job(), False) + class CallIntoPython(unittest.TestCase): def setUp(self): @@ -465,6 +473,26 @@ def test_add_callable(self): self.assertEqual(f(), 42) + def test_execute_pending_job(self): + f = quickjs.Function( + "f", """ + obj = {x: 0, y: 0}; + async function a() { + obj.x = await 1; + } + a(); + Promise.resolve().then(() => {obj.y = 1}); + function f() { + return obj.x + obj.y; + } + """) + self.assertEqual(f(), 0) + self.assertEqual(f.execute_pending_job(), True) + self.assertEqual(f(), 1) + self.assertEqual(f.execute_pending_job(), True) + self.assertEqual(f(), 2) + self.assertEqual(f.execute_pending_job(), False) + class JavascriptFeatures(unittest.TestCase): def test_unicode_strings(self):