Skip to content

Commit 222ce4f

Browse files
author
Kjetil Kjeka
committed
LLVM Bitcode Linker: Added crate
1 parent ed252e9 commit 222ce4f

File tree

11 files changed

+338
-0
lines changed

11 files changed

+338
-0
lines changed

Cargo.lock

+11
Original file line numberDiff line numberDiff line change
@@ -2265,6 +2265,17 @@ checksum = "f9d642685b028806386b2b6e75685faadd3eb65a85fff7df711ce18446a422da"
22652265
name = "lld-wrapper"
22662266
version = "0.1.0"
22672267

2268+
[[package]]
2269+
name = "llvm-bitcode-linker"
2270+
version = "0.0.1"
2271+
dependencies = [
2272+
"anyhow",
2273+
"clap",
2274+
"thiserror",
2275+
"tracing",
2276+
"tracing-subscriber",
2277+
]
2278+
22682279
[[package]]
22692280
name = "lock_api"
22702281
version = "0.4.11"

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ members = [
3434
"src/tools/expand-yaml-anchors",
3535
"src/tools/jsondocck",
3636
"src/tools/jsondoclint",
37+
"src/tools/llvm-bitcode-linker",
3738
"src/tools/html-checker",
3839
"src/tools/bump-stage0",
3940
"src/tools/replace-version-placeholder",

src/bootstrap/src/core/build_steps/tool.rs

+1
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,7 @@ tool_extended!((self, builder),
795795
Rls, "src/tools/rls", "rls", stable=true, tool_std=true;
796796
RustDemangler, "src/tools/rust-demangler", "rust-demangler", stable=false, tool_std=true;
797797
Rustfmt, "src/tools/rustfmt", "rustfmt", stable=true, add_bins_to_sysroot = ["rustfmt", "cargo-fmt"];
798+
LlvmBitcodeLinker, "src/tools/llvm-bitcode-linker", "llvm-bitcode-linker", stable=false, add_bins_to_sysroot = ["llvm-bitcode-linker"];
798799
);
799800

800801
impl<'a> Builder<'a> {

src/bootstrap/src/core/builder.rs

+1
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,7 @@ impl<'a> Builder<'a> {
763763
tool::RustdocGUITest,
764764
tool::OptimizedDist,
765765
tool::CoverageDump,
766+
tool::LlvmBitcodeLinker
766767
),
767768
Kind::Check | Kind::Clippy | Kind::Fix => describe!(
768769
check::Std,
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "llvm-bitcode-linker"
3+
version = "0.0.1"
4+
description = "A self-contained linker for llvm bitcode"
5+
license = "MIT OR Apache-2.0"
6+
edition = "2021"
7+
publish = false
8+
9+
[dependencies]
10+
anyhow = "1.0"
11+
tracing = "0.1"
12+
tracing-subscriber = {version = "0.3.0", features = ["std"] }
13+
clap = { version = "4.3", features = ["derive"] }
14+
thiserror = "1.0.24"
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# LLVM Bitcode Linker
2+
The LLVM bitcode linker can be used to link targets without any dependency on system libraries.
3+
The code will be linked in llvm-bc before compiling to native code. For some of these targets
4+
(e.g. ptx) there does not exist a sensible way to link the native format at all. A bitcode linker
5+
is required to link code compiled for such targets.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use std::path::PathBuf;
2+
3+
use clap::Parser;
4+
5+
use llvm_bitcode_linker::{Optimization, Session, Target};
6+
7+
#[derive(Debug, Parser)]
8+
/// Linker for embedded code without any system dependencies
9+
pub struct Args {
10+
/// Input files - objects, archives and static libraries.
11+
///
12+
/// An archive can be, but not required to be, a Rust rlib.
13+
files: Vec<PathBuf>,
14+
15+
/// A symbol that should be exported
16+
#[arg(long)]
17+
export_symbol: Vec<String>,
18+
19+
/// Input files directory
20+
#[arg(short = 'L')]
21+
input_dir: Vec<PathBuf>,
22+
23+
/// Target triple for which the code is compiled
24+
#[arg(long)]
25+
target: Target,
26+
27+
/// The target cpu
28+
#[arg(long)]
29+
target_cpu: Option<String>,
30+
31+
/// Write output to the filename
32+
#[arg(short, long)]
33+
output: PathBuf,
34+
35+
// Enable link time optimization
36+
#[arg(long)]
37+
lto: bool,
38+
39+
/// Emit debug information
40+
#[arg(long)]
41+
debug: bool,
42+
43+
/// The optimization level
44+
#[arg(short = 'O', value_enum, default_value = "0")]
45+
optimization: Optimization,
46+
}
47+
48+
fn main() -> anyhow::Result<()> {
49+
tracing_subscriber::FmtSubscriber::builder().with_max_level(tracing::Level::DEBUG).init();
50+
51+
let args = Args::parse();
52+
53+
let mut linker = Session::new(args.target, args.target_cpu, args.output);
54+
55+
linker.add_exported_symbols(args.export_symbol);
56+
57+
for rlib in args.files {
58+
linker.add_file(rlib);
59+
}
60+
61+
linker.lto(args.optimization, args.debug)
62+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
mod linker;
2+
mod opt;
3+
mod target;
4+
5+
pub use linker::Session;
6+
pub use opt::Optimization;
7+
pub use target::Target;
+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::Context;
4+
5+
use crate::Optimization;
6+
use crate::Target;
7+
8+
#[derive(Debug)]
9+
pub struct Session {
10+
target: Target,
11+
cpu: Option<String>,
12+
symbols: Vec<String>,
13+
14+
/// A file that `llvm-link` supports, like a bitcode file or an archive.
15+
files: Vec<PathBuf>,
16+
17+
// Output files
18+
link_path: PathBuf,
19+
opt_path: PathBuf,
20+
sym_path: PathBuf,
21+
out_path: PathBuf,
22+
}
23+
24+
impl Session {
25+
pub fn new(target: crate::Target, cpu: Option<String>, out_path: PathBuf) -> Self {
26+
let link_path = out_path.with_extension("o");
27+
let opt_path = out_path.with_extension("optimized.o");
28+
let sym_path = out_path.with_extension("symbols.txt");
29+
30+
Session {
31+
target,
32+
cpu,
33+
symbols: Vec::new(),
34+
files: Vec::new(),
35+
link_path,
36+
opt_path,
37+
sym_path,
38+
out_path,
39+
}
40+
}
41+
42+
/// Add a file, like an rlib or bitcode file that should be linked
43+
pub fn add_file(&mut self, path: PathBuf) {
44+
self.files.push(path);
45+
}
46+
47+
/// Add a Vec of symbols to the list of exported symbols
48+
pub fn add_exported_symbols(&mut self, symbols: Vec<String>) {
49+
self.symbols.extend(symbols);
50+
}
51+
52+
/// Reads every file that was added to the session and link them without optimization.
53+
///
54+
/// The resulting artifact will be written to a file that can later be read to perform
55+
/// optimizations and/or compilation from bitcode to the final artifact.
56+
fn link(&mut self) -> anyhow::Result<()> {
57+
tracing::info!("Linking {} files using llvm-link", self.files.len());
58+
59+
let llvm_link_output = std::process::Command::new("llvm-link")
60+
.arg("--ignore-non-bitcode")
61+
.args(&self.files)
62+
.arg("-o")
63+
.arg(&self.link_path)
64+
.output()
65+
.unwrap();
66+
67+
if !llvm_link_output.status.success() {
68+
tracing::error!(
69+
"llvm-link returned with Exit status: {}\n stdout: {}\n stderr: {}",
70+
llvm_link_output.status,
71+
String::from_utf8(llvm_link_output.stdout).unwrap(),
72+
String::from_utf8(llvm_link_output.stderr).unwrap(),
73+
);
74+
anyhow::bail!("llvm-link failed to link files {:?}", self.files);
75+
}
76+
77+
Ok(())
78+
}
79+
80+
/// Optimize and compile to native format using `opt` and `llc`
81+
///
82+
/// Before this can be called `link` needs to be called
83+
fn optimize(&mut self, optimization: Optimization, mut debug: bool) -> anyhow::Result<()> {
84+
let mut passes = format!("default<{}>", optimization);
85+
86+
// FIXME(@kjetilkjeka) Debug symbol generation is broken for nvptx64 so we must remove them even in debug mode
87+
if debug && self.target == crate::Target::Nvptx64NvidiaCuda {
88+
tracing::warn!("nvptx64 target detected - stripping debug symbols");
89+
debug = false;
90+
}
91+
92+
// We add an internalize pass as the rust compiler as we require exported symbols to be explicitly marked
93+
passes.push_str(",internalize,globaldce");
94+
let symbol_file_content = self.symbols.iter().fold(String::new(), |s, x| s + &x + "\n");
95+
std::fs::write(&self.sym_path, symbol_file_content)
96+
.context(format!("Failed to write symbol file: {}", self.sym_path.display()))?;
97+
98+
tracing::info!("optimizing bitcode with passes: {}", passes);
99+
let mut opt_cmd = std::process::Command::new("opt");
100+
opt_cmd
101+
.arg(&self.link_path)
102+
.arg("-o")
103+
.arg(&self.opt_path)
104+
.arg(format!("--internalize-public-api-file={}", self.sym_path.display()))
105+
.arg(format!("--passes={}", passes));
106+
107+
if !debug {
108+
opt_cmd.arg("--strip-debug");
109+
}
110+
111+
let opt_output = opt_cmd.output().unwrap();
112+
113+
if !opt_output.status.success() {
114+
tracing::error!(
115+
"opt returned with Exit status: {}\n stdout: {}\n stderr: {}",
116+
opt_output.status,
117+
String::from_utf8(opt_output.stdout).unwrap(),
118+
String::from_utf8(opt_output.stderr).unwrap(),
119+
);
120+
anyhow::bail!("opt failed optimize bitcode: {}", self.link_path.display());
121+
};
122+
123+
Ok(())
124+
}
125+
126+
/// Compile the optimized bitcode file to native format using `llc`
127+
///
128+
/// Before this can be called `optimize` needs to be called
129+
fn compile(&mut self) -> anyhow::Result<()> {
130+
let mut lcc_command = std::process::Command::new("llc");
131+
132+
if let Some(mcpu) = &self.cpu {
133+
lcc_command.arg("--mcpu").arg(mcpu);
134+
}
135+
136+
let lcc_output =
137+
lcc_command.arg(&self.opt_path).arg("-o").arg(&self.out_path).output().unwrap();
138+
139+
if !lcc_output.status.success() {
140+
tracing::error!(
141+
"llc returned with Exit status: {}\n stdout: {}\n stderr: {}",
142+
lcc_output.status,
143+
String::from_utf8(lcc_output.stdout).unwrap(),
144+
String::from_utf8(lcc_output.stderr).unwrap(),
145+
);
146+
147+
anyhow::bail!(
148+
"llc failed to compile {} into {}",
149+
self.opt_path.display(),
150+
self.out_path.display()
151+
);
152+
}
153+
154+
Ok(())
155+
}
156+
157+
/// Links, optimizes and compiles to the native format
158+
pub fn lto(&mut self, optimization: crate::Optimization, debug: bool) -> anyhow::Result<()> {
159+
self.link()?;
160+
self.optimize(optimization, debug)?;
161+
self.compile()
162+
}
163+
}
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use std::fmt::Display;
2+
use std::fmt::Formatter;
3+
4+
#[derive(Debug, Clone, Copy, Default, Hash, Eq, PartialEq, clap::ValueEnum)]
5+
pub enum Optimization {
6+
#[default]
7+
#[value(name = "0")]
8+
O0,
9+
#[value(name = "1")]
10+
O1,
11+
#[value(name = "2")]
12+
O2,
13+
#[value(name = "3")]
14+
O3,
15+
#[value(name = "s")]
16+
Os,
17+
#[value(name = "z")]
18+
Oz,
19+
}
20+
21+
#[derive(Debug, Clone, Copy, thiserror::Error)]
22+
/// An invalid optimization level
23+
#[error("invalid optimization level")]
24+
pub struct InvalidOptimizationLevel;
25+
26+
impl std::str::FromStr for Optimization {
27+
type Err = InvalidOptimizationLevel;
28+
29+
fn from_str(s: &str) -> Result<Self, Self::Err> {
30+
match s {
31+
"0" | "O0" => Ok(Optimization::O0),
32+
"1" | "O1" => Ok(Optimization::O1),
33+
"2" | "O2" => Ok(Optimization::O2),
34+
"3" | "O3" => Ok(Optimization::O3),
35+
"s" | "Os" => Ok(Optimization::Os),
36+
"z" | "Oz" => Ok(Optimization::Oz),
37+
_ => Err(InvalidOptimizationLevel),
38+
}
39+
}
40+
}
41+
42+
impl Display for Optimization {
43+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
44+
match *self {
45+
Optimization::O0 => write!(f, "O0"),
46+
Optimization::O1 => write!(f, "O1"),
47+
Optimization::O2 => write!(f, "O2"),
48+
Optimization::O3 => write!(f, "O3"),
49+
Optimization::Os => write!(f, "Os"),
50+
Optimization::Oz => write!(f, "Oz"),
51+
}
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, clap::ValueEnum)]
2+
pub enum Target {
3+
Nvptx64NvidiaCuda,
4+
}
5+
6+
#[derive(Debug, Clone, Copy, thiserror::Error)]
7+
/// The target is not supported by this linker
8+
#[error("unsupported target")]
9+
pub struct UnsupportedTarget;
10+
11+
impl std::str::FromStr for Target {
12+
type Err = UnsupportedTarget;
13+
14+
fn from_str(s: &str) -> Result<Target, UnsupportedTarget> {
15+
match s {
16+
"nvptx64-nvidia-cuda" => Ok(Target::Nvptx64NvidiaCuda),
17+
_ => Err(UnsupportedTarget),
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)