pyroxide

Pyroxide: A high-performance, lock-free background task broker for Python powered by Rust.

Exposes a thread-safe background execution engine for offloading operations from the main Python interpreter. Supports Python callable tasks, sandboxed WebAssembly execution, and dynamically compiled shared library (dylib) plugins.

Exports: - task: Decorator to submit Python functions to the background execution pool. - TaskHandle: Object returned by task submission to query status and await results. - register_wasm / wasm_task: WebAssembly sandbox registration and execution. - compile_dylib / dylib_task: Dynamic shared library compilation and execution.

 1"""
 2Pyroxide: A high-performance, lock-free background task broker for Python powered by Rust.
 3
 4Exposes a thread-safe background execution engine for offloading operations from
 5the main Python interpreter. Supports Python callable tasks, sandboxed WebAssembly
 6execution, and dynamically compiled shared library (dylib) plugins.
 7
 8Exports:
 9    - task: Decorator to submit Python functions to the background execution pool.
10    - TaskHandle: Object returned by task submission to query status and await results.
11    - register_wasm / wasm_task: WebAssembly sandbox registration and execution.
12    - compile_dylib / dylib_task: Dynamic shared library compilation and execution.
13"""
14
15from ._pyroxide import submit_task, get_status, register_dylib  # noqa: F401
16from .decorators import task
17from .types import TaskHandle
18from .wasm import register_wasm, wasm_task
19from .plugins import compile_dylib, dylib_task, compile_c, compile_zig
20from .workflows import group, TaskGroup
21
22__version__ = "0.5.1"
23
24__all__ = [
25    "task",
26    "TaskHandle",
27    "register_wasm",
28    "wasm_task",
29    "compile_dylib",
30    "dylib_task",
31    "compile_c",
32    "compile_zig",
33    "group",
34    "TaskGroup",
35]
def task(func_or_none=None, *, isolated: bool = False):
11def task(func_or_none=None, *, isolated: bool = False):
12    """
13    Decorator to offload a Python function to the Rust background worker pool.
14
15    The decorated function will be executed on a background OS thread managed
16    by Pyroxide's lock-free task broker. If `isolated=True` is set, the task
17    is executed in a separate OS process, providing full crash safety and GIL-free
18    concurrency for pure Python CPU tasks.
19
20    Args:
21        func_or_none: The Python callable to execute.
22        isolated: Set to True to run the task in an isolated worker process.
23    """
24
25    def decorator(func: Callable[[P], R]) -> Callable[[P], TaskHandle]:
26        @functools.wraps(func)
27        def wrapper(payload: P) -> TaskHandle:
28            import os
29            if os.environ.get("PYROXIDE_WORKER") == "1":
30                return func(payload)
31
32            target_callable = wrapper if isolated else func
33            task_id = submit_task(target_callable, payload, isolated=isolated)
34            return TaskHandle(task_id)
35
36        def batch(payloads: list) -> list[TaskHandle]:
37            from ._pyroxide import submit_batch
38            import os
39            if os.environ.get("PYROXIDE_WORKER") == "1":
40                return [func(p) for p in payloads]
41
42            target_callable = wrapper if isolated else func
43            task_ids = submit_batch(target_callable, payloads, isolated=isolated)
44            return [TaskHandle(tid) for tid in task_ids]
45
46        wrapper.batch = batch
47        return wrapper
48
49    if func_or_none is None:
50        return decorator
51    else:
52        return decorator(func_or_none)

Decorator to offload a Python function to the Rust background worker pool.

The decorated function will be executed on a background OS thread managed by Pyroxide's lock-free task broker. If isolated=True is set, the task is executed in a separate OS process, providing full crash safety and GIL-free concurrency for pure Python CPU tasks.

Args: func_or_none: The Python callable to execute. isolated: Set to True to run the task in an isolated worker process.

class TaskHandle:
 6class TaskHandle:
 7    def __init__(self, task_id: int) -> None:
 8        self.task_id: int = task_id
 9        self._consumed: bool = False
