Skip to content

Commit 4612c2f

Browse files
authored
refactor: share runfiles-stub logic across platforms, shrink unsafe (#41)
Invert the three near-duplicate platform files into a shared core plus thin per-OS backends, holding (and net-reducing) binary size. Shared core, compiled on every platform: - common.rs manifest parser (Manifest/ManifestLines), byte helpers - runfiles.rs Runfiles discovery + rlocation - run.rs argc/flags/arg-resolution/argv[0] flow (zero unsafe) - placeholders.rs #[used] static-mut placeholders via a per-OS-section macro, read through addr_of! accessors (clears the static-mut warnings) - allocator.rs talc global allocator + panic handler Per-OS backends (linux/macos/windows) provide a cfg-gated free-function seam (print/exit/path_exists/get_env_var/load_manifest/is_absolute/to_native_path/ launch). No trait, so it monomorphizes to identical codegen under LTO; bytes are the lingua franca and each backend marshals to execve argv/envp or CreateProcessW cmdline + UTF-16 env block. Other changes: - Replace hand-rolled str_eq/str_starts_with/find_byte/strlen with core slice methods (==, starts_with, iter().position). - Dedup Linux per-arch syscall asm via cfg-gated macros; output is byte-identical to before on x86_64/aarch64/s390x. - Replace the Windows 128 KB `static mut` env buffer with a heap Vec<u16>, removing the fixed cap and the abort-on-overflow path. Source 3662 -> 2107 lines (-42%); unsafe occurrences 70 -> 43 (-39%), all remaining unsafe inherent (syscalls/FFI/allocator/mmap/_start) and isolated. Release binary sizes net -3464 bytes across the 7 targets (worst case +0.9%); placeholder byte-layout unchanged so finalize-stub is unaffected.
1 parent 2dbcfb8 commit 4612c2f

9 files changed

Lines changed: 1275 additions & 2830 deletions

File tree

runfiles-stub/src/allocator.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Global allocator shared by all platforms: talc over a static memory arena.
2+
// 8 MiB is plenty for manifest parsing, path resolution, and environment handling.
3+
// Single-threaded use, so no locking is needed.
4+
5+
use core::alloc::{GlobalAlloc, Layout};
6+
use core::cell::UnsafeCell;
7+
use talc::{ClaimOnOom, Span, Talc};
8+
9+
static mut ARENA: [u8; 8 * 1024 * 1024] = [0; 8 * 1024 * 1024];
10+
11+
struct TalcAllocator(UnsafeCell<Talc<ClaimOnOom>>);
12+
unsafe impl Sync for TalcAllocator {}
13+
14+
unsafe impl GlobalAlloc for TalcAllocator {
15+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
16+
(*self.0.get()).malloc(layout).map_or(core::ptr::null_mut(), |p| p.as_ptr())
17+
}
18+
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
19+
(*self.0.get()).free(core::ptr::NonNull::new_unchecked(ptr), layout);
20+
}
21+
}
22+
23+
#[global_allocator]
24+
static ALLOCATOR: TalcAllocator = TalcAllocator(UnsafeCell::new(Talc::new(unsafe {
25+
ClaimOnOom::new(Span::from_array(core::ptr::addr_of!(ARENA).cast_mut()))
26+
})));

