Why do so many examples use 'extern crate'?

When I started learning Rust, I saw extern crate <crate>in a lot of code. Sometime annontated with #[macro_use] and sometimes not and it confused me.
I think use is quite easy to understand coming from another language, because it is similar to import statements in other languages. extern crate has no equivalent in other languages that I know of and it was confusing why it is needed when you also have to add the crate to cargo.toml.

It took a bit of googling to find out that as of Rust 2018 it is never required except for "internal" crates like proc_macro, std, core, alloc and test. And as far as I can tell #[macro_use] is never needed anymore.

Why do so many examples in the Rust books and crate documentation still use this? Are people still using Rust 2015? Did they not notice that it is not needed anymore? Or is it just legacy that nobody had the time to update? Clippy doesn't even have a lint for it, which I think is surprising considering these statements are just superfluous and confusing to newcomers.
Can somebody please explain this? Am I missing something obvious?

1 Like

If I remember correctly, we could call extern crate my_crate_from_crates_dot_io inside lib.rs (or a document test) and it would automatically be added to the system without needing to define it in Cargo.toml.

I thought extern crate to be no longer needed in the 2018 edition in favour of use, but recently I noticed that some crates only work when included with extern crate and not with use (such as pest), so I'm also wondering where the difference lies.

I believe that if a crate exports a proc macro, then you need to use extern crate to make use of it.

1 Like

Proc macros can be imported like any other item.

1 Like

extern <crate> syntax is only required in the niche cases you mentioned there, in 2018 edition. A lot of code and documentation was written prior to the release of 2018 edition, so it is simply a part of a larger effort to move docs to 2018 edition. One thing that should be kept in mind however, is that 2018 edition and 2015 edition both use the same compiler, it's simply a flag to switch.

So the straightforward answer to the question in the title is that examples should be migrated. New code should use 2018 edition, and people who write 2015 edition code will have to know the differences with the new 2018 edition (which aren't all that many).

The more complicated answer is that some documentation and examples for rust publish all versions of the book, so you might be reading version 1.0 of the book by accident, even though the latest and greatest is already out and is made for 2018 edition. It then also comes to the issue that crates that were developed a long time ago, and are no longer maintained (possibly because they've no need to be maintained). There are a plethora of potential issues which could impede the transition of examples from 2015 edition to 2018 edition, ranging from versioning to niche cases to simply lack of necessity.

In any case, both 2015 edition and 2018 edition are supported by the compiler, and always will be, hence it is recommended you learn to read 2015 edition. Mind you, there aren't all that many differences, and the ones that do exist are meant to be mostly invisible (Non Lexical Lifetimes (NLL)), or simply become silently unnecessary in the newer version (extern crate declaration).

3 Likes

No, you can't do that. You still have to add it in Cargo.toml.

2 Likes

NLL was backported to the 2015 edition, so all code uses it now.

It is still needed for internal usage.

" Note that you'll still need #[macro_use] to use macros you've defined in your own crate; this feature only works for importing macros from external crates."

https://doc.rust-lang.org/edition-guide/rust-2018/macros/macro-changes.html

Thank you for your elaborate answer! I noticed that a lot (most?) of the documentation and blogs on Rust was written in 2018, which was before Rust 2018 Edition. That is probably the reason why a lot of it still uses Rust 2015 style.

Why is Rust dedicated to not deprecate old versions at some point? Especially if they live on in the same compiler this has to make maintaining and testing the compiler more and more complicated over time. Is this really useful? Can't they just say "If you want to use Rust 2015 you can, but only up to Rust version x. If you want newer features, migrate to Rust 2018"? As far as I can tell migrating is pretty easy.

Shouldn't Rust learn from past mistakes, like that of Python to support Python 2 for so long? In my opinion this was the worst decision the Python community could have made, which led to the fact that even 12 years after the release of Python 3 some libraries still use Python 2.

The bad decision was the breaking change in the first place. Any mature programming language is going to have its share of unmaintained-but-still-useful libraries, and any change that prevents them from being used will face significant backlash. There are two main ways to reduce this problem:

  • Make breaking changes often. If nobody expects to be able to run code from 18 months ago, unmaintained projects will naturally die quickly and everyone will just get used to the effort required to stay on the upgrade treadmill.
  • Commit to backwards compatibility. Old software that works will continue to work, but bad decisions stay with the language forever. It”s more work for the language development team, which leads to a more conservative language design process.

The Rust team has chosen the second path, to take the burden of ensuring that working software continues to work upon themselves instead if imposing it on their users. I suspect it will lead to a lower maintenance cost overall, but concentrated in one place.

5 Likes

Just for completeness: while it is true that extern crate "statements" are not needed in edition 2018 anymore (except for test, or, for older compiler version, proc_macro), the #[macro_use] extern crate ... idiom can still be useful (in that it cannot be replicated by use "statements"). Indeed, once you #[macro_use] extern crate some_crate, all the macros exported by ::some_crate are in scope even inside (sub)modules:

//! Entrypoint: `lib.rs` or `main.rs`

#[macro_use]
extern crate log;

mod something {
    // `debug!` can be used here
}

vs.

mod something {
    // `::log::debug!` (or `log::debug!` if no (other) `log` item in scope) can be used here
}
// or
mod alternative {
    use ::log::debug; // or `log::debug` if no (other) `log` item in scope

    // `debug!` can be used here.
}

Basically, with use "statements", macros behave like any other item (function, type) regarding namespaces and scopes, but it can sometimes be convenient to be able to add something to this "global / ubiquitous scope" (called the prelude). And the only way to do so currently is by using #[macro_use] extern crate ....

Ideally, there should be an actual mechanism to customize one's prelude, and once that's available, #[macro_use] extern crate could definitely be deprecated.


Also, see this other post where I detail how one can "hand-roll a custom prelude": What's the difference between 'extern crate' and write into cargo.toml

2 Likes

The problem with Python is, as far as I know as someone not using Python myself, the fact that you can't just mix and match Python 2 and Python 3 code. It is not possible to migrate to Python 3 before all your dependencies have been. Those dependencies may want to keep using Python 2 to avoid breaking projects that still use Python 2. This is a cycle that can be hard to overcome. Rust on the other hand chose to make it possible to mix Rust 2015 and Rust 2018 crates almost seamlessly in the same project. This makes it much easier to gradually switch from Rust 2015 to Rust 2018 without needing something like a flag day.

8 Likes

" all the macros exported by ::some_crate are in scope even inside (sub)modules"

Hmm, isn't it the case that macros "live" at the root of a crate ( have crate scope), even if they are defined in a sub-module?

Right, I see there was an ambiguity in they way I phrased it: I meant submodules from the downstream / user crate, such as something or alternative in the examples above.

Indeed, if a crate exports a macro they defined, be it through a #[macro_export] macro_rules! ..., or, in the case of a proc-macro = "true" crate, through a #[proc_macro...] pub fn ..., will appear as if they were defined at the root (crate:: path) of that crate even if they weren't.

  • But if a crate re-exports a macro (rather than defining it and exporting it), then the macro will appear at the path it was re-exported.

    See the following "hacks" to get macros exported from within a submodule:

    //! Crate exporting a macro
    mod submodule {
        #[macro_export] #[doc(hidden)] /** Not part of the public API **/
        macro_rules! __foo__ { ... }
    
        pub use crate::__foo__ as foo;
    }
    

    Then a downstream user would write ::external_crate::submodule::foo!.

    • Problem: the re-exported macro is currently #[doc(hidden)] too :sob:

    • Annoylingly cumbersome solution: use an extra "upstream" crate to define the initial __foo__:

      Click to see
      mod submodule {
          pub use ::macros::foo;
      }
      
      # `./Cargo.toml`
      [dependencies.macros]
      version = "..." # Same as package version
      path = "src/macros/mod.rs"
      
      [workspace]
      members = ["src/macros", ]
      
      # `src/macros/Cargo.toml`
      [lib]
      path = "mod.rs"
      
      [package]
      version = "..." # Same as main package version
      
      //! `src/macros/mod.rs`
      
      #[macro_export]
      macro_rules! foo { ... }
      
      /// or, if [lib] proc-macro = true in `src/macros/Cargo.toml`
      #[proc_macro...] pub
      fn ...
      
      • Sub-problem: $crate does not work anymore :sob:
3 Likes

I am concerned about this implicit importing of macros. I can imagine an accidental name collision where I think I'm using foo!() from one crate, but I forgot to extern it. Instead, I get a foo!() from another crate that I did extern. Even if the functionality is the same, the quality and security guarantees might be different, e.g., a random number macro.

I think the solution would be to never use extern crate and to use use instead to import only the macros you want.

I have it my mind that use does not actually import anything and it might avoid confusion to not talk about it like that.

For example in my program I can write:

use clap::App;
...
...
let matches = App::new("my_prog")
...

Or not have use at all:

let matches = clap::App::new("myprog")

Clearly clap is available in my program, it has been imported somehow. I did not need use to import it.

The actual import is down to the deps I write in my Cargo.toml.

The use statement is only providing a short hand for what I have imported already.

Or am I looking at this wrongly?

4 Likes

The meaning of "import" I see here is "import from the global namespace into the local namespace", creating a local App binding that refers to ::clap::App.

(Though there are exceptions like use panic_abort as _; because of what seem like compiler bugs in not actually including all crates if they're unreferenced).

One could indeed make that interpretation.

But that is mightily confusing because I "import" the names of my structs, functions etc from other files in my program with:

mod my_module;

and I might also create a shorthand for those things with:

use my_module::my_func;

Having only the use here would not "import" anything.