10
11    def __repr__(self) -> str:
12        return f"<TaskHandle id={self.task_id} status={self.status}>"
13
14    @property
15    def status(self) -> str:
16        """Queries the current status from the Rust Slab."""
17        return get_status(self.task_id)
18
19    def cancel(self) -> bool:
20        """
21        Attempts to cancel the task. Returns True if successfully cancelled,
22        False if the task is already finished or cancelled.
23        """
24        from ._pyroxide import cancel_task
25
26        return cancel_task(self.task_id)
27
28    def wait(
29        self, poll_interval_ms: int = 10, timeout_sec: Optional[float] = None
30    ) -> str:
31        """
32        Blocks the Python runtime until the background Rust worker completes the task.
33        Uses native Rust condvar signal to sleep with 0% CPU usage.
34        """
35        timeout_ms: Optional[int] = (
36            int(timeout_sec * 1000) if timeout_sec is not None else None
37        )
38        current_status: str = wait_status(self.task_id, timeout_ms)
39
40        if timeout_sec is not None and current_status not in ("Completed", "Failed"):
41            raise TimeoutError(f"Task {self.task_id} timed out.")
42
43        return current_status
44
45    def result(self, timeout_sec: Optional[float] = None, consume: bool = True) -> Any:
46        """
47        Blocks until the task is complete, then returns the result.
48        If the task failed, raises the exception encountered.
49
50        Args:
51            timeout_sec: Maximum time in seconds to wait.
52            consume: If True, automatically evicts the task from the Rust Slab once retrieved.
53        """
54        self.wait(timeout_sec=timeout_sec)
55        from ._pyroxide import get_result, free_task
56
57        res = get_result(self.task_id)
58        if consume:
59            free_task(self.task_id)
60            self._consumed = True
61        return res
62
63    async def result_async(
64        self, timeout_sec: Optional[float] = None, consume: bool = True
65    ) -> Any:
66        """
67        Asynchronously awaits the task result, yielding control to the event loop.
68        """
69        import asyncio
70
71        loop = asyncio.get_running_loop()
72        # Offload the blocking wait() call to the loop's default thread pool executor
73        await loop.run_in_executor(None, self.wait, 10, timeout_sec)
74        return self.result(timeout_sec=0, consume=consume)
75
76    def __del__(self) -> None:
77        """
78        Garbage collection destructor.
79        Automatically frees the task memory in the Rust Slab when the Python handle is deleted/dropped.
80        """
81        if getattr(self, "_consumed", False):
82            return
83        try:
84            from ._pyroxide import free_task
85
86            free_task(self.task_id)
87            self._consumed = True
88        except Exception:
89            pass
TaskHandle(task_id: int)
7    def __init__(self, task_id: int) -> None:
8        self.task_id: int = task_id
9        self._consumed: bool = False
task_id: int
status: str
14    @property
15    def status(self) -> str:
16        """Queries the current status from the Rust Slab."""
17        return get_status(self.task_id)

Queries the current status from the Rust Slab.

def cancel(self) -> bool:
19    def cancel(self) -> bool:
20        """
21        Attempts to cancel the task. Returns True if successfully cancelled,
22        False if the task is already finished or cancelled.
23        """
24        from ._pyroxide import cancel_task
25
26        return cancel_task(self.task_id)

Attempts to cancel the task. Returns True if successfully cancelled, False if the task is already finished or cancelled.

def wait( self, poll_interval_ms: int = 10, timeout_sec: Optional[float] = None) -> str:
28    def wait(
29        self, poll_interval_ms: int = 10, timeout_sec: Optional[float] = None
30    ) -> str:
31        """
32        Blocks the Python runtime until the background Rust worker completes the task.
33        Uses native Rust condvar signal to sleep with 0% CPU usage.
34        """
35        timeout_ms: Optional[int] = (
36            int(timeout_sec * 1000) if timeout_sec is not None else None
37        )
38        current_status: str = wait_status(self.task_id, timeout_ms)
39
40        if timeout_sec is not None and current_status not in ("Completed", "Failed"):
41            raise TimeoutError(f"Task {self.task_id} timed out.")
42
43        return current_status

Blocks the Python runtime until the background Rust worker completes the task. Uses native Rust condvar signal to sleep with 0% CPU usage.

