Rust Module Visibility: Fixing Private Method Cross-File Errors

Summary

A developer encountered a compilation error in Rust when attempting to call a private method across two different files, even though both implementations belonged to the same struct CPU. The error stemmed from a misunderstanding of how module visibility and implementation blocks interact. While the developer intended for the methods to be part of a single logical unit, Rust treats separate impl blocks in different modules as having distinct visibility boundaries.

Root Cause

The issue is caused by the fundamental definition of privacy in Rust.

  • Visibility is Module-Based: In Rust, privacy is not determined by the type (the struct), but by the module hierarchy.
  • The impl Split: When you split an impl block into a.rs and b.rs, you are effectively placing those methods in two different modules.
  • Private Boundary: A method marked with fn name() (without pub) is private to the module it is defined in.
  • Cross-Module Access: Even though b.rs uses use super::CPU, it only gains access to the type definition and its public members. It does not bypass the privacy rules of the module where the private method resides.

Why This Happens in Real Systems

In large-scale systems, developers often try to “organize” code by splitting large impl blocks into multiple files to prevent files from becoming thousands of lines long.

  • Logical vs. Physical Organization: Developers mistake logical grouping (the struct) for access control.
  • Granular Modularity: Modern software engineering encourages small, encapsulated modules. However, if a developer splits a single logical component across files without explicitly managing visibility, they inadvertently trigger encapsulation errors.
  • Refactoring Friction: As a codebase grows, moving a method from a “main” file to a “helper” file can silently change its accessibility, breaking builds in seemingly unrelated parts of the system.

Real-World Impact

  • Broken Builds: Immediate failure in CI/CD pipelines when architectural changes are made to improve file organization.
  • Increased Complexity: Developers may resort to making methods pub(crate) or pub just to “make it work,” which violates encapsulation and exposes internal implementation details to the entire crate.
  • Maintenance Debt: Over-reliance on pub(crate) to bypass module boundaries leads to a “leaky abstraction” where the internal state of a component is no longer protected from misuse.

Example or Code

// a.rs
pub struct CPU;

impl CPU {
    pub fn interrupt(&mut self) {
        // This fails because irq() is private to module b
        self.irq(); 
    }
}

// b.rs
use super::CPU;

impl CPU {
    fn irq(&mut self) {
        println!("Handling IRQ");
    }
}

How Senior Engineers Fix It

Senior engineers recognize that if two methods must interact privately, they must reside in the same module or use explicit visibility modifiers.

  • Consolidate Modules: If the methods are tightly coupled and require private access to each other, they should stay in the same file.
  • Use pub(crate): If the methods must be in different files but should be accessible to the entire crate (but not the end-user), use pub(crate) fn irq().
  • Module Re-exporting: Use a parent module (mod.rs or lib.rs) to manage the hierarchy, ensuring that the sub-modules are treated as a single cohesive unit where possible.
  • Prefer Composition over Splitting: Instead of splitting an impl block, split the data into smaller, private structs and use composition to build the larger CPU struct.

Why Juniors Miss It

  • Object-Oriented Bias: Juniors coming from Java or C++ often assume that private means “private to the class.” In Rust, private means “private to the module.”
  • Misunderstanding the Module Tree: There is often a lack of clarity on how mod, use, and super create a tree structure that governs visibility.
  • Focus on Syntax over Semantics: They focus on the fact that self.irq() looks correct syntactically, without realizing that the compiler’s visibility checker evaluates the path through the module tree, not just the type name.

Leave a Comment