From bd9325b03da6418d784f2d3bd777c3365401e800 Mon Sep 17 00:00:00 2001 From: Ben Creech Date: Tue, 9 Apr 2024 17:10:14 -0400 Subject: [PATCH] Add PyMiniRacer v0.11.1 post --- content/post/mini-racer-v0.11.1.md | 689 +++++++++++++++++++++++++++++ content/post/mini-racer.md | 2 +- static/img/waldos.jpg | Bin 0 -> 31248 bytes 3 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 content/post/mini-racer-v0.11.1.md create mode 100644 static/img/waldos.jpg diff --git a/content/post/mini-racer-v0.11.1.md b/content/post/mini-racer-v0.11.1.md new file mode 100644 index 0000000..14556be --- /dev/null +++ b/content/post/mini-racer-v0.11.1.md @@ -0,0 +1,689 @@ +--- +title: "PyMiniRacer v0.11.1" +date: "2024-04-08" +lead: "Poke objects! Call functions! Await promises!" +disable_comments: false # Optional, disable Disqus comments if true +authorbox: true # Optional, enable authorbox for specific post +toc: true # Optional, enable Table of Contents for specific post +mathjax: true # Optional, enable MathJax for specific post +categories: + - "programming" +tags: + - "javascript" + - "typescript" + - "python" +--- + +In [this last blog post](https://bpcreech.com/post/mini-racer/), I discussed my +revival of [PyMiniRacer](https://github.com/bpcreech/PyMiniRacer), a neat +project created by [Sqreen](https://github.com/sqreen) to embed the V8 +JavaScript into Python. As of that post (`v0.8.0`), I hadn't touched the C++ +code yet. Here we talk about some extensions to PyMiniRacer, rolling up the +changes up to `v0.11.1`: JS `Object` and `Array` manipulation, directly calling +JS functions from Python, `async` support, and a discussion of the C++ changes +needed to make all that work. + + + +```python +from py_mini_racer import MiniRacer + +mr = MiniRacer() + +# Direct object and array access! +>>> obj = mr.eval('let obj = {"foo": "bar"}; obj') +>>> obj["foo"] +'bar' +>>> obj["baz"] = mr.eval('[]') +>>> obj["baz"].append(42) +>>> mr.eval('JSON.stringify(obj)') +'{"foo":"bar","baz":[42]}' + +# Call JS functions directly! +>>> func = mr.eval('(a) => a*7') +>>> func(6) +42 + +# Promise await support, so you can wait in two languages at once! +>>> async def will_you_wait_just_one_second_please(): +... promise = mr.eval('new Promise((res, rej) => setTimeout(res, 1000))') +... await promise +... +>>> import asyncio +>>> asyncio.run(will_you_wait_just_one_second_please()) # does exactly as requested! +>>> +``` + +_Other new features can be found +[on the relnotes page](https://bpcreech.com/PyMiniRacer/history/), where v0.8.0 +was the subject of the [last blog post](https://bpcreech.com/post/mini-racer/)._ + +## New feature rundown + +First, I'll discuss new features for PyMiniRacer. These require an incremental +overhaul of the C++ side of PyMiniRacer, which is discussed below. + +### Manipulating objects and arrays + +As of `v0.8.0` and earlier, PyMiniRacer could create and manipulate objects and +arrays only at a distance: you could create them in the JS context via +`MiniRacer.eval` statements, and poke at them via _more_ `MiniRacer.eval` +statements to either pull out individual values or `JSON.stringify` them in +bulk. `MiniRacer.eval` could convert primitives like numbers and strings +directly to Python objects, but to get a member of an object, you had to run an +evaluation of some JS code which would extract that member, like +`mr.eval(my_obj["my_property"])` instead of simply writing +`my_obj["my_property"]` in Python. + +It feels like programming with +[waldos](https://en.wikipedia.org/wiki/Remote_manipulator): + +
+ +![A "waldo" or "remote manipulator"](/img/waldos.jpg)

