Keywords: Rust package management | Cargo configuration | library and executable
Abstract: This article provides a comprehensive analysis of how to structure Rust packages that contain both reusable libraries and executable binaries. By examining Cargo.toml configurations, source code organization, and module system mechanics, we explore three primary implementation approaches: explicit configuration, default path conventions, and workspace solutions. The paper focuses on technical details of the optimal practice, including explicit lib/bin declarations, path configurations, and module system improvements since Rust 2018, while comparing alternative approaches with their respective use cases and trade-offs.
Introduction and Problem Context
In Rust ecosystem development, a common requirement is creating packages that contain both reusable libraries and executables that utilize those libraries. This structure allows developers to encapsulate core logic in libraries while providing convenient command-line tools or applications. However, Rust's module system and Cargo build tool offer multiple implementation approaches, each with specific configuration requirements and applicable scenarios. This article systematically analyzes solutions to this problem based on high-quality Q&A data from Stack Overflow.
Core Solution: Explicit Configuration Approach
According to the best answer (score 10.0), the most direct and flexible implementation involves explicitly declaring library and binary targets in the Cargo.toml file. This approach provides maximum control, allowing developers to customize target names and source paths.
The project directory structure appears as follows:
.
├── Cargo.toml
└── src
├── bin.rs
└── lib.rs
The Cargo.toml configuration file must contain:
[package]
name = "mything"
version = "0.0.1"
authors = ["me <me@gmail.com>"]
[lib]
name = "mylib"
path = "src/lib.rs"
[[bin]]
name = "mybin"
path = "src/bin.rs"
In the library file src/lib.rs, we define publicly accessible functions:
pub fn test() {
println!("Test");
}
In the binary file src/bin.rs, we utilize the library:
use mylib::test;
pub fn main() {
test();
}
Notably, since Rust 2018 edition, extern crate declarations are no longer required, as Cargo automatically resolves modules based on dependencies in Cargo.toml. This explicit configuration approach offers clarity and is particularly suitable for projects requiring multiple binary targets or non-standard paths.
Alternative Approach 1: Default Path Conventions
The second approach (score 7.3) leverages Cargo's default conventions without modifying Cargo.toml. This approach has two variants:
Simple Variant: Create src/main.rs as the default executable. When a project contains both src/lib.rs and src/main.rs, Cargo automatically compiles src/lib.rs as a library and src/main.rs as an executable with the package name.
Project structure:
.
├── Cargo.toml
└── src
├── lib.rs
└── main.rs
src/main.rs can directly use the library:
fn main() {
println!(
"I'm using the library: {:?}",
example::really_complicated_code(1, 2)
);
}
Flexible Variant: Create a src/bin directory to store multiple binary files. Each .rs file compiles into a separate executable, with the filename becoming the binary name.
Project structure:
.
├── Cargo.toml
└── src
├── bin
│ └── mybin.rs
└── lib.rs
Run specific binary: cargo run --bin mybin
This approach's advantage lies in simplified configuration, adhering to the "convention over configuration" principle, making it ideal for small projects or rapid prototyping.
Alternative Approach 2: Workspace Solution
The third approach (score 3.2) suggests using Cargo workspaces to separate libraries and binaries into distinct packages. This solution suits medium to large projects where libraries and binaries may have different dependency requirements.
Project structure:
the-binary
├── Cargo.toml
├── src
│ └── main.rs
└── the-library
├── Cargo.toml
└── src
└── lib.rs
Binary package's Cargo.toml:
[package]
name = "the-binary"
version = "0.1.0"
edition = "2018"
[workspace]
[dependencies]
the-library = { path = "the-library" }
Key advantages of this approach include:
- Dependency Isolation: Binaries can include dependencies specific to user interfaces (like command-line parsers) without "polluting" the library's dependencies.
- Build Optimization: Workspaces share build caches, avoiding redundant compilation.
- Code Organization: Clear separation of concerns facilitates team collaboration and code maintenance.
Technical Details and Best Practices
When selecting an implementation approach, consider these technical factors:
Module System Evolution: Rust 2018 edition introduced significant module system improvements. In earlier versions, binary files required explicit extern crate mylib; declarations. Since 2018 edition, Cargo automatically handles module imports based on dependencies in Cargo.toml, resulting in cleaner code.
Path Resolution Rules: Cargo resolves source paths in this order:
- If explicit
[lib]or[[bin]]configurations exist, use configured paths - Otherwise, check default paths:
src/lib.rs,src/main.rs,src/bin/*.rs - Workspace solutions follow independent sub-package configurations
Version Compatibility: Explicit configuration works with all Rust versions, while automatic dependency resolution requires Rust 2018 or later. Projects needing backward compatibility must consider version differences.
Build Performance Considerations: For large projects with multiple binary targets, workspace solutions offer better incremental compilation performance since library changes don't trigger complete recompilation of all binaries.
Approach Comparison and Selection Guidelines
The following table summarizes characteristics of the three main approaches:
<table> <tr> <th>Approach</th> <th>Configuration Complexity</th> <th>Flexibility</th> <th>Use Cases</th> <th>Cargo.toml Modification</th> </tr> <tr> <td>Explicit Configuration</td> <td>Medium</td> <td>High</td> <td>Projects needing custom names or paths</td> <td>Required</td> </tr> <tr> <td>Default Path</td> <td>Low</td> <td>Medium</td> <td>Small projects, rapid prototyping</td> <td>Not required</td> </tr> <tr> <td>Workspace</td> <td>High</td> <td>Highest</td> <td>Medium/large projects, dependency isolation needed</td> <td>Required</td> </tr>Selection recommendations:
- For learning purposes or simple tools: Default path approach
- For libraries with reference implementations to publish on crates.io: Explicit configuration approach
- For enterprise applications or frameworks: Workspace approach
Conclusion
Rust's Cargo build tool provides flexible and powerful package management capabilities, supporting multiple organizational patterns for packages containing both libraries and executables. The explicit configuration approach serves as best practice with clear control interfaces; default path conventions simplify configuration for small projects; workspace solutions offer advanced code organization and dependency management for complex projects. Developers should select the most appropriate approach based on project scale, team structure, and distribution requirements. As the Rust ecosystem matures, these patterns will continue evolving, providing solid foundations for building maintainable, high-performance software systems.