Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 91745fa

Browse files
authored
Disallow multiple threads using the same Context, even at different times (PetterS#4)
* Disallow multiple threads using the same Context, even at different times. * Bump version.
1 parent d4f1fda commit 91745fa

File tree

4 files changed

+95
-10
lines changed

4 files changed

+95
-10
lines changed

‎module.c‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,20 @@ static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) {
211211
JSValue exception = JS_GetException(context);
212212
JSValue error_string = JS_ToString(context, exception);
213213
const char *cstring = JS_ToCString(context, error_string);
214-
if (strstr(cstring, "stack overflow") != NULL) {
215-
PyErr_Format(StackOverflow, "%s", cstring);
214+
if (cstring != NULL) {
215+
if (strstr(cstring, "stack overflow") != NULL) {
216+
PyErr_Format(StackOverflow, "%s", cstring);
217+
} else {
218+
PyErr_Format(JSException, "%s", cstring);
219+
}
220+
JS_FreeCString(context, cstring);
216221
} else {
217-
PyErr_Format(JSException, "%s", cstring);
222+
// This has been observed to happen when different threads have used the same QuickJS
223+
// runtime, but not at the same time.
224+
// Could potentially be another problem though, since JS_ToCString may return NULL.
225+
PyErr_Format(JSException,
226+
"(Failed obtaining QuickJS error string. Concurrency issue?)");
218227
}
219-
JS_FreeCString(context, cstring);
220228
JS_FreeValue(context, error_string);
221229
JS_FreeValue(context, exception);
222230
} else if (tag == JS_TAG_FLOAT64) {

‎quickjs/__init__.py‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import concurrent.futures
12
import json
23
import threading
4+
from typing import Tuple
35

46
import _quickjs
57

@@ -15,15 +17,33 @@ def test():
1517

1618

1719
class Function:
18-
def __init__(self, name: str, code: str) -> None:
19-
self._context = Context()
20-
self._context.eval(code)
21-
self._f = self._context.get(name)
20+
# There are unit tests demonstrating that we are crashing if different threads are accessing the
21+
# same runtime, even if it is not at the same time. So we run everything on the same thread in
22+
# order to prevent this.
23+
_threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
24+
25+
def __init__(self, name: str, code: str, *, own_executor=False) -> None:
26+
"""
27+
Arguments:
28+
name: The name of the function in the provided code that will be executed.
29+
code: The source code of the function and possibly helper functions, classes, global
30+
variables etc.
31+
own_executor: Create an executor specifically for this function. The default is False in
32+
order to save system resources if a large number of functions are created.
33+
"""
34+
if own_executor:
35+
self._threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
2236
self._lock = threading.Lock()
2337

38+
future = self._threadpool.submit(self._compile, name, code)
39+
concurrent.futures.wait([future])
40+
self._context, self._f = future.result()
41+
2442
def __call__(self, *args, run_gc=True):
2543
with self._lock:
26-
return self._call(*args, run_gc=run_gc)
44+
future = self._threadpool.submit(self._call, *args, run_gc=run_gc)
45+
concurrent.futures.wait([future])
46+
return future.result()
2747

2848
def set_memory_limit(self, limit):
2949
with self._lock:
@@ -49,6 +69,12 @@ def gc(self):
4969
with self._lock:
5070
self._context.gc()
5171

72+
def _compile(self, name: str, code: str) -> Tuple[Context, Object]:
73+
context = Context()
74+
context.eval(code)
75+
f = context.get(name)
76+
return context, f
77+
5278
def _call(self, *args, run_gc=True):
5379
def convert_arg(arg):
5480
if isinstance(arg, (type(None), str, bool, float, int)):

‎setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def get_c_sources(include_headers=False):
4848
author_email="petter.strandmark@gmail.com",
4949
name='quickjs',
5050
url='https://github.com/PetterS/quickjs',
51-
version='1.5.0',
51+
version='1.5.1',
5252
description='Wrapping the quickjs C library.',
5353
long_description=long_description,
5454
packages=["quickjs"],

‎test_quickjs.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import concurrent.futures
12
import json
23
import unittest
34

@@ -340,3 +341,53 @@ def test_unicode(self):
340341
for x in ["äpple", "≤≥", "☺"]:
341342
self.assertEqual(identity(x), x)
342343
self.assertEqual(context.eval('(function(){ return "' + x + '";})()'), x)
344+
345+
346+
class Threads(unittest.TestCase):
347+
def setUp(self):
348+
self.context = quickjs.Context()
349+
self.executor = concurrent.futures.ThreadPoolExecutor()
350+
351+
def tearDown(self):
352+
self.executor.shutdown()
353+
354+
def test_concurrent(self):
355+
"""Demonstrates that the execution will crash unless the function executes on the same
356+
thread every time.
357+
358+
If the executor in Function is not present, this test will fail.
359+
"""
360+
data = list(range(1000))
361+
jssum = quickjs.Function(
362+
"sum", """
363+
function sum(data) {
364+
return data.reduce((a, b) => a + b, 0)
365+
}
366+
""")
367+
368+
futures = [self.executor.submit(jssum, data) for _ in range(10)]
369+
expected = sum(data)
370+
for future in concurrent.futures.as_completed(futures):
371+
self.assertEqual(future.result(), expected)
372+
373+
def test_concurrent_own_executor(self):
374+
data = list(range(1000))
375+
jssum1 = quickjs.Function("sum",
376+
"""
377+
function sum(data) {
378+
return data.reduce((a, b) => a + b, 0)
379+
}
380+
""",
381+
own_executor=True)
382+
jssum2 = quickjs.Function("sum",
383+
"""
384+
function sum(data) {
385+
return data.reduce((a, b) => a + b, 0)
386+
}
387+
""",
388+
own_executor=True)
389+
390+
futures = [self.executor.submit(f, data) for _ in range(10) for f in (jssum1, jssum2)]
391+
expected = sum(data)
392+
for future in concurrent.futures.as_completed(futures):
393+
self.assertEqual(future.result(), expected)

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /