modules

George Shuklin
journey to rust
Published in
8 min readAug 9, 2017

It was a long and busy delay, but I’m back.

Step back

I’d tried to cheat on my learning process… More specific, I’d tried to change the way I read tutorial. I simply read few chapters just before going to bed, it was easy to do.

…Good luck, that I caught early enough that I no longer really follow the book. I realized this at the moment when some error message was discussed in the book, and I hadn’t understood what happens and why. The error in question was simple and trivial but I didn’t payed enough attention to stuff before it to understand context even for trivial errors.

Therefore, I find my original way to learn a language — by digging into tiny-tiny details, by asking random questions and trying to find answer even if answer is in the next chapter, much more productive. It gives me not only sense of familiarity to all aspects, as I follow the text, but also prepare my questions to be answered by book in the future. If I receive answer without proper question in my head, there is a high chance that I simply glance over it, instead of understanding it.

Therefore, I return back to my old style, which is very slow, yet produces the best result.

--lib

When cargo creates a library package, it creates a src/lib.rs file. And that file is compiled by cargo build command. How it search for files to compile? Just *.rsin src?

But before answering that question I have stumbled upon more intriguing mystery. I decided to test if lib.rs has been actually compiled and made a deliberate mistake in it. Amazingly, the project compiled without errors. I suspected that it does not compile lib.rs, and put more junk in it. This caused error, as expected. So I got invalid code which is … valid. What’s going on?

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
a
}
}

There is no variable ‘a’, at least, to my knowledge. There is a thing called ‘prelude’ in Rust programs, as I learned earlier. Is ‘a’ a part of prelude?

I switched to previous project where I have main.rs.

This code fails to compile:

fn main() {
println!("{}", a);
}

… and this code does not compile too:

fn boo(){
a
}

Now it’s a mystery.

I switched back to ‘library’ project and made few experiments.

  1. Any valid syntax is OK, not only variable with name ‘a’
  2. Invalid syntax cause errors
  3. I can pass non-existing variables into functions as well as return them from functions.

I’ve been mangling source until I found a line which cause magic:

#[cfg(test)]

(not like this line is less magic to me…)

When I removed it, compiler came to senses and started to complain ‘not found in this scope’ for all undefined variables.

A google pointed me back to the Rust Book, to chapter 11. There was the definitive answer:

The #[cfg(test)] annotation on the tests module tells Rust to compile and run the test code only when we run cargo test, and not when we run cargo build.

And, indeed, all my mischefefe with undefined variables become a compiler victim as soon as I run cargo test. Mystery solved, therefore I returned back to my original question: how cargo finds files to compile?

I created ‘bad.rs’ file in my project with bad content. It was ignored by build command.

I tried to link lib.rs to bad.rs by ‘use bad;’, but it didn’t been accepted by compiler.

I renamed ‘bad.rs’ into main.rs, and compiler found errors in it.

I returned back to my previous project and add ‘incorrect’ lib.rs into project. It was ignored by cargo build command.

It looks like cargo has a fixed list of names for different types of projects. I’ll come back to this question later.

modules

I found this line in the book: While in this case we’re building a library, there’s nothing special about src/lib.rs.

It’s not true, as I showed before, that main.rs and lib.rs will be used by default by cargo, but other names will not.

Other files will be compiled only if they were referenced by lib.rs (but not by main.rs, as ‘use client;’ in main.rs cause error “ no `client` in the root”).

I done modules chapter with heavy heart.

Whole area of file naming, regardless of been carefully explained in chapter, still looks murky to me. Too many special names. mod.rs, main.rs, lib.rs, and some of them have meaning at top level, some in sub-directory. Messy. For the sake of justice I need to acknowledge that file naming and meaning of ‘special filenames’ are messy in all languages. Just look how empty __init__.py inside of directory with tests changes behavior of import of tested module (hello, py.test, I’m talking about you).

Publicity

Rust requires amazing amount of attention to all not-the-safest-in-the-world features of language. We need explicit mut for mutable variables, and we need explicit pub for every piece we want to expose. If we want to expose some function in module we need two pubs: first for the module and second for the function itself.

… Sudden question: What will happen if I put pub inside non-library?

  1. I can compile it.
  2. If I add pub to the main it does not change debug or release binaries.
  3. Actually, I just found that Rust creates reproducible builds! Same code after compiling yields same binary (on the same compiler!). No more dh-strip-nondeterminism magic! You need just compile sources to check if binary has been produced from the same source code. Amazingly, even debug builds are reproducible.

Step back again

I tried to solve ‘exercise’ for modifying code to rid of the last warning, but I suddenly realize that I have no idea what is ‘communicator’ stands for in this code:

extern crate communicator;fn main(){
communicator::client::connect();
}

Cargo.toml? Yes, indeed.

[package]
name = "communicator"

