Developing IoT Applications with Rust: Using a Rust Development Environment
Developers are most productive when they have tools to support all aspects of the software development cycle.
This includes programming languages, library and package managers, and testing and deployment tools. The Rust programming language has matured to the point that its ecosystem now includes an array of key support tools. One of these tools is Cargo.
Cargo is a package manager that supports all the core tasks of package management in both binary and library form. Cargo handles a lot of tasks, such as building your code, downloading the libraries your code depends on, and building those libraries, or dependencies.
Once Rust is installed, you canuse Cargo’s from the command line. Cargo commands are intuitive to the point of being obvious. For example, the cargonew’ command instructs Cargo to create a new package, which includes a directory structure for the package and a specification for the package, which is called a manifest. The manifest will contain metadata used by the Rust compiler.
Application code often has dependencies on other libraries and artifacts. When a Rust application has dependencies, Cargo can get those dependencies during the build process. Not surprisingly, to build a package, we use the ‘rustc’ command, to call the Rust compiler.
Cargo observes conventions regarding package directory structure. Subdirectories include src (for source code), benches (for benchmark code) and tests (for integration test). The default executable file is src/main.rs. However, you can find other binaries in /src/bin/.
Cargo’s configuration metadata is defined in Cargo.toml and will be created and maintained manually. Another metadata file, Cargo.lock, contains metadata about dependencies. Unlike Cargo.toml, Cargo.lock is
maintained by Cargo and won’t be edited by developers. Configuration data is specified in TOML format, which is designed to have obvious semantics.
Cargo.toml contains sections describing the package, dependencies and features. The package section includes name, version, authors, path to build script, and license information. The dependencies section holds package dependencies, test dependencies, and build dependencies. Lastly, the features section specifies conditional compilation conditions, which is useful when working with multiple environments that need different configurations.
Developers can specify integration tests and run those tests using the ‘cargo test’ command. It’s common practice to use both unit tests and integration tests. Unit tests are designed to test one component or application function at a time, while integration tests will test the application in ways similar to how the application will be used in production. By convention, Rust expects to find tests in the tests subdirectory of a package. Each file in the tests subdirectory is compiled as a separate crate.
Often, developers will re-use common functions across tests and this is easily done with Rust. Developers can create public functions in modules and then import them into tests.
When looking at the organization of a Rust application, there are multiple levels of structure. The lowest level is the function, common across programming languages. Modules are different related functions grouped into a single file. Crates, the next level up, contain modules and, as noted earlier, are the unit of compilation. The last organizational unit is workspaces. These are used to organize multiple crates in the same application.
Rust extends the advantages of this modular application structure beyond just what one developer creates. The Crates.io site contains tens of thousands of crates that are available for use. There are widely used crates for common functional requirements, like generating random numbers, logging, and parsing command line arguments. In addition, there are many specialized crates, including ones for IoT application development. There will be more specifics on those IoT crates in the next article of this series.