Lib.rs declare module publicly visible only to main.rs

I have the following layout where I want reuse.rs to be public but main.rs and all other modules private:

crate/
  src/
    processing/
      a.rs
    lib.rs
    main.rs
    reuse.rs
  Cargo.toml

Unfortunately main.rs can only call processing::a if it is public, like:

// contents of lib.rs

// processing happens once, so keep this code private only for main.rs
pub mod processing {
    pub mod a;
}
// reuse in other projects
pub mod reuse;

I was hoping to keep it private though, like:

// contents of lib.rs

pub(crate) mod processing {  // or pub (crate::main)
    pub mod a;
}
pub mod reuse;

Is there any way to specially grant public access to just main.rs?

You can't mark things as "conditionally public" unfortunately. You can hide an item from the documentation with #[doc(hidden)] though, which is usually good enough.

This comes up particularly in crates which export a public macro that needs access to items in the crate that shouldn't really be part of its public API. Those crates generally have a module at the top level of the crate named __private or something which is marked public, hidden from documentation, and often has a warning in the source code that you shouldn't use the contents of the module if you're a normal user of the crate.

For examples of this pattern, tracing currently has a __macro_support module, and serde has a __private module.

6 Likes

If the modules don't need to exist at all for the library functionality, then mounting them directly into the binary crate instead of the library crate (put mod processing in main.rs instead of lib.rs) is the ideal thing to do. (At that point, you should probably split the binary into /bin/* instead of /src/* to keep the files separate and it more apparent which files are part of which crate.)

If the functionality needs to exist for the library and be exposed to the binary, though, you're unfortunately stuck in the world of various hacky compromises. The most principled approach would probably be to have a separate package used by both the library and the binary, such that its public API can be versioned separately from the main library's API. (E.g. the regex crate uses regex-automata and regex-syntax, which see breaking releases significantly more regularly than the main regex crate, and a big motivation for regex-automata 0.3 was moving internals out of the regex crate so they're more easily tested.)

6 Likes

You just got me very close to a sensible solution. I'd say I'm at least on the putting green, just needing one or two last tips!:

Note: my crate is named my_crate in my Cargo.toml for these examples.

  • I moved all mod definitions to main.rs with pub(crate) for explicit exclusivity from lib.rs
    • I included reuse so macros check all mod dependencies against Cargo.toml:
      • e.g.: #![deny(unused_crate_dependencies)]
      • oddly, I now must do use my_crate as _; to please this macro... no idea why
  • lib.rs redundantly has pub mod reuse; defined so it can be used externally.

It works! But am I perceiving this correctly?:

  • lib.rs is namespaced by my_crate.
    • If I wanted to use reuse from lib.rs, I use my_crate::reuse
  • main.rs is namespaced by crate.
    • If I want to use reuse from main.rs, I use crate::reuse

So what is really the difference between my_crate and crate in this context? Second, what are the consequences (if any) of defining the reuse mod in both lib.rs and main.rs? Note it's small, so I'm not worried about double the build time if that is the only concern.

You have now created a situation where your binary crate does not actually make use of anything in your library crate. That can be legitimate (which is why unused_crate_dependencies is not warn-by-default) but in this case it's a sign of trouble; see below.

(By the way, that's an attribute, not a macro. #[anything] or #![anything] is the syntax for an attribute. Some attributes are macros but deny is not.)

The keyword crate always refers to the current crate.

  • If you use it in lib.rs then it's the library crate and if you use it in main.rs it's the binary crate.
  • You cannot use my_crate inside the library to refer to itself, unless you explicitly create an alias, which you can do with extern crate self as my_crate; — this is sometimes done to help procedural macros, but is not idiomatic to do for general use.

The compiler will compile the code twice. This means the build is slower, but it also means that any types or traits defined in that module have two different definitions, that will not be considered equal to each other. This will be a problem if you want to

It would be better to keep things non-duplicated: in your main.rs you can

mod processing;
use my_crate::reuse;   // not "mod reuse;"

and similarly if processing makes use of reuse then it should refer to my_crate::reuse.

1 Like

use my_crate::reuse; was my first instinct too, but here's why I got turned away from it:

In that case, #![deny(unused_crate_dependencies)] will not run on reuse when called from main.rs since reuse is no longer a part of main.rs scope, so I'd have to also place that attribute inside lib.rs.

But if I do that, then all the other dependencies will fail the check from inside lib.rs (that aren't used in reuse but are used in main).

Hence why duplicate building of reuse felt like the lesser evil, since it is a tiny bit of shared code with very little chance of ever growing.

Still, I would love a cleaner way...

This sort of problem is exactly the problem with unused_crate_dependencies:

This lint is "allow" by default because it can provide false positives depending on how the build system is configured. For example, when using Cargo, a "package" consists of multiple crates (such as a library and a binary), but the dependencies are defined for the package as a whole. If there is a dependency that is only used in the binary, but not the library, then the lint will be incorrectly issued in the library.

I strongly recommend that you prefer not enabling the unused_crate_dependencies lint over compiling code twice. There are many reasons why compiling code twice is to be avoided: it results in duplicate item definitions, it's slower, it produces bigger binaries, and it will produce spurious dead_code warnings if any item is used in one case and not in the other — and dead_code is a lot more frequently helpful than unused_crate_dependencies.

Don't let a flawed lint shape how you write the rest of your program.

1 Like

I agree it's dirty... Fortunately reuse probably will only ever have a few dependencies, so much to my chagrin I will not run unused_crate_dependencies for it alone...

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.