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
implSplit: When you split animplblock intoa.rsandb.rs, you are effectively placing those methods in two different modules. - Private Boundary: A method marked with
fn name()(withoutpub) is private to the module it is defined in. - Cross-Module Access: Even though
b.rsusesuse 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)orpubjust 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), usepub(crate) fn irq(). - Module Re-exporting: Use a parent module (
mod.rsorlib.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
implblock, split the data into smaller, private structs and use composition to build the largerCPUstruct.
Why Juniors Miss It
- Object-Oriented Bias: Juniors coming from Java or C++ often assume that
privatemeans “private to the class.” In Rust,privatemeans “private to the module.” - Misunderstanding the Module Tree: There is often a lack of clarity on how
mod,use, andsupercreate 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.