Protection against exposing private types has negative effect on macro writing

Some people have wrote about this issue before but I'm not sure people have realized the (negative) implication for macros, so maybe I am missing something. What I want to write is this:

#![allow(dead_code)]

mod top {
    mod outer {
        struct PrivateType();

        mod inner {
            use super::PrivateType;

            pub fn foo() -> PrivateType { PrivateType() }
        }

        fn use_foo() {
            inner::foo();
        }
    }
}

Intuitively, I would expect this to work because anyone who can use inner::foo also has access to PrivateType.

However, this errors:

error[E0446]: private type `PrivateType` in public interface
  --> src/lib.rs:10:13
   |
5  |         struct PrivateType();
   |         --------------------- `PrivateType` declared as private
...
10 |             pub fn foo() -> PrivateType { PrivateType() }
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^ can't leak private type

Assuming you want to keep PrivateType private, and you want the ability to call inner::foo from inside the outer module, AFAICT your only choice to fix it is by making foo be pub(super) instead of pub:

pub(super) fn foo() -> PrivateType { PrivateType() }

Being pub(super) prevents putting a pub use declaration in outer exporting foo to top, so it seems the compiler is pre-emptively worried I might do that, even though I haven't. If you are surprised by this there is an existing issue that is closed because the behavior is considered intended.

The nice thing about being able to use pub instead of pub(super) would be that it would let the author of the outer module control whether or not foo is exposed to higher level modules without changing the code of inner, by choosing whether or not to re-export. However because of this error the visibility modifier on PrivateType and on foo must change together. Either PrivateType is pub, and is intended to be used in top, and then foo also needs to be pub, or PrivateType is private, and then foo needs to be pub(super).

Why is this a problem? Macros can't generate new identifiers, so if you want to generate multiple items instead you generate a single module with multiple items inside. However, if inner is generated by say generate_mod!(inner, PrivateType), then generate_mod can't just assume foo can be made pub and let the user control visibility by deciding whether or not to re-export. Instead the macro must take the desired visibility as an argument, generate_mod!(inner, PrivateType, pub(super)). However, that visibility doesn't obviously map to anything that the user would care about! It's not the visibility of the generated module, and it's not the visibility of the passed in type. As a user I would be completely confused if I were asked to supply this argument.

Note that this problem only happens because the compiler is overly conservative and guards against a problem that doesn't actually exist. If it delayed erroring until I actually exposed the type, this would all go away.

I might be misunderstanding, but in this particular case, you could move foo to be a method of PrivateType and that would fix it, right?

I hadn't considered it, but in a situation where a library is defining generate_mod, it's probably intended to be used by users of the library on their own types. The macro can't provide an impl for PrivateType in that case because it's not defined in the same crate.

But maybe it works to define a trait inside the mod and an impl for PrivateType of that trait? I tried it, but then it becomes about the publicness of the trait instead of the function and you get the same issue, on top of needing to explicitly import the trait.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.