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

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 (lines 71-78). This struct serves as a named allocation domain that tracks exact byte usage, optional limits, and reset callbacks:

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) provides a zero-cost handle that ties every allocation to a specific context:

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
  • 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:

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):

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:

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:

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, 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 module provides wrapper types like PgVec and PgBox that simplify this integration for common use cases.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →