logicaffeine_compile/codegen_c/
value_runtime.rs1use std::path::{Path, PathBuf};
14
15pub(crate) const DECLARATION: &str = "#define LOGOS_LINKS_VALUE_RUNTIME 1";
17
18pub fn links(code: &str) -> bool {
20 code.contains(DECLARATION)
21}
22
23pub fn link_args(code: &str) -> Result<Vec<String>, String> {
26 if !links(code) {
27 return Ok(Vec::new());
28 }
29 archive_link_args(Target::Host)
30}
31
32pub fn archive_link_args(target: Target) -> Result<Vec<String>, String> {
34 let archive = archive_for(target)?;
35 let mut args = vec![archive.path.display().to_string()];
36 args.extend(archive.system_libs.iter().cloned());
37 Ok(args)
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum Target {
43 Host,
45 Riscv64,
47}
48
49impl Target {
50 pub fn triple(self) -> Option<&'static str> {
52 match self {
53 Target::Host => None,
54 Target::Riscv64 => Some("riscv64gc-unknown-linux-gnu"),
55 }
56 }
57
58 fn system_libs_file(self) -> String {
60 match self.triple() {
61 None => SYSTEM_LIBS.to_string(),
62 Some(triple) => format!("system-libs-{triple}.txt"),
63 }
64 }
65
66 fn link_libs(self, libs: Vec<String>) -> Vec<String> {
69 match self {
70 Target::Host => libs,
71 Target::Riscv64 => libs
72 .into_iter()
73 .map(|l| if l == "-lgcc_s" { "-lgcc_eh".to_string() } else { l })
74 .collect(),
75 }
76 }
77}
78
79fn built_archive(target_dir: &Path, target: Target) -> PathBuf {
81 match target.triple() {
82 None => target_dir.join("release").join(ARCHIVE),
83 Some(triple) => target_dir.join(triple).join("release").join(ARCHIVE),
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Archive {
90 pub path: PathBuf,
92 pub system_libs: Vec<String>,
94}
95
96const ROOT: &str = "//! The C value runtime: `logicaffeine-data`'s C ABI and the allocator that counts what it holds.\n\
98pub use logicaffeine_data::abi::*;\n\
99\n\
100#[global_allocator]\n\
101static ALLOCATOR: logicaffeine_data::abi::Counting = logicaffeine_data::abi::Counting;\n";
102
103const ARCHIVE: &str = "liblogos_value_runtime.a";
105
106const SYSTEM_LIBS: &str = "system-libs.txt";
108
109fn manifest(data: &str) -> String {
111 format!(
112 "[package]\nname = \"logos-value-runtime\"\nversion = \"0.0.0\"\nedition = \"2021\"\npublish = false\n\n\
113 [lib]\npath = \"lib.rs\"\ncrate-type = [\"staticlib\"]\n\n\
114 [dependencies]\n{data}\n\n\
115 [profile.release]\ndebug = false\n\n\
116 [workspace]\n"
117 )
118}
119
120fn data_dependency() -> String {
123 match crate::compile::find_workspace_root() {
124 Ok(root) => format!(
125 "logicaffeine-data = {{ path = {:?}, features = [\"c-abi\"] }}",
126 root.join("crates/logicaffeine_data").display().to_string()
127 ),
128 Err(_) => format!(
129 "logicaffeine-data = {{ version = \"={}\", features = [\"c-abi\"] }}",
130 env!("CARGO_PKG_VERSION")
131 ),
132 }
133}
134
135fn system_libs_of(note: &str) -> Option<Vec<String>> {
137 let (_, libs) = note.split_once("native-static-libs:")?;
138 Some(libs.split_whitespace().map(str::to_string).collect())
139}
140
141fn content_name(bytes: &[u8]) -> String {
143 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
144 for b in bytes {
145 hash ^= u64::from(*b);
146 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
147 }
148 format!("logos_value_runtime-{hash:016x}.a")
149}
150
151fn write_if_changed(path: &Path, contents: &str) -> Result<(), String> {
154 if std::fs::read_to_string(path).is_ok_and(|have| have == contents) {
155 return Ok(());
156 }
157 std::fs::write(path, contents).map_err(|e| format!("cannot write {}: {e}", path.display()))
158}
159
160pub fn archive() -> Result<std::sync::Arc<Archive>, String> {
162 archive_for(Target::Host)
163}
164
165#[cfg(not(target_arch = "wasm32"))]
167pub fn archive_for(target: Target) -> Result<std::sync::Arc<Archive>, String> {
168 use std::collections::HashMap;
169 use std::sync::{Arc, Mutex, OnceLock};
170 static BUILT: OnceLock<Mutex<HashMap<Target, Arc<Archive>>>> = OnceLock::new();
171 let mut built = BUILT
172 .get_or_init(|| Mutex::new(HashMap::new()))
173 .lock()
174 .unwrap_or_else(|poisoned| poisoned.into_inner());
175 if let Some(archive) = built.get(&target) {
176 return Ok(Arc::clone(archive));
177 }
178 let archive = Arc::new(build(target)?);
179 built.insert(target, Arc::clone(&archive));
180 Ok(archive)
181}
182
183#[cfg(target_arch = "wasm32")]
184pub fn archive_for(_target: Target) -> Result<std::sync::Arc<Archive>, String> {
185 Err("the C value runtime is built by a native toolchain".to_string())
186}
187
188#[cfg(not(target_arch = "wasm32"))]
191fn project_dir() -> PathBuf {
192 use std::hash::{Hash, Hasher};
193 let mut hash = std::collections::hash_map::DefaultHasher::new();
194 env!("CARGO_MANIFEST_DIR").hash(&mut hash);
195 std::env::temp_dir().join(format!("logos_value_runtime-{:016x}", hash.finish()))
196}
197
198#[cfg(not(target_arch = "wasm32"))]
199fn build(target: Target) -> Result<Archive, String> {
200 use std::process::Command;
201 let project = project_dir();
202 std::fs::create_dir_all(project.join("archives"))
203 .map_err(|e| format!("cannot create {}: {e}", project.display()))?;
204 let lock_path = project.with_extension("lock");
205 let lock = std::fs::OpenOptions::new()
206 .read(true)
207 .write(true)
208 .create(true)
209 .truncate(false)
210 .open(&lock_path)
211 .map_err(|e| format!("cannot open {}: {e}", lock_path.display()))?;
212 fs2::FileExt::lock_exclusive(&lock).map_err(|e| format!("cannot lock {}: {e}", lock_path.display()))?;
213
214 write_if_changed(&project.join("Cargo.toml"), &manifest(&data_dependency()))?;
215 write_if_changed(&project.join("lib.rs"), ROOT)?;
216 if let Ok(root) = crate::compile::find_workspace_root() {
217 let seed = root.join("Cargo.lock");
218 let lockfile = project.join("Cargo.lock");
219 if seed.exists() && !lockfile.exists() {
220 std::fs::copy(&seed, &lockfile).map_err(|e| format!("cannot seed {}: {e}", lockfile.display()))?;
221 }
222 }
223
224 let target_dir = project.join("target");
225 let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
226 let mut command = Command::new(cargo);
227 command
228 .args(["rustc", "--release", "--lib", "--quiet", "--manifest-path"])
229 .arg(project.join("Cargo.toml"))
230 .arg("--target-dir")
231 .arg(&target_dir);
232 if let Some(triple) = target.triple() {
233 command.args(["--target", triple]);
234 }
235 let out = command
236 .args(["--", "--print", "native-static-libs"])
237 .env_remove("RUSTFLAGS")
238 .env_remove("CARGO_ENCODED_RUSTFLAGS")
239 .env_remove("CARGO_BUILD_RUSTFLAGS")
240 .env_remove("CARGO_TARGET_DIR")
241 .output()
242 .map_err(|e| format!("cargo did not start: {e}"))?;
243 let said = String::from_utf8_lossy(&out.stderr);
244 if !out.status.success() {
245 let install = target
246 .triple()
247 .map(|triple| format!(" (its standard library installs with `rustup target add {triple}`)"))
248 .unwrap_or_default();
249 return Err(format!("cargo could not build the C value runtime{install}:\n{said}"));
250 }
251 let libs_path = project.join(target.system_libs_file());
252 let system_libs = match said.lines().find_map(system_libs_of) {
253 Some(libs) => {
254 write_if_changed(&libs_path, &libs.join(" "))?;
255 libs
256 }
257 None => std::fs::read_to_string(&libs_path)
258 .map(|kept| kept.split_whitespace().map(str::to_string).collect())
259 .map_err(|_| "cargo built the C value runtime without naming the system libraries it calls".to_string())?,
260 };
261
262 let built = built_archive(&target_dir, target);
263 let bytes = std::fs::read(&built).map_err(|e| format!("cannot read {}: {e}", built.display()))?;
264 let path = project.join("archives").join(content_name(&bytes));
265 if !path.exists() {
266 let partial = path.with_extension(format!("a.{}", std::process::id()));
267 std::fs::write(&partial, &bytes).map_err(|e| format!("cannot write {}: {e}", partial.display()))?;
268 std::fs::rename(&partial, &path).map_err(|e| format!("cannot keep {}: {e}", path.display()))?;
269 }
270 drop(lock);
271 Ok(Archive {
272 path,
273 system_libs: target.link_libs(system_libs),
274 })
275}
276
277#[cfg(test)]
278#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_c/value_runtime/tests.rs"]
279mod tests;