Skip to content

Commit 2e95392

Browse files
committed
Add set_time_limit.
1 parent 1612806 commit 2e95392

File tree

3 files changed

+120
-5
lines changed

3 files changed

+120
-5
lines changed

module.c

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#include <time.h>
2+
13
#include <Python.h>
24

35
#include "third-party/quickjs.h"
@@ -6,6 +8,8 @@
68
typedef struct {
79
PyObject_HEAD JSRuntime *runtime;
810
JSContext *context;
11+
int has_time_limit;
12+
clock_t time_limit;
913
} ContextData;
1014

1115
// The data of the type _quickjs.Object.
@@ -22,6 +26,38 @@ static PyObject *JSException = NULL;
2226
// Takes ownership of the JSValue and will deallocate it (refcount reduced by 1).
2327
static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value);
2428

29+
// Keeps track of the time if we are using a time limit.
30+
typedef struct {
31+
clock_t start;
32+
clock_t limit;
33+
} InterruptData;
34+
35+
// Returns nonzero if we should stop due to a time limit.
36+
static int js_interrupt_handler(JSRuntime *rt, void *opaque) {
37+
InterruptData *data = opaque;
38+
if (clock() - data->start >= data->limit) {
39+
return 1;
40+
} else {
41+
return 0;
42+
}
43+
}
44+
45+
// Sets up a context and an InterruptData struct if the context has a time limit.
46+
static void setup_time_limit(ContextData *context, InterruptData *interrupt_data) {
47+
if (context->has_time_limit) {
48+
JS_SetInterruptHandler(context->runtime, js_interrupt_handler, interrupt_data);
49+
interrupt_data->limit = context->time_limit;
50+
interrupt_data->start = clock();
51+
}
52+
}
53+
54+
// Restores the context if the context has a time limit.
55+
static void teardown_time_limit(ContextData *context) {
56+
if (context->has_time_limit) {
57+
JS_SetInterruptHandler(context->runtime, NULL, NULL);
58+
}
59+
}
60+
2561
// Creates an instance of the Object class.
2662
static PyObject *object_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
2763
ObjectData *self;
@@ -134,7 +170,10 @@ static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds) {
134170
// function from JS, this needs to be reversed or improved.
135171
JSValue value;
136172
Py_BEGIN_ALLOW_THREADS;
173+
InterruptData interrupt_data;
174+
setup_time_limit(self->context, &interrupt_data);
137175
value = JS_Call(self->context->context, self->object, JS_NULL, nargs, jsargs);
176+
teardown_time_limit(self->context);
138177
Py_END_ALLOW_THREADS;
139178

140179
for (int i = 0; i < nargs; ++i) {
@@ -213,6 +252,8 @@ static PyObject *context_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
213252
// _quickjs.Context can be used concurrently.
214253
self->runtime = JS_NewRuntime();
215254
self->context = JS_NewContext(self->runtime);
255+
self->has_time_limit = 0;
256+
self->time_limit = 0;
216257
}
217258
return (PyObject *)self;
218259
}
@@ -239,7 +280,10 @@ static PyObject *context_eval(ContextData *self, PyObject *args) {
239280
// function from JS, this needs to be reversed or improved.
240281
JSValue value;
241282
Py_BEGIN_ALLOW_THREADS;
283+
InterruptData interrupt_data;
284+
setup_time_limit(self, &interrupt_data);
242285
value = JS_Eval(self->context, code, strlen(code), "<input>", JS_EVAL_TYPE_GLOBAL);
286+
teardown_time_limit(self);
243287
Py_END_ALLOW_THREADS;
244288
return quickjs_to_python(self, value);
245289
}
@@ -270,11 +314,35 @@ static PyObject *context_set_memory_limit(ContextData *self, PyObject *args) {
270314
Py_RETURN_NONE;
271315
}
272316

