Skip to content
This repository was archived by the owner on Jan 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
79 changes: 51 additions & 28 deletions module.c
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."},
Expand Down
4 changes: 4 additions & 0 deletions quickjs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions test_quickjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down