diff --git a/.github/workflows/build-site.yml b/.github/workflows/build-site.yml index cdf7826f..7d2b0495 100644 --- a/.github/workflows/build-site.yml +++ b/.github/workflows/build-site.yml @@ -33,6 +33,8 @@ jobs: uses: peaceiris/actions-mdbook@v2 with: mdbook-version: "0.4.13" + - name: Setup wasm + run: just setup-cargo-wasm - name: Build website run: just build-site - name: Upload artifact diff --git a/Cargo.toml b/Cargo.toml index 9334e895..6d75a68f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "emulator/mgba", "emulator/mgba-sys", "emulator/test-runner", + "website/backtrace", ] exclude = [ diff --git a/agb-debug/Cargo.toml b/agb-debug/Cargo.toml index 4c3f41f9..12977469 100644 --- a/agb-debug/Cargo.toml +++ b/agb-debug/Cargo.toml @@ -8,7 +8,7 @@ description = "CLI utility to convert agb stack trace dumps into human readable repository = "https://github.com/agbrs/agb" [dependencies] -anyhow = "1" +thiserror = "1" clap = { version = "4", features = ["derive"] } addr2line = { version = "0.22", default-features = false, features = [ "rustc-demangle", diff --git a/agb-debug/src/gwilym_encoding.rs b/agb-debug/src/gwilym_encoding.rs index 1ce957f5..f192c28a 100644 --- a/agb-debug/src/gwilym_encoding.rs +++ b/agb-debug/src/gwilym_encoding.rs @@ -1,8 +1,20 @@ use std::{slice::ChunksExact, sync::OnceLock}; +use thiserror::Error; + const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; -pub fn gwilym_decode(input: &str) -> anyhow::Result> { +#[derive(Debug, Error)] +pub enum GwilymDecodeError { + #[error("Does not contain version")] + NoVersion, + #[error("Only version 1 is supported")] + WrongVersion, + #[error("Input must be a multiple of 3 but have {0}")] + LengthWrong(usize), +} + +pub fn gwilym_decode(input: &str) -> Result, GwilymDecodeError> { GwilymDecodeIter::new(input) } @@ -11,21 +23,21 @@ pub struct GwilymDecodeIter<'a> { } impl<'a> GwilymDecodeIter<'a> { - fn new(input: &'a str) -> anyhow::Result { + fn new(input: &'a str) -> Result { let input = input .strip_prefix("https://agbrs.dev/crash#") .unwrap_or(input); let Some((input, version)) = input.rsplit_once('v') else { - anyhow::bail!("Does not contain version"); + return Err(GwilymDecodeError::NoVersion); }; if version != "1" { - anyhow::bail!("Only version 1 is supported"); + return Err(GwilymDecodeError::WrongVersion); } if input.len() % 3 != 0 { - anyhow::bail!("Input string must have length a multiple of 3"); + return Err(GwilymDecodeError::LengthWrong(input.len())); } Ok(Self { @@ -75,11 +87,11 @@ fn get_value_for_char(input: u8) -> u32 { #[cfg(test)] mod test { - use super::{gwilym_decode, ALPHABET}; + use super::*; use std::fmt::Write; #[test] - fn should_correctly_decode_16s() -> anyhow::Result<()> { + fn should_correctly_decode_16s() -> Result<(), GwilymDecodeError> { assert_eq!( &gwilym_decode("2QI65Q69306Kv1")?.collect::>(), &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] @@ -112,7 +124,7 @@ mod test { } #[test] - fn should_correctly_decode_16s_and_32s() -> anyhow::Result<()> { + fn should_correctly_decode_16s_and_32s() -> Result<(), Box> { let trace: &[u32] = &[ 0x0300_2990, 0x0800_3289, @@ -143,7 +155,7 @@ mod test { } #[test] - fn should_strip_the_agbrsdev_prefix() -> anyhow::Result<()> { + fn should_strip_the_agbrsdev_prefix() -> Result<(), Box> { assert_eq!( &gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::>(), &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] diff --git a/agb-debug/src/lib.rs b/agb-debug/src/lib.rs new file mode 100644 index 00000000..5c2c69e2 --- /dev/null +++ b/agb-debug/src/lib.rs @@ -0,0 +1,93 @@ +mod gwilym_encoding; +mod load_dwarf; + +use addr2line::gimli; +pub use gwilym_encoding::{gwilym_decode, GwilymDecodeError}; +pub use load_dwarf::{load_dwarf, GimliDwarf, LoadDwarfError}; +use thiserror::Error; + +pub use addr2line; + +pub struct AddressInfo { + pub location: Location, + pub is_interesting: bool, + pub is_inline: bool, + pub function: String, +} + +#[derive(Debug, Error)] +pub enum AddressInfoError { + #[error(transparent)] + Gimli(#[from] gimli::Error), +} + +pub struct Location { + pub filename: String, + pub line: u32, + pub col: u32, +} + +pub type Addr2LineContext = addr2line::Context>; + +impl Default for Location { + fn default() -> Self { + Self { + filename: "??".to_string(), + line: 0, + col: 0, + } + } +} + +pub fn address_info( + ctx: &Addr2LineContext, + address: u64, +) -> Result, AddressInfoError> { + let mut frames = ctx.find_frames(address).skip_all_loads()?; + + let mut is_first = true; + + let mut infos = Vec::new(); + + while let Some(frame) = frames.next()? { + let function_name = if let Some(ref func) = frame.function { + func.demangle()?.into_owned() + } else { + "unknown function".to_string() + }; + + let location = frame + .location + .as_ref() + .map(|location| Location { + filename: location.file.unwrap_or("??").to_owned(), + line: location.line.unwrap_or(0), + col: location.column.unwrap_or(0), + }) + .unwrap_or_default(); + + let is_interesting = is_interesting_function(&function_name, &location.filename); + + infos.push(AddressInfo { + location, + is_interesting, + is_inline: !is_first, + function: function_name, + }); + is_first = false; + } + + Ok(infos) +} + +fn is_interesting_function(function_name: &str, path: &str) -> bool { + if function_name == "rust_begin_unwind" { + return false; // this is the unwind exception call + } + + if path.ends_with("panicking.rs") { + return false; // probably part of rust's internal panic mechanisms + } + + true +} diff --git a/agb-debug/src/load_dwarf.rs b/agb-debug/src/load_dwarf.rs index 394e5e57..d4b13ab1 100644 --- a/agb-debug/src/load_dwarf.rs +++ b/agb-debug/src/load_dwarf.rs @@ -4,38 +4,52 @@ use addr2line::{ gimli, object::{self, Object}, }; -use anyhow::bail; +use thiserror::Error; -pub fn load_dwarf( - file_content: &[u8], -) -> anyhow::Result>> { +#[derive(Debug, Error)] +pub enum LoadDwarfError { + #[error("Gba file is empty")] + GbaFileEmpty, + #[error("Failed to load debug information from ROM file, it might not have been included?")] + NoDebugInformation, + #[error("Failed to load debug information: {0}")] + DeserializationError(#[from] rmp_serde::decode::Error), + #[error(transparent)] + GimliError(#[from] gimli::Error), +} + +pub type GimliDwarf = gimli::Dwarf>; + +pub fn load_dwarf(file_content: &[u8]) -> Result { if let Ok(object) = object::File::parse(file_content) { - return load_from_object(&object); + return Ok(load_from_object(&object)?); } // the file might have been padded, so ensure we skip any padding before continuing let last_non_zero_byte = file_content .iter() .rposition(|&b| b != 0) - .ok_or_else(|| anyhow::anyhow!("Gba file is empty"))?; + .ok_or_else(|| LoadDwarfError::GbaFileEmpty)?; let file_content = &file_content[..last_non_zero_byte + 1]; let last_8_bytes = &file_content[file_content.len() - 8..]; - let len = u32::from_le_bytes(last_8_bytes[0..4].try_into()?) as usize; + let len = u32::from_le_bytes( + last_8_bytes[0..4] + .try_into() + .or(Err(LoadDwarfError::NoDebugInformation))?, + ) as usize; let version = &last_8_bytes[4..]; if version != b"agb1" { - bail!("Failed to load debug information from ROM file, it might not have been included?"); + return Err(LoadDwarfError::NoDebugInformation); } let compressed_debug_data = &file_content[file_content.len() - len - 8..file_content.len() - 8]; let decompressing_reader = lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data)); - let debug_info: HashMap> = - rmp_serde::decode::from_read(decompressing_reader) - .map_err(|e| anyhow::anyhow!("Failed to load debug information: {e}"))?; + let debug_info: HashMap> = rmp_serde::decode::from_read(decompressing_reader)?; let dwarf = gimli::Dwarf::load(|id| { let data = debug_info @@ -54,7 +68,7 @@ pub fn load_dwarf( fn load_from_object<'file>( object: &object::File<'file, &'file [u8]>, -) -> anyhow::Result>> { +) -> Result { let endian = if object.is_little_endian() { gimli::RunTimeEndian::Little } else { diff --git a/agb-debug/src/main.rs b/agb-debug/src/main.rs index efb5bd62..9364a018 100644 --- a/agb-debug/src/main.rs +++ b/agb-debug/src/main.rs @@ -1,18 +1,15 @@ use std::{ borrow::Cow, + error::Error, fs::{self, File}, io::Read, path::PathBuf, time::SystemTime, }; -use addr2line::gimli; +use agb_debug::{address_info, AddressInfo, Location}; use clap::Parser; use colored::Colorize; -use load_dwarf::load_dwarf; - -mod gwilym_encoding; -mod load_dwarf; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -24,23 +21,7 @@ struct Args { dump: String, } -struct Location { - filename: String, - line: u32, - col: u32, -} - -impl Default for Location { - fn default() -> Self { - Self { - filename: "??".to_string(), - line: 0, - col: 0, - } - } -} - -fn main() -> anyhow::Result<()> { +fn main() -> Result<(), Box> { let cli = Args::parse(); let modification_time = fs::metadata(&cli.elf_path)? @@ -48,82 +29,51 @@ fn main() -> anyhow::Result<()> { .unwrap_or(SystemTime::UNIX_EPOCH); let file = fs::read(&cli.elf_path)?; - let dwarf = load_dwarf(&file)?; + let dwarf = agb_debug::load_dwarf(&file)?; let ctx = addr2line::Context::from_dwarf(dwarf)?; - for (i, address) in gwilym_encoding::gwilym_decode(&cli.dump)?.enumerate() { - print_address(&ctx, i, address.into(), modification_time)?; + for (i, address) in agb_debug::gwilym_decode(&cli.dump)?.enumerate() { + let infos = address_info(&ctx, address.into())?; + for info in infos { + print_address_info(&info, i, modification_time)?; + } } Ok(()) } -fn print_address( - ctx: &addr2line::Context, +fn print_address_info( + info: &AddressInfo, index: usize, - address: u64, elf_modification_time: SystemTime, -) -> anyhow::Result<()> { - let mut frames = ctx.find_frames(address).skip_all_loads()?; +) -> Result<(), Box> { + let function_name_to_print = &info.function; - let mut is_first = true; + if !info.is_inline { + print!("{index}:\t{function_name_to_print}"); + } else { + print!("\t(inlined into) {function_name_to_print}"); + } - while let Some(frame) = frames.next()? { - let function_name = if let Some(ref func) = frame.function { - func.demangle()?.into_owned() - } else { - "unknown function".to_string() - }; + println!( + " {}:{}", + prettify_path(&info.location.filename).green(), + info.location.line.to_string().green() + ); - let location = frame - .location - .as_ref() - .map(|location| Location { - filename: location.file.unwrap_or("??").to_owned(), - line: location.line.unwrap_or(0), - col: location.column.unwrap_or(0), - }) - .unwrap_or_default(); - - let is_interesting = is_interesting_function(&function_name, &location.filename); - let function_name_to_print = if is_interesting { - function_name.bold() - } else { - function_name.normal() - }; - - if is_first { - print!("{index}:\t{function_name_to_print}"); - } else { - print!("\t(inlined into) {function_name_to_print}"); - } - - println!( - " {}:{}", - prettify_path(&location.filename).green(), - location.line.to_string().green() - ); - - if location.line != 0 && is_interesting { - print_line_of_code(&frame, location, elf_modification_time)?; - } - - is_first = false; + if info.location.line != 0 && info.is_interesting { + print_line_of_code(&info.location, elf_modification_time)?; } Ok(()) } fn print_line_of_code( - frame: &addr2line::Frame<'_, impl gimli::Reader>, - location: Location, + location: &Location, elf_modification_time: SystemTime, -) -> anyhow::Result<()> { - let Some(filename) = frame.location.as_ref().and_then(|location| location.file) else { - return Ok(()); - }; - +) -> Result<(), Box> { + let filename = &location.filename; let Ok(mut file) = File::open(filename) else { return Ok(()); }; @@ -171,15 +121,3 @@ fn prettify_path(path: &str) -> Cow<'_, str> { Cow::Borrowed(path) } } - -fn is_interesting_function(function_name: &str, path: &str) -> bool { - if function_name == "rust_begin_unwind" { - return false; // this is the unwind exception call - } - - if path.ends_with("panicking.rs") { - return false; // probably part of rust's internal panic mechanisms - } - - true -} diff --git a/justfile b/justfile index c6ffcec3..b2e203e5 100644 --- a/justfile +++ b/justfile @@ -87,6 +87,15 @@ release +args: (_run-tool "release" args) miri: (cd agb-hashmap && cargo miri test) +setup-cargo-wasm: + cargo install wasm-pack + +build-website-backtrace: + (cd website/backtrace && wasm-pack build --target web) + rm -rf website/agb/src/app/vendor/backtrace + mkdir -p website/agb/src/app/vendor + cp website/backtrace/pkg website/agb/src/app/vendor/backtrace -r + build-mgba-wasm: rm -rf website/agb/src/app/mgba/vendor mkdir website/agb/src/app/mgba/vendor @@ -96,10 +105,16 @@ build-combo-rom-site: just _build-rom "examples/combo" "AGBGAMES" gzip -9 -c examples/target/examples/combo.gba > website/agb/src/app/combo.gba.gz -build-site-app: build-mgba-wasm build-combo-rom-site + +setup-app-build: build-mgba-wasm build-combo-rom-site build-website-backtrace (cd website/agb && npm install --no-save --prefer-offline --no-audit) + +build-site-app: setup-app-build (cd website/agb && npm run build) +serve-site-dev: setup-app-build + (cd website/agb && npm run dev) + build-site: build-site-app build-book rm -rf website/build cp website/agb/out website/build -r diff --git a/website/agb/src/app/crash/backtrace.tsx b/website/agb/src/app/crash/backtrace.tsx index 30570fca..589d1137 100644 --- a/website/agb/src/app/crash/backtrace.tsx +++ b/website/agb/src/app/crash/backtrace.tsx @@ -1,33 +1,93 @@ "use client"; -import { FC } from "react"; -import { useClientValue } from "../useClientValue.hook"; +import { useEffect, useState } from "react"; + +import { ContentBlock } from "../contentBlock"; +import { GameDeveloperSummary } from "./gameDeveloperSummary"; import { styled } from "styled-components"; +import { Debug } from "./debug"; -const BacktraceWrapper = styled.section` - display: flex; - gap: 10px; - justify-content: center; -`; - -function getBacktrace() { - return window.location.hash.slice(1); -} - -export function BacktraceDisplay() { - const backtrace = useClientValue(getBacktrace) ?? ""; +export function BacktracePage() { + const [backtrace, setBacktrace] = useState(""); + useEffect(() => { + setBacktrace(getBacktrace()); + }, []); + return ( + +

agbrs crash backtrace viewer

+

+ You likely got here from the link / QR code that was displayed when a + game you were playing crashed. This is the default crash page for games + made using the agb library. +

+

+ The creator of the game is very likely interested in the code + below along with a description of what you were doing at the + time.{" "} + Send these to the creator of the game you are playing. +

+ +

+ + The owners of this website are not necessarily the creators of the + game you are playing. + +

+

Backtrace

+ {backtrace && } + +
+ ); +} + +function BacktraceCopyDisplay({ + backtrace, + setBacktrace, +}: { + backtrace: string; + setBacktrace: (newValue: string) => void; +}) { return ( - - - + ); } + +const BacktraceInputBox = styled.input` + font-size: larger; + background-color: #eee; + border: 1px solid #aaa; + border-radius: 4px; + min-width: 0; + + flex-grow: 999; +`; + +const BacktraceWrapper = styled.section` + display: flex; + gap: 10px; + justify-content: center; + align-items: center; +`; + +const BacktraceCopyButton = styled.button` + padding: 10px; +`; + +function getBacktrace() { + return window.location.hash.slice(1); +} diff --git a/website/agb/src/app/crash/debug.tsx b/website/agb/src/app/crash/debug.tsx new file mode 100644 index 00000000..3765f14f --- /dev/null +++ b/website/agb/src/app/crash/debug.tsx @@ -0,0 +1,210 @@ +import { styled } from "styled-components"; +import { AddressInfo, AgbDebug, useAgbDebug } from "../useAgbDebug.hook"; +import { ReactNode, useMemo, useState } from "react"; + +const BacktraceListWrapper = styled.div` + font-size: 1rem; +`; + +const BacktraceList = styled.ol` + overflow-x: scroll; + white-space: nowrap; +`; + +interface DebugProps { + encodedBacktrace: string; +} + +export function Debug(props: DebugProps) { + const debug = useAgbDebug(); + if (debug) { + return ; + } else { + return

Loading debug viewer...

; + } +} + +interface DebugBacktraceDecodeProps extends DebugProps { + debug: AgbDebug; +} + +const NonWrapCode = styled.code` + white-space: nowrap; +`; + +function DebugBacktraceDecode({ + encodedBacktrace, + debug, +}: DebugBacktraceDecodeProps) { + const backtraceAddresses = useBacktraceData(debug, encodedBacktrace); + const [backtraceLocations, setBacktraceLocations] = useState( + [] + ); + + const [backtraceLocationsError, setBacktraceLocationsError] = + useState(""); + + if (typeof backtraceAddresses === "string") { + return ( + + Something went wrong decoding the backtrace: {backtraceAddresses} + + ); + } + + return ( + <> + + + {backtraceAddresses.map((x, idx) => ( +
  • + +
  • + ))} +
    +
    +

    + If you add the elf file used to make the GBA file, or the GBA file + itself if it was made with agb-gbafix --debug + , you can see: function names, file names, line numbers, and column + numbers. +

    + + {backtraceLocationsError && ( + + Something went wrong looking up the addresses in the file provided:{" "} + {backtraceLocationsError} + + )} + + ); +} + +const ErrorBlock = styled.div` + background-color: #f78f8f; + border: 2px solid #9c0a0a; + border-radius: 8px; + padding: 20px; + margin-top: 10px; +`; + +function DebugError({ children }: { children: ReactNode }) { + return {children}; +} + +function makeNicePath(path: string) { + const srcIndex = path.lastIndexOf("/src/"); + if (srcIndex < 0) return path; + + const crateNameStartIndex = path.slice(0, srcIndex).lastIndexOf("/"); + const crateName = + crateNameStartIndex < 0 + ? "" + : path.slice(crateNameStartIndex + 1, srcIndex); + + return `<${crateName}>/${path.slice(srcIndex + 5)}`; +} + +const GreenSpan = styled.span` + color: green; +`; + +const BacktraceAddressLine = styled.ul` + list-style-type: none; + padding-left: 20px; +`; + +function BacktraceAddressInfo({ + address, + info, +}: { + address: number; + info: AddressInfo[] | undefined; +}) { + const formattedAddress = `0x${address.toString(16).padStart(8, "0")}`; + if (!info) { + return {formattedAddress}; + } + + if (info.length === 0) { + return ( + +
  • + (no info) {formattedAddress} +
  • +
    + ); + } + + function FunctionName({ + interesting, + functionName, + }: { + interesting: boolean; + functionName: string; + }) { + if (interesting) { + return {functionName}; + } + return functionName; + } + + console.log(info); + + return ( + + {info.map((x, idx) => ( +
  • + + {x.is_inline && "(inlined into)"}{" "} + {" "} + + {makeNicePath(x.filename)}:{x.line_number}:{x.column} + + +
  • + ))} +
    + ); +} + +async function loadLocations(debug: AgbDebug, addresses: number[], file: File) { + const buf = await file.arrayBuffer(); + const view = new Uint8Array(buf); + + const agbDebugFile = debug.debug_file(view); + const debugInfo = addresses.map((x) => agbDebugFile.address_info(x)); + return debugInfo; +} + +function useBacktraceData(debug: AgbDebug, trace: string) { + return useMemo(() => { + try { + const addresses = debug?.decode_backtrace(trace); + return addresses && Array.from(addresses); + } catch (e: unknown) { + return `${e}`; + } + }, [debug, trace]); +} diff --git a/website/agb/src/app/crash/gameDeveloperSummary.tsx b/website/agb/src/app/crash/gameDeveloperSummary.tsx new file mode 100644 index 00000000..e67d9044 --- /dev/null +++ b/website/agb/src/app/crash/gameDeveloperSummary.tsx @@ -0,0 +1,23 @@ +import { styled } from "styled-components"; + +export function GameDeveloperSummary() { + return ( +
    + For game developers +

    If you don't want players to be sent to this page, you can:

    +
      +
    1. Configure the backtrace page to point to your own site
    2. +
    3. Configure the backtrace page to not point to a site at all
    4. +
    5. Not use the backtrace feature
    6. +
    +
    + ); +} + +const Details = styled.details` + margin-top: 10px; +`; + +const Summary = styled.summary` + font-weight: bold; +`; diff --git a/website/agb/src/app/crash/page.tsx b/website/agb/src/app/crash/page.tsx index 3b97c492..e9d4c70d 100644 --- a/website/agb/src/app/crash/page.tsx +++ b/website/agb/src/app/crash/page.tsx @@ -1,21 +1,10 @@ import { Metadata } from "next"; -import { BacktraceDisplay } from "./backtrace"; -import { ContentBlock } from "../contentBlock"; +import { BacktracePage } from "./backtrace"; export const metadata: Metadata = { title: "agbrs crash backtrace", }; -export default function Crash() { - return ( - -

    agbrs crash backtrace viewer

    -

    This page will eventually let you view backtraces in the browser.

    -

    - For now you can copy the backtrace code here and use it with{" "} - agb-addr2line -

    - -
    - ); +export default function Backtrace() { + return ; } diff --git a/website/agb/src/app/useAgbDebug.hook.ts b/website/agb/src/app/useAgbDebug.hook.ts new file mode 100644 index 00000000..4ed74e4e --- /dev/null +++ b/website/agb/src/app/useAgbDebug.hook.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from "react"; +import debugInit, { + decode_backtrace, + DebugFile, + InitOutput, + AddressInfo, +} from "./vendor/backtrace/backtrace"; + +let agbDebug: Promise | undefined; + +export { AddressInfo }; + +export interface AgbDebug { + decode_backtrace: (backtrace: string) => Uint32Array; + debug_file: (file: Uint8Array) => DebugFile; +} + +export function useAgbDebug(): AgbDebug | undefined { + const [debug, setDebug] = useState(); + + useEffect(() => { + (async () => { + if (agbDebug === undefined) { + agbDebug = debugInit(); + } + + await agbDebug; + + setDebug({ + decode_backtrace, + debug_file: (file: Uint8Array) => new DebugFile(file), + }); + })(); + }, []); + + return debug; +} diff --git a/website/backtrace/Cargo.toml b/website/backtrace/Cargo.toml new file mode 100644 index 00000000..222c93f5 --- /dev/null +++ b/website/backtrace/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "backtrace" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.84" +agb-debug = { path="../../agb-debug" } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" \ No newline at end of file diff --git a/website/backtrace/src/lib.rs b/website/backtrace/src/lib.rs new file mode 100644 index 00000000..c334fc95 --- /dev/null +++ b/website/backtrace/src/lib.rs @@ -0,0 +1,49 @@ +use agb_debug::{addr2line::Context, load_dwarf, Addr2LineContext}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn decode_backtrace(backtrace: &str) -> Result, JsError> { + Ok(agb_debug::gwilym_decode(backtrace)?.collect()) +} + +#[wasm_bindgen] +pub struct DebugFile { + dwarf: Addr2LineContext, +} + +#[wasm_bindgen(getter_with_clone)] +pub struct AddressInfo { + pub filename: String, + pub function_name: String, + pub line_number: u32, + pub column: u32, + pub is_interesting: bool, + pub is_inline: bool, +} + +#[wasm_bindgen] +impl DebugFile { + #[wasm_bindgen(constructor)] + pub fn new(data: &[u8]) -> Result { + Ok(DebugFile { + dwarf: Context::from_dwarf(load_dwarf(data)?)?, + }) + } + + pub fn address_info(&self, address: u32) -> Result, JsError> { + let info = agb_debug::address_info(&self.dwarf, address.into())?; + let address_infos = info + .into_iter() + .map(|x| AddressInfo { + filename: x.location.filename, + line_number: x.location.line, + column: x.location.col, + is_interesting: x.is_interesting, + is_inline: x.is_inline, + function_name: x.function, + }) + .collect(); + + Ok(address_infos) + } +}