# How Memory Context (Mcx) Management Works in pgrust: A Rust Implementation of PostgreSQL's MemoryContext

> Discover how pgrust implements PostgreSQL's MemoryContext (Mcx) management in Rust. Learn about its safe Rust approach, Allocator trait integration, and borrow checker benefits for robust memory handling.

- Repository: [Michael Malis/pgrust](https://github.com/malisper/pgrust)
- Tags: internals
- Published: 2026-07-13

---

**The pgrust project recreates PostgreSQL's MemoryContext hierarchy in safe Rust using the `mcx` crate, providing a copyable `Mcx` handle that implements the `Allocator` trait while using Rust's borrow checker to prevent use-after-reset bugs without thread-local state.**

The `pgrust` repository implements a faithful, type-safe recreation of PostgreSQL's memory management system in Rust. By leveraging the `mcx` crate located at `crates/_support/mcx`, the project replaces C's explicit memory context tracking with Rust's ownership and lifetime guarantees, ensuring that allocations cannot outlive their owning contexts while maintaining the hierarchical accounting and reset semantics of the original PostgreSQL implementation.

## Core Architecture of the Mcx System

The memory context (Mcx) management system in pgrust consists of three primary components that mirror PostgreSQL's C implementation while adding compile-time safety guarantees.

### MemoryContext: The Allocation Domain

At the foundation lies the **`MemoryContext`** struct defined in [`crates/_support/mcx/src/lib.rs`](https://github.com/malisper/pgrust/blob/main/crates/_support/mcx/src/lib.rs) (lines 71-78). This struct serves as a named allocation domain that tracks exact byte usage, optional limits, and reset callbacks:

```rust
pub struct MemoryContext {
    // Holds an Acct node for accounting and a Backend for allocation
    // Child contexts are owned by the caller, not by the context tree itself
}

```

Unlike PostgreSQL's C implementation where child contexts are linked in a tree owned by the parent, pgrust's Rust version maintains the hierarchy through accounting propagation while leaving ownership to the caller, avoiding complex self-referential structures.

### Mcx<'mcx>: The Copyable Allocator Handle

The **`Mcx<'mcx>`** struct (lines 19-23 of [`lib.rs`](https://github.com/malisper/pgrust/blob/main/lib.rs)) provides a zero-cost handle that ties every allocation to a specific context:

```rust
pub struct Mcx<'mcx>(&'mcx MemoryContext);

```

This handle implements the standard **`Allocator`** trait (lines 445-510), allowing it to be passed directly to collections like `PgVec`, `PgBox`, and `PgHashMap`. The lifetime parameter `'mcx` guarantees that allocations cannot outlive their owning context, eliminating the need for a global `CurrentMemoryContext` thread-local variable.

### Backend: Pluggable Allocation Strategies

The **`Backend`** enum (lines 49-81) provides the same allocation strategies as PostgreSQL's C implementation:

- **Aset**: Arena-style allocation matching PostgreSQL's [`aset.c`](https://github.com/malisper/pgrust/blob/main/aset.c)
- **Malloc**: Direct system allocator passthrough
- **Bump**: Simple bump pointer arena
- **BumpDrop**: Bump arena with a per-context drop list for owned Rust values

## Allocation Accounting and Hierarchy

### Charge and Uncharge Operations

Every allocation in pgrust undergoes eager accounting through the **`charge`** method (lines 739-773) and its counterpart **`uncharge`** (lines 774-793). When `Mcx` allocates memory, it performs the following:

1. Adds the requested size to `self_used` (local usage)
2. Propagates the charge up the parent hierarchy to `subtree_used`
3. Validates against optional byte limits set via `with_limit` on any context in the path

On deallocation, `uncharge` reverses this process, subtracting bytes from both local and subtree counters. This ensures that parent contexts always know the total memory usage of their descendants, enabling accurate limit enforcement across the hierarchy.

### Reset Callbacks and Lifecycle Management

The **`reset`** method (lines 777-836) clears a context and its allocations while respecting LIFO semantics:

```rust
pub fn reset(&mut self) {
    // 1. Fire registered reset callbacks in LIFO order
    // 2. Reset the backend (e.g., bump.reset())
    // 3. Run the drop list for BumpDrop backends
    // 4. Recompute statistics
}

```

Crucially, `reset` requires `&mut self`, which the Rust borrow checker enforces cannot exist while any `Mcx` references are alive. This compile-time guarantee prevents use-after-reset bugs that are common in the C implementation.

## Drop-Aware Memory Management

### The BumpDrop Backend

The **`BumpDrop`** variant extends the standard bump arena to support owned Rust values. Unlike traditional bump allocators that leak memory until reset, `BumpDrop` maintains a drop list of destructors to run when the context resets.

### Registering Drop Glue

When allocating owned values in a `BumpDrop` context, the system uses **`register_drop`** (lines 708-715):

```rust
unsafe fn register_drop(&self, addr: *mut u8, glue: unsafe fn(*mut u8)) -> bool

```

The glue function is a monomorphized `drop_glue::<T>` that invokes `T`'s destructor. This allows code to allocate `String`, `Vec`, or other owned types in a bump arena while ensuring proper cleanup during reset, matching PostgreSQL's `AllocSetReset` model but with type safety.

## Practical Usage Examples

Creating hierarchical contexts and allocating collections follows this pattern:

```rust
use mcx::{MemoryContext, Mcx, PgVec};

// Create a root context (similar to TopTransactionContext in PG)
let ctx = MemoryContext::new("root");

// Allocate a vector in that context
let mut v: PgVec<'_, u8> = PgVec::new_in(ctx.mcx());
v.extend_from_slice(&[1, 2, 3]);

// Create a child context that inherits accounting limits
let child = ctx.new_child("per‑tuple");
let mut child_vec = PgVec::with_capacity_in(10, child.mcx());

// Register a reset callback (runs LIFO on reset or drop)
child.register_reset_callback(|| eprintln!("child context reset"));

// Reset the child – all allocations tied to child are reclaimed
// The borrow checker ensures no live Mcx references exist here
child.reset();

// When root goes out of scope, remaining charges clear and callbacks run
drop(ctx);

```

For drop-aware allocation:

```rust
let bump_ctx = MemoryContext::new_bumpdrop("temp");
let boxed = mcx::alloc_in(bump_ctx.mcx(), String::from("owned"));
let borrowed = mcx::leak_in(boxed);   // borrowed lives until bump_ctx resets

// When reset runs, String's destructor is invoked exactly once
bump_ctx.reset();

```

## Summary

- **MemoryContext** encapsulates accounting trees and allocator backends in [`crates/_support/mcx/src/lib.rs`](https://github.com/malisper/pgrust/blob/main/crates/_support/mcx/src/lib.rs), providing the foundation for hierarchical memory management.
- **Mcx<'mcx>** offers a cheap, copyable handle implementing the `Allocator` trait, enabling standard Rust collections to allocate within specific contexts.
- **Charge/uncharge operations** propagate usage statistics up the context tree while enforcing optional byte limits through the `with_limit` API.
- **Reset callbacks and drop lists** provide LIFO cleanup semantics and proper destruction of owned Rust values, particularly in the `BumpDrop` backend.
- **Borrow checker integration** eliminates the need for global "current context" thread-local state by ensuring allocations cannot outlive their contexts through lifetime parameters.

## Frequently Asked Questions

### How does pgrust prevent use-after-reset bugs without a global current context variable?

The Rust borrow checker enforces that `MemoryContext::reset` requires `&mut self`, which cannot coexist with any outstanding `Mcx` references. Since `Mcx` contains a shared reference `&'mcx MemoryContext`, attempting to reset while allocations are live results in a compile-time error. This replaces PostgreSQL's runtime `CurrentMemoryContext` pointer with compile-time lifetime guarantees.

### What is the difference between the Bump and BumpDrop backends?

The **Bump** backend provides a standard bump allocator that resets by discarding all memory, suitable for POD (plain old data) types. **BumpDrop** extends this with a drop list tracking `(ptr, glue)` pairs, allowing it to run destructors for owned Rust values like `String` or `Vec` during reset. This matches PostgreSQL's `AllocSet` behavior where memory is bulk-freed but custom cleanup can occur.

### How does memory limit enforcement work across the context hierarchy?

When allocating through any `Mcx`, the `charge` method validates the request against limits on every context in the parent chain. If any ancestor has a limit set via `with_limit` and the new allocation would exceed it, the operation returns `AllocError` before touching the backend allocator. This propagates charges up the tree via `subtree_used` counters, ensuring parents track total descendant usage.

### Can I use standard Rust collections with pgrust's memory contexts?

Yes, because `Mcx` implements the standard **`Allocator`** trait defined in `std::alloc`. You can use `Vec::new_in`, `HashMap::new_in`, or any allocator-aware collection by passing `ctx.mcx()` as the allocator argument. The [`crates/_support/mcx/src/owned.rs`](https://github.com/malisper/pgrust/blob/main/crates/_support/mcx/src/owned.rs) module provides wrapper types like `PgVec` and `PgBox` that simplify this integration for common use cases.