Library crates vs. binary crates
- A library crate is a crate that other people can pull into their projects as a dependency
- We can create one using a command like
cargo new communicator --lib
, where âcommunicatorâ is the name of our crate- Notice how we pass
--lib
instead of--bin
at the end of our command
- Notice how we pass
- This creates a file
src/lib.rs
instead ofsrc/main.rs
that contains an example test
- We can create one using a command like
The module filesystem
- We can define a module in Rust with the following syntax:
mod network {
fn connect() {
}
mod client {
fn connect() {
}
}
}
- Here, we define 2 modules: ânetworkâ and âclientâ
- If we want to call the âconnectâ functions in this code outside of the network module, we need to use the
::
operator - Notice that even though the two functions are named the same, theyâre namespaced differently - as
network::connect
andnetwork::client::connect
- Thus they can have completely independent implementations and donât conflict!
- This is analogous to file systems. You can think of âclientâ as a directory within ânetworkâ (its parent directory)
- If we want to call the âconnectâ functions in this code outside of the network module, we need to use the
- Now letâs say we want to define a third module, âserverâ, but want to initialize it somewhere else to avoid cluttering this file
- Then we can declare
mod server;
at the top of our file and then create a fileserver.rs
in the root directory that contains our module definition- This is basically telling the compiler to look for a file
src/server.rs
and reference it for all function definitions from this module - By default Rust only knows about
src/lib.rs
, and we need to declare other files that we want to bring into scope
- This is basically telling the compiler to look for a file
- Be careful not to surround your code in
server.rs
withmod server {
- this is implicit!
- Then we can declare
- Now letâs say we have a submodule defined in
server.rs
with the following contents:
mod auth {
fn login() {
}
}
- And letâs say that our âloginâ function is super long and we wanted to cut down on the clutter
- Simply declaring
mod auth;
and defining auth insrc/auth.rs
would trigger a compiler error, because weâre declaring this in a submodule - Thus we need to move the content of
server.rs
intoserver/mod.rs
and then move our âloginâ function intoserver/auth.rs
- Now, we can proceed with initializing
mod auth;
inserver/mod.rs
!
- Simply declaring
- In summary:
- If a module has no submodules, then you can put its declarations in
[name].rs
file - If a module does have submodules, then place its contents in
[name]/mod.rs
and its submodule(s) in[name]/[submodule_name].rs
- If a module has no submodules, then you can put its declarations in
Controlling visibility with pub
- So, why does the compiler warn us that the function declaration
auth::login
is going unused? Isnât this technically misleading, since libraries are supposed to be used by users/other programs?- No! This is because the default state of all code is private, which means that the function
auth::login
is private and if we donât call it within our library code, no other file has rights to use it, even if itâs called in viaextern crate communicator
andcommunicator::network::server::auth::login();
- When a function is marked as public (which must be done explicitly), Rust assumes that itâll be called outside the current module and those compiler warnings go away
- No! This is because the default state of all code is private, which means that the function
- We can declare a function as public by adding the
pub
keyword before its declaration- But to make
auth::login
public, we need to make both âauthâ and the function âloginâ public, as follows:
- But to make
// [src/network/mod.rs]
pub mod auth;
// [src/network/auth.rs]
pub fn login() {
}
- General rules for item visibility:
- If an item is public, it can be accessed through any of its parent modules
- If an item is private, it can be accessed only by its immediate parent module and any of the parentâs child modules
The use
keyword, glob
syntax, and super
- Letâs say we have a function nested within several modules, that we need to access like
a::series::of::nested_modules()
- This can get quite lengthy, so we take advantage of Rustâs
use
keyword to bring part of the path into scope
- This can get quite lengthy, so we take advantage of Rustâs
- Letâs say for example that weâre using a bunch of functions defined in the âofâ module and donât want to specify this lengthy path over and over again; we can write the following:
use a::series::of;
fn main() {
of::nested_modules();
}
- Notice that all of the children of modules arenât brought into scope - just the modules themselves (this is why we still need to specify âofâ in
of::nested_modules()
) - Since enums form a namespace as well, we can do a similar thing for them!
enum TrafficLight {
Red,
Yellow,
Green,
}
use TrafficLight::{Red, Yellow};
fn main() {
let red = Red;
let yellow = Yellow;
let green = TrafficLight::Green;
}
- If we want to pull in all items in a namespace at once, we can use the
*
syntax, which is called the glob operator. Below is a refactoring of the code we just wrote using this syntax:
use TrafficLight::*;
fn main() {
let green = Green;
}
- Use globs sparingly, because they usually bring more than what you need into context and sometimes cause naming conflicts
- Letâs take the following code snippet:
pub mod client;
#[cfg(test)]
mod tests {
fn call_connect() {
client::connect();
}
}
- Our call within
call_connect()
would result in a compiler error, because technicallyclient::connect
belongs to the root (or in this case, parent) module and we are within a submoduletest
when weâre calling it - There are two ways to fix this:
::client::connect()
, where the::
prefix means that we need to specify the full-length path from root- Usually not preferred because itâs quite lengthy in most cases
super::client::connect()
, where thesuper::
prefix moves us up one level and allows us to call any sibling modules (whichclient
qualifies as)- Itâs annoying to type
super::
before every declaration, so we usually just include something likeuse super::client;
at the top of our module
- Itâs annoying to type
References
- Chapter 7 of The Rust Programming Language by Steve Nichols and Nicole Klabnick.