PetterS / quickjs

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

Reimplement functions using a custom class and an opaque pointer #96

Closed qwenger closed 2 years ago

qwenger commented 2 years ago

So that we get rid of the "arbitrary" number limitation of magics.

Python callables are stored in a doubly linked list on the runtime data and QuickJS functions reference the nodes via their opaque pointer. On JS' side, those functions use the same prototype as standard ones, so they should not be distinguishable.

Managing the nodes' memory is done in runtime_add_callable and js_python_function_finalizer, therefore the doubly linked list is needed to be able to delete a node while keeping the list consistent. Creating, accessing and deleting a function is O(1).

Example: memory management with dependency cycles

c = quickjs.Context()
inner = c.eval("(function() { return 42; })")
c.add_callable("f", inner)
del inner
del c

The callable (inner) is an ObjectData and therefore depends on the context, and the context stores a reference (in python_callables) to the callable. Therefore we have a dependency cycle. Once del c is ran, neither the context nor the callable are accessible anymore. So at the next Python GC run, they may get collected. The Python GC traverses the context through runtime_traverse, which marks all linked callables to be visited. The Python GC traverses the callable through object_traverse, which marks the context to be visited. That way, the dependency cycle is found. Clearing the cycle the involves calling runtime_clear, where the link to the callable will be broken. In turn the callable can be deallocated (object_dealloc), which breaks the link to the context that will be deallocated as well (runtime_dealloc). This deallocation involves freeing the QuickJS context and runtime, so that js functions are freed and js_python_function_finalizer is called, thereby freeing the node in python_callable (with a Py_XDECREF for the callable, because in this case it was already cleared in runtime_clear, so only the node's own memory remains to be cleared).

Something similar happens when one adds a callable that comes from Python, but references the context itself (such as c.add_callable("f", lambda: c.eval("1 + 1")). The only difference is that there the GC passes through the (builtin) lambda traversing function (instead of object_traverse), which will eventually mark the context to be visited as well.