def result(self, timeout_sec: Optional[float] = None, consume: bool = True) -> Any:
45    def result(self, timeout_sec: Optional[float] = None, consume: bool = True) -> Any:
46        """
47        Blocks until the task is complete, then returns the result.
48        If the task failed, raises the exception encountered.
49
50        Args:
51            timeout_sec: Maximum time in seconds to wait.
52            consume: If True, automatically evicts the task from the Rust Slab once retrieved.
53        """
54        self.wait(timeout_sec=timeout_sec)
55        from ._pyroxide import get_result, free_task
56
57        res = get_result(self.task_id)
58        if consume:
59            free_task(self.task_id)
60            self._consumed = True
61        return res

Blocks until the task is complete, then returns the result. If the task failed, raises the exception encountered.

Args: timeout_sec: Maximum time in seconds to wait. consume: If True, automatically evicts the task from the Rust Slab once retrieved.

async def result_async(self, timeout_sec: Optional[float] = None, consume: bool = True) -> Any:
63    async def result_async(
64        self, timeout_sec: Optional[float] = None, consume: bool = True
65    ) -> Any:
66        """
67        Asynchronously awaits the task result, yielding control to the event loop.
68        """
69        import asyncio
70
71        loop = asyncio.get_running_loop()
72        # Offload the blocking wait() call to the loop's default thread pool executor
73        await loop.run_in_executor(None, self.wait, 10, timeout_sec)
74        return self.result(timeout_sec=0, consume=consume)

Asynchronously awaits the task result, yielding control to the event loop.

def register_wasm(module_name: str, wasm_bytes: bytes):
 7def register_wasm(module_name: str, wasm_bytes: bytes):
 8    """
 9    Registers a pre-compiled WebAssembly module in the global registry.
10    """
11    register_wasm_module(module_name, wasm_bytes)

Registers a pre-compiled WebAssembly module in the global registry.

def wasm_task(module_name: str, func_name: str = 'run', *, isolated: bool = False):
14def wasm_task(module_name: str, func_name: str = "run", *, isolated: bool = False):
15    """
16    Decorator to submit string or byte payloads to be processed by a registered WASM module.
17    """
18
19    def decorator(func):
20        @functools.wraps(func)
21        def wrapper(payload) -> TaskHandle:
22            task_id = submit_wasm_task(module_name, func_name, payload, isolated=isolated)
23            return TaskHandle(task_id)
24
25        return wrapper
26
27    return decorator

Decorator to submit string or byte payloads to be processed by a registered WASM module.

