PetterS / quickjs

Thin Python wrapper of https://bellard.org/quickjs/
https://github.com/bellard/QuickJS
MIT License
181 stars 19 forks source link

Strange behavior when Context used in Python object #7

Closed elgow closed 3 years ago

elgow commented 5 years ago

For some reason a quickjs Context held in an attribute of a Python class throws StackOverflow errors when its eval() method is called. Here's an ipython session demonstrating:

In [1]: import quickjs

In [2]: class QJS(object):
   ...:     def __init__(self):
   ...:         self.interp = quickjs.Context()
   ...:         self.interp.eval('var foo = "bar";')
   ...:

In [3]: qjs = QJS()

In [4]: c = quickjs.Context()

In [5]: c
Out[5]: <_quickjs.Context at 0x102be8630>

In [6]: qjs.interp
Out[6]: <_quickjs.Context at 0x102c09270>

In [7]: c.eval('2+2')
Out[7]: 4

In [8]: qjs.interp.eval('2+2')
StackOverflow                             Traceback (most recent call last)
<ipython-input-8-bc13b324733b> in <module>
----> 1 qjs.interp.eval('2+2')

StackOverflow: InternalError: stack overflow

In [9]: qjs.interp.eval('foo')
StackOverflow                             Traceback (most recent call last)
<ipython-input-9-663400a086bb> in <module>
----> 1 qjs.interp.eval('foo')

StackOverflow: InternalError: stack overflow

In [10]: qjs.interp.get('foo')
Out[10]: 'bar'

In [11]: c.eval('var foo = "bar"')

In [12]: c.get('foo')
Out[12]: 'bar'

In [13]: c.eval('foo')
Out[13]: 'bar'

As you can see in the session, calling get() on the instance context works properly to return the value of the variable foo but eval(), which should return the same value, fails. When the same is tried on a Context that is not in an object instance it works properly .

This is using Python 3.6.8 and quickjs 1.6.0.

PetterS commented 5 years ago

Thanks for the report. That is weird and indeed looks like a bug. Will look into it at some point.

PetterS commented 5 years ago

It has to do with how quickjs detects the current stack pointer in order to calculate the remaining stack. Somehow Python is in a very different place when evaluating an __init__ method.

If you write qjs.interp = quickjs.Context() it works.

PetterS commented 5 years ago

The Function class runs everything in its own threadpool to get around QuickJS being so thread-hostile. I am told (via the mailing list) that this hostility will be relaxed in the future.

So the work-around is to use quikcjs.Function or a similar method.

PetterS commented 5 years ago

I added a test case so we can track this.

elgow commented 5 years ago

Thank you both for creating the Python binding for quickjs and for your very quick action on this issue.

Based on your analysis I tried some further experiments and what I found fits your conclusion

In [19]: class QJS(object):
    ...:     def __init__(self, qjs=quickjs.Context()):
    ...:         self.interp = qjs
    ...:         self.interp.eval('var foo = "bar";')
    ...:     def mc(self):
    ...:         self.interp = quickjs.Context()
    ...:

In [20]: qjs = QJS()

In [21]: qjs.interp.eval('2+2')
---------------------------------------------------------------------------
StackOverflow                             Traceback (most recent call last)
<ipython-input-21-bc13b324733b> in <module>
----> 1 qjs.interp.eval('2+2')

StackOverflow: InternalError: stack overflow

In [22]: qjs = QJS(quickjs.Context())

In [23]: qjs.interp.eval('2+2')
Out[23]: 4

In [24]: qjs.mc()

In [25]: qjs.interp.eval('2+2')
---------------------------------------------------------------------------
StackOverflow                             Traceback (most recent call last)
<ipython-input-25-bc13b324733b> in <module>
----> 1 qjs.interp.eval('2+2')

StackOverflow: InternalError: stack overflow

So the gist of it seems to be that a quickjs.Context created when running in any method call on an object instance gets shortchanged on stack space while one created in the interpreter context is fine.

Also, creating the Context within a top-level function doesn't cause the problem:

In [35]: def mkq():
    ...:     qjs = QJS(quickjs.Context())
    ...:     print(f">   {qjs.interp.eval('2+2')}", flush=True)
    ...:     qjs = QJS()
    ...:     #print(f">>   {qjs.interp.eval('2+2')}", flush=True)
    ...:

In [36]: mkq()
>   4

So it does seem to only afflict methods of a class instance. In the above example if the 2nd print statement is uncommented then you get a StackOverflow

PetterS commented 5 years ago

It is a bit unorthodox, to be sure! Will update this issue if the situation changes.

elgow commented 5 years ago

Forgive me for being so vociferous in commenting, but I'm hoping it might help others who encounter this issue to understand and work around it.

I have to revise the last bit of the previous comment. Executing the eval() within the same context in which the quickjs.Context was created works, but any use outside that context fails. So the final test works because the qjs.interp.eval() call inside of the mkq() function is still in the context where quickjs.Context() was called.

As long as you can set up your quickjs.Context, execute your Javascript, and get your result within a single function call or interpreter context then you're OK. This covers using quickjs.py to execute Javascript code for a single HTTP request handler, which is sufficient for my immediate purpose.

Thanks again for bringing quickjs to the Python world.