Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,25 +399,25 @@ have for JavaScript. Wherever possible, we avoid interchanging raw pointers betw
and Python. Instead, we interchange integer IDs. The C++ side of PyMiniRacer can convert
integer IDs to raw pointers using a map, after validating that the IDs are still valid.

### ... except for `BinaryValueHandle` pointers
### ... except for `ValueHandle` pointers

We break the above rule for `BinaryValueHandle` pointers. PyMiniRacer uses
`BinaryValueHandle` to exchange most data between Python and C++. Python directly reads
the contents of `BinaryValueHandle` pointers, to read primitive values (e.g., booleans,
integers, and strings).
We break the above rule for `ValueHandle` pointers. PyMiniRacer uses `ValueHandle` to
exchange most data between Python and C++. Python directly reads the contents of
`ValueHandle` pointers, to read primitive values (e.g., booleans, integers, and
strings).

We do this for theoretical performance reasons which have not yet been validated. To be
consistent with the rest of PyMiniRacer's design, we _could_ create an API like:

1. C++ generates a numeric `value_id` and stores a BinaryValue in a
`std::unordered_map<uint64_t, std::shared_ptr<BinaryValue>>`.
1. C++ generates a numeric `value_id` and stores a Value in a
`std::unordered_map<uint64_t, std::shared_ptr<Value>>`.
1. C++ gives Python that `value_id` to Python.
1. To get any data Python has to call APIs like `mr_value_type(context_id, value_id)`,
`mr_value_as_bool(context_id, value_id)`,
`mr_value_as_string_len(context_id, value_id)`,
`mr_value_as_string(context_id, value_id, buf, buflen)`, ...
1. Eventually Python calls `mr_value_free(context_id, value_id)` which wipes out the map
entry, thus freeing the `BinaryValue`.
entry, thus freeing the `Value`.

_\*\*Note: We don't do this. The above is \_not_ how PyMiniRacer actually handles
values.\*\*\_
Expand All @@ -428,30 +428,30 @@ would be nice to switch to that model if it's sufficiently performant.

For now at least, we instead use raw pointers for this case.

We still don't fully trust Python with the lifecyce of `BinaryValueHandle` pointers;
when Python passes these pointers back to C++, we still check validity by looking up the
We still don't fully trust Python with the lifecyce of `ValueHandle` pointers; when
Python passes these pointers back to C++, we still check validity by looking up the
pointer as a key into a map (which then lets the C++ side of PyMiniRacer find the _rest_
of the `BinaryValue` object). The C++ `MiniRacer::BinaryValueFactory` can
authoritatively destruct any dangling `BinaryValue` objects when it exits.
of the `Value` object). The C++ `MiniRacer::ValueFactory` can authoritatively destruct
any dangling `Value` objects when it exits.

This last especially helps with an odd scenario introduced by Python `__del__`: the
order in which Python calls `__del__` on a collection of objects is neither guaranteed
nor very predictable. When a Python program drops references to a Python `MiniRacer`
object, it's common for Python to call `_Context.__del__` before it calls
`ValHandle.__del__`, thus destroying _the container for_ the value before it destroys
the value itself. The C++ side of PyMiniRacer can easily detect this scenario: First,
when destroying the `MiniRacer::Context`, it sees straggling `BinaryValue`s and destroys
them. Then, when Python asks C++ to destroy the straggling `BinaryValueHandle`s, the C++
when destroying the `MiniRacer::Context`, it sees straggling `Value`s and destroys them.
Then, when Python asks C++ to destroy the straggling `ValueHandle`s, the C++
`mr_free_value` API sees the `MiniRacer::Context` is already gone, and ignores the
redundant request.

The above scenario does imply a possibility for dangling pointer access: if Python calls
`_Context.__del__` then tries to read the memory addressed by the raw
`BinaryValueHandle` pointers, it will be committing a use-after-free error. We mitigate
this problem by hiding `BinaryValueHandle` within PyMiniRacer's Python code, and by
giving `ValHandle` (our Python wrapper of `BinaryValueHandle`) a reference to the
`_Context`, preventing the context from being finalized until the `ValHandle` is _also_
in Python's garbage list and on its way out.
`_Context.__del__` then tries to read the memory addressed by the raw `ValueHandle`
pointers, it will be committing a use-after-free error. We mitigate this problem by
hiding `ValueHandle` within PyMiniRacer's Python code, and by giving `ValHandle` (our
Python wrapper of `ValueHandle`) a reference to the `_Context`, preventing the context
from being finalized until the `ValHandle` is _also_ in Python's garbage list and on its
way out.

### Only touch (most of) the `v8::Isolate` from within the message loop

Expand Down Expand Up @@ -485,10 +485,10 @@ an easy API to submit tasks, whose callbacks accept as their first-and-only argu
saving a copy of the pointer and using it later would defeat the point; don't do that.)

