DEV Community

SickleFire
SickleFire

Posted on

Designing a cross-platform terminal memory visualizer in Rust

The Why

Before diving into the design of m-vis, let's first get into why. The spark for this project came while I was building a 3D interactive model viewer in C++ using OpenGL. As the complexity of the renderer grew, so did a notoriously frustrating problem: memory leaks. Tracking down stray allocations in a real-time, graphics-heavy application quickly turned into a tedious game of whack-a-mole.

I realized I didn't just want to fix my code; I wanted to fundamentally solve the problem of how we detect, track, and understand memory leaks in the first place. That frustration is what drove me to build a tool that makes memory management visual, intuitive, and painless.

The Vision

I envisioned this project to prioritize user experience and simplicity above all else. Not only that it must be cross-platform; Because of this Rust is the perfect choice, because of its compiler make cross-compilation remarkably smooth.

A memory visualizer is useless if it slows down the target application so much that it alters its runtime behavior or masks the very bugs you are trying to find. In graphics-heavy or real-time applications like a 3D model viewer, keeping execution overhead to an absolute minimum is mandatory. Because of that requirement, non-invasive memory scanning techniques are used.

The Architecture

M-vis has 3 distinct stages:

  • Memory Provider (OS Layer)
  • Core Engine
  • UI (CLI, TUI)

The Memory Provider is the most interesting stage because this stage gathers heap and region metadata that the Core uses. However, one of our requirements is it must be a cross-platform. So how does the Memory Provider know which Operating System you are running on without adding a bunch of messy runtime checks?

The answer is Rust's conditional compilation. We can tell the compiler which code to run, based on your toolchain.

The Memory Provider Trait

To bridge the gap between platform-specific chaos and a clean, unified architecture, we need a strict contract. In Rust, that means defining a trait.

The Core Engine shouldn't care whether it is talking to Linux's /proc filesystem or the Windows Win32 API; it just needs data. By defining a single interface, every operating system module must implement the exact same behaviors.

/// Abstraction over OS-specific memory inspection APIs.
pub trait MemoryProvider {
    /// Returns all virtual memory regions mapped into the process with the given `pid`.
    fn walk_regions(&self, pid: u32) -> Result<Vec<Region>, String>;
    /// Returns all heap blocks (both used and free) for the process with the given `pid`.
    fn walk_heap(&self, pid: u32) -> Result<Vec<HeapBlock>, String>;
    /// Returns loaded modules for the process with the given `pid`.
    ///
    /// Pass `"-t"` as `flag` to restrict the output to tampered or injected modules only.
    fn list_modules(&self, pid: u32, flag: String) -> Result<Vec<ModuleInfo>, String>;
}
Enter fullscreen mode Exit fullscreen mode

This is the Memory Provider trait that every operating system backend must implement. Its core job is translation. It maps messy, platform-specific memory data into clean, platform-agnostic data structures—specifically HeapBlock and Region—that our core engine can easily understand.

Region Struct:

pub struct Region {
    pub base: usize,
    pub size: usize,
    pub state: RegionState,
    pub kind: RegionKind,
    pub protect: RegionProtect,
    pub name: String,
}
Enter fullscreen mode Exit fullscreen mode

Heap Block:

pub struct HeapBlock {
    pub address: usize,
    pub size: usize,
    pub is_free: bool,
    pub vm_protect: RegionProtect,
}
Enter fullscreen mode Exit fullscreen mode

The Core

The Core is the central engine of m-vis, responsible for two primary tasks: process scanning and data differentiation. It takes the platform-agnostic blocks provided by the OS layer and processes them to identify trends and track allocations.

We won't dive deep into the Core's internals in this post, as our primary goal is to look at the cross-platform engineering behind gathering the data itself.

The UI (CLI & TUI)

The final stage of the architecture is the presentation layer. Once the Core Engine has processed the raw memory data into identifiable trends, it hands that data package off to the user interface.

To keep the project lightweight and accessible, the UI layer is split into two modes:

  • CLI Mode: A traditional command-line interface designed for quick, scriptable, or text-only memory snapshots. It is perfect if you want a fast terminal output without any visual overhead.

  • TUI Mode: This is where m-vis comes to life. Written natively in Rust using Ratatui, the Terminal User Interface transforms the raw allocation into interactive blocks, Leak delta chart, and Allocation tables.

Conclusion

By designing a clean Memory Provider trait and leaning on Rust's compile-time conditional compilation, the tool stays completely cross-platform without adding any runtime bloat.

Building m-vis started because I was exhausted of encountering memory leaks in OpenGL/C++. My personal frustration turned into a passion project that will not only help me but will also make finding memory leaks effortless for other people.


Check out m-vis: https://github.com/SickleFire/m-vis

Authors:

  • SickleFire - Owner
  • Djo - Community Manager

Top comments (0)