mirror of
https://github.com/hjdhnx/dr_py.git
synced 2024-11-24 09:48:24 -06:00
830 lines
28 KiB
C
830 lines
28 KiB
C
#include <Python.h>
|
|
#include <time.h>
|
|
|
|
#include "third-party/quickjs.h"
|
|
|
|
// Node of Python callable that the context needs to keep available.
|
|
typedef struct PythonCallableNode PythonCallableNode;
|
|
struct PythonCallableNode {
|
|
PyObject *obj;
|
|
// Internal ID of the callable function. "magic" is QuickJS terminology.
|
|
int magic;
|
|
PythonCallableNode *next;
|
|
};
|
|
|
|
// Keeps track of the time if we are using a time limit.
|
|
typedef struct {
|
|
clock_t start;
|
|
clock_t limit;
|
|
} InterruptData;
|
|
|
|
// The data of the type _quickjs.Context.
|
|
typedef struct {
|
|
PyObject_HEAD JSRuntime *runtime;
|
|
JSContext *context;
|
|
int has_time_limit;
|
|
clock_t time_limit;
|
|
// Used when releasing the GIL.
|
|
PyThreadState *thread_state;
|
|
InterruptData interrupt_data;
|
|
// NULL-terminated singly linked list of callable Python objects that we need to keep alive.
|
|
PythonCallableNode *python_callables;
|
|
} ContextData;
|
|
|
|
// The data of the type _quickjs.Object.
|
|
typedef struct {
|
|
PyObject_HEAD;
|
|
ContextData *context;
|
|
JSValue object;
|
|
} ObjectData;
|
|
|
|
// The exception raised by this module.
|
|
static PyObject *JSException = NULL;
|
|
static PyObject *StackOverflow = NULL;
|
|
// Converts a JSValue to a Python object.
|
|
//
|
|
// Takes ownership of the JSValue and will deallocate it (refcount reduced by 1).
|
|
static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value);
|
|
// Whether converting item to QuickJS would be possible.
|
|
static int python_to_quickjs_possible(ContextData *context, PyObject *item);
|
|
// Converts item to QuickJS.
|
|
//
|
|
// If the Python object is not possible to convert to JS, undefined will be returned. This fallback
|
|
// will not be used if python_to_quickjs_possible returns 1.
|
|
static JSValueConst python_to_quickjs(ContextData *context, PyObject *item);
|
|
|
|
static PyTypeObject Object;
|
|
|
|
// Returns nonzero if we should stop due to a time limit.
|
|
static int js_interrupt_handler(JSRuntime *rt, void *opaque) {
|
|
InterruptData *data = opaque;
|
|
if (clock() - data->start >= data->limit) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Sets up a context and an InterruptData struct if the context has a time limit.
|
|
static void setup_time_limit(ContextData *context, InterruptData *interrupt_data) {
|
|
if (context->has_time_limit) {
|
|
JS_SetInterruptHandler(context->runtime, js_interrupt_handler, interrupt_data);
|
|
interrupt_data->limit = context->time_limit;
|
|
interrupt_data->start = clock();
|
|
}
|
|
}
|
|
|
|
// Restores the context if the context has a time limit.
|
|
static void teardown_time_limit(ContextData *context) {
|
|
if (context->has_time_limit) {
|
|
JS_SetInterruptHandler(context->runtime, NULL, NULL);
|
|
}
|
|
}
|
|
|
|
// This method is always called in a context before running JS code in QuickJS. It sets up time
|
|
// limites, releases the GIL etc.
|
|
static void prepare_call_js(ContextData *context) {
|
|
// We release the GIL in order to speed things up for certain use cases.
|
|
assert(!context->thread_state);
|
|
context->thread_state = PyEval_SaveThread();
|
|
setup_time_limit(context, &context->interrupt_data);
|
|
}
|
|
|
|
// This method is called right after returning from running JS code. Aquires the GIL etc.
|
|
static void end_call_js(ContextData *context) {
|
|
teardown_time_limit(context);
|
|
assert(context->thread_state);
|
|
PyEval_RestoreThread(context->thread_state);
|
|
context->thread_state = NULL;
|
|
}
|
|
|
|
// Called when Python is called again from inside QuickJS.
|
|
static void prepare_call_python(ContextData *context) {
|
|
assert(context->thread_state);
|
|
PyEval_RestoreThread(context->thread_state);
|
|
context->thread_state = NULL;
|
|
}
|
|
|
|
// Called when the operation started by prepare_call_python is done.
|
|
static void end_call_python(ContextData *context) {
|
|
assert(!context->thread_state);
|
|
context->thread_state = PyEval_SaveThread();
|
|
}
|
|
|
|
// GC traversal.
|
|
static int object_traverse(ObjectData *self, visitproc visit, void *arg) {
|
|
Py_VISIT(self->context);
|
|
return 0;
|
|
}
|
|
|
|
// Creates an instance of the Object class.
|
|
static PyObject *object_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
|
|
ObjectData *self = PyObject_GC_New(ObjectData, type);
|
|
if (self != NULL) {
|
|
self->context = NULL;
|
|
}
|
|
return (PyObject *)self;
|
|
}
|
|
|
|
// Deallocates an instance of the Object class.
|
|
static void object_dealloc(ObjectData *self) {
|
|
if (self->context) {
|
|
PyObject_GC_UnTrack(self);
|
|
JS_FreeValue(self->context->context, self->object);
|
|
// We incremented the refcount of the context when we created this object, so we should
|
|
// decrease it now so we don't leak memory.
|
|
Py_DECREF(self->context);
|
|
}
|
|
PyObject_GC_Del(self);
|
|
}
|
|
|
|
// _quickjs.Object.get
|
|
//
|
|
// Gets a Javascript property of the object.
|
|
static PyObject *object_get(ObjectData *self, PyObject *args) {
|
|
const char *name;
|
|
if (!PyArg_ParseTuple(args, "s", &name)) {
|
|
return NULL;
|
|
}
|
|
JSValue value = JS_GetPropertyStr(self->context->context, self->object, name);
|
|
return quickjs_to_python(self->context, value);
|
|
}
|
|
|
|
static JSValue js_c_function(
|
|
JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) {
|
|
ContextData *context = (ContextData *)JS_GetContextOpaque(ctx);
|
|
if (context->has_time_limit) {
|
|
return JS_ThrowInternalError(ctx, "Can not call into Python with a time limit set.");
|
|
}
|
|
PythonCallableNode *node = context->python_callables;
|
|
while (node && node->magic != magic) {
|
|
node = node->next;
|
|
}
|
|
if (!node) {
|
|
return JS_ThrowInternalError(ctx, "Internal error.");
|
|
}
|
|
prepare_call_python(context);
|
|
|
|
PyObject *args = PyTuple_New(argc);
|
|
if (!args) {
|
|
end_call_python(context);
|
|
return JS_ThrowOutOfMemory(ctx);
|
|
}
|
|
int tuple_success = 1;
|
|
for (int i = 0; i < argc; ++i) {
|
|
PyObject *arg = quickjs_to_python(context, JS_DupValue(ctx, argv[i]));
|
|
if (!arg) {
|
|
tuple_success = 0;
|
|
break;
|
|
}
|
|
PyTuple_SET_ITEM(args, i, arg);
|
|
}
|
|
if (!tuple_success) {
|
|
Py_DECREF(args);
|
|
end_call_python(context);
|
|
return JS_ThrowInternalError(ctx, "Internal error: could not convert args.");
|
|
}
|
|
|
|
PyObject *result = PyObject_CallObject(node->obj, args);
|
|
Py_DECREF(args);
|
|
if (!result) {
|
|
end_call_python(context);
|
|
return JS_ThrowInternalError(ctx, "Python call failed.");
|
|
}
|
|
JSValue js_result = JS_NULL;
|
|
if (python_to_quickjs_possible(context, result)) {
|
|
js_result = python_to_quickjs(context, result);
|
|
} else {
|
|
PyErr_Clear();
|
|
js_result = JS_ThrowInternalError(ctx, "Can not convert Python result to JS.");
|
|
}
|
|
Py_DECREF(result);
|
|
|
|
end_call_python(context);
|
|
return js_result;
|
|
}
|
|
|
|
// _quickjs.Object.set
|
|
//
|
|
// Sets a Javascript property to the object. Callables are supported.
|
|
static PyObject *object_set(ObjectData *self, PyObject *args) {
|
|
const char *name;
|
|
PyObject *item;
|
|
if (!PyArg_ParseTuple(args, "sO", &name, &item)) {
|
|
return NULL;
|
|
}
|
|
int ret = 0;
|
|
if (PyCallable_Check(item) && (!PyObject_IsInstance(item, (PyObject *)&Object) || JS_IsFunction(
|
|
self->context->context, ((ObjectData *)item)->object))) {
|
|
PythonCallableNode *node = PyMem_Malloc(sizeof(PythonCallableNode));
|
|
if (!node) {
|
|
return NULL;
|
|
}
|
|
Py_INCREF(item);
|
|
node->magic = 0;
|
|
if (self->context->python_callables) {
|
|
node->magic = self->context->python_callables->magic + 1;
|
|
}
|
|
node->obj = item;
|
|
node->next = self->context->python_callables;
|
|
self->context->python_callables = node;
|
|
|
|
JSValue function = JS_NewCFunctionMagic(
|
|
self->context->context,
|
|
js_c_function,
|
|
name,
|
|
0, // TODO: Should we allow setting the .length of the function to something other than 0?
|
|
JS_CFUNC_generic_magic,
|
|
node->magic);
|
|
// If this fails we don't notify the caller of this function.
|
|
ret = JS_SetPropertyStr(self->context->context, self->object, name, function);
|
|
if (ret != 1) {
|
|
PyErr_SetString(PyExc_TypeError, "Failed setting the variable as a callable.");
|
|
return NULL;
|
|
} else {
|
|
Py_RETURN_NONE;
|
|
}
|
|
} else {
|
|
if (python_to_quickjs_possible(self->context, item)) {
|
|
ret = JS_SetPropertyStr(self->context->context, self->object, name,
|
|
python_to_quickjs(self->context, item));
|
|
if (ret != 1) {
|
|
PyErr_SetString(PyExc_TypeError, "Failed setting the variable.");
|
|
}
|
|
}
|
|
if (ret == 1) {
|
|
Py_RETURN_NONE;
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// _quickjs.Object.__call__
|
|
static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds);
|
|
|
|
// _quickjs.Object.json
|
|
//
|
|
// Returns the JSON representation of the object as a Python string.
|
|
static PyObject *object_json(ObjectData *self) {
|
|
JSContext *context = self->context->context;
|
|
JSValue json_string = JS_JSONStringify(context, self->object, JS_UNDEFINED, JS_UNDEFINED);
|
|
return quickjs_to_python(self->context, json_string);
|
|
}
|
|
|
|
// All methods of the _quickjs.Object class.
|
|
static PyMethodDef object_methods[] = {
|
|
{"get", (PyCFunction)object_get, METH_VARARGS, "Gets a Javascript property of the object."},
|
|
{"set", (PyCFunction)object_set, METH_VARARGS, "Sets a Javascript property to the object."},
|
|
{"json", (PyCFunction)object_json, METH_NOARGS, "Converts to a JSON string."},
|
|
{NULL} /* Sentinel */
|
|
};
|
|
|
|
// Define the quickjs.Object type.
|
|
static PyTypeObject Object = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickjs.Object",
|
|
.tp_doc = "Quickjs object",
|
|
.tp_basicsize = sizeof(ObjectData),
|
|
.tp_itemsize = 0,
|
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
|
|
.tp_traverse = (traverseproc)object_traverse,
|
|
.tp_new = object_new,
|
|
.tp_dealloc = (destructor)object_dealloc,
|
|
.tp_call = (ternaryfunc)object_call,
|
|
.tp_methods = object_methods};
|
|
|
|
// Whether converting item to QuickJS would be possible.
|
|
static int python_to_quickjs_possible(ContextData *context, PyObject *item) {
|
|
if (PyBool_Check(item)) {
|
|
return 1;
|
|
} else if (PyLong_Check(item)) {
|
|
return 1;
|
|
} else if (PyFloat_Check(item)) {
|
|
return 1;
|
|
} else if (item == Py_None) {
|
|
return 1;
|
|
} else if (PyUnicode_Check(item)) {
|
|
return 1;
|
|
} else if (PyObject_IsInstance(item, (PyObject *)&Object)) {
|
|
ObjectData *object = (ObjectData *)item;
|
|
if (object->context != context) {
|
|
PyErr_Format(PyExc_ValueError, "Can not mix JS objects from different contexts.");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
} else {
|
|
PyErr_Format(PyExc_TypeError,
|
|
"Unsupported type when converting a Python object to quickjs: %s.",
|
|
Py_TYPE(item)->tp_name);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Converts item to QuickJS.
|
|
//
|
|
// If the Python object is not possible to convert to JS, undefined will be returned. This fallback
|
|
// will not be used if python_to_quickjs_possible returns 1.
|
|
static JSValueConst python_to_quickjs(ContextData *context, PyObject *item) {
|
|
if (PyBool_Check(item)) {
|
|
return JS_MKVAL(JS_TAG_BOOL, item == Py_True ? 1 : 0);
|
|
} else if (PyLong_Check(item)) {
|
|
int overflow;
|
|
long value = PyLong_AsLongAndOverflow(item, &overflow);
|
|
if (overflow) {
|
|
PyObject *float_value = PyNumber_Float(item);
|
|
double double_value = PyFloat_AsDouble(float_value);
|
|
Py_DECREF(float_value);
|
|
return JS_NewFloat64(context->context, double_value);
|
|
} else {
|
|
return JS_MKVAL(JS_TAG_INT, value);
|
|
}
|
|
} else if (PyFloat_Check(item)) {
|
|
return JS_NewFloat64(context->context, PyFloat_AsDouble(item));
|
|
} else if (item == Py_None) {
|
|
return JS_NULL;
|
|
} else if (PyUnicode_Check(item)) {
|
|
return JS_NewString(context->context, PyUnicode_AsUTF8(item));
|
|
} else if (PyObject_IsInstance(item, (PyObject *)&Object)) {
|
|
return JS_DupValue(context->context, ((ObjectData *)item)->object);
|
|
} else {
|
|
// Can not happen if python_to_quickjs_possible passes.
|
|
return JS_UNDEFINED;
|
|
}
|
|
}
|
|
|
|
// _quickjs.Object.__call__
|
|
static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds) {
|
|
if (self->context == NULL) {
|
|
// This object does not have a context and has not been created by this module.
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
// We first loop through all arguments and check that they are supported without doing anything.
|
|
// This makes the cleanup code simpler for the case where we have to raise an error.
|
|
const int nargs = PyTuple_Size(args);
|
|
for (int i = 0; i < nargs; ++i) {
|
|
PyObject *item = PyTuple_GetItem(args, i);
|
|
if (!python_to_quickjs_possible(self->context, item)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Now we know that all arguments are supported and we can convert them.
|
|
JSValueConst *jsargs = malloc(nargs * sizeof(JSValueConst));
|
|
for (int i = 0; i < nargs; ++i) {
|
|
PyObject *item = PyTuple_GetItem(args, i);
|
|
jsargs[i] = python_to_quickjs(self->context, item);
|
|
}
|
|
|
|
prepare_call_js(self->context);
|
|
JSValue value;
|
|
value = JS_Call(self->context->context, self->object, JS_NULL, nargs, jsargs);
|
|
for (int i = 0; i < nargs; ++i) {
|
|
JS_FreeValue(self->context->context, jsargs[i]);
|
|
}
|
|
free(jsargs);
|
|
end_call_js(self->context);
|
|
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).
|
|
static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) {
|
|
JSContext *context = context_obj->context;
|
|
int tag = JS_VALUE_GET_TAG(value);
|
|
// A return value of NULL means an exception.
|
|
PyObject *return_value = NULL;
|
|
|
|
if (tag == JS_TAG_INT) {
|
|
return_value = Py_BuildValue("i", JS_VALUE_GET_INT(value));
|
|
} else if (tag == JS_TAG_BIG_INT) {
|
|
const char *cstring = JS_ToCString(context, value);
|
|
return_value = PyLong_FromString(cstring, NULL, 10);
|
|
JS_FreeCString(context, cstring);
|
|
} else if (tag == JS_TAG_BOOL) {
|
|
return_value = Py_BuildValue("O", JS_VALUE_GET_BOOL(value) ? Py_True : Py_False);
|
|
} else if (tag == JS_TAG_NULL) {
|
|
return_value = Py_None;
|
|
} else if (tag == JS_TAG_UNDEFINED) {
|
|
return_value = Py_None;
|
|
} else if (tag == JS_TAG_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) {
|
|
const char *cstring = JS_ToCString(context, value);
|
|
return_value = Py_BuildValue("s", cstring);
|
|
JS_FreeCString(context, cstring);
|
|
} else if (tag == JS_TAG_OBJECT || tag == JS_TAG_MODULE || tag == JS_TAG_SYMBOL) {
|
|
// This is a Javascript object or function. We wrap it in a _quickjs.Object.
|
|
return_value = PyObject_CallObject((PyObject *)&Object, NULL);
|
|
ObjectData *object = (ObjectData *)return_value;
|
|
// This is important. Otherwise, the context may be deallocated before the object, which
|
|
// will result in a segfault with high probability.
|
|
Py_INCREF(context_obj);
|
|
object->context = context_obj;
|
|
PyObject_GC_Track(object);
|
|
object->object = JS_DupValue(context, value);
|
|
} else {
|
|
PyErr_Format(PyExc_TypeError, "Unknown quickjs tag: %d", tag);
|
|
}
|
|
|
|
JS_FreeValue(context, value);
|
|
if (return_value == Py_None) {
|
|
// Can not simply return PyNone for refcounting reasons.
|
|
Py_RETURN_NONE;
|
|
}
|
|
return return_value;
|
|
}
|
|
|
|
static PyObject *test(PyObject *self, PyObject *args) {
|
|
return Py_BuildValue("i", 42);
|
|
}
|
|
|
|
// Global state of the module. Currently none.
|
|
struct module_state {};
|
|
|
|
// GC traversal.
|
|
static int context_traverse(ContextData *self, visitproc visit, void *arg) {
|
|
PythonCallableNode *node = self->python_callables;
|
|
while (node) {
|
|
Py_VISIT(node->obj);
|
|
node = node->next;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// GC clearing. Object does not have a clearing method, therefore dependency cycles
|
|
// between Context and Object will always be cleared starting here.
|
|
static int context_clear(ContextData *self) {
|
|
PythonCallableNode *node = self->python_callables;
|
|
while (node) {
|
|
Py_CLEAR(node->obj);
|
|
node = node->next;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Creates an instance of the _quickjs.Context class.
|
|
static PyObject *context_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
|
|
ContextData *self = PyObject_GC_New(ContextData, type);
|
|
if (self != NULL) {
|
|
// We never have different contexts for the same runtime. This way, different
|
|
// _quickjs.Context can be used concurrently.
|
|
self->runtime = JS_NewRuntime();
|
|
self->context = JS_NewContext(self->runtime);
|
|
self->has_time_limit = 0;
|
|
self->time_limit = 0;
|
|
self->thread_state = NULL;
|
|
self->python_callables = NULL;
|
|
JS_SetContextOpaque(self->context, self);
|
|
PyObject_GC_Track(self);
|
|
}
|
|
return (PyObject *)self;
|
|
}
|
|
|
|
// Deallocates an instance of the _quickjs.Context class.
|
|
static void context_dealloc(ContextData *self) {
|
|
JS_FreeContext(self->context);
|
|
JS_FreeRuntime(self->runtime);
|
|
PyObject_GC_UnTrack(self);
|
|
PythonCallableNode *node = self->python_callables;
|
|
self->python_callables = NULL;
|
|
while (node) {
|
|
PythonCallableNode *this = node;
|
|
node = node->next;
|
|
// this->obj may already be NULL if GC'ed right before through context_clear.
|
|
Py_XDECREF(this->obj);
|
|
PyMem_Free(this);
|
|
}
|
|
PyObject_GC_Del(self);
|
|
}
|
|
|
|
// Evaluates a Python string as JS and returns the result as a Python object. Will return
|
|
// _quickjs.Object for complex types (other than e.g. str, int).
|
|
static PyObject *context_eval_internal(ContextData *self, PyObject *args, int eval_type) {
|
|
const char *code;
|
|
if (!PyArg_ParseTuple(args, "s", &code)) {
|
|
return NULL;
|
|
}
|
|
prepare_call_js(self);
|
|
JSValue value;
|
|
value = JS_Eval(self->context, code, strlen(code), "<input>", eval_type);
|
|
end_call_js(self);
|
|
return quickjs_to_python(self, value);
|
|
}
|
|
|
|
// _quickjs.Context.eval
|
|
//
|
|
// Evaluates a Python string as JS and returns the result as a Python object. Will return
|
|
// _quickjs.Object for complex types (other than e.g. str, int).
|
|
static PyObject *context_eval(ContextData *self, PyObject *args) {
|
|
return context_eval_internal(self, args, JS_EVAL_TYPE_GLOBAL);
|
|
}
|
|
|
|
// _quickjs.Context.module
|
|
//
|
|
// Evaluates a Python string as JS module. Otherwise identical to eval.
|
|
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
|
|
// return _quickjs.Object for complex types (other than e.g. str, int).
|
|
static PyObject *context_parse_json(ContextData *self, PyObject *args) {
|
|
const char *data;
|
|
if (!PyArg_ParseTuple(args, "s", &data)) {
|
|
return NULL;
|
|
}
|
|
JSValue value;
|
|
Py_BEGIN_ALLOW_THREADS;
|
|
value = JS_ParseJSON(self->context, data, strlen(data), "context_parse_json.json");
|
|
Py_END_ALLOW_THREADS;
|
|
return quickjs_to_python(self, value);
|
|
}
|
|
|
|
// _quickjs.Context.get_global
|
|
//
|
|
// Retrieves the global object of the JS context.
|
|
static PyObject *context_get_global(ContextData *self) {
|
|
return quickjs_to_python(self, JS_GetGlobalObject(self->context));
|
|
}
|
|
|
|
// _quickjs.Context.get
|
|
//
|
|
// Retrieves a global variable from the JS context.
|
|
static PyObject *context_get(ContextData *self, PyObject *args) {
|
|
PyErr_WarnEx(PyExc_DeprecationWarning,
|
|
"Context.get is deprecated, use Context.get_global().get instead.", 1);
|
|
PyObject *global = context_get_global(self);
|
|
if (global == NULL) {
|
|
return NULL;
|
|
}
|
|
PyObject *ret = object_get((ObjectData *)global, args);
|
|
Py_DECREF(global);
|
|
return ret;
|
|
}
|
|
|
|
// _quickjs.Context.set
|
|
//
|
|
// Sets a global variable to the JS context.
|
|
static PyObject *context_set(ContextData *self, PyObject *args) {
|
|
PyErr_WarnEx(PyExc_DeprecationWarning,
|
|
"Context.set is deprecated, use Context.get_global().set instead.", 1);
|
|
PyObject *global = context_get_global(self);
|
|
if (global == NULL) {
|
|
return NULL;
|
|
}
|
|
PyObject *ret = object_set((ObjectData *)global, args);
|
|
Py_DECREF(global);
|
|
return ret;
|
|
}
|
|
|
|
// _quickjs.Context.set_memory_limit
|
|
//
|
|
// Sets the memory limit of the context.
|
|
static PyObject *context_set_memory_limit(ContextData *self, PyObject *args) {
|
|
Py_ssize_t limit;
|
|
if (!PyArg_ParseTuple(args, "n", &limit)) {
|
|
return NULL;
|
|
}
|
|
JS_SetMemoryLimit(self->runtime, limit);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
// _quickjs.Context.set_time_limit
|
|
//
|
|
// Sets the CPU time limit of the context. This will be used in an interrupt handler.
|
|
static PyObject *context_set_time_limit(ContextData *self, PyObject *args) {
|
|
double limit;
|
|
if (!PyArg_ParseTuple(args, "d", &limit)) {
|
|
return NULL;
|
|
}
|
|
if (limit < 0) {
|
|
self->has_time_limit = 0;
|
|
} else {
|
|
self->has_time_limit = 1;
|
|
self->time_limit = (clock_t)(limit * CLOCKS_PER_SEC);
|
|
}
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
// _quickjs.Context.set_max_stack_size
|
|
//
|
|
// Sets the max stack size in bytes.
|
|
static PyObject *context_set_max_stack_size(ContextData *self, PyObject *args) {
|
|
Py_ssize_t limit;
|
|
if (!PyArg_ParseTuple(args, "n", &limit)) {
|
|
return NULL;
|
|
}
|
|
JS_SetMaxStackSize(self->runtime, limit);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
// _quickjs.Context.memory
|
|
//
|
|
// Sets the CPU time limit of the context. This will be used in an interrupt handler.
|
|
static PyObject *context_memory(ContextData *self) {
|
|
PyObject *dict = PyDict_New();
|
|
if (dict == NULL) {
|
|
return NULL;
|
|
}
|
|
JSMemoryUsage usage;
|
|
JS_ComputeMemoryUsage(self->runtime, &usage);
|
|
#define MEM_USAGE_ADD_TO_DICT(key) \
|
|
{ \
|
|
PyObject *value = PyLong_FromLongLong(usage.key); \
|
|
if (PyDict_SetItemString(dict, #key, value) != 0) { \
|
|
return NULL; \
|
|
} \
|
|
Py_DECREF(value); \
|
|
}
|
|
MEM_USAGE_ADD_TO_DICT(malloc_size);
|
|
MEM_USAGE_ADD_TO_DICT(malloc_limit);
|
|
MEM_USAGE_ADD_TO_DICT(memory_used_size);
|
|
MEM_USAGE_ADD_TO_DICT(malloc_count);
|
|
MEM_USAGE_ADD_TO_DICT(memory_used_count);
|
|
MEM_USAGE_ADD_TO_DICT(atom_count);
|
|
MEM_USAGE_ADD_TO_DICT(atom_size);
|
|
MEM_USAGE_ADD_TO_DICT(str_count);
|
|
MEM_USAGE_ADD_TO_DICT(str_size);
|
|
MEM_USAGE_ADD_TO_DICT(obj_count);
|
|
MEM_USAGE_ADD_TO_DICT(obj_size);
|
|
MEM_USAGE_ADD_TO_DICT(prop_count);
|
|
MEM_USAGE_ADD_TO_DICT(prop_size);
|
|
MEM_USAGE_ADD_TO_DICT(shape_count);
|
|
MEM_USAGE_ADD_TO_DICT(shape_size);
|
|
MEM_USAGE_ADD_TO_DICT(js_func_count);
|
|
MEM_USAGE_ADD_TO_DICT(js_func_size);
|
|
MEM_USAGE_ADD_TO_DICT(js_func_code_size);
|
|
MEM_USAGE_ADD_TO_DICT(js_func_pc2line_count);
|
|
MEM_USAGE_ADD_TO_DICT(js_func_pc2line_size);
|
|
MEM_USAGE_ADD_TO_DICT(c_func_count);
|
|
MEM_USAGE_ADD_TO_DICT(array_count);
|
|
MEM_USAGE_ADD_TO_DICT(fast_array_count);
|
|
MEM_USAGE_ADD_TO_DICT(fast_array_elements);
|
|
MEM_USAGE_ADD_TO_DICT(binary_object_count);
|
|
MEM_USAGE_ADD_TO_DICT(binary_object_size);
|
|
return dict;
|
|
}
|
|
|
|
// _quickjs.Context.gc
|
|
//
|
|
// Runs garbage collection.
|
|
static PyObject *context_gc(ContextData *self) {
|
|
JS_RunGC(self->runtime);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyObject *context_add_callable(ContextData *self, PyObject *args) {
|
|
PyErr_WarnEx(PyExc_DeprecationWarning,
|
|
"Context.add_callable is deprecated, use Context.get_global().set instead.", 1);
|
|
PyObject *global = context_get_global(self);
|
|
if (global == NULL) {
|
|
return NULL;
|
|
}
|
|
PyObject *ret = object_set((ObjectData *)global, args);
|
|
Py_DECREF(global);
|
|
return ret;
|
|
}
|
|
|
|
// All methods of the _quickjs.Context class.
|
|
static PyMethodDef context_methods[] = {
|
|
{"eval", (PyCFunction)context_eval, METH_VARARGS, "Evaluates a Javascript string."},
|
|
{"module",
|
|
(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_global", (PyCFunction)context_get_global, METH_NOARGS, "Gets the Javascript global object."},
|
|
{"get", (PyCFunction)context_get, METH_VARARGS, "Gets a Javascript global variable."},
|
|
{"set", (PyCFunction)context_set, METH_VARARGS, "Sets a Javascript global variable."},
|
|
{"set_memory_limit",
|
|
(PyCFunction)context_set_memory_limit,
|
|
METH_VARARGS,
|
|
"Sets the memory limit in bytes."},
|
|
{"set_time_limit",
|
|
(PyCFunction)context_set_time_limit,
|
|
METH_VARARGS,
|
|
"Sets the CPU time limit in seconds (C function clock() is used)."},
|
|
{"set_max_stack_size",
|
|
(PyCFunction)context_set_max_stack_size,
|
|
METH_VARARGS,
|
|
"Sets the maximum stack size in bytes. Default is 256kB."},
|
|
{"memory", (PyCFunction)context_memory, METH_NOARGS, "Returns the memory usage as a dict."},
|
|
{"gc", (PyCFunction)context_gc, METH_NOARGS, "Runs garbage collection."},
|
|
{"add_callable", (PyCFunction)context_add_callable, METH_VARARGS, "Wraps a Python callable."},
|
|
{NULL} /* Sentinel */
|
|
};
|
|
|
|
// Define the _quickjs.Context type.
|
|
static PyTypeObject Context = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickjs.Context",
|
|
.tp_doc = "Quickjs context",
|
|
.tp_basicsize = sizeof(ContextData),
|
|
.tp_itemsize = 0,
|
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
|
|
.tp_traverse = (traverseproc)context_traverse,
|
|
.tp_clear = (inquiry)context_clear,
|
|
.tp_new = context_new,
|
|
.tp_dealloc = (destructor)context_dealloc,
|
|
.tp_methods = context_methods};
|
|
|
|
// All global methods in _quickjs.
|
|
static PyMethodDef myextension_methods[] = {{"test", (PyCFunction)test, METH_NOARGS, NULL},
|
|
{NULL, NULL}};
|
|
|
|
// Define the _quickjs module.
|
|
static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT,
|
|
"quickjs",
|
|
NULL,
|
|
sizeof(struct module_state),
|
|
myextension_methods,
|
|
NULL,
|
|
NULL,
|
|
NULL,
|
|
NULL};
|
|
|
|
// This function runs when the module is first imported.
|
|
PyMODINIT_FUNC PyInit__quickjs(void) {
|
|
if (PyType_Ready(&Context) < 0) {
|
|
return NULL;
|
|
}
|
|
if (PyType_Ready(&Object) < 0) {
|
|
return NULL;
|
|
}
|
|
|
|
PyObject *module = PyModule_Create(&moduledef);
|
|
if (module == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
JSException = PyErr_NewException("_quickjs.JSException", NULL, NULL);
|
|
if (JSException == NULL) {
|
|
return NULL;
|
|
}
|
|
StackOverflow = PyErr_NewException("_quickjs.StackOverflow", JSException, NULL);
|
|
if (StackOverflow == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
Py_INCREF(&Context);
|
|
PyModule_AddObject(module, "Context", (PyObject *)&Context);
|
|
Py_INCREF(&Object);
|
|
PyModule_AddObject(module, "Object", (PyObject *)&Object);
|
|
PyModule_AddObject(module, "JSException", JSException);
|
|
PyModule_AddObject(module, "StackOverflow", StackOverflow);
|
|
return module;
|
|
}
|