One odd tidbit of PyMiniRacer is that _even object destruction_ has to use the above
pattern. For example, it is (probably) not safe to free a `v8::Persistent` without
holding the isolate lock, so when a non-message-loop thread needs to destroy a wrapped
V8 value, we enqueue a pretty trivial task for the message loop:
`isolate_manager->Run([persistent]() { delete persistent; })`.
pattern. For example, it is (probably) not safe to free a `v8::Global` without holding
the isolate lock, so when a non-message-loop thread needs to destroy a wrapped V8 value,
we enqueue a pretty trivial task for the message loop:
`isolate_manager->Run([global]() { delete global; })`.

See [here](https://groups.google.com/g/v8-users/c/glG3-3pufCo) for some discussion of
this design on the v8-users mailing list.
Expand Down
10 changes: 9 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# History

## 0.14.1 (2026-01-31)

- Fix memory leak when tracking JS objects in Python. We were never calling
v8::Persistent::Reset(). Now we use v8::Global which has no such requirement.
- In the C++ implementation, rename BinaryValue to just Value and optimize things a bit
by collapsing various data members into a Variant.
- Upgrade to V8 14.4 from 14.3.

## 0.14.0 (2026-01-03)

- Major revamp of Python-side async handling: `PyMiniRacer` now manages most
Expand Down Expand Up @@ -107,7 +115,7 @@
calling the function from Python, e.g., `mr.eval("myfunc")()`.

- Hardening (meaning "fixing potential but not-yet-seen bugs") related to freeing
`BinaryValue` instances (which convey data from C++ to Python).
`Value` instances (which convey data from C++ to Python).

- More hardening related to race conditions on teardown of the `MiniRacer` object in the
unlikely condition that `eval` operations are still executing on the C++ side, and
Expand Down
2 changes: 1 addition & 1 deletion builder/v8_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
LOGGER = getLogger(__name__)
LOGGER.setLevel(DEBUG)
ROOT_DIR = Path(__file__).absolute().parents[1]
V8_VERSION = "branch-heads/14.3"
V8_VERSION = "branch-heads/14.4"


@cache
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ requires = ["setuptools>=80.9"]

[project]
name = "mini-racer"
version = "0.14.0"
version = "0.14.1"
dynamic = ["readme"]
description = "Minimal, modern embedded V8 for Python."
license = "ISC"
Expand Down Expand Up @@ -66,7 +66,6 @@ dev = [
"packaging>=25.0",
"pytest>=9.0.2",
"ruff>=0.14.8",
"rust-just>=1.43.1",
"setuptools[core]>=80.9.0",
"types-setuptools>=80.9.0.20250822",
]
6 changes: 3 additions & 3 deletions src/py_mini_racer/_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,14 +259,14 @@ class ObjectFactoryImpl:
def value_handle_to_python( # noqa: C901, PLR0911, PLR0912
self, ctx: Context, val_handle: ValueHandle
) -> PythonJSConvertedTypes:
"""Convert a binary value handle from the C++ side into a Python object."""
"""Convert a value handle from the C++ side into a Python object."""

# A MiniRacer binary value handle is a pointer to a structure which, for some
# A MiniRacer value handle is a pointer to a structure which, for some
# simple types like ints, floats, and strings, is sufficient to describe the
# data, enabling us to convert the value immediately and free the handle.

# For more complex types, like Objects and Arrays, the handle is just an opaque
# pointer to a V8 object. In these cases, we retain the binary value handle,
# pointer to a V8 object. In these cases, we retain the value handle,
# wrapping it in a Python object. We can then use the handle in follow-on API
# calls to work with the underlying V8 object.

Expand Down
4 changes: 2 additions & 2 deletions src/v8_py_frontend/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ v8_shared_library("mini_racer") {
output_name = "mini_racer"
sources = [
"exports.cc",
"binary_value.h",
"binary_value.cc",
"value.h",
"value.cc",
"cancelable_task_runner.h",
"cancelable_task_runner.cc",
"code_evaluator.h",
Expand Down
171 changes: 0 additions & 171 deletions src/v8_py_frontend/binary_value.h

This file was deleted.

6 changes: 3 additions & 3 deletions src/v8_py_frontend/callback.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

#include <cstdint>
#include <functional>
#include "binary_value.h"
#include "value.h"

namespace MiniRacer {

using RawCallback = void (*)(uint64_t, BinaryValueHandle*);
using RawCallback = void (*)(uint64_t, ValueHandle*);

using CallbackFn = std::function<void(uint64_t, BinaryValue::Ptr)>;
using CallbackFn = std::function<void(uint64_t, Value::Ptr)>;

} // end namespace MiniRacer

Expand Down
Loading
Loading