PyMiniRacer

repository·main·Indexed 42 days ago

https://github.com/bpcreech/pyminiracer

A minimal, modern embedded V8 engine for Python that allows developers to execute JavaScript code, run WebAssembly, and use ECMAScript features. It supports asynchronous execution with Promises, Python callbacks via wrap_py_function, and resource constraints like timeout_sec to prevent runaway execution. Compatible with Python 3.10-3.14 across macOS, Windows, and various Linux distributions.

Tokens
8.3K
Snippets
29
Records
43
Agent score
51%

What's inside PyMiniRacer

  1. Safety of C++ object tracking via Integer IDs

    main

    To prevent use-after-free errors and manage memory safely when interacting with JavaScript, PyMiniRacer avoids passing raw C++ pointers to the JavaScript environment. Instead, it uses Integer IDs.

    The Workflow:

    1. The C++ side allocates an object and stores it in a map (e.g., std::unordered_map<uint64_t, std::shared_ptr<T>>).
    2. C++ provides only the numeric uint64_t ID to JavaScript.
    3. When JavaScript calls back into C++, the ID is used to look up the pointer in the map.
    4. The C++ side validates that the ID is still present in the map before using the pointer.

    This allows the C++ side to be the authoritative owner of object lifecycles. If an object is deleted, the ID becomes invalid, and subsequent attempts by JavaScript to use that ID can be safely ignored or handled as errors.

  2. Security considerations for running untrusted JavaScript

    main

    PyMiniRacer is designed to run untrusted JavaScript by leveraging V8's sandboxing properties. To maintain isolation, users should follow these patterns:

    1. Use separate instances: Create multiple MiniRacer instances. Each instance has its own V8 isolate, which prevents different blobs of untrusted code from accessing each other's state.
    2. Apply constraints: Use optional timeouts and memory constraints on the code being executed to prevent resource exhaustion.
    3. Evaluate environment needs: If you are running highly adversarial code in a high-security environment, consider using a purpose-built isolation environment like containers on gVisor instead of relying solely on PyMiniRacer.

    Warning on Callbacks: Using MiniRacer.wrap_py_function to expose Python functions to JavaScript creates an extension framework that breaches the V8 isolation boundary. Only use this if the JavaScript code is trusted or if the exposed Python function is strictly validated and safe for untrusted callers.

  3. How PyMiniRacer handles thread safety and the V8 Isolate

    main

    Most V8 methods and objects are not thread-safe and require holding a v8::Locker lock. To avoid complex locking issues between Python threads and V8's internal background work, PyMiniRacer uses a message loop pattern.

    Instead of accessing the v8::Isolate directly from Python threads, all operations that touch the isolate (including running JS code, manipulating objects, or deleting V8 objects) are submitted as tasks to the isolate's own message loop.

    Key components:

    • MiniRacer::IsolateManager: Provides an API to submit tasks to the message loop.
    • Task Execution: Tasks are executed within the message loop, which is the only part of the system that holds the v8::Locker lock. This ensures that only one thread (the message loop thread) ever interacts with the v8::Isolate directly.
  4. Map JavaScript Objects and Arrays to Python

    main

    JavaScript Objects are modeled as MutableMapping (dictionaries) and JavaScript Arrays are modeled as MutableSequence (lists).

    # JavaScript Object to Python Dict
    obj = ctx.eval("var obj = {'foo': 'bar'}; obj")
    print(obj["foo"]) # 'bar'
    
    # JavaScript Array to Python List
    arr = ctx.eval("var arr = ['a', 'b']; arr")
    print(arr[1]) # 'b'
  5. Use asynchronous execution with Promises

    main

    MiniRacer supports JavaScript Promise instances. In synchronous contexts, you can use promise.get() to block until the promise resolves. In asynchronous (asyncio) contexts, you must await the promise directly.

    # Synchronous blocking
    promise = ctx.eval("new Promise((res) => setTimeout(() => res(42), 1000))")
    print(promise.get()) # 42
    
    # Asynchronous (inside an async function)
    # await promise
  6. Manage JavaScript resource constraints

    main

    To prevent buggy or adversarial JavaScript from consuming excessive system resources, the PyMiniRacer Python API provides mechanisms to enforce limits. You can apply optional constraints on:

    • Memory usage: Limits how much memory the V8 engine can allocate.
    • Timeouts: Prevents infinite loops or long-running scripts from hanging the process.

    These constraints are enforced at the C++ level to ensure that even if the JavaScript code is intentionally malicious, it cannot crash the host process or exhaust system resources.

  7. Understand the PyMiniRacer architecture and components

    main

    PyMiniRacer consists of a C++ frontend for V8 and a pure-Python implementation that interfaces with it via ctypes.

    Core Components

    • src/py_mini_racer/ (Python Layer): The pure-Python implementation. It uses Python's ctypes system to load the compiled dynamic-link library and manage V8 contexts.
    • src/v8_py_frontend/ (C++ Frontend): A small C++ frontend for V8. It manages initialization and marshals inputs/outputs through V8's type system. It is designed to be friendly to ctypes and does not link to libpython or use Python.h.
    • Compiled Libraries: The engine is shipped as a platform-specific dynamic-link library:
      • Windows: mini_racer.dll
      • Linux: libmini_racer.so
      • macOS: libmini_racer.dylib
    • icudtl.dat: A required runtime artifact for V8 internationalization (i18n) shipped with the package.
  8. Manage object lifecycles with Context Managers

    main

    While PyMiniRacer uses Python's garbage collection (__del__) for eventual cleanup, relying on it can be unpredictable. For applications requiring strong guarantees about when C++ resources are torn down, use the Python context manager interface.

    The _Context object (which wraps a C++ MiniRacer::Context) supports the with statement to ensure deterministic teardown of the V8 isolate and associated resources.

    # Example of using the context manager for deterministic teardown
    with MiniRacer() as context:
        result = context.eval("1 + 1")
        print(result)
    # Resources are guaranteed to be cleaned up here
  9. Run PyMiniRacer on Alpine Linux

    main

    PyMiniRacer wheels for Alpine Linux are built as cross-compatible glibc binaries. Because Alpine uses musl instead of glibc, you must install compatibility layers and set a specific environment variable for the wheel to function correctly.

    To use the Alpine wheel, perform the following steps:

    1. Install gcompat and libgcc using apk.
    2. Set the LD_PRELOAD environment variable to point to libgcompat.so.0.

    Required Setup:

    apk add gcompat libgcc
    export LD_PRELOAD="/lib/libgcompat.so.0"
  10. How JavaScript functions are represented in Python

    main

    When executing JavaScript code, functions are returned as JSFunction objects in synchronous contexts. You can invoke them from Python using the call syntax func(*args, this=..., timeout_sec=...).

    Synchronous Execution

    In synchronous code, you can pass a timeout_sec argument to limit the execution time.

    Asynchronous Execution and Cancellation

    If you are running in an async context, you must not use timeout_sec. Instead, use the .cancelable() method to obtain a CancelableJSFunction. Calls on a CancelableJSFunction are always asynchronous in Python. To apply a timeout to a cancelable function, use standard Python tools like asyncio.wait_for().

    The this argument

    When calling a function, you can specify the this context by passing a JSObject or JSUndefinedType.

    # Synchronous call with timeout
    result = js_func(arg1, arg2, timeout_sec=1.0)
    
    # Asynchronous/Cancelable call
    import asyncio
    
    cancelable_func = js_func.cancelable()
    try:
        result = await asyncio.wait_for(cancelable_func(arg1), timeout=1.0)
    except asyncio.TimeoutError:
        print("Function timed out")
  11. How to handle JavaScript Promises in Python

    main

    JavaScript Promises are returned as JSPromise objects. To retrieve the resolved value in Python, you have two options depending on your execution context:

    1. Synchronous/Blocking: Call promise.get(timeout=...). This will block the Python thread until the promise resolves or rejects. If the promise is rejected, it raises a Python exception.
    2. Asynchronous: Use await promise. This is the preferred method in async code. If the promise is rejected, it raises a Python exception.

    Note: If you call an async JavaScript function, it returns a JSPromise, which requires a "double await" pattern in Python (once for the function call itself and once for the promise result).

    # Blocking approach
    result = promise.get(timeout=2.0)
    
    # Async approach
    result = await promise