… and, again, what’s the mod function? Ok, getting back to the chapter beginning.

  • module (mod) is a namespace. For the python developers: module in Rust is not bounded to specific file or directory. You may declare few modules inside a single file, or you may have a single module (namespace!) been spread into few files. You even may have a single file with few modules and a ‘main’ using those modules.
  • You may hide functions inside that single file from different functions in the same file:
mod foo {
pub fn bar() {
}

fn never(){
}
}
fn main() {
foo::bar();
}

never is hidden, and one may not call it from main.

  • That means that visibility in Rust by default is tied by file scope, but it may be easily changed in both directions (more or less visibility as needed).
  • I played with use keyword, and it has as postfix:
use foo::bar as baz;
  • use keyword has nothing to do with ‘module management’, it just join some namespace within current. extern keyword used to point to other modules.
  • There is a way to steal from one namespace into another. Example:
mod baz {
pub fn trivial() {
}
}
mod foo {
pub use baz::trivial as boo;
}
fn main() {
foo::boo();
}
  • mod keyword used in two context: if it has body (stuff in {}) it creates module. If it used without body, but with ‘;’, it:
  • - declares that module (at syntax level)
  • - forces compiler to search for that code around. mod keyword makes compiler to include another file into compilation process. That was really important thing and I missed it while I read chapter.
  • mod.rs filename inside a directory is similar to __init__.py for Python.
  • First time I missed how important is difference between package and crate in Rust. Package is ‘the thing’ created by cargo new, and crate is an inner unit of structuring. (Description is vague as I haven’t got it completely yet). extern crate command allow me to use crate from the same package or from other packages.

It’s very murky. I declare crate in Cargo.toml, and that gives me one layer of namespace. I’ll use name="alice" for this test.

So, Cargo.toml declares name=”alice”.
lib.rs: pub fn foo(){ }
main.rs: extern crate alice; fn main(){ alice::foo(); }

And it works. Now, if I want to add some modules inside alice, I’ll use pub mod alice{...} inside lib.rs. If I wish to use it, it would be alice::alice::foo().

If I write mod alice; in lib.rs, and creates alice.rs file with ‘pub fn foo() {}’, it would still be used as alice::alice::foo().

If I write pub mod alice { pub fn foo(){} } inside lib.rs, it would needed to use the same alice::alice::foo() from main.

If I remove alice.rs and instead will write code from above pub mod alice { pub fn foo(){} } into alice/mod.rs, it still would be called as alice::alice::alice::foo().

If I rename alice/mod.rs into alice/alice.rs, than I need to point compiler to it. I need to create alice/mod.rs with pub mod alice; declaration. That code can be called with 4th alice: alice::alice::alice::alice::foo();

Rules

Each of those add one level into namespaces list:

  • extern crate + Cargo.toml + code in lib.rs
  • mod inside lib.rs (body may be inside ‘mod’, or in separate file without word mod).
  • mod inside separate file or in separarate directory with ‘mod.rs’.
  • declaring mod inside directory/mod.rs

Mess, mess. Complicated rules I need to learn by heart.

More than one crate

Ok, extern crate allow me to reference to lib.rs from main.rs. But can I have more than one crate in one package?

The answer is yes and no. Yes, it’s possible. No, it’s just nested packages. Example (found via reddit).

use and super

I glanced over ‘use’ usage as it was obvious. I found my myself ‘as’ keyword, and globbing with enumeration were simple. One surprising news was that enum is a namespace, and may be dealt accordingly (used).

All accesses to objects are relative to the current path in the package, but use counts paths from the root of the package.

Use ‘::’ to point to root.

Use ‘super::’ to move one step above.

Use use with super:: for relative ‘use’.

Conclusion

So far ‘module’ chapter was the hardest. Nice expressive abstractions felt into muffed list of rules. They kinda ‘ok’, still I don’t like them. The problem is that those ‘ok rules’ are tightly linked into core part of the language syntax governing visibility of objects. It’s critically important and yet annoyingly complicated. There are file rules, and there are ‘super::' rules, use behaves differently from everyone else, Cargo.toml influence interpretation of lib.rs content.

Actually, no, I’m not OK with those rules. I start to hate them. Not at the same level of hate, as setup.py mysteries, but still, I dislike them a lot.

Postscriptum

While I wrote this test, new Rust has been arrived on my home machine. Rust 1.18 replaced rust 1.17. And there were new rules to learn! How lucky am I! I can learn more of those dull rules!

pub keyword now may have a restriction, like this: pub(crate) bar; or this: pub(in a::b::c) foo;

That means we have now ‘imperative visibility rules’. In addition to inheritance governed by the long list of generic rules we may start adding explicit rules for specific cases. It this good or bad?

--

--

George Shuklin
journey to rust

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux. My hobby is Rust.