use std::path::PathBuf;
use std::process::Command;
use object::elf;
use object::read::elf::{FileHeader, SectionHeader, Sym};
use object::{Endianness, Object, ObjectSection};
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
fn compile(dir: &std::path::Path, name: &str, wat: &str) -> PathBuf {
let src = dir.join(format!("{name}.wat"));
std::fs::write(&src, wat).unwrap();
let out = dir.join(format!("{name}.o"));
let status = Command::new(synth())
.args([
"compile",
src.to_str().unwrap(),
"-t",
"cortex-m3",
"--all-exports",
"--relocatable",
"-o",
out.to_str().unwrap(),
])
.output()
.expect("run synth");
assert!(
status.status.success(),
"synth compile failed: {}",
String::from_utf8_lossy(&status.stderr)
);
out
}
fn module_with_internal_helper(export: &str, mul: u32) -> String {
format!(
r#"(module
(func $helper (param i32) (result i32) (i32.mul (local.get 0) (i32.const {mul})))
(func (export "{export}") (param i32) (result i32) (call $helper (local.get 0))))"#
)
}
type SymEntry = (String, u32, elf::SymbolBind, elf::SymbolType, bool);
fn read_symbols(bytes: &[u8]) -> (Vec<SymEntry>, u32) {
let header = elf::FileHeader32::<Endianness>::parse(bytes).expect("valid ELF32");
let endian = header.endian().unwrap();
let sections = header.sections(endian, bytes).unwrap();
let (symtab_index, _symtab) = sections
.iter()
.enumerate()
.find(|(_, s)| s.sh_type(endian) == elf::SHT_SYMTAB)
.expect("symtab present");
let sh_info = sections.iter().nth(symtab_index).unwrap().sh_info(endian);
let symbols = sections
.symbol_table_by_index(endian, bytes, object::SectionIndex(symtab_index))
.expect("parse symtab");
let strings = symbols.strings();
let syms = symbols
.iter()
.map(|sym| {
(
String::from_utf8_lossy(sym.name(endian, strings).unwrap_or(b"")).into_owned(),
sym.st_value(endian),
sym.st_bind(),
sym.st_type(),
sym.st_shndx(endian) != elf::SHN_UNDEF,
)
})
.collect();
(syms, sh_info)
}
#[test]
fn internal_funcs_local_exports_global_sh_info_656() {
let dir = std::env::temp_dir().join("synth_656_bindings");
std::fs::create_dir_all(&dir).unwrap();
let obj = compile(
&dir,
"gpio",
&module_with_internal_helper("gpio_configure", 3),
);
let bytes = std::fs::read(&obj).unwrap();
let (syms, sh_info) = read_symbols(&bytes);
let find = |name: &str| {
syms.iter()
.find(|s| s.0 == name)
.unwrap_or_else(|| panic!("symbol {name} missing"))
};
let helper = find("func_0");
assert_eq!(
helper.2,
elf::STB_LOCAL,
"#656: internal func_0 must be STB_LOCAL"
);
let alias = find("func_1");
assert_eq!(
alias.2,
elf::STB_LOCAL,
"#656: func_N call alias must be STB_LOCAL"
);
let export = find("gpio_configure");
assert_eq!(export.2, elf::STB_GLOBAL, "export stays STB_GLOBAL");
assert_eq!(export.1, alias.1, "alias and export share the address");
let first_global = syms
.iter()
.position(|s| s.2 != elf::STB_LOCAL)
.expect("has a global symbol");
assert!(
syms[first_global..].iter().all(|s| s.2 != elf::STB_LOCAL),
"no LOCAL symbol may follow a global"
);
assert_eq!(
sh_info, first_global as u32,
"`.symtab` sh_info must be the first-non-local index (was hardcoded 1, #430)"
);
assert!(sh_info > 1, "this object has real locals");
}
#[test]
fn relocations_reindexed_consistently_656() {
let dir = std::env::temp_dir().join("synth_656_relocs");
std::fs::create_dir_all(&dir).unwrap();
let obj = compile(
&dir,
"gpio",
&module_with_internal_helper("gpio_configure", 3),
);
let bytes = std::fs::read(&obj).unwrap();
let (syms, _) = read_symbols(&bytes);
let header = elf::FileHeader32::<Endianness>::parse(&*bytes).expect("valid ELF32");
let endian = header.endian().unwrap();
let sections = header.sections(endian, &*bytes).unwrap();
let rel_section = sections
.iter()
.find(|s| s.sh_type(endian) == elf::SHT_REL)
.expect(".rel.text present");
let (rels, _link) = rel_section
.rel(endian, &*bytes)
.expect("parse rel")
.expect("REL entries");
assert_eq!(rels.len(), 1, "one internal BL call site");
let rel = &rels[0];
assert_eq!(
rel.r_type(endian).0,
10, "Thumb BL uses R_ARM_THM_CALL (#167)"
);
let sym = &syms[rel.r_sym(endian) as usize];
assert_eq!(sym.0, "func_0", "BL resolves against the internal helper");
assert_eq!(sym.2, elf::STB_LOCAL, "which is LOCAL after #656");
assert!(sym.4, "and defined in this object");
assert_eq!(sym.1 & !1, 0, "helper is the first function in .text");
}
#[test]
fn co_link_two_dissolved_objects_656() {
let dir = std::env::temp_dir().join("synth_656_colink");
std::fs::create_dir_all(&dir).unwrap();
let a = compile(
&dir,
"gpio",
&module_with_internal_helper("gpio_configure", 3),
);
let b = compile(&dir, "spi", &module_with_internal_helper("spi_begin", 5));
let linker = ["arm-none-eabi-ld", "ld.lld"].iter().find(|ld| {
Command::new(ld)
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
});
let Some(linker) = linker else {
eprintln!("skip: no arm-none-eabi-ld / ld.lld on PATH — bindings asserted elsewhere");
return;
};
let merged = dir.join("merged.o");
let out = Command::new(linker)
.args(["-r", "-o", merged.to_str().unwrap()])
.arg(&a)
.arg(&b)
.output()
.expect("run linker");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success() && !stderr.contains("multiple definition"),
"#656: co-linking two dissolved objects must not collide: {stderr}"
);
let bytes = std::fs::read(&merged).unwrap();
let (syms, _) = read_symbols(&bytes);
for export in ["gpio_configure", "spi_begin"] {
let s = syms
.iter()
.find(|s| s.0 == export)
.unwrap_or_else(|| panic!("{export} missing after merge"));
assert_eq!(s.2, elf::STB_GLOBAL);
assert!(s.4, "{export} defined in merged object");
}
let local_helpers = syms
.iter()
.filter(|s| s.0 == "func_0" && s.2 == elf::STB_LOCAL)
.count();
assert_eq!(local_helpers, 2, "each object keeps its own local func_0");
}
#[test]
fn arm_attributes_emitted_for_thumb_object_637() {
let dir = std::env::temp_dir().join("synth_637_attrs");
std::fs::create_dir_all(&dir).unwrap();
let obj = compile(
&dir,
"gpio",
&module_with_internal_helper("gpio_configure", 3),
);
let bytes = std::fs::read(&obj).unwrap();
let file = object::File::parse(&*bytes).expect("parse ELF");
let attrs = file
.section_by_name(".ARM.attributes")
.expect("#637: .ARM.attributes section must be emitted");
let data = attrs.data().unwrap();
assert_eq!(data[0], b'A', "attributes format version");
assert_eq!(&data[5..11], b"aeabi\0");
let pairs = &data[16..];
assert_eq!(pairs, &[6, 10, 7, b'M', 9, 2]);
let header = elf::FileHeader32::<Endianness>::parse(&*bytes).unwrap();
let endian = header.endian().unwrap();
let sections = header.sections(endian, &*bytes).unwrap();
assert!(
sections
.iter()
.any(|s| s.sh_type(endian) == elf::SHT_ARM_ATTRIBUTES),
"sh_type must be SHT_ARM_ATTRIBUTES (0x70000003)"
);
}
#[test]
fn disasm_prints_thumb_mnemonics_637() {
let has_objdump = Command::new("objdump")
.args(["--version"])
.output()
.is_ok_and(|o| o.status.success());
if !has_objdump {
eprintln!("skip: no objdump on PATH");
return;
}
let dir = std::env::temp_dir().join("synth_637_disasm");
std::fs::create_dir_all(&dir).unwrap();
let obj = compile(
&dir,
"maskshift",
r#"(module (func (export "f") (param i32 i32) (result i32)
(i32.shl (local.get 0) (i32.and (local.get 1) (i32.const 31)))))"#,
);
let out = Command::new(synth())
.arg("disasm")
.arg(&obj)
.output()
.expect("run synth disasm");
if !out.status.success() {
eprintln!(
"skip: synth disasm failed on this host: {}",
String::from_utf8_lossy(&out.stderr)
);
return;
}
let text = String::from_utf8_lossy(&out.stdout).to_lowercase();
assert!(
text.contains("push") && text.contains("pop"),
"#637: expected Thumb prologue/epilogue mnemonics, got:\n{text}"
);
assert!(
text.contains("lsl"),
"#637/#682: masked-shift fixture must show the register shift:\n{text}"
);
assert!(
!text.contains("andhs") && !text.contains("andeq"),
"#637: the A32 mis-decode signature must be gone:\n{text}"
);
}