def compile_dylib( name: str, source_code: str, dependencies: Optional[Dict[str, str]] = None) -> str:
 11def compile_dylib(
 12    name: str, source_code: str, dependencies: Optional[Dict[str, str]] = None
 13) -> str:
 14    """
 15    Compiles Rust source code on-the-fly into a dynamic shared library (.so / .dylib / .dll),
 16    and registers it with the Pyroxide background broker for GIL-free execution.
 17
 18    The compilation is handled automatically by invoking ``cargo build --release`` inside
 19    a temporary directory. The user does not need to install or configure anything beyond
 20    having a working Rust toolchain (``rustc`` + ``cargo``).
 21
 22    Args:
 23        name: Unique name for the dylib. Used to reference it in ``@dylib_task``.
 24        source_code: Raw Rust source code string. Must export two C-compatible symbols:
 25
 26            - ``pyroxide_plugin_run(ptr, len, out_len) -> *mut u8``
 27            - ``pyroxide_plugin_free(ptr, len)``
 28        dependencies: Optional dict of Cargo dependencies, e.g. ``{"serde": "1.0"}``.
 29
 30    Returns:
 31        Absolute path to the compiled shared library file.
 32
 33    Raises:
 34        RuntimeError: If the Cargo compilation fails.
 35        FileNotFoundError: If the compiled library binary is not found after build.
 36
 37    Example:
 38        >>> compile_dylib("my_lib", RUST_SOURCE_CODE)
 39        >>> @dylib_task("my_lib")
 40        ... def process(payload): pass
 41        >>> handle = process("hello")
 42        >>> print(handle.result())
 43    """
 44    temp_dir = tempfile.mkdtemp(prefix=f"pyroxide_dylib_{name}_")
 45    try:
 46        # Run cargo init
 47        subprocess.run(
 48            ["cargo", "init", "--lib", "--name", name],
 49            cwd=temp_dir,
 50            check=True,
 51            stdout=subprocess.DEVNULL,
 52            stderr=subprocess.DEVNULL,
 53        )
 54
 55        cargo_toml_path = os.path.join(temp_dir, "Cargo.toml")
 56        with open(cargo_toml_path, "r") as f:
 57            cargo_content = f.read()
 58
 59        # Force Edition 2021 to prevent newer Rust 2024 edition strict compiler errors
 60        cargo_content = cargo_content.replace('edition = "2024"', 'edition = "2021"')
 61
 62        # Add cdylib configuration
 63        cargo_content += '\n[lib]\ncrate-type = ["cdylib"]\n'
 64
 65        # Add dependencies
 66        if dependencies:
 67            cargo_content += "\n[dependencies]\n"
 68            for dep, ver in dependencies.items():
 69                cargo_content += f'{dep} = "{ver}"\n'
 70
 71        with open(cargo_toml_path, "w") as f:
 72            f.write(cargo_content)
 73
 74        # Write Rust source code to src/lib.rs
 75        lib_rs_path = os.path.join(temp_dir, "src", "lib.rs")
 76        with open(lib_rs_path, "w") as f:
 77            f.write(source_code)
 78
 79        # Run cargo build in release mode
 80        res = subprocess.run(
 81            ["cargo", "build", "--release"],
 82            cwd=temp_dir,
 83            capture_output=True,
 84            text=True,
 85        )
 86        if res.returncode != 0:
 87            raise RuntimeError(f"Cargo build failed:\n{res.stderr}\n{res.stdout}")
 88
 89        # Find compiled library
 90        lib_ext = "dylib" if sys.platform == "darwin" else "so"
 91        if sys.platform == "win32":
 92            lib_ext = "dll"
 93
 94        lib_name = f"lib{name}.{lib_ext}"
 95        if sys.platform == "win32":
 96            lib_name = f"{name}.{lib_ext}"
 97
 98        compiled_path = os.path.join(temp_dir, "target", "release", lib_name)
 99        if not os.path.exists(compiled_path):
100            raise FileNotFoundError(f"Compiled library not found at: {compiled_path}")
101
102        # Register dylib with the Rust core engine
103        register_dylib(name, compiled_path)
104        return compiled_path
105
106    except Exception as e:
107        raise RuntimeError(f"Failed to compile dylib '{name}' via Cargo: {e}") from e

Compiles Rust source code on-the-fly into a dynamic shared library (.so / .dylib / .dll), and registers it with the Pyroxide background broker for GIL-free execution.

The compilation is handled automatically by invoking cargo build --release inside a temporary directory. The user does not need to install or configure anything beyond having a working Rust toolchain (rustc + cargo).

Args: name: Unique name for the dylib. Used to reference it in @dylib_task. source_code: Raw Rust source code string. Must export two C-compatible symbols:

    - ``pyroxide_plugin_run(ptr, len, out_len) -> *mut u8``
    - ``pyroxide_plugin_free(ptr, len)``
dependencies: Optional dict of Cargo dependencies, e.g. ``{"serde": "1.0"}``.

Returns: Absolute path to the compiled shared library file.

Raises: RuntimeError: If the Cargo compilation fails. FileNotFoundError: If the compiled library binary is not found after build.

Example:

compile_dylib("my_lib", RUST_SOURCE_CODE) @dylib_task("my_lib") ... def process(payload): pass handle = process("hello") print(handle.result())

def dylib_task(dylib_name: str, *, isolated: bool = False):
196def dylib_task(dylib_name: str, *, isolated: bool = False):
197    """
198    Decorator that routes task payloads to a registered dynamic shared library (dylib)
199    for GIL-free execution on the background Rust worker pool.
200
201    The dylib must have been previously compiled and registered via ``compile_dylib()``.
202
203    Args:
204        dylib_name: The name of the dylib as registered with ``compile_dylib()``.
205        isolated: Set to True to run in an isolated worker process for crash isolation.
206
207    Example:
208        >>> @dylib_task("my_lib", isolated=True)
209        ... def process(payload: str) -> str:
210        ...     pass  # Execution is handled by the compiled dylib in a separate process
211        >>> handle = process("hello")
212        >>> print(handle.result())
213    """
214
215    def decorator(func: Callable[[Any], Any]) -> Callable[[Any], TaskHandle]:
216        def wrapper(payload: Any) -> TaskHandle:
217            task_id = submit_dylib_task(dylib_name, payload, isolated=isolated)
218            return TaskHandle(task_id)
219
220        def batch(payloads: list) -> list[TaskHandle]:
221            return [wrapper(p) for p in payloads]
222
223        wrapper.batch = batch
224        return wrapper
225
226    return decorator