runfiles-stub/src/common.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Platform-agnostic helpers shared by every backend: small byte utilities and the
2+
// memory-mapped runfiles MANIFEST parser.
3+
4+
extern crate alloc;
5+
6+
use crate::platform;
7+
8+
/// Length of a NUL-terminated byte string stored in a fixed-size buffer:
9+
/// the offset of the first NUL, or the full length if there is none.
10+
pub fn cstr_len(s: &[u8]) -> usize {
11+
s.iter().position(|&b| b == 0).unwrap_or(s.len())
12+
}
13+
14+
/// Print a decimal number to stdout (used in diagnostics).
15+
#[allow(dead_code)] // only some backends print numeric diagnostics
16+
pub fn print_number(mut n: usize) {
17+
if n == 0 {
18+
platform::print(b"0");
19+
return;
20+
}
21+
let mut buf = [0u8; 20]; // enough for 64-bit numbers
22+
let mut i = 0;
23+
while n > 0 {
24+
buf[i] = b'0' + (n % 10) as u8;
25+
n /= 10;
26+
i += 1;
27+
}
28+
while i > 0 {
29+
i -= 1;
30+
platform::print(&buf[i..i + 1]);
31+
}
32+
}
33+
34+
// Memory-mapped manifest: a pointer/length pair into the mapped file. The
35+
// manifest is scanned lazily on each lookup (O(n)), so no per-entry allocation
36+
// is needed and arbitrarily large manifests are supported. The kernel caches
37+
// the file's pages for us.
38+
pub struct Manifest {
39+
ptr: *const u8,
40+
len: usize,
41+
}
42+
43+
impl Manifest {
44+
/// Construct from a mapping that lives for the rest of the process.
45+
///
46+
/// # Safety
47+
/// `ptr`/`len` must come from a successful read-only mapping that is leaked
48+
/// for the lifetime of the process (until execve/ExitProcess reclaims it).
49+
pub unsafe fn from_mapping(ptr: *const u8, len: usize) -> Self {
50+
Manifest { ptr, len }
51+
}
52+
53+
#[inline]
54+
fn data(&self) -> &[u8] {
55+
// Safety: ptr/len come from a leaked, process-lifetime mapping (see `from_mapping`).
56+
unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
57+
}
58+
59+
pub fn lookup(&self, key: &str) -> Option<&str> {
60+
let key_bytes = key.as_bytes();
61+
for (k, v) in ManifestLines::new(self.data()) {
62+
if k == key_bytes {
63+
return core::str::from_utf8(v).ok();
64+
}
65+
}
66+
None
67+
}
68+
69+
/// Find the longest manifest entry whose key is a prefix of `path` at a '/' boundary.
70+
/// Returns (resolved_value, suffix) where suffix includes the leading '/'.
71+
pub fn prefix_lookup<'a, 'b>(&'a self, path: &'b str) -> Option<(&'a str, &'b str)> {
72+
let path_bytes = path.as_bytes();
73+
let mut best: Option<(&'a str, &'b str)> = None;
74+
let mut best_len: usize = 0;
75+
for (k, v) in ManifestLines::new(self.data()) {
76+
if path_bytes.len() > k.len()
77+
&& k.len() > best_len
78+
&& &path_bytes[..k.len()] == k
79+
&& path_bytes[k.len()] == b'/'
80+
{
81+
// Values were owned UTF-8 Strings before; keep that guarantee by
82+
// only considering candidates whose value is valid UTF-8.
83+
if let Ok(value) = core::str::from_utf8(v) {
84+
best_len = k.len();
85+
best = Some((value, &path[k.len()..]));
86+
}
87+
}
88+
}
89+
best
90+
}
91+
}
92+
93+
/// Iterator over `(key, value)` byte slices of a Bazel runfiles MANIFEST.
94+
/// Replicates `str::lines()` + `split_once(' ')`: split on '\n', strip one
95+
/// trailing '\r' (CRLF), and skip lines without a space (e.g. the
96+
/// "<workspace>/.runfile" marker).
97+
struct ManifestLines<'a> {
98+
rest: &'a [u8],
99+
}
100+
101+
impl<'a> ManifestLines<'a> {
102+
fn new(data: &'a [u8]) -> Self {
103+
Self { rest: data }
104+
}
105+
}
106+
107+
impl<'a> Iterator for ManifestLines<'a> {
108+
type Item = (&'a [u8], &'a [u8]);
109+
110+
fn next(&mut self) -> Option<(&'a [u8], &'a [u8])> {
111+
while !self.rest.is_empty() {
112+
let (mut line, remainder) = match self.rest.iter().position(|&b| b == b'\n') {
113+
Some(nl) => (&self.rest[..nl], &self.rest[nl + 1..]),
114+
None => (self.rest, &self.rest[self.rest.len()..]),
115+
};
116+
self.rest = remainder;
117+
// Strip one trailing '\r' (handles CRLF line endings).
118+
if let Some((&b'\r', head)) = line.split_last() {
119+
line = head;
120+
}
121+
if let Some(sp) = line.iter().position(|&b| b == b' ') {
122+
return Some((&line[..sp], &line[sp + 1..]));
123+
}
124+
// No space -> skip this line and continue.
125+
}
126+
None
127+
}
128+
}

0 commit comments

Comments
 (0)