Hur många gånger har du inte öppnat ett nytt C++ projekt och först behövt lista ut om det
byggs med make, CMake, Bazel eller ett handskrivet skript — och sen vilket av
Conan, vcpkg eller FetchContent som sköter tredjepartsbiblioteken? I Rust-världen
finns inte den frågan. Ett enda verktyg, cargo, skapar projektet, kompilerar det, kör
testerna, genererar dokumentationen och hämtar hem beroenden — med samma kommandon,
i alla projekt, från dag ett. I den här artikeln går jag igenom hur cargo fått till detta,
varifrån idén kommer, och avslutar med ett litet demo-program som lånar in ett par
tredjepartsbibliotek för att rita en Normal-fördelning som ett stjärn-histogram.
Kompilering av Rust
Med bara en enda källkodsfil hello.rs är det enkelt att kompilera till en exekverbar fil.
fn main() {
println!("Hello from a Rust program");
}
$ rustc hello.rs -o hello-rs
$ ./hello-rs
Hello from a Rust program
Principen är ju den samma som för en C++ källkodsfil hello.cxx.
#include <print>
int main() {
std::println("Hello from a C++ program");
}
$ g++ -std=c++23 -Wall hello.cxx -o hello-cxx
$ ./hello-cxx
Hello from a C++ program
Blir det *.o filer också i Rust?
Javisst, det fungerar på samma sätt. Normalt genererar kompilatorn dessa i en
temporär-katalog och tar bort dessa vid ett lyckosamt resultat. I Rust kan man
be kompilatorn om att spara *.o filen på följande vis.
$ rustc --emit=obj,link hello.rs -o hello-rs
$ ls -lhF *-rs*
-rwxrwxr-x 1 jensr jensr 4.2M Sep 3 16:37 hello-rs*
-rw-rw-r-- 1 jensr jensr 5.6K Sep 3 16:37 hello-rs.o
$ nm --demangle hello-rs.o
0000000000000000 r GCC_except_table8
0000000000000000 T main
0000000000000000 T std::rt::lang_start::<()>
0000000000000000 t std::sys::backtrace::__rust_begin_short_backtrace::<fn(), ()>
0000000000000000 t std::rt::lang_start::<()>::{closure#0}
0000000000000000 T hello::main
U std::rt::lang_start_internal
U std::io::stdio::_print
# Stripped for brievity
För C++ ser ut det ut på liknande sätt, fast man behöver utföra det i två steg. Först kompileringen och sedan länkningen.
$ g++ -std=c++23 -Wall -c hello.cxx -o hello-cxx.o
$ g++ hello-cxx.o -o hello-cxx
$ ls -lhF *-cxx*
-rwxrwxr-x 1 jensr jensr 241K Sep 3 16:44 hello-cxx*
-rw-rw-r-- 1 jensr jensr 458K Sep 3 16:42 hello-cxx.o
$ nm --demangle hello-cxx.o
...812 lines of output...
Byggnationsverktyg
Så fort man har fler än några få källkodsfiler blir det snabbt besvärligt att hålla reda på olika kompileringskommandon. I UNIX barndom (första hälften av 1970-talet) var shell-scripts ett viktigt hjälpmedel. Ett dylikt är en textfil med vilka kommandon som ska utföras och operativet (kernel) känner igen ett skript och utför instruktionerna.
Med ett flertal källkodsfiler och olika typer av genereringar med flera, blev det besvärligt även med shell-scripts, eftersom man gärna önskade att bara kompilera om de filer som behövdes.
Stuart Feldman vid Bell Labs (det legendariska stället som frambringat UNIX, C, C++ och mycket annat)
skrev den första versionen av make, där man kunde skapa kompileringsregler och verktyget
beräknade vilka filer som behövde omkompileras och i vilken ordningsföljd. Under flera
decennier var make det primära byggverktyget, även för Java under andra hälften av 1990-talet.
För just Java-världen —i takt med att språket blev allt populärare— blev det
pinsamt att man inte kunde bygga plattformsoberoende, eftersom det var ju själva språkets
USP (Unique Selling Point). Direkt efter millennieskiftet skapade James Duncan Davidson
Java-verktyget Ant (Another Neat Tool), där man skapade byggregler i en XML-fil.
I takt med att Java blev ännu mer populärt och det skapades en kopiös mängd av open-source
bibliotek, uppstod behovet av en rationell och enkel hantering av primära bibliotek och
alla deras sekundära bibliotek (beroenden). Några år senare (2002) skapade Jason van Zyl
den första versionen av Maven, vilket snabbt kom att ersätta Ant. Maven byggde på några
genomgripande principer:
- Configuration by Conventions
- En standardstruktur för projektkatalogen, så hittar alla verktyg källkodsfilerna
- Standard set of commands
- Standardiserade kommandon för att kompilera, testa, bygga, skapa dokumentation
- Dependency Management & Maven-Central
- Via en s.k. maven-koordinat (grupp, namn, version) kunde verktyget självt ladda ned bibliotek och inkorporera dessa för byggnation och exekvering
- Plugins
- Maven core är en plugin-motor med de väsentliga reglerna inbyggda, resten utgörs av plugins, vilka kan kompletteras eller ersättas och möjliggör obegränsat med utökningar
Det här med att skapa en central plats för bibliotek (server) och ett kommandoradsprogram, som söker, laddar ned, installerar och inkorporerar bibliotek, tog Jason från CPAN.
Under 1990-talet i takt med att språket Perl blev populärt uppstod behovet av en dylik tjänst. CPAN (Comprehensive Perl Archive Network) skapades i början på 1990-talet och bidrog starkt till att göra Perl industriellt tillämpbart. Man brukade skämta om att nästa krasch på Wall Street kunde bero på en bugg i ett Perl program.
CPAN var emellertid inte först med detta utan inom TeX-världen (typsättning för matematisk text) skapades några år innan, CTAN (Comprehensive TeX Archive Network). CPAN har fått många efterföljare inklusive CEAN (Comprehensive Erlang Archive Network) som Joe Armstrong vid Ericsson/Ellemtell i Älvsjö satte upp i slutet på 1990-talet.
Sedan CPAN och Maven Central, så har alla moderna programmeringsspråk en motsvarighet, dels en gemensam server med sökbarhet och en massa bibliotek och dels ett CLI program som sköter hanteringen på utvecklarsidan.
Emellertid, är det synd och skam att vi inte har motsvarande inom C++ världen. Det finns dessvärre
flera konkurrerande system, såsom Conan, vcpkg med flera. Även CMake har sin egen
FetchContent. Detta sammantaget gör att ingen hittills kunnat fungera enande, utan vi har
en diversifierad soppa av olika byggverktyg.
Cargo
Rust är tack och lov ett modernt programmeringsspråk och har därför den typ av supportsystem
man numera förväntar sig. Byggverktyget för Rust heter cargo och täcker i stort sett hela
arbetsflödet runt ett Rust-projekt:
- projektskapare —
cargo newsätter upp mappstruktur, manifest och ett git-repo - pakethanterare —
cargo addhämtar tredjepartsbibliotek från registret crates.io och håller reda på exakta versioner - byggverktyg —
cargo buildkompilerar projektet och allt det beror på, i rätt ordning, parallellt - testkörare —
cargo testhittar och kör alla tester i projektet - dokumentationsgenerator —
cargo docbygger en HTML-sajt ur doc-kommentarerna - publiceringsverktyg —
cargo publishladdar upp en crate till crates.io (utanför den här artikelns ämne)
Poängen är att allt detta följer med. Installerar man Rust via rustup får man rustc
(kompilatorn) och cargo i samma svep, och cargo är i praktiken det enda kommando man
behöver lära sig.
Så här skapar men ett nytt Rust-projekt:
$ cargo new hello
Creating binary (application) `hello` package
Resultatet är minimalt:
hello
├── Cargo.toml # projektets manifest
├── .gitignore # innehåller raden /target
└── src
└── main.rs # fn main() { println!("Hello, world!"); }
Cargo initierar dessutom ett git-repo på köpet. Med option --vcs <name> kan man initiera
för andra versionshanteringssystem (VCS - Version Control System), där name kan vara ett av
git, hg, pijul, fossil, none. Den sistnämnda innebär inget av dem.
Designen av cargo har inspirerats av Jason van Zyl's princip om configuration by conventions.
All källkod ligger under katalogen src/ och filnamnet main.rs är fixerat och styr att
cargo ska bygga en exekverbar fil. Direktiven till cargo är standardiserade likväl som i Maven,
cargo build|test|run|.... Alla byggartefakter hamnar i katalogen target/, precis som i
Maven.
Konfiguration (Cargo.toml)
Cargo har en primär konfigurationsfil (plus en för avancerade byggen) kallad Cargo.toml
och är på formatet TOML (Tom's Obvious, Minimal Language). Detta format skapades av Tom
Preston-Werner och är en renodlad och standardiserad form av INI-filer från den gamla MS-DOS
tiden. Så här kan en Cargo.toml se ut:
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]
Här ser vi namn och version på projektet, samt vilken språk-version projektet använder sig av.
edition är Rusts sätt att införa språkändringar utan att bryta gammal kod. Giltiga värden är
2015, 2018, 2021 eller 2024. Edition-värdet har viss likhet med -std=c++23 i C++,
men är mer genomgripande.
Lådprojekt (crate)
Ett Cargo-projekt benämnes som crate. Ordet betyder låda på svenska (faktiskt även ölback,
men det tiger vi om). Vi ser också att det finns ett tomt avsnitt för externa bibliotek
(dependencies), dvs external dependency crates. Detta ska vi diskutera litet senare i denna artikel.
Programbyggnation
Man kan kompilera och länka separat först (cargo build) och sedan exekvera (cargo run),
eller låta den sistnämnda utföra det förstnämnda.
$ cargo run
Compiling hello v0.1.0 (/home/jensr/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
Running `target/debug/hello`
Hello from a Rust program
Bibliotek
En crate kan också definiera ett bibliotek, dvs en crate som länkas till andra crates och
bildar ett program (exekverbar fil). Genom att byta namn på src/main.rs till src/lib.rs,
så hux flux har man ett biblioteks-projekt (library crate). Som synes, styr filnamnet huruvida
ens crate utgör en executable eller library.
my_project
├── Cargo.toml
└── src/
└── lib.rs
Måste man välja? Nej, en crate kan ha båda formerna src/{main,lib}.rs. Själva filnamnet är
dock fixerat. Givetvis kan man dela upp källkoden på flera filer, så kallade moduler, vilket vi
ska diskutera härnäst.
Moduler
I ett C++ program delar man upp koden i flera filer (.cxx), kompilerar dessa separat till objekt-filer (.o) och skriver inkluderings-filer (*.hxx) innehållande typ-definitioner och funktions-deklarationer, så att kompilatorn och länkaren kan para ihop ett anrop av en funktion med var denna är definierad. Med C++20 kom modules, som är tänkt att förenkla denna hantering och korta byggtiderna. Emellertid, är detta, ett drygt halvt decennium senare, fortfarande på experimentstadiet.
Bakom kulisserna i Rust försiggår ungefär samma operationer, men språket har haft stöd för moduler
redan från första början. Även här följer man configuration by conventions, genom att skapa
en underkatalog med modul-namnet och en fil med det standardiserade namnet mod.rs.
my_project
├── Cargo.toml
└── src/
├── main.rs
└── numbers/
└── mod.rs
Sedan, måste källkodsfilen på samma katalognivå deklarera modulen via mod <name>.
mod numbers;
fn main() { . . . }
Det finns en principiell och en pragmatisk sida av detta. Beträffande det sistnämnda så
klagade utvecklare på att ha en bunt med filer med samma namn (mod.rs) öppna i sin utvecklingsmiljö
(IDE). Så från edition 2018 och rustc 1.30, infördes en förenkling; man skapar kort och gott en
Rust fil med modul-namnet, på samma nivå som den som definierar modulen. Så här kan det se ut då:
my_project
├── Cargo.toml
└── src/
├── main.rs
└── numbers.rs
För mindre moduler, räknat i antal rader kod, kan man placera koden direkt i den fil som deklarerar denna. Detta kallas för en inline module.
fn main() {
println!("origo: {:?}", geometry::origin());
}
mod geometry {
#[derive(Debug)]
pub struct Point { pub x: f64, pub y: f64 }
pub fn origin() -> Point { Point { x: 0.0, y: 0.0 } }
}
Rust använder sig frekvent av annoteringar med syntaxen #[action(key=value...)].
En vanlig annotering är cfg(...) som står för configuration. Ett sådant exempel är
plattforms-beroende definitioner:
#[cfg(target_os = "linux")]
mod platform {
pub fn config_dir() -> String {
std::env::var("XDG_CONFIG_HOME")
.unwrap_or_else(|_| format!("{}/.config", std::env::var("HOME").unwrap()))
}
pub const LINE_ENDING: &str = "\n";
}
#[cfg(target_os = "windows")]
mod platform {
pub fn config_dir() -> String {
std::env::var("APPDATA").unwrap()
}
pub const LINE_ENDING: &str = "\r\n";
}
#[cfg(target_os = "macos")]
mod platform {
pub fn config_dir() -> String {
format!("{}/Library/Application Support", std::env::var("HOME").unwrap())
}
pub const LINE_ENDING: &str = "\n";
}
fn main() {
println!("config: {}", platform::config_dir());
print!("done{}", platform::LINE_ENDING);
}
Här ovan har vi också villkorlig kompilering (hejsan #ifdef), bara en av dessa tre alternativ
stannar kvar, resten plockas bort. Givet då att man inte försöker kompilera på en fjärde
plattform... 🫣
Annoteringarna ovan är inte på något sätt begränsade till inline modules, utan kan appliceras på vanliga fil-baserade moduler också.
Multipla program
För en crate gäller att man kan definiera högst ett bibliotek, men noll, ett eller flera program.
Vi har ju sett att ett program definieras med src/main.rs, så hur skapar man övriga? Jo, genom
att placera dessa under src/bin/. Om man så vill kan man betrakta main.rs som det primära
programmet och övriga i src/bin/ som sekundära. Emellertid, kanske man inte har något program som
är primärt, eftersom alla är likvärdiga. Då är det bättre att lägga samtliga program under src/bin/
för symmetrins skull.
my_project
├── Cargo.toml
└── src/
├── lib.rs
├── numbers.rs
└── bin/
├── generate.rs
└── print.rs
Hur anropar man då ett av flera definierade program? Jo, genom att bifoga namnet.
$ cargo run --bin generate
$ cargo run --bin print
Nu kan det vara så att man önskar att ett av dessa program ska kunna anropas utan att ange dess
namn, vilket låter sig göras i konfigurationsfilen med default-run.
# Cargo.toml
[package]
name = "hello"
default-run = "print"
. . .
Vidare, kanske man anser att namnet på den genererade exekverbara filen ska vara något annat, vilket
också kan åtgärdas i konfigurationsfilen med ett (eller flera) [[bin]] block. Dubbla hakparenteser
i TOML betyder att man skapar en lista, dvs multipla block.
# Cargo.toml
[package]
name = "hello"
[[bin]]
name = "print-baby-print"
path = "src/bin/print.rs"
# if you read my article about Apollo 11, you get the joke
. . .
Om man har både ett bibliotek plus ett (eller flera) program, så måste detta (dessa) importera biblioteket.
Namnet fås av vad som anges i Cargo.toml. Givet att projektet ser ut som nedan:
my_project
├── Cargo.toml
└── src/
├── lib.rs
└── main.rs
# Cargo.toml
[package]
name = "hello"
. . .
Så, ser Rust koden ut som följer:
// src/lib.rs
pub fn message() {
let txt = "Tjabba Habba";
println!("{} --> {}", txt, txt.to_uppercase());
}
// src/main.rs
use hello::message;
fn main() {
println!("Hello from a Rust program");
message();
}
Kör man sen programmet, ser det ut så här:
$ cargo run
Compiling hello v0.1.0 (/home/jensr/RustroverProjects/my_project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/hello`
Hello from a Rust program
Tjabba Habba --> TJABBA HABBA
$
$ ./target/debug/hello
Hello from a Rust program
Tjabba Habba --> TJABBA HABBA
Testprogram
Det finns två testformer i Rust (det finns en tredje form, som jag vägrar kalla "test" - återkommer); enhetstest (unit tests) och integrationstest (integration tests).
Den första formen definieras vanligtvis med en inline module med namnet tests och annoterad
med #[cfg(test)], samt varje testfunktion annoteras med #[test].
pub fn sum(n: i32) -> u64 {
(n * (n + 1) / 2) as u64
}
#[cfg(test)]
mod tests {
use super::*; //import parent
// use crate::numbers::sum; //fully qualified import
#[test]
fn sum_of_zero_should_return_zero() {
assert_eq!(sum(0), 0);
}
#[test]
fn sum_of_one_should_return_one() {
assert_eq!(sum(1), 1);
}
#[test]
fn sum_of_ten_should_return_fiftyfive() {
assert_eq!(sum(10), 55);
}
}
Sen kör man alla test med cargo test.
$ cargo test
...
running 3 tests
test numbers::tests::sum_of_one_should_return_one ... ok
test numbers::tests::sum_of_ten_should_return_fiftyfive ... ok
test numbers::tests::sum_of_zero_should_return_zero ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
...
Den andra formen utgörs av separata (test-)program i katalogen tests/ och körs tillsammans
med övriga test. Varje testfunktion ska annoteras på samma sätt som för enhetstesterna.
my_project
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── numbers_test.rs
Genererad programdokumentation
Ända sedan James Gosling införde genererad programdokumentation (JavaDoc), baserad på en idé
av Donald Knuth (han med TeX och algoritm-böckerna) kallat literate programming, så har
varje självrespekterande programmeringsspråk något liknande. Självfallet har Rust sin variant
(rustdoc), inspirerad av javadoc-syntaxen och viss mån doxygen i C/C++-världen.
//! Simple _number-oriented_ functions
//!
//! # Examples
//! ```
//! use hello::numbers;
//! let k = 10;
//! println!("SUM(1..{}) = {}", k, numbers::sum(k));
//! assert_eq!(numbers::sum(10), 55);
//! ```
/// Computes the sum `1+2+...+n`
/// using the Gauss summation formula.
pub fn sum(n: u32) -> u64 {
(n * (n + 1) / 2) as u64
}
Syntaxen i en doc-comment är Markdown, som du kan se i kod-fragmentet. Kursiv text _number-oriented_
respektive rubrik # Examples.
Du har säkert redan noterat att det finns två kommentarsformer; /// ... och //! ....
Skillnaden är lite subtil och lätt att förbise; den första används före ett element, såsom
funktionen sum() ovan. Den andra används inuti ett element, i detta fall inuti modulen
numbers.
Alternativt, för den som föredrar den javadoc inspirerade syntaxen ser det ut så här:
/*! Simple _number-oriented_ functions
*
* # Examples
*
* ```
* use hello::numbers::sum;
* let k = 10;
* println!("SUM(1..{}) = {}", k, sum(k));
* assert_eq!(sum(10), 55);
* ```
*/
/**
* Computes the sum `1+2+...+n` using the Gauss summation formula.
*/
pub fn sum(n: u32) -> u64 {
(n * (n + 1) / 2) as u64
}
Du genererar dokumentationen med cargo doc (föga förvånande). Vad som inte är uppenbart
till att börja med är att bara publika medlemmar av definierat bibliotekt tas med. Av det
skälet är det vanligt att man under utvecklingen tar med både privata medlemmar och alla
program, på följande vis:
$ cargo doc --document-private-items --bins --open
Documenting hello v0.1.0 (/home/jensr/RustroverProjects/simple-project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.68s
Opening /home/jensr/RustroverProjects/simple-project/target/doc/hello_app/index.html
Kodfragment i dokumentationen
Om du kikar noga i dokumentationskommentaren ovan, ser du att det finns kodfragment enligt Markdown-syntaxen. Dylika fragment kompileras i samband med genereringen, så att man snabbt kan bli varse eventuella regressionsproblem (någon ändrar i produktionskoden men glömmer att uppdatera kommentaren).
Om fragmentet också innehåller testsatser såsom assert_eq!(sum(10), 55);, utförs dessa
som en "tredje" form av test, kallat doctests, i samband med att man kör cargo test.
Externa tredjeparts-bibliotek
Som jag var inne på i början av denna artikel, så utgör möjligheten att inkorporera främmande bibliotek en essentiell komponent i ett byggnationsverktyg. Cargo har givetvis denna funktionalitet precis som Maven i Java-världen.
Första steget är att leta reda på vilket eller vilka bibliotek man behöver. I Java-världen söker man hos MVN Repository, i Node.js-världen söker man hos NPM.js och så vidare.
I Rust-världen söker man på crates.io (det officiella registret) eller alternativet lib.rs (ett tredjepartsgränssnitt med bättre kategorisering och kvalitetssignaler). Dokumentationen för varje publicerad crate byggs automatiskt och ligger på docs.rs — ingen behöver hantera en server för sina egna API-docs.
Att lägga till ett beroende görs med cargo add, som använder den senaste versionen och
uppdaterar konfigurationsfilen:
$ cargo add rand
Updating crates.io index
Adding rand v0.10.2 to dependencies
Features:
+ std
+ std_rng
+ thread_rng
- serde
- simd_support
...
# Cargo.toml
[dependencies]
rand = "0.10.2"
Ett versionskrav som "0.9" är inte "exakt 0.9" utan ett intervall. Cargo följer
SemVer (major.minor.patch): en inkrementering av den major betyder
"kan bryta bakåtkompatibilitet".
cargo build skapar också Cargo.lock, som listar den exakta version som valdes för
varje crate i hela grafen — direkta beroenden såväl som deras beroenden. Nästa bygge,
på vilken maskin som helst, använder exakt samma versioner tills man kör cargo update.
Det här är precis samma typ av hantering som package-lock.json i Node.js-världen.
Man kan lista alla beroenden av ett projekt med cargo tree.
$ cargo tree --depth 2
hello v0.1.0 (/home/jensr/RustroverProjects/simple-project)
├── rand v0.10.2
│ ├── chacha20 v0.10.2
│ ├── getrandom v0.4.3
│ └── rand_core v0.10.1
└── rand_distr v0.6.0
├── num-traits v0.2.19
│ [build-dependencies]
└── rand v0.10.2 (*)
Plottning av Normal-fördelning
Som avslutning på denna artikel visar jag ett litet program som använder två externa bibliotek och plottar en typisk Normal-fördelning som ett histogram av stjärnor. Först behöver vi lägga till våra två beroenden:
$ cargo add rand_distr rand
Vilket uppdaterar konfigurationsfilen på följande vis:
# Cargo.toml
# ...
[dependencies]
rand = "0.10.2"
rand_distr = "0.6.0"
Själva programmet ser ut så här:
use rand::rng;
use rand_distr::{Distribution, Normal};
const N: usize = 10_000; // number of samples
const MEAN: f64 = 0.0; // parameters
const STDDEV: f64 = 2.0;
const BUCKETS: usize = 21; // histogram resolution
const BOUND: f64 = 4.0 * STDDEV; // histogram range
const LOW: f64 = -BOUND;
const HIGH: f64 = BOUND;
const MAX_STARS: f64 = 40.0; // max width of stars
fn main() {
// 1. Generate the samples into a Vec.
let mut rng = rng();
let samples: Vec<f64> = Normal::new(MEAN, STDDEV).unwrap()
.sample_iter(&mut rng)
.take(N)
.collect();
// 2. Aggregate into a histogram table (bucket index -> count).
let width = (HIGH - LOW) / BUCKETS as f64;
let mut counts = vec![0usize; BUCKETS]; //initialize with BUCKETS number of zeroes
for &x in &samples {
let raw = ((x - LOW) / width).floor() as isize; //isize similar to long int
let idx = raw.clamp(0, BUCKETS as isize - 1) as usize; //usize similar to unsigned long int
counts[idx] += 1;
}
// 3. Print it: one row per bucket, N stars proportional to the count.
let peak = counts.iter().copied().max().unwrap_or(1).max(1) as f64;
println!("n = {N}, mean = {MEAN}, stddev = {STDDEV}");
for (idx, &cnt) in counts.iter().enumerate() {
let lb = LOW + width * idx as f64;
let ub = lb + width;
let freq = cnt as f64;
let stars = (freq / peak * MAX_STARS).round() as usize;
println!("[{lb:+5.2}, {ub:+5.2}) {cnt:5} {}", "*".repeat(stars));
}
}
Kör vi sen programmet kan det se ut så här:
$ cargo run --bin plot
Compiling hello v0.1.0 (/home/jensr/RustroverProjects/simple-project)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/plot`
n = 10000, mean = 0, stddev = 2
[-8.00, -7.24) 2
[-7.24, -6.48) 2
[-6.48, -5.71) 17
[-5.71, -4.95) 73 **
[-4.95, -4.19) 104 ***
[-4.19, -3.43) 241 ******
[-3.43, -2.67) 466 ************
[-2.67, -1.90) 790 *********************
[-1.90, -1.14) 1119 ******************************
[-1.14, -0.38) 1430 **************************************
[-0.38, +0.38) 1496 ****************************************
[+0.38, +1.14) 1457 ***************************************
[+1.14, +1.90) 1176 *******************************
[+1.90, +2.67) 779 *********************
[+2.67, +3.43) 438 ************
[+3.43, +4.19) 237 ******
[+4.19, +4.95) 104 ***
[+4.95, +5.71) 45 *
[+5.71, +6.48) 12
[+6.48, +7.24) 6
[+7.24, +8.00) 6
$
Sammanfattning
Vi har rest en bra bit i den här artikeln: från rustc hello.rs och dess *.o-filer,
via make, Ant och Maven fram till cargo. Längs vägen har vi tittat på hur cargo
strukturerar ett projekt (src/main.rs, src/lib.rs, src/bin/), hur moduler byggs upp
(fil-baserade, inline, med #[cfg(...)]), hur man testar (enhetstest, integrationstest
och doctests), hur man genererar dokumentation (rustdoc) och slutligen hur man
inkorporerar tredjeparts-bibliotek från crates.io — med ett
litet demo-program som facit.
Den röda tråden genom hela artikeln är egentligen bara en enda mening, gömd i titeln:
konvention framför konfiguration. Samma idé som Jason van Zyl gav Java-världen med
Maven, har Rust fått med sig från start, inbyggd i själva verktyget. Filnamnet main.rs
är konfigurationen. Katalogen src/bin/ är konfigurationen. Det finns i praktiken
inget att ställa in för att komma igång — man skapar, kompilerar, testar,
dokumenterar och publicerar med samma verktyg, samma kommandon och samma vokabulär,
oavsett vilket Rust-projekt man öppnar.
Jämför man med C++, där varje projekt kan ha sin egen kombination av CMake/make,
Conan/vcpkg/FetchContent, Doxygen och en handfull hemsnickrade skript, blir
skillnaden tydlig. Det är inte det att C++ saknar verktyg — det är att det saknar
ett verktyg alla är överens om. Cargo må vara "bara" ett byggverktyg, men det är
förmodligen den enskilda komponent som gjort mest för Rusts rykte om att vara ett
utvecklarvänligt språk.
Länkar
Senaste Artiklarna
-
Cargo – konvention framför konfiguration
4 september 2026 -
Referenser & pekare i Rust jämfört med C++
28 augusti 2026 -
När Rust får panik gör C++ ett undantag
21 augusti 2026 -
Rust syntax jämfört med C++
14 augusti 2026 -
Fem skäl att välja Rust över C++
10 augusti 2026 -
Apollo 11 och kvinnan bakom koden
12 april 2026 -
Hur gör man en Ralph Loop?
6 april 2026 -
Kan du alla nya AI termer?
22 mars 2026