Decorator that routes task payloads to a registered dynamic shared library (dylib) for GIL-free execution on the background Rust worker pool.

The dylib must have been previously compiled and registered via compile_dylib().

Args: dylib_name: The name of the dylib as registered with compile_dylib(). isolated: Set to True to run in an isolated worker process for crash isolation.

Example:

@dylib_task("my_lib", isolated=True) ... def process(payload: str) -> str: ... pass # Execution is handled by the compiled dylib in a separate process handle = process("hello") print(handle.result())

def compile_c(name: str, source_code: str) -> str:
110def compile_c(name: str, source_code: str) -> str:
111    """
112    Compiles C source code on-the-fly into a dynamic shared library (.so / .dylib / .dll),
113    and registers it with the Pyroxide background broker for GIL-free execution.
114
115    Args:
116        name: Unique name for the library. Used to reference it in @dylib_task.
117        source_code: Raw C source code string. Must export two functions:
118            - ``pyroxide_plugin_run(ptr, len, out_len) -> uint8_t*``
119            - ``pyroxide_plugin_free(ptr, len)``
120    """
121    temp_dir = tempfile.mkdtemp(prefix=f"pyroxide_c_{name}_")
122    try:
123        src_path = os.path.join(temp_dir, f"{name}.c")
124        with open(src_path, "w") as f:
125            f.write(source_code)
126
127        cc = os.environ.get("CC", "clang" if sys.platform == "darwin" else "gcc")
128        lib_ext = "dylib" if sys.platform == "darwin" else "so"
129        if sys.platform == "win32":
130            lib_ext = "dll"
131
132        lib_name = f"lib{name}.{lib_ext}"
133        if sys.platform == "win32":
134            lib_name = f"{name}.{lib_ext}"
135        compiled_path = os.path.join(temp_dir, lib_name)
136
137        cmd = [cc, "-shared", "-o", compiled_path, "-fPIC", src_path]
138        res = subprocess.run(cmd, capture_output=True, text=True)
139        if res.returncode != 0:
140            raise RuntimeError(f"C compilation failed:\n{res.stderr}\n{res.stdout}")
141
142        if not os.path.exists(compiled_path):
143            raise FileNotFoundError(f"Compiled C library not found at: {compiled_path}")
144
145        register_dylib(name, compiled_path)
146        return compiled_path
147
148    except Exception as e:
149        raise RuntimeError(f"Failed to compile C library '{name}': {e}") from e

Compiles C source code on-the-fly into a dynamic shared library (.so / .dylib / .dll), and registers it with the Pyroxide background broker for GIL-free execution.

Args: name: Unique name for the library. Used to reference it in @dylib_task. source_code: Raw C source code string. Must export two functions: - pyroxide_plugin_run(ptr, len, out_len) -> uint8_t* - pyroxide_plugin_free(ptr, len)

