One Package — Two Entities: Myth or Reality?
\n\nImagine: you've written an amazing Rust library for working with big data. It's lightning-fast, safe, and elegant. But you need not only an API for other developers, but also a ready-made CLI utility to test the library or use it in pipelines. A classic question arises, one that has gathered thousands of views on Stack Overflow: "Package with both a library and a binary?"
\n\nFor a Rust beginner, this sounds like a puzzle. For an experienced developer, it's a task of optimizing project structure and performance. In this article, we won't just answer this question — we'll do it with a focus on optimization and performance — the two things Rust is most loved for.
\n\nWe'll break down how to properly organize such a package, avoid code duplication, speed up compilation, and make your binary truly fast. Let's go!
\n\nPackage Structure: Cargo.toml Decides Everything
\n\nIn Rust, a single Cargo package can contain both a library (lib) and multiple binaries (bin). This isn't magic, but a simple folder structure convention. Let's look at a minimal example.
\n\nCreate a project:
\n\ncargo new my_tool --lib\n\nNow modify the Cargo.toml file to explicitly specify that we have both a library and a binary:
[package]\nname = "my_tool"\nversion = "0.1.0"\nedition = "2021"\n\n[lib]\nname = "my_tool_lib"\npath = "src/lib.rs"\n\n[[bin]]\nname = "my_tool"\npath = "src/main.rs"\n\n[dependencies]\n\n\nThe folder structure will look like this:
\n\nmy_tool/\n├── Cargo.toml\n├── src/\n│ ├── lib.rs // Library\n│ └── main.rs // Binary\n\n\nSimple, right? But the devil is in the details. If you just copy the code from the library into main.rs, you'll get duplication and performance issues. You need to be smarter.
\n\nThe Right Approach: Binary Uses the Library
\n\nThe binary should import the library as an external crate. Even if they are in the same package. This not only reduces duplication but also allows the Rust compiler to better optimize code through cross-crate optimization (LTO).
\n\nExample src/lib.rs:
pub fn calculate(data: &[u64]) -> u64 {\n data.iter().sum()\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_calculate() {\n assert_eq!(calculate(&[1, 2, 3]), 6);\n }\n}\n\n\nExample src/main.rs:
use my_tool_lib::calculate;\n\nfn main() {\n let numbers = vec![10, 20, 30];\n println!("Sum: {}", calculate(&numbers));\n}\n\n\nNotice: we use my_tool_lib — this is the name we set for the library in Cargo.toml. The compiler will automatically link them into one binary, while maintaining modularity.
Performance Optimization: LTO, Inline, and Benchmarks
\n\nNow that the structure is ready, let's talk about the main thing — how to squeeze the maximum out of your package. After all, if you're creating a data processing tool, every microsecond counts.
\n\n1. Enable Link-Time Optimization (LTO)
\n\nLTO allows the compiler to optimize code across crate boundaries. Without LTO, functions from the library and binary are optimized separately. With LTO — as a single unit.
\n\nAdd to Cargo.toml:
[profile.release]\nlto = true\ncodegen-units = 1\n\n\nThis will increase compilation time, but will make the binary noticeably faster. For the educational site pythonlib.ru, it's important to emphasize: LTO is the standard for production builds in Rust.
\n\n2. Use Inline Hints
\n\nIf there are critically important functions in the library,