+ +_Working with Objects and Arrays in PyMiniRacer v0.8.0. +[source](https://en.wikipedia.org/wiki/Remote_manipulator)_ + +
+ +Well, now you can directly mess with objects and arrays! I added a +[`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping) +(`dict`-like) interface for all derivatives of JS Objects, and a +[`MutableSequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableSequence) +(`list`-like) interface for JS Arrays. You can now use Pythonic idioms to read +and write `Object` properties and `Array` elements in Python, including +recursively (i.e., you can read `Object`s embedded in other `Object`s, and embed +your own). + +This required tracking v8 object handles within Python (instead of reading and +disposing them at the end of every `MiniRacer.eval` call), which in turn +required revamping the C++ memory management model. More on that +[below](#relieving-python-of-the-duty-of-managing-c-memory). + +### Direct function calls + +As of `v0.8.0` and earlier, PyMiniRacer couldn't directly call a function. You +could only evaluate some JS code which would call your function. Passing in +function parameters was likewise awkward; you had to serialize them into JS +code. So instead of doing `foo(my_str, my_int)` in Python, you had to do +something like `mr.eval(f'foo("{my_str}", {my_int})')`. The `MiniRacer.call` +method helped with this by JSON-serializing your data for you, but the wrapper +it puts around your code isn't always quite right (as reported on the original +[sqreen/PyMiniRacer](https://github.com/sqreen/PyMiniRacer) GitHub issue +tracker). + +Well, now you can retrieve a function from JS, and then... just call it: + +```python +>>> reverseString = mr.eval(""" +function reverseString(str) { + return str.split("").reverse().join(""); +} +reverseString // return the function +""") +>>> reverseString + +>>> reverseString("reviled diaper") +'repaid deliver' +``` + +You can also specify `this` as a keyword argument, because JavaScript: + +```python +>>> get_whatever = mr.eval(""" +function get_whatever(str) { + return this.whatever; +} +get_whatever // return the function +""") +>>> obj = mr.eval("let obj = {whatever: 42}; obj") +>>> get_whatever(this=obj) +42 +``` + +As with direct `Object` and `Array` manipulation, aside from a straightforward +exposure of C++ APIs to Python, to make this work we have to revamp the C++ +object lifecyle model; more +[below](#relieving-python-of-the-duty-of-managing-c-memory). + +### Async and timeouts + +It seemed like a big gap that PyMiniRacer, as of `v0.8.0` and earlier, could +create JS Promises, but couldn't do anything to asynchronously await them. +[I wasn't the only one with this feeling](https://github.com/bpcreech/PyMiniRacer/issues/7). +One of the PyMiniRacer tests did work with promises, but only by +[polling for completion](https://github.com/bpcreech/PyMiniRacer/blob/5161d99a39076c36209519c9601d32546c8d276b/tests/test_eval.py#L215). + +Both Python and JavaScript have a concept of `async` code. Can we hook them up? +Yes! + +Now you can create a Promise on the JS side and await it either using `asyncio` +or using a blocking `.get` call: + +```python +>>> promise = mr.eval('new Promise((res, rej) => setTimeout(() => res(42), 1000))') +>>> await promise # only works within Python "async def" functions +42 +>>> promise.get() # works outside of python "async def" functions and not recommended +42 # within async functions (because it would block up the asyncio loop) +``` + +To make this demo work, I actually also had to write a `setTimeout` function, +which is funny because it's so ubiquitous you might forget that it's not part of +the ECMA standard, and thus not part of V8's standard library. (`setTimeout` is +[a _web_ standard](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout), +and also +[exists in NodeJS](https://nodejs.org/api/timers.html#settimeoutcallback-delay-args). +E.g., in browser scripts, `setTimeout` lives on the `window` object, but +PyMiniRacer has no `window` object.) Turns out we can write a pure-JS +`setTimeout` using only the ECMA standard libraries using +[a somewhat hacky wrapper](https://github.com/bpcreech/PyMiniRacer/blob/560d5ac7de6b0b92a12d7a4a8062b9392a28a1b4/src/py_mini_racer/py_mini_racer.py#L684) +of +[`Atomics.waitAsync`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync). +I stole this insight from the PyMiniRacer unit tests and spun it into a +glorified `settTimeout` / `clearTimeout` wrapper in what felt like one of those +silly improbable interview questions ("Please build thing A using only +half-broken tools B and C!"). + +Moreover, this required making PyMiniRacer better at running code indefinitely +so it would actually process the async work reliably—more on that below. + +## Changes to the PyMiniRacer C++ backend + +So, the above features involve overhauling PyMiniRacer. Let's talk about how I +did that! + +### `clang-tidy` + +First, before getting into C++ changes, I wanted to inherit the best of +automated wisdom about how to write C++. I personally have been writing C++ for +over two decades, but having taken a break from it for 5 years, I missed out on +the fun of both C++17 and C++20! + +So I added +[a `clang-tidy` pre-commit](https://github.com/bpcreech/PyMiniRacer/blob/560d5ac7de6b0b92a12d7a4a8062b9392a28a1b4/.pre-commit-config.yaml#L36). +As well as a `clang-format` pre-commit, because duh. Some observations: + +1. `clang-tidy` has taught me lots of things I didn't know, e.g., when to + `std::move` stuff and when to rely on + [guaranteed copy elision](https://en.cppreference.com/w/cpp/language/copy_elision), + and how to annoy others by using + [trailing return types](https://en.wikipedia.org/wiki/Trailing_return_type) + _everywhere, absolutely everywhere_. +2. It continually catches my mistakes, like extra copies, extra lambda + parameters, and whatnot! +3. There is unfortunately no good set of recommended `clang-tidy` checks + everyone should enable (as compared to ESLint for JavaScript, Ruff for + Python, etc, which work pretty well out of the box). Some bundled checks are + broadly useful for most everyone, and some are are clearly only intended for + a limited audience. E.g., the llvm checks are doomed to fail if you're not + _writing llvm itself_. The `altera` checks are intended for people writing + code for FGPAs. The `fuschia` checks have some very unusual opinions that I'm + sure make sense for the Fuschia project but I cannot imagine there is + consenus that, e.g., defaults in function parameters are bad. So everyone + using `clang-tidy` has to figure out, by trial and error, which checks have + weird non-applicable opinions and thus have to be disabled. +4. The memory management checks seem unhelpful in that I, like most C++ + programmers, use smart pointers _everywhere_, so when the checks fail it's + just noise 100% of the time so far. It seems like these complicated + memory-tracking checks could almost be simplified into "uh you used new or + delete without a shared pointer", and then would only _usefully_ trigger for + novice C++ programmers. +5. `clang-tidy` is slow; something like 100 times slower than `clang` itself. It + takes about 20 minutes on my old laptop to run over PyMiniRacer, which is + only 29 small files. + +Anyway, `clang-tidy` is super useful; would recommend! + +### Inverting PyMiniRacer's threading model + +So, to start off, for `async` code to work correctly in PyMiniRacer (and also, +to run code off the Python thread, thus enabling `KeyboardInterrupt` of +PyMiniRacer JS code), we need V8 to execute code _continually_, e.g., to process +delayed callbacks from `setTimeout`. In other words, if want to be able to use +`setTimeout` to schedule work for `N` seconds from now, and have it actually, +you know, _run_ that delayed work, we need to convince V8 to actually run, +continually, until explicitly shut down. + +However, PyMiniRacer was set up like _both_ of V8's +[extensive list of two samples](https://v8.github.io/api/head/examples.html). It +ran a thing once, and pumped the V8 message loop a bit, and quit, never to call +V8 again (until the next user input). This seems odd: how do you know there is +no delayed work? I guess you just assume there's no delayed work. But at the +same time, programs like Chrome, which +[a few people use](https://backlinko.com/chrome-users), and NodeJS, +[likewise](https://radixweb.com/blog/nodejs-usage-statistics), obviously use V8 +in continually-operating form. How do we do it? + +A couple facts make "how do we do it" tricky to answer: + +1. The `v8::Isolate`, the environment in which all your JS code runs, is not + inherently thread-safe. You need to grab a `v8::Locker` first to use it + safely. +1. `v8::platform::PumpMessageLoop`, the thing that powers all work beyond an + initial code evaluation in V8, needs the `v8::Isolate` lock. However, it does + not actually _ask for_ the lock. It does not release the lock either, + apparently. And yet we need it to run, continually and without returning + control to us. We have to use its wait-for-work mode, (unless we want to + [use a lot of electricity](https://en.wikipedia.org/wiki/Busy_waiting)), + which means the message pump is doomed to sit around a lot, doing nothing but + hogging the `v8::Isolate` lock. + +So you need to get the lock to use the `Isolate`, but you also need to spend a +lot of time calling this thing (`PumpMessageLoop`) hogs that lock. How do you +reconcile these? + +My solution was inspired by [the `d8` tool](https://v8.dev/docs/d8), which ships +with V8: all code which interacts with the `Isolate` is +["posted" as a "task"](https://github.com/v8/v8/blob/2ce051bb21edff5e66c4e87180c9d90b18fdf526/include/v8-platform.h#L82) +on the `Isolate`'s `TaskRunner`. Then it will run _under_ the `PumpMessageLoop` +call, where _it already has that `Isolate` lock which `PumpMessageLoop` has been +hogging_. Nobody needs to grab the `Isolate` lock, because _they already have +it_, having been sequenced into the `PumpMessageLoop` thread as tasks. + +This seems to work, but involved reorganizing all of PyMiniRacer, inverting +control such that a thread running `PumpMessageLoop` is the center of the +universe, and everything else just asks it to do work. Even things like "hey I'd +like to delete this object handle" need to be put onto the `v8::Isolate`'s task +queue. + +The resulting setup looks roughly like this: + +```goat ++--------------------------------------------------------------------+ +| MiniRacer::IsolateManager | +| | +| .--------------------------------------------. | +| | MiniRacer::IsolateMessagePump thread | | +| | | | +| | +-------------+ | | +| | 1. creates, ----->| | | | +| | | v8::Isolate | | | +| | 2. exposes, | | | | +| v8::Isolate* <-------------------------+ | | | +| ^ | +--+----------+ | | +| | 6. enqueues | | ^ | | +| | | 3. ... then runs: `-' PumpMessages | | +| | | (looping until shutdown) | | +| | '---------------------------------------------' | +| | | ++-----|--------------------------------------------------------------+ + | 5. MiniRacer::IsolateManager + | ::Run(task) + | ++--------------------------+ +-----------------------+ +| | | | +| MiniRacer::CodeEvaluator +----------->| MiniRacer::AdHocTask | +| (etc) | 4. creates | | ++--------------------------+ +-----------------------+ +``` + +Reading that diagram in order: + +1. The `MiniRacer::IsolateMessagePump` runs a thread which creates a + `v8::Isolate`, +2. ... exposes it to the `MiniRacer::IsolateManager`, +3. ... and loops on `v8::platform::PumpMessageLoop` until shutdown. +4. Then, any code which wants to _use_ the `Isolate`, such as + `MiniRacer::CodeEvaluator` (the class which implements the `MiniRacer.eval` + function to run arbitrary JS code) can package up tasks into + `MiniRacer::AdHocTask` and +5. ... throw them onto the `Isolate` work queue to actually run, on the + message-pumping thread. + +#### `std::shared_ptr` all the things! + +Our control inversion to an event-driven design (explained above) is complicated +to pull off safely in C++, where object lifecycle is up to the developer. Since +everything is event-driven, we have to be very careful to control the lifecycle +of every object, ensuring objects outlive all the tasks which reference them. +After trying to explicitly control everything, I gave up and +[settled on](https://github.com/bpcreech/PyMiniRacer/pull/38) basically using +`std::shared_ptr` to manage lifecycle for just about everything (regressing to +"C++ developer phase 1" as described +[here](https://www.reddit.com/r/cpp_questions/comments/17tl7oh/best_practice_smart_pointer_use/)). +If a task has a `std::shared_ptr` pointing to all the objects it needs to run, +the objects are guaranteed to still be around when the task runs. This in turn +involves some refactoring of classes, to ensure there are no references cycles +when we implement this strategy. Reference cycles and `std::shared_ptr` do not +get along. + +#### Threading, in summary + +The above story seems like it should be common to most use of V8, yet all seems +underdocumented in V8. The solution I landed on involved some educated guesses +about thread safety of V8 components (like, can you safely add a task to the +`Isolate`'s foreground task runner without the `Isolate` lock? The +implementation seems to say so, but the docs... don't exist! Does +`v8::platform::PumpMessageLoop` need the lock? It seems to crash when I don't +have it; core files are a form of library documentation I guess, but maybe I was +simply holding it wrong when it crashed?) I have +[put a question to the v8-users group](https://groups.google.com/g/v8-users/c/glG3-3pufCo/m/-rSSMTLEAAAJ) +to see if I can get any confirmation of my assumptions here. + +### Relieving Python of the duty of managing C++ memory + +There are 5 places where Python and C++ need to talk about objects in memory: + +1. The overall MiniRacer context object which creates and owns everything + (including the `v8::Isolate` discussed above). + +2. JS values created by MiniRacer and passed back to Python (and, when we start + doing mutable objects or function calls, _JS values created in Python and + passed into JS_!). + +3. Callback function addresses from C++ to Python. + +4. Callback context to go along with the above, so that when Python receives a + callback it understands what the callback is about. (E.g., we typically use a + Python-side `Future` as the callback context here; the callback sends both + _data_ and _context_. The callback function just has to take the _data_, + i.e., a result value, and stuff it into a `Future`, which it conveniently can + find given the callback's _context_.) + +5. Task handles for long-running `eval` tasks, so we can cancel them. + +While [Python's garbage collector](https://docs.python.org/3/library/gc.html) +tracks references and automatically deletes unreachable _Python_ objects, if +you're extending Python with C (or C++) code, you have to explicitly delete the +C/C++ objects. There are kinda two approaches for systematically ensuring +deletion happens: + +1. **`with` statements:** + + You can treat C++ objects as external resources and use + [`with statements and context managers](https://peps.python.org/pep-0343/) to + explicitly manage object lifecycle, in user code. There is + [an absolutist view](https://stackoverflow.com/questions/1481488/what-is-the-del-method-and-how-do-i-call-it#answer-1481512) + (previously held by me after being burned by finalizers before; see below) + that this is the only way proper way to manage external resources (_but are + in-process C++ objects really "external resources"?_). Code using context + managers to explicitly deallocate all allocated objects would look like, in + absolutist form: + + ```python + with MiniRacer() as mr: + with mr.eval("[]") as array: + array.append(42) + with mr.eval('JSON.stringify') as stringify_function: + with stringify_function(array) as stringified: + print(stringified) # prints '[42]' + # at this point, the value behind "stringified" is freed by calling a C API. + # at this point, the value behind "stringify_function" is freed by calling a C API. + # at this point, the value behind "array" is freed by calling a C API. + # at this point, the MiniRacer context is freed by calling a C API. + ``` + + _(Astute Pythonistas may note that two of those `with` statements could be + collapsed into one to make it "simpler". Yay.)_ + + This is nicely explicit, but extremely annoying to use. (I actually + implemented this, but undid it when I started updating the PyMiniRacer + `README` and saw how annoying it is.) + +2. **Finalizers, aka + [the `__del__` method](https://docs.python.org/3/reference/datamodel.html#object.__del__):** + + You can create a Python-native wrapper class whose `__del__` method frees the + underlying C++ resource. This looks like, e.g.: + + ```python + class _Context: + def __init__(self, dll): + self.dll = dll # ("dll" here comes from the ctypes API) + self._c_object = self.dll.mr_init_context() + + def __del__(self): + self.dll.mr_free_context(self._c_object) + + ... + ``` + + Then user code doesn't have to remember to free things at all! _What could go + possibly wrong?_ + + ```goat + +--------------+ +--------------+ + | +--->| | + | A (Python) | | B (Python) | + | |<---+ | + +--------------+ +--------------+ + ``` + +#### The trouble with finalizers + +_What could possibly go wrong_ is that finalizers are called somewhat lazily by +Python, and in kind of unpredictable order. This problem is shared by Java, +[C#](https://stackoverflow.com/questions/30368849/destructor-execution-order) +and I assume every other garbage-collected language which supports finalizers: +if you have two objects `A` and `B` which refer to each other (aka a reference +cycle), but which are obviously otherwise unreachable, obviously you should +garbage-collect them. + +But if _both_ `A` and `B` have finalizers, which do you call first? If you call +`A`'s finalizer, great, but later `B`'s finalizer might try and reach out to +`A`, _which has already been finalized!_ Likewise, if you finalize `B` first, +then `A`'s finalizer might try and reach out to `B` which is already gone. You +can't win! So if you're the Python garbage collector, you just call the +finalizers for both objects, in _whatever_ order you like, and you declare it to +be programmer error if these objects refer to each other in their finalizers, +and you generally _declare that finalization order doesn't matter_. + +```goat + +--------------+ +--------------+ + | +--->| | + | A | | B | + | |<---+ | + +-------+------+ +-------+------+ +Python space | __del__ | __del__ +~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~ +C++ space | | + v v + +--------------+ +--------------+ + | | | | + | C +--->| D | + | | | | + +--------------+ +--------------+ +``` + +Unfortunately, _order sometimes matters_. Let's say those objects `A` and `B` +are each managing C++ objects, `C` and `D`, respectively, as depicted above. +Obviously, in the above picture, we should tear down `C` before `D` so we don't +leave a dangling reference from `C` to `D`. The best teardown order here is: +`A`, `C`, `B`, _then_ `D`. But _Python doesn't know that_! It has no idea about +the link between C++ objects `C` and `D`. It is likely to call `B`'s finalizer +first, tearing down `D` before `C`, thus leaving a dangling reference on the C++ +side. + +```goat + +------------------------------+ +--------------------------+ + | +--->| | + | _ValueHandle | | _Context | + | |<---+ | + +----------+-------------------+ +---+----------------------+ + | | +Python space | __del__ | __del__ +~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~ +C++ space | (calls mr_value_free | (calls mr_context_free + | with a pointer) | with a pointer) + v v + +------------------------------+ +--------------------------+ + | | | | + | MiniRacer::BinaryValue | | MiniRacer::Context | + | | | | + +----------------+-------------+ +------------+-------------+ + | (points into) | (owns) + | v + | +--------------------------+ + | | | + +----------------->| v8::Isolate | + | | + +--------------------------+ +``` + +This happens in practice in MiniRacer: a Python `_Context` wraps a +`MiniRacer::Context`, and a Python `_ValueHandle` wraps a +`MiniRacer::BinaryValue`. But within C++ land, that `MiniRacer::Context` is what +created those `MiniRacer::BinaryValue` objects, and they each know have pointers +into the `v8::Isolate` object which the `Context` owns. If you free the +`MiniRacer::Context` before you free the _values pointing into its +`v8::Isolate`_, things get very crashy, fast. We _want_ Python to free all the +`_ValueHandle` objects _before_ the `_Context`, but there's no way to tell it +that, and worse, the garbage collection ordering is nondeterministic, so it will +get it wrong, and crash, _randomly_. + +The same situation arises for task handles (`MiniRacer::CancelableTaskHandle` in +C++), and we have other more mundane risky use of pointers and Python-owned +memory with callback function pointers and callback context data. + +When I started work on it, MiniRacer tracked raw C++ pointers for the MiniRacer +context _only_, using `__del__` to clean it up. It only used values ephemerally, +converting and cleaning them up immediately (except for `memoryview` objects +into +[JS `ArrayBuffers`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) +but that was a rare case). If you're only using finalizers with one type of +object, and instances of that object don't interact, the reference cycle problem +explained above doesn't exist. + +This worked fine until... I introduced more object types (async tasks, callback +functions, callback contexts, and persistent values to track `Object`s and +`Array`s). More object types means more opportunities for Python to call +finalizers _in the wrong order_. + +#### Making finalizers safe + +So the principle I derive from the above: _**we must refactor things so that +finalizer order doesn't matter**_. But how do we do it? And otherwise make our +Python/C++ boundary safe from memory management bugs? + +I developed the following strategy: + +1. Avoid passing pointers at all. Instead, pass integer identifiers + (specifically `uint64_t`) between C++ and Python. The identifiers are all + created and registered in maps on the C++ side. The map lets the C++ side + safely validate, _then_ convert from the identifier to actual object + pointers. + + - Two exceptions: + + 1. For JS values, for performance reasons, we pass a special + `BinaryValueHandle` which doubles as an identifier _and_ an address + which can peek into data for primitive types. (In that case, the map key + is the `BinaryValueHandle` pointer instead of an ID. The mapped value is + the full `BinaryValue` object pointer, which is _not_ directly + accessible to Python.) Note that the C++ side still validates any + `BinaryValueHandle` values passed in from Python. + + 2. For callback addresses from C++ to Python, we _have_ to use a pointer. + Which then means we are at risk of + [bugs](https://docs.python.org/3/library/ctypes.html#callback-functions) + wherein PyMiniRacer accidentally disposes the callback before the C++ + side is totally done with it. C'est la vie.) + +2. Thus the C++ side can _check for validity of any references it receives from + Python_, eliminating any bugs related to use-after-free or Python freeing + things in the wrong order (i.e., freeing a whole `v8::Isolate` and only + _then_ freeing a JS value which lived in that isolate). + +3. ... And the C++ side can also avoid _memory leaks_, in that when an "owning + container" (like the context object) is torn down, the container (on the C++ + side) has a holistic view of _all_ objects created within that container, and + can free any stragglers itself, on the spot. + +In this way, we _still use_ `__del__` on the Python side, but _only_ as an +opportunistic memory-usage-reducer. The C++ side is actually tracking all memory +usage deterministically and doesn't rely on Python to get it right—especially +not the order of operations when freeing things. + +As a cute bonus trick, since we're tracking all living objects on the C++ side, +can expose methods which count them, +[for use in our unit tests](https://github.com/bpcreech/PyMiniRacer/blob/560d5ac7de6b0b92a12d7a4a8062b9392a28a1b4/tests/conftest.py#L7), +to ensure we don't have any designed memory leaks! (I +[caught](https://github.com/bpcreech/PyMiniRacer/blob/main/HISTORY.md#0111-2024-04-08) +one pre-existing leak this way!) + +The resulting setup, just looking at Contexts and JS Values, sort of looks like +the following diagram (and similar goes for async task handles): + +```goat + +---------------------------------+ +------------------------------+ + | +--->| | + | _ValueHandle | | _Context | + | |<---+ | + +------------------------------+--+ +---+--------------------------+ + | | +Python space __del__ | | __del__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +C++ space (calls mr_value_free | | (calls mr_context_free with + with a context ID + | | a uint64 id) + value handle pointer) | | + | v + | +-----------------------------+ + +------>| | + | MiniRacer::ContextFactory | + | (validates context IDs...) | + +-----------+-----------------+ + | (...and passes valid + v calls to...) + +------------------------------+ + | | + +---------------------------| MiniRacer::Context | + | (passes individual value | | + | deletions to, and/or +----------+-------------------+ + | deletes in bulk upon | + v context teardown) | + +---------------------------------+ | + | | | + | MiniRacer::BinaryValueFactory | | + | (validates handle ptrs) | | + +----+----------------------------+ | + | delete (if handle is valid, or upon | + v factory teardown if never deleted) | + +---------------------------------+ | + | | | + | MiniRacer::BinaryValue | | + | | | + +----------------+----------------+ |(owns) + | (points into) v + | +--------------------------+ + | | | + +----------------->| v8::Isolate | + | | + +--------------------------+ +``` + +With this new setup, if Python finalizes ("calls `__del__` on") the `_Context` +_before_ a `_ValueHandle` that belonged to that context, aka "the wrong order", +what happens now is: + +1. `_Context.__del__` calls `MiniRacer::ContextFactory` to destroy its + `MiniRacer::Context`. `MiniRacer::Context` destroys the + `MiniRacer::BinaryValueFactory`, which in turn notices some C++ + `MiniRacer::BinaryValue` objects were left alive (before this change, the + `BinaryValueFactory` wasn't tracking them and thus didn't even know they + still existed). `BinaryValueFactory` goes ahead and tears all those leftover + `BinaryValue` objects down. This avoids a memory leak, and avoids dangling + pointers on the C++ side. + +2. When Python later calls `_ValueHandle.__del__`, it passes a context ID to + `MiniRacer::ContextFactory`. This context ID is no longer valid because the + context was torn down already. Thus, this call can be safely ignored. + +I think the design strategy is generalizable to most Python/C++ integrations, +and potentially likewise Java/C++ and C#/C++ integrations. (_TL;DR: use integer +identifiers instead of passing pointers, track object lifecycle in maps on the +C++ side, validate all incoming identifiers from Python, and refactor so that out-of-order finalization doesn't hurt +anything_). I wonder if it's written down anywhere. + +## Contribute! + +That wraps up this overly detailed changelog for now! + +If you're reading this and want to contribute, go for it! See +[the contribution guide](https://bpcreech.com/PyMiniRacer/contributing/). + +One thing I wish PyMiniRacer really had, and don't have time to build right now, +is extension via user-supplied Python callback functions. A more detailed +descrition and implementation ideas can be found +[here](https://github.com/bpcreech/PyMiniRacer/issues/39). diff --git a/content/post/mini-racer.md b/content/post/mini-racer.md index 57c9f4f..a4cfd0b 100644 --- a/content/post/mini-racer.md +++ b/content/post/mini-racer.md @@ -35,7 +35,7 @@ from py_mini_racer import MiniRacer mr = MiniRacer() # Let's run some JavaScript from Python! ->>> mr.run("Math.pow(7, 3);") +>>> mr.eval("Math.pow(7, 3);") 343 # Updated, for the first time since 2021! diff --git a/static/img/waldos.jpg b/static/img/waldos.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1cc8282f29cae098150c211daf080fda5fc7298b GIT binary patch literal 31248 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

11H!=aIPAL8_DsK8tLKCboIvoGh(QGt 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 literal 0 HcmV?d00001