def compile_zig(name: str, source_code: str) -> str:
152def compile_zig(name: str, source_code: str) -> str:
153    """
154    Compiles Zig source code on-the-fly into a dynamic shared library (.so / .dylib / .dll),
155    and registers it with the Pyroxide background broker for GIL-free execution.
156
157    Args:
158        name: Unique name for the library. Used to reference it in @dylib_task.
159        source_code: Raw Zig source code string. Must export two functions:
160            - ``pyroxide_plugin_run(ptr, len, out_len) -> [*]u8``
161            - ``pyroxide_plugin_free(ptr, len)``
162    """
163    temp_dir = tempfile.mkdtemp(prefix=f"pyroxide_zig_{name}_")
164    try:
165        src_path = os.path.join(temp_dir, f"{name}.zig")
166        with open(src_path, "w") as f:
167            f.write(source_code)
168
169        # Compiles dynamic library. Zig build-lib generates output in cwd
170        cmd = ["zig", "build-lib", "-dynamic", "-O", "ReleaseFast", src_path]
171        res = subprocess.run(cmd, cwd=temp_dir, capture_output=True, text=True)
172        if res.returncode != 0:
173            raise RuntimeError(f"Zig compilation failed:\n{res.stderr}\n{res.stdout}")
174
175        lib_ext = "dylib" if sys.platform == "darwin" else "so"
176        if sys.platform == "win32":
177            lib_ext = "dll"
178
179        lib_name = f"lib{name}.{lib_ext}"
180        if sys.platform == "win32":
181            lib_name = f"{name}.{lib_ext}"
182
183        compiled_path = os.path.join(temp_dir, lib_name)
184        if not os.path.exists(compiled_path):
185            raise FileNotFoundError(
186                f"Compiled Zig library not found at: {compiled_path}"
187            )
188
189        register_dylib(name, compiled_path)
190        return compiled_path
191
192    except Exception as e:
193        raise RuntimeError(f"Failed to compile Zig library '{name}': {e}") from e

Compiles Zig source code on-the-fly into a dynamic shared library (.so / .dylib / .dll), and registers it with the Pyroxide background broker for GIL-free execution.

Args: name: Unique name for the library. Used to reference it in @dylib_task. source_code: Raw Zig source code string. Must export two functions: - pyroxide_plugin_run(ptr, len, out_len) -> [*]u8 - pyroxide_plugin_free(ptr, len)

def group( handles: Iterable[TaskHandle]) -> TaskGroup:
39def group(handles: Iterable[TaskHandle]) -> TaskGroup:
40    """Wraps multiple task handles into a parallel TaskGroup."""
41    return TaskGroup(handles)

Wraps multiple task handles into a parallel TaskGroup.

class TaskGroup:
 6class TaskGroup:
 7    """A collection of tasks that run in parallel and can be managed as a unit."""
 8    def __init__(self, handles: Iterable[TaskHandle]):
 9        self.handles = list(handles)
10
11    def __repr__(self) -> str:
12        return f"<TaskGroup handles={self.handles} status={self.status}>"
13
14    @property
15    def status(self) -> str:
16        statuses = [h.status for h in self.handles]
17        if "Failed" in statuses:
18            return "Failed"
19        if "Cancelled" in statuses:
20            return "Cancelled"
21        if all(s == "Completed" for s in statuses):
22            return "Completed"
23        return "Running"
24        
25    def wait(self):
26        """Blocks until all tasks in the group are completed."""
27        for h in self.handles:
28            h.wait()
29            
30    def result(self, consume: bool = True) -> List:
31        """Waits for all tasks and returns their results in order."""
32        return [h.result(consume=consume) for h in self.handles]
33        
34    def cancel(self) -> bool:
35        """Cancels all tasks in the group. Returns True if all were successfully cancelled."""
36        results = [h.cancel() for h in self.handles]
37        return all(results)

A collection of tasks that run in parallel and can be managed as a unit.

TaskGroup(handles: Iterable[TaskHandle])
8    def __init__(self, handles: Iterable[TaskHandle]):
9        self.handles = list(handles)
handles
status: str
14    @property
15    def status(self) -> str:
16        statuses = [h.status for h in self.handles]
17        if "Failed" in statuses:
18            return "Failed"
19        if "Cancelled" in statuses:
20            return "Cancelled"
21        if all(s == "Completed" for s in statuses):
22            return "Completed"
23        return "Running"
def wait(self):
25    def wait(self):
26        """Blocks until all tasks in the group are completed."""
27        for h in self.handles:
28            h.wait()

Blocks until all tasks in the group are completed.

def result(self, consume: bool = True) -> List:
30    def result(self, consume: bool = True) -> List:
31        """Waits for all tasks and returns their results in order."""
32        return [h.result(consume=consume) for h in self.handles]

Waits for all tasks and returns their results in order.

def cancel(self) -> bool:
34    def cancel(self) -> bool:
35        """Cancels all tasks in the group. Returns True if all were successfully cancelled."""
36        results = [h.cancel() for h in self.handles]
37        return all(results)

Cancels all tasks in the group. Returns True if all were successfully cancelled.