317+
// _quickjs.Context.set_time_limit
318+
//
319+
// Retrieves a global variable from the JS context.
320+
static PyObject *context_set_time_limit(ContextData *self, PyObject *args) {
321+
double limit;
322+
if (!PyArg_ParseTuple(args, "d", &limit)) {
323+
return NULL;
324+
}
325+
if (limit < 0) {
326+
self->has_time_limit = 0;
327+
} else {
328+
self->has_time_limit = 1;
329+
self->time_limit = (clock_t)(limit * CLOCKS_PER_SEC);
330+
}
331+
Py_RETURN_NONE;
332+
}
333+
273334
// All methods of the _quickjs.Context class.
274335
static PyMethodDef context_methods[] = {
275336
{"eval", (PyCFunction)context_eval, METH_VARARGS, "Evaluates a Javascript string."},
276337
{"get", (PyCFunction)context_get, METH_VARARGS, "Gets a Javascript global variable."},
277-
{"set_memory_limit", (PyCFunction)context_set_memory_limit, METH_VARARGS, "Sets the memory limit in bytes."},
338+
{"set_memory_limit",
339+
(PyCFunction)context_set_memory_limit,
340+
METH_VARARGS,
341+
"Sets the memory limit in bytes."},
342+
{"set_time_limit",
343+
(PyCFunction)context_set_time_limit,
344+
METH_VARARGS,
345+
"Sets the CPU time limit in seconds (C function clock() is used)."},
278346
{NULL} /* Sentinel */
279347
};
280348

quickjs/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ def __call__(self, *args):
2424
with self._lock:
2525
return self._call(*args)
2626

27+
def set_memory_limit(self, limit):
28+
with self._lock:
29+
return self._context.set_memory_limit(limit)
30+
31+
def set_time_limit(self, limit):
32+
with self._lock:
33+
return self._context.set_time_limit(limit)
34+
2735
def _call(self, *args):
2836
def convert_arg(arg):
2937
if isinstance(arg, (type(None), str, bool, float, int)):

test_quickjs.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,36 @@ def get_f():
7575

7676
def test_memory_limit(self):
7777
code = """
78-
let arr = [];
79-
for (let i = 0; i < 1000; ++i) {
80-
arr.push(i);
81-
}
78+
(function() {
79+
let arr = [];
80+
for (let i = 0; i < 1000; ++i) {
81+
arr.push(i);
82+
}
83+
})();
8284
"""
8385
self.context.eval(code)
8486
self.context.set_memory_limit(1000)
8587
with self.assertRaisesRegex(quickjs.JSException, "null"):
8688
self.context.eval(code)
89+
self.context.set_memory_limit(1000000)
90+
self.context.eval(code)
91+
92+
def test_time_limit(self):
93+
code = """
94+
(function() {
95+
let arr = [];
96+
for (let i = 0; i < 100000; ++i) {
97+
arr.push(i);
98+
}
99+
return arr;
100+
})();
101+
"""
102+
self.context.eval(code)
103+
self.context.set_time_limit(0)
104+
with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"):
105+
self.context.eval(code)
106+
self.context.set_time_limit(-1)
107+
self.context.eval(code)
87108

88109

89110
class Object(unittest.TestCase):
@@ -224,6 +245,24 @@ def test_dict(self):
224245
}""")
225246
self.assertEqual(f({"data": {"value": 42}}), {"value": 42})
226247

248+
def test_time_limit(self):
249+
f = quickjs.Function(
250+
"f", """
251+
function f() {
252+
let arr = [];
253+
for (let i = 0; i < 100000; ++i) {
254+
arr.push(i);
255+
}
256+
return arr;
257+
}
258+
""")
259+
f()
260+
f.set_time_limit(0)
261+
with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"):
262+
f()
263+
f.set_time_limit(-1)
264+
f()
265+
227266

228267
class Strings(unittest.TestCase):
229268
def test_unicode(self):

0 commit comments

Comments
 (0)