summaryrefslogtreecommitdiff
path: root/tools/asm-processor/rust/src/main.rs
diff options
context:
space:
mode:
authorDerek Hensley <hensley.derek58@gmail.com>2025-09-04 17:56:01 -0700
committerGitHub <noreply@github.com>2025-09-04 17:56:01 -0700
commit61a38830b94c28b3fcead3e22161c3dd621d25c9 (patch)
tree8ae9664d2feae319bb7da12ddd7cc7e06751f1ae /tools/asm-processor/rust/src/main.rs
parenta4d5910cdb9bed05fa5aea4476a3cf4ef178be06 (diff)
Spimdisasm update (#243)HEADmain
* git subrepo pull tools/asm-processor subrepo: subdir: "tools/asm-processor" merged: "5c3984fc" upstream: origin: "git@github.com:simonlindholm/asm-processor.git" branch: "main" commit: "5c3984fc" git-subrepo: version: "0.4.9" origin: "git@github.com:ingydotnet/git-subrepo.git" commit: "ea10886" * Fix things needed for new spimdisasm release
Diffstat (limited to 'tools/asm-processor/rust/src/main.rs')
-rw-r--r--tools/asm-processor/rust/src/main.rs518
1 files changed, 518 insertions, 0 deletions
diff --git a/tools/asm-processor/rust/src/main.rs b/tools/asm-processor/rust/src/main.rs
new file mode 100644
index 0000000..8e214ee
--- /dev/null
+++ b/tools/asm-processor/rust/src/main.rs
@@ -0,0 +1,518 @@
+mod postprocess;
+mod preprocess;
+
+use std::{
+ borrow::Cow,
+ ffi::OsString,
+ fmt::Display,
+ fs::{self, File},
+ io::Write,
+ path::{Path, PathBuf},
+ process::{exit, Command},
+ str::FromStr,
+};
+
+use anyhow::Result;
+use argp::{EarlyExit, FromArgs, HelpStyle};
+use encoding_rs::EUC_JP;
+use enum_map::{Enum, EnumMap};
+use temp_dir::TempDir;
+
+use postprocess::fixup_objfile;
+use preprocess::parse_source;
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum)]
+enum OutputSection {
+ Text,
+ Data,
+ Rodata,
+ Bss,
+}
+
+impl OutputSection {
+ fn as_str(&self) -> &'static str {
+ match self {
+ OutputSection::Text => ".text",
+ OutputSection::Data => ".data",
+ OutputSection::Rodata => ".rodata",
+ OutputSection::Bss => ".bss",
+ }
+ }
+}
+
+impl Display for OutputSection {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.as_str())
+ }
+}
+
+#[derive(Clone, Debug)]
+struct Function {
+ text_glabels: Vec<String>,
+ asm_conts: Vec<String>,
+ late_rodata_dummy_bytes: Vec<[u8; 4]>,
+ jtbl_rodata_size: usize,
+ late_rodata_asm_conts: Vec<String>,
+ fn_desc: String,
+ data: EnumMap<OutputSection, (Option<String>, usize)>,
+}
+
+#[derive(Clone, Copy, Debug)]
+enum Encoding {
+ Latin1,
+ Custom(&'static encoding_rs::Encoding),
+}
+
+impl Encoding {
+ fn encode<'a>(&self, s: &'a str) -> Result<Cow<'a, [u8]>> {
+ match self {
+ Encoding::Latin1 => {
+ if encoding_rs::mem::is_str_latin1(s) {
+ return Ok(encoding_rs::mem::encode_latin1_lossy(s));
+ }
+ }
+ Encoding::Custom(enc) => {
+ if *enc == EUC_JP {
+ let s = s.replace("〜", "~");
+ let (ret, _, failed) = enc.encode(&s);
+ if !failed {
+ return Ok(Cow::Owned(ret.into_owned()));
+ }
+ } else {
+ let (ret, _, failed) = enc.encode(s);
+ if !failed {
+ return Ok(ret);
+ }
+ }
+ }
+ }
+ Err(anyhow::anyhow!("Failed to encode string: {}", s))
+ }
+
+ fn decode<'a>(&self, bytes: &'a [u8]) -> Result<Cow<'a, str>> {
+ match self {
+ Encoding::Latin1 => Ok(encoding_rs::mem::decode_latin1(bytes)),
+ Encoding::Custom(enc) => {
+ let (ret, _, failed) = enc.decode(bytes);
+ if !failed {
+ Ok(ret)
+ } else {
+ Err(anyhow::anyhow!("Failed to decode string: {}", ret))
+ }
+ }
+ }
+ }
+}
+
+impl FromStr for Encoding {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s == "latin1" {
+ Ok(Encoding::Latin1)
+ } else {
+ match encoding_rs::Encoding::for_label(s.as_bytes()) {
+ Some(enc) => Ok(Encoding::Custom(enc)),
+ None => Err(format!("Unsupported encoding: {}", s)),
+ }
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+enum ConvertStatics {
+ No,
+ Local,
+ Global,
+ GlobalWithFilename,
+}
+
+impl FromStr for ConvertStatics {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s {
+ "no" => ConvertStatics::No,
+ "local" => ConvertStatics::Local,
+ "global" => ConvertStatics::Global,
+ "global-with-filename" => ConvertStatics::GlobalWithFilename,
+ _ => return Err("invalid value for symbol visibility".into()),
+ })
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum OptLevel {
+ O0,
+ O1,
+ O2,
+ G,
+ G3,
+}
+
+/// Pre-process .c files and post-process .o files to enable embedding MIPS assembly into IDO-compiled C.
+#[derive(FromArgs)]
+struct AsmProcArgs {
+ /// Path to a file containing a prelude to the assembly file (with .set and .macro directives, e.g.)
+ #[argp(option, arg_name = "FILE")]
+ asm_prelude: Option<PathBuf>,
+
+ /// Input encoding (default: latin1)
+ #[argp(
+ option,
+ default = "Encoding::Latin1",
+ from_str_fn(FromStr::from_str),
+ arg_name = "ENCODING"
+ )]
+ input_enc: Encoding,
+
+ /// Output encoding (default: latin1)
+ #[argp(
+ option,
+ default = "Encoding::Latin1",
+ from_str_fn(FromStr::from_str),
+ arg_name = "ENCODING"
+ )]
+ output_enc: Encoding,
+
+ /// Drop mdebug and gptab sections
+ #[argp(switch)]
+ drop_mdebug_gptab: bool,
+
+ /// Change symbol visibility for static variables. Mode must be one of:
+ /// no, local, global, global-with-filename (default: local)
+ #[argp(
+ option,
+ default = "ConvertStatics::Local",
+ from_str_fn(FromStr::from_str),
+ arg_name = "MODE"
+ )]
+ convert_statics: ConvertStatics,
+
+ /// Force processing of files without GLOBAL_ASM blocks
+ #[argp(switch)]
+ force: bool,
+
+ /// Emit temporary files to this directory
+ #[argp(option, arg_name = "DIR")]
+ keep_preprocessed: Option<PathBuf>,
+
+ /// Replace floats with their encoded hexadecimal representation in CutsceneData data
+ #[argp(switch)]
+ encode_cutscene_data_floats: bool,
+
+ /// Don't generate a .d make dependency file
+ #[argp(switch)]
+ no_dep_file: bool,
+
+ #[argp(positional, greedy)]
+ rest: Vec<String>,
+}
+
+struct CompileOpts {
+ opt: OptLevel,
+ framepointer: bool,
+ mips1: bool,
+ kpic: bool,
+ pascal: bool,
+}
+
+fn extract_compiler_input_output(
+ compile_args: &[String],
+) -> Result<(PathBuf, PathBuf, Vec<String>), &'static str> {
+ let mut compile_args: Vec<String> = compile_args.to_vec();
+ let out_ind = compile_args
+ .iter()
+ .position(|arg| arg == "-o")
+ .ok_or("missing -o argument")?;
+ let out_filename = compile_args
+ .get(out_ind + 1)
+ .ok_or("missing argument after -o")?
+ .clone();
+ compile_args.remove(out_ind + 1);
+ compile_args.remove(out_ind);
+
+ let in_file_str = compile_args
+ .last()
+ .ok_or("missing input file argument")?
+ .clone();
+ compile_args.pop();
+
+ let out_file: PathBuf = out_filename.into();
+ let in_file: PathBuf = in_file_str.into();
+
+ Ok((in_file, out_file, compile_args))
+}
+
+fn parse_compile_args(
+ compile_args: &[String],
+ in_file: &Path,
+) -> Result<CompileOpts, &'static str> {
+ let mut opt_flags = vec![];
+ for x in compile_args {
+ opt_flags.push(match x.as_str() {
+ "-g" => OptLevel::G,
+ "-O0" => OptLevel::O0,
+ "-O1" => OptLevel::O1,
+ "-O2" => OptLevel::O2,
+ _ => continue,
+ });
+ }
+
+ if opt_flags.len() != 1 {
+ return Err("exactly one of -g/-O0/-O1/-O2 must be passed");
+ }
+ let mut opt = opt_flags[0];
+
+ let mips1 = !compile_args.contains(&"-mips2".to_string());
+ let framepointer = compile_args.contains(&"-framepointer".to_string());
+ let kpic = compile_args.contains(&"-KPIC".to_string());
+ if compile_args.contains(&"-g3".to_string()) {
+ if opt != OptLevel::O2 {
+ return Err("-g3 is only supported together with -O2");
+ }
+ opt = OptLevel::G3;
+ }
+
+ if mips1 && (!matches!(opt, OptLevel::O1 | OptLevel::O2) || framepointer) {
+ return Err("-mips1 is only supported together with -O1 or -O2");
+ }
+
+ let in_file_str = in_file.to_string_lossy();
+ let pascal = in_file_str.ends_with(".p")
+ || in_file_str.ends_with(".pas")
+ || in_file_str.ends_with(".pp");
+
+ if pascal && !matches!(opt, OptLevel::O1 | OptLevel::O2 | OptLevel::G3) {
+ return Err("Pascal is only supported together with -O1, -O2 or -O2 -g3");
+ }
+
+ Ok(CompileOpts {
+ opt,
+ framepointer,
+ mips1,
+ kpic,
+ pascal,
+ })
+}
+
+fn parse_rest(rest: &[String]) -> Option<(&[String], &[String], &[String])> {
+ let mut iter = rest.splitn(3, |x| *x == "--");
+ let compiler = iter.next()?;
+ let assembler = iter.next()?;
+ let compile_args = iter.next()?;
+ assert!(iter.next().is_none());
+ Some((compiler, assembler, compile_args))
+}
+
+struct ParsedArgs {
+ args: AsmProcArgs,
+ compiler: Vec<String>,
+ assembler: Vec<String>,
+ compile_args: Vec<String>,
+ in_file: PathBuf,
+ out_file: PathBuf,
+ opts: CompileOpts,
+}
+
+/// Parse command line arguments while allowing --a=b syntax.
+///
+/// This provides backward compatibility with Python argparse.
+fn from_args_allow_eq(progname: &[&str], args: &[OsString]) -> Result<AsmProcArgs, EarlyExit> {
+ let base_res = AsmProcArgs::from_args(progname, args);
+ if base_res.is_ok() {
+ return base_res;
+ }
+
+ // Try splitting up = chars successively, until we get a valid parse.
+ // This ensures we don't impact the compiler/assembler parts.
+ //
+ // Technically this might end up splitting --a --b=c into --a --b c even
+ // where --a is a flag that takes an argument --b=c, but this seems unlikely
+ // with our use case.
+ let mut i = 0;
+ let mut args = args.to_vec();
+ while i < args.len() {
+ let arg = args[i].as_encoded_bytes();
+ if arg.starts_with(b"--") {
+ if let Some(eq) = arg.iter().position(|&x| x == b'=') {
+ // SAFETY: splitting on ASCII still results in valid encoded bytes
+ let before = unsafe { OsString::from_encoded_bytes_unchecked(arg[0..eq].into()) };
+ let after = unsafe { OsString::from_encoded_bytes_unchecked(arg[eq + 1..].into()) };
+ args.splice(i..i + 1, [before, after]);
+ let new_res = AsmProcArgs::from_args(progname, &args);
+ if new_res.is_ok() {
+ return new_res;
+ }
+ i += 1;
+ }
+ }
+ i += 1;
+ }
+
+ base_res
+}
+
+fn parse_args_or_exit() -> ParsedArgs {
+ let argv: Vec<_> = std::env::args_os().collect();
+ let help_style = HelpStyle {
+ short_usage: true,
+ ..HelpStyle::default()
+ };
+ let progname = argv[0].to_string_lossy();
+
+ let args = from_args_allow_eq(&[&progname], &argv[1..]).unwrap_or_else(|early_exit| {
+ exit(match early_exit {
+ EarlyExit::Help(help) => {
+ println!(
+ "{}",
+ help.generate(&help_style).replace(
+ "[rest...]",
+ "<compiler...> -- <assembler...> -- <compiler flags...>"
+ )
+ );
+ 0
+ }
+ EarlyExit::Err(err) => {
+ eprintln!("{}\nRun {} --help for more information.", err, progname);
+ 1
+ }
+ })
+ });
+
+ let Some((compiler, assembler, compile_args)) = parse_rest(&args.rest) else {
+ eprintln!(
+ "Usage: {} [options] <compiler...> -- <assembler...> -- <compiler flags...>",
+ progname
+ );
+ eprintln!("Run {} --help for more information.", progname);
+ exit(1);
+ };
+
+ let (in_file, out_file, compile_args) = extract_compiler_input_output(compile_args)
+ .unwrap_or_else(|err| {
+ eprintln!("Failed to parse compiler flags: {}", err);
+ exit(1);
+ });
+
+ let opts = parse_compile_args(&compile_args, &in_file).unwrap_or_else(|err| {
+ eprintln!("Unsupported compiler flags: {}", err);
+ exit(1);
+ });
+
+ let compiler = compiler.into();
+ let assembler = assembler.into();
+
+ ParsedArgs {
+ args,
+ compiler,
+ assembler,
+ compile_args,
+ in_file,
+ out_file,
+ opts,
+ }
+}
+
+fn main() -> Result<()> {
+ let ParsedArgs {
+ args,
+ compiler,
+ assembler,
+ compile_args,
+ in_file,
+ out_file,
+ opts,
+ } = parse_args_or_exit();
+
+ let assembler_sh = assembler
+ .iter()
+ .map(|s| shlex::try_quote(s).unwrap().into_owned())
+ .collect::<Vec<String>>()
+ .join(" ");
+
+ let in_dir = fs::canonicalize(in_file.parent().unwrap().join("."))?;
+
+ let temp_dir = TempDir::with_prefix("asm_processor")?;
+ let preprocessed_filename = format!(
+ "preprocessed_{}",
+ in_file.file_name().unwrap().to_str().unwrap()
+ );
+ let preprocessed_path = temp_dir.path().join(&preprocessed_filename);
+ let mut preprocessed_file = File::create(&preprocessed_path)?;
+
+ let res = parse_source(&in_file, &args, &opts, true)?;
+ preprocessed_file.write_all(&res.output)?;
+
+ if let Some(keep_output_dir) = &args.keep_preprocessed {
+ fs::create_dir_all(keep_output_dir)?;
+ fs::copy(
+ &preprocessed_path,
+ keep_output_dir.join(&preprocessed_filename),
+ )?;
+ }
+
+ // Run compiler
+ let mut compile_command = Command::new(&compiler[0]);
+ compile_command
+ .args(&compile_args)
+ .arg("-I")
+ .arg(in_dir)
+ .arg("-o")
+ .arg(&out_file)
+ .arg(&preprocessed_path);
+
+ match compile_command.status() {
+ Ok(status) if status.success() => {}
+ _ => {
+ return Err(anyhow::anyhow!(
+ "Failed to compile file {}. Command line:\n\n{:?}\n",
+ in_file.display(),
+ compile_command
+ ));
+ }
+ }
+
+ if !res.functions.is_empty() || args.force {
+ let prelude_str;
+ let asm_prelude = match &args.asm_prelude {
+ Some(prelude) => {
+ if let Ok(res) = fs::read_to_string(prelude) {
+ prelude_str = res;
+ &prelude_str
+ } else {
+ return Err(anyhow::anyhow!("Failed to read asm prelude"));
+ }
+ }
+ None => include_str!("../../prelude.inc"),
+ };
+
+ fixup_objfile(
+ &out_file,
+ &res.functions,
+ asm_prelude,
+ &assembler_sh,
+ &args.output_enc,
+ args.drop_mdebug_gptab,
+ args.convert_statics,
+ )?;
+ }
+
+ if !res.deps.is_empty() && !args.no_dep_file {
+ let deps_file = out_file.with_extension("asmproc.d");
+ let mut deps_file = File::create(&deps_file)?;
+
+ writeln!(
+ deps_file,
+ "{}: {}",
+ out_file.to_str().unwrap(),
+ res.deps.join(" \\\n ")
+ )?;
+
+ for dep in res.deps {
+ writeln!(deps_file, "\n{dep}:")?;
+ }
+ }
+
+ Ok(())
+}