Compare commits

...

28 Commits

Author SHA1 Message Date
LagoLunatic
f2a591356e Improve automatic symbol pairing for nameless literals (#247)
* Improve automatic symbol pairing for nameless literals

* Fix data reloc diffing when the reloc points to an in-function static symbol

* Only pair up literals that match perfectly

* Clippy

* Do two separate passes when pairing up literals

* Fix partially-matching literal pairups not working right

* Remove duplicate $ splitting code

* Implement $ splitting for section names too

* Minor cleanup
2025-08-30 23:07:43 -06:00
8d24ec6373 Detect x86 instruction size differences
Check raw code length when instructions have same disassembly
but different encodings. Marks as OpMismatch to indicate
encoding difference.

Fixes #242
Fixes #233
2025-08-30 22:57:39 -06:00
58430d947b ci: Remove quotes from CARGO_ZIGBUILD_ZIG_PATH 2025-08-30 15:49:14 -06:00
1533125f9d Resolve objdiff-wasm clippy warnings 2025-08-30 15:02:10 -06:00
5654060dc8 ci: Use uv tool instead of venv 2025-08-30 15:02:10 -06:00
84079c3934 objdiff-wasm build improvements 2025-08-30 15:02:10 -06:00
LagoLunatic
48804dc2e3 Merge pull request #245 from LagoLunatic/subi
PPC pooled data references: Add support for `subi`, `addis`, and `subis` instructions
2025-08-23 18:05:47 -04:00
LagoLunatic
8cfa8b7dab Update default watch patterns to include more extensions (#246) 2025-08-23 15:37:34 -06:00
LagoLunatic
93a4d7e55d PPC pooled data references: Add support for subi, addis, and subis instructions 2025-08-23 16:37:57 -04:00
iFarbod
7c424a7966 Ignore switchD_ labels generated by Ghidra (#241)
Seeing how commonly used boricj's delinker extension is, it makes sense for this one to be included by default, before #238 is considered and worked on.
2025-08-19 10:23:07 -06:00
678210d58a Change file watcher log message to debug 2025-08-15 16:38:06 -06:00
4302821615 Fix Windows build 2025-08-15 16:31:33 -06:00
c4b4244b59 Version v3.0.0 2025-08-15 16:27:27 -06:00
52c138bf06 Add "ignore_patterns" option to config
This allows explicitly ignoring changes to certain
files or directories, even if the changed file ends
up matching `watch_patterns`. The default value,
`build/**/*` will ensure that changes in the build
directory will not trigger a duplicate rebuild.

Resolves #143
Resolves #215
2025-08-15 16:24:26 -06:00
813c8aa539 Add "Diff fill color" option to Appearance
Allows configuring the background color of
lines with a diff.

Resolves #230
2025-08-15 15:43:23 -06:00
0f0aaab795 Fix WSL path handling
Resolves #170
2025-08-15 15:34:58 -06:00
b21892be31 Add CLI args to objdiff-gui (incl. --project-dir/-p)
Resolves #41
Resolves #211
2025-08-15 15:25:55 -06:00
247d6da94b Restore extab diff view 2025-08-15 15:06:16 -06:00
bd95faa9c3 Remove objdiff-cli diff JSON output mode
This has been unimplemented since v3.0.0-alpha.1,
and I don't currently have plans to bring it back.
If you need it for something, please open an issue!
2025-08-15 14:57:34 -06:00
2c57e4960f Add ARM logic for inferred function size padding
Resolves #237
2025-08-15 14:48:26 -06:00
cff4be2979 Update gimli, object
Resolves #228
2025-08-15 14:47:06 -06:00
LagoLunatic
e1da90943c Version v3.0.0-beta.14 2025-08-13 01:45:05 -04:00
LagoLunatic
b5713db333 Update dependencies 2025-08-13 01:42:34 -04:00
LagoLunatic
4c3f5e9836 Merge pull request #236 from LagoLunatic/symbol-ctx
Fix context menu not appearing when right clicking the function name in the function diff view
2025-08-11 17:28:26 -04:00
LagoLunatic
e9ce02feb0 Disable double tooltip for elided function name in function diff view 2025-08-08 20:36:48 -04:00
LagoLunatic
9a378d85ed Fix context menu on function name in function diff view 2025-08-08 20:35:16 -04:00
LagoLunatic
c8ff45f2c8 Merge pull request #234 from LagoLunatic/no-diff-data
Fix data section not showing when there is no section on the other side
2025-08-08 20:32:28 -04:00
LagoLunatic
34e4513c69 Fix data section not showing when there is no section on the other side 2025-08-06 16:09:18 -04:00
41 changed files with 973 additions and 912 deletions

View File

@@ -36,9 +36,9 @@ jobs:
- name: Cache Rust workspace - name: Cache Rust workspace
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Cargo check - name: Cargo check
run: cargo check --all-targets --all-features run: cargo check --all-targets --all-features --workspace
- name: Cargo clippy - name: Cargo clippy
run: cargo clippy --all-targets --all-features run: cargo clippy --all-targets --all-features --workspace
fmt: fmt:
name: Format name: Format
@@ -92,7 +92,7 @@ jobs:
- name: Cache Rust workspace - name: Cache Rust workspace
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Cargo test - name: Cargo test
run: cargo test --release --features all run: cargo test --release --all-features --workspace
build-cli: build-cli:
name: Build objdiff-cli name: Build objdiff-cli
@@ -146,13 +146,14 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install uv
if: matrix.build == 'zigbuild'
uses: astral-sh/setup-uv@v6
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
if: matrix.build == 'zigbuild' if: matrix.build == 'zigbuild'
run: | run: |
python3 -m venv .venv uv tool install cargo-zigbuild==0.20.1 --with-executables-from ziglang==0.15.1
. .venv/bin/activate echo "CARGO_ZIGBUILD_ZIG_PATH=$(uv tool dir)/cargo-zigbuild/bin/python-zig" >> $GITHUB_ENV
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0.post1 cargo-zigbuild==0.19.8
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
@@ -213,13 +214,14 @@ jobs:
sudo apt-get -y install ${{ matrix.packages }} sudo apt-get -y install ${{ matrix.packages }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install uv
if: matrix.build == 'zigbuild'
uses: astral-sh/setup-uv@v6
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
if: matrix.build == 'zigbuild' if: matrix.build == 'zigbuild'
run: | run: |
python3 -m venv .venv uv tool install cargo-zigbuild==0.20.1 --with-executables-from ziglang==0.15.1
. .venv/bin/activate echo "CARGO_ZIGBUILD_ZIG_PATH=$(uv tool dir)/cargo-zigbuild/bin/python-zig" >> $GITHUB_ENV
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0.post1 cargo-zigbuild==0.19.8
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:

View File

@@ -24,7 +24,7 @@ repos:
description: Run cargo clippy on all project files. description: Run cargo clippy on all project files.
language: system language: system
entry: cargo entry: cargo
args: ["+nightly", "clippy", "--all-targets", "--all-features"] args: ["+nightly", "clippy", "--all-targets", "--all-features", "--workspace"]
pass_filenames: false pass_filenames: false
- id: cargo-deny - id: cargo-deny
name: cargo deny name: cargo deny

410
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,18 +5,28 @@ members = [
"objdiff-gui", "objdiff-gui",
"objdiff-wasm", "objdiff-wasm",
] ]
default-members = [
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
# Exclude objdiff-wasm by default
]
resolver = "3" resolver = "3"
[workspace.package]
version = "3.0.1"
authors = ["Luke Street <luke@street.dev>"]
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.88"
[profile.release-lto] [profile.release-lto]
inherits = "release" inherits = "release"
lto = "fat" lto = "fat"
strip = "debuginfo" strip = "debuginfo"
codegen-units = 1 codegen-units = 1
[workspace.package] [profile.release-min]
version = "3.0.0-beta.13" inherits = "release-lto"
authors = ["Luke Street <luke@street.dev>"] opt-level = "z"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.88"

View File

@@ -90,22 +90,31 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
"build_base": true, "build_base": true,
"watch_patterns": [ "watch_patterns": [
"*.c", "*.c",
"*.cc",
"*.cp", "*.cp",
"*.cpp", "*.cpp",
"*.cxx", "*.cxx",
"*.c++",
"*.h", "*.h",
"*.hh",
"*.hp", "*.hp",
"*.hpp", "*.hpp",
"*.hxx", "*.hxx",
"*.h++",
"*.pch",
"*.pch++",
"*.inc",
"*.s", "*.s",
"*.S", "*.S",
"*.asm", "*.asm",
"*.inc",
"*.py", "*.py",
"*.yml", "*.yml",
"*.txt", "*.txt",
"*.json" "*.json"
], ],
"ignore_patterns": [
"build/**/*"
],
"units": [ "units": [
{ {
"name": "main/MetroTRK/mslsupp", "name": "main/MetroTRK/mslsupp",
@@ -141,6 +150,10 @@ It's unlikely you'll want to disable this, unless you're using an external tool
If any of these files change, objdiff will automatically rebuild the objects and re-compare them. If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
If not specified, objdiff will use the default patterns listed above. If not specified, objdiff will use the default patterns listed above.
`ignore_patterns` _(optional)_: A list of glob patterns to explicitly ignore when watching for changes.
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
If not specified, objdiff will use the default patterns listed above.
`units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation. `units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used. > `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.

View File

@@ -57,23 +57,39 @@
}, },
"default": [ "default": [
"*.c", "*.c",
"*.cc",
"*.cp", "*.cp",
"*.cpp", "*.cpp",
"*.cxx", "*.cxx",
"*.c++",
"*.h", "*.h",
"*.hh",
"*.hp", "*.hp",
"*.hpp", "*.hpp",
"*.hxx", "*.hxx",
"*.h++",
"*.pch",
"*.pch++",
"*.inc",
"*.s", "*.s",
"*.S", "*.S",
"*.asm", "*.asm",
"*.inc",
"*.py", "*.py",
"*.yml", "*.yml",
"*.txt", "*.txt",
"*.json" "*.json"
] ]
}, },
"ignore_patterns": {
"type": "array",
"description": "List of glob patterns to explicitly ignore when watching for changes.\nFiles matching these patterns will not trigger a rebuild.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"items": {
"type": "string"
},
"default": [
"build/**/*"
]
},
"objects": { "objects": {
"type": "array", "type": "array",
"description": "Use units instead.", "description": "Use units instead.",

View File

@@ -74,6 +74,7 @@ ignore = [
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" }, { id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an indirect dependency" },
] ]
# If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library. # If this is false, then it uses a built-in git library.
@@ -241,8 +242,8 @@ allow-git = []
[sources.allow-org] [sources.allow-org]
# github.com organizations to allow git sources for # github.com organizations to allow git sources for
github = [ github = [
"enarx", # flagset
"encounter", "encounter",
"gimli-rs", # gimli
] ]
# gitlab.com organizations to allow git sources for # gitlab.com organizations to allow git sources for
gitlab = [] gitlab = []

View File

@@ -19,7 +19,6 @@ use crossterm::{
}, },
}; };
use objdiff_core::{ use objdiff_core::{
bindings::diff::DiffResult,
build::{ build::{
BuildConfig, BuildStatus, BuildConfig, BuildStatus,
watcher::{Watcher, create_watcher}, watcher::{Watcher, create_watcher},
@@ -28,7 +27,7 @@ use objdiff_core::{
ProjectConfig, ProjectObject, ProjectObjectMetadata, build_globset, ProjectConfig, ProjectObject, ProjectObjectMetadata, build_globset,
path::{check_path_buf, platform_path, platform_path_serde_option}, path::{check_path_buf, platform_path, platform_path_serde_option},
}, },
diff::{self, DiffObjConfig, MappingConfig, ObjectDiff}, diff::{DiffObjConfig, MappingConfig, ObjectDiff},
jobs::{ jobs::{
Job, JobQueue, JobResult, Job, JobQueue, JobResult,
objdiff::{ObjDiffConfig, start_build}, objdiff::{ObjDiffConfig, start_build},
@@ -40,10 +39,7 @@ use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::{ use crate::{
cmd::apply_config_args, cmd::apply_config_args,
util::{ util::term::crossterm_panic_handler,
output::{OutputFormat, write_output},
term::crossterm_panic_handler,
},
views::{EventControlFlow, EventResult, UiView, function_diff::FunctionDiffUi}, views::{EventControlFlow, EventResult, UiView, function_diff::FunctionDiffUi},
}; };
@@ -63,12 +59,6 @@ pub struct Args {
#[argp(option, short = 'u')] #[argp(option, short = 'u')]
/// Unit name within project /// Unit name within project
unit: Option<String>, unit: Option<String>,
#[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file (one-shot mode) ("-" for stdout)
output: Option<Utf8PlatformPathBuf>,
#[argp(option)]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
#[argp(positional)] #[argp(positional)]
/// Function symbol to diff /// Function symbol to diff
symbol: Option<String>, symbol: Option<String>,
@@ -171,12 +161,8 @@ pub fn run(args: Args) -> Result<()> {
_ => bail!("Either target and base or project and unit must be specified"), _ => bail!("Either target and base or project and unit must be specified"),
}; };
if let Some(output) = &args.output {
run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref())
} else {
run_interactive(args, target_path, base_path, project_config) run_interactive(args, target_path, base_path, project_config)
} }
}
fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> { fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> {
let mut diff_config = DiffObjConfig::default(); let mut diff_config = DiffObjConfig::default();
@@ -194,28 +180,6 @@ fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)>
Ok((diff_config, mapping_config)) Ok((diff_config, mapping_config))
} }
fn run_oneshot(
args: &Args,
output: &Utf8PlatformPath,
target_path: Option<&Utf8PlatformPath>,
base_path: Option<&Utf8PlatformPath>,
) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let (diff_config, mapping_config) = build_config_from_args(args)?;
let target = target_path
.map(|p| obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {p}")))
.transpose()?;
let base = base_path
.map(|p| obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {p}")))
.transpose()?;
let result =
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
write_output(&DiffResult::new(left, right), Some(output), output_format)?;
Ok(())
}
pub struct AppState { pub struct AppState {
pub jobs: JobQueue, pub jobs: JobQueue,
pub waker: Arc<TermWaker>, pub waker: Arc<TermWaker>,
@@ -378,10 +342,12 @@ fn run_interactive(
}; };
if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) { if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) {
let watch_patterns = project_config.build_watch_patterns()?; let watch_patterns = project_config.build_watch_patterns()?;
let ignore_patterns = project_config.build_ignore_patterns()?;
state.watcher = Some(create_watcher( state.watcher = Some(create_watcher(
state.modified.clone(), state.modified.clone(),
project_dir.as_ref(), project_dir.as_ref(),
build_globset(&watch_patterns)?, build_globset(&watch_patterns)?,
build_globset(&ignore_patterns)?,
Waker::from(state.waker.clone()), Waker::from(state.waker.clone()),
)?); )?);
} }

View File

@@ -132,12 +132,12 @@ itertools = { version = "0.14", default-features = false, features = ["use_alloc
log = { version = "0.4", default-features = false, optional = true } log = { version = "0.4", default-features = false, optional = true }
memmap2 = { version = "0.9", optional = true } memmap2 = { version = "0.9", optional = true }
num-traits = { version = "0.2", default-features = false, optional = true } num-traits = { version = "0.2", default-features = false, optional = true }
object = { git = "https://github.com/gimli-rs/object", rev = "16ff70aa6fbd97d6bb7b92375929f4d72414c32b", default-features = false, features = ["read_core", "elf", "coff"] } object = { version = "0.37", default-features = false, features = ["read_core", "elf", "coff"] }
pbjson = { version = "0.8", default-features = false, optional = true } pbjson = { version = "0.8", default-features = false, optional = true }
prost = { version = "0.14", default-features = false, features = ["derive"], optional = true } prost = { version = "0.14", default-features = false, features = ["derive"], optional = true }
regex = { version = "1.11", default-features = false, features = [], optional = true } regex = { version = "1.11", default-features = false, features = [], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
similar = { version = "2.7", default-features = false, features = ["hashbrown"], optional = true, git = "https://github.com/encounter/similar.git", branch = "no_std" } similar = { git = "https://github.com/encounter/similar.git", branch = "no_std", default-features = false, features = ["hashbrown"], optional = true }
typed-path = { version = "0.11", default-features = false, optional = true } typed-path = { version = "0.11", default-features = false, optional = true }
# config # config
@@ -146,7 +146,7 @@ semver = { version = "1.0", default-features = false, optional = true }
serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true } serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
# dwarf # dwarf
gimli = { version = "0.32", default-features = false, features = ["read"], optional = true } gimli = { git = "https://github.com/gimli-rs/gimli", rev = "7335f00e7c39fd501511584fefb0ba974117c950", default-features = false, features = ["read"], optional = true }
typed-arena = { version = "2.0", default-features = false, optional = true } typed-arena = { version = "2.0", default-features = false, optional = true }
# ppc # ppc

View File

@@ -1,165 +0,0 @@
syntax = "proto3";
package objdiff.diff;
// A symbol
message Symbol {
// Name of the symbol
string name = 1;
// Demangled name of the symbol
optional string demangled_name = 2;
// Symbol address
uint64 address = 3;
// Symbol size
uint64 size = 4;
// Bitmask of SymbolFlag
uint32 flags = 5;
}
// Symbol visibility flags
enum SymbolFlag {
SYMBOL_NONE = 0;
SYMBOL_GLOBAL = 1;
SYMBOL_LOCAL = 2;
SYMBOL_WEAK = 4;
SYMBOL_COMMON = 8;
SYMBOL_HIDDEN = 16;
}
// A single parsed instruction
message Instruction {
// Instruction address
uint64 address = 1;
// Instruction size
uint32 size = 2;
// Instruction opcode
uint32 opcode = 3;
// Instruction mnemonic
string mnemonic = 4;
// Instruction formatted string
string formatted = 5;
// Original (unsimplified) instruction string
optional string original = 6;
// Instruction arguments
repeated Argument arguments = 7;
// Instruction relocation
optional Relocation relocation = 8;
// Instruction branch destination
optional uint64 branch_dest = 9;
// Instruction line number
optional uint32 line_number = 10;
}
// An instruction argument
message Argument {
oneof value {
// Plain text
string plain_text = 1;
// Value
ArgumentValue argument = 2;
// Relocation
ArgumentRelocation relocation = 3;
// Branch destination
uint64 branch_dest = 4;
}
}
// An instruction argument value
message ArgumentValue {
oneof value {
// Signed integer
int64 signed = 1;
// Unsigned integer
uint64 unsigned = 2;
// Opaque value
string opaque = 3;
}
}
// Marker type for relocation arguments
message ArgumentRelocation {
}
message Relocation {
uint32 type = 1;
string type_name = 2;
RelocationTarget target = 3;
}
message RelocationTarget {
uint32 symbol_index = 1;
int64 addend = 2;
}
message InstructionDiffRow {
DiffKind diff_kind = 1;
optional Instruction instruction = 2;
optional InstructionBranchFrom branch_from = 3;
optional InstructionBranchTo branch_to = 4;
repeated ArgumentDiff arg_diff = 5;
}
message ArgumentDiff {
optional uint32 diff_index = 1;
}
enum DiffKind {
DIFF_NONE = 0;
DIFF_REPLACE = 1;
DIFF_DELETE = 2;
DIFF_INSERT = 3;
DIFF_OP_MISMATCH = 4;
DIFF_ARG_MISMATCH = 5;
}
message InstructionBranchFrom {
repeated uint32 instruction_index = 1;
uint32 branch_index = 2;
}
message InstructionBranchTo {
uint32 instruction_index = 1;
uint32 branch_index = 2;
}
message SymbolDiff {
Symbol symbol = 1;
repeated InstructionDiffRow instruction_rows = 2;
optional float match_percent = 3;
// The symbol index in the _other_ object that this symbol was diffed against
optional uint32 target_symbol = 5;
}
message DataDiff {
DiffKind kind = 1;
bytes data = 2;
// May be larger than data
uint64 size = 3;
}
message SectionDiff {
string name = 1;
SectionKind kind = 2;
uint64 size = 3;
uint64 address = 4;
reserved 5;
repeated DataDiff data = 6;
optional float match_percent = 7;
}
enum SectionKind {
SECTION_UNKNOWN = 0;
SECTION_TEXT = 1;
SECTION_DATA = 2;
SECTION_BSS = 3;
}
message ObjectDiff {
repeated SectionDiff sections = 1;
repeated SymbolDiff symbols = 2;
}
message DiffResult {
optional ObjectDiff left = 1;
optional ObjectDiff right = 2;
}

View File

@@ -464,6 +464,22 @@ impl Arch for ArchArm {
} }
flags flags
} }
fn infer_function_size(
&self,
symbol: &Symbol,
section: &Section,
mut next_address: u64,
) -> Result<u64> {
// Trim any trailing 4-byte zeroes from the end (padding)
while next_address >= symbol.address + 4
&& let Some(data) = section.data_range(next_address - 4, 4)
&& data == [0u8; 4]
{
next_address -= 4;
}
Ok(next_address.saturating_sub(symbol.address))
}
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]

View File

@@ -6,6 +6,7 @@ use alloc::{
vec::Vec, vec::Vec,
}; };
use core::{ use core::{
any::Any,
ffi::CStr, ffi::CStr,
fmt::{self, Debug}, fmt::{self, Debug},
}; };
@@ -305,7 +306,7 @@ impl dyn Arch {
} }
} }
pub trait Arch: Send + Sync + Debug { pub trait Arch: Any + Debug + Send + Sync {
/// Finishes arch-specific initialization that must be done after sections have been combined. /// Finishes arch-specific initialization that must be done after sections have been combined.
fn post_init(&mut self, _sections: &[Section], _symbols: &[Symbol]) {} fn post_init(&mut self, _sections: &[Section], _symbols: &[Symbol]) {}

View File

@@ -675,7 +675,7 @@ fn make_symbol_ref(symbol: &object::Symbol) -> Result<ExtabSymbolRef> {
#[derive(Debug)] #[derive(Debug)]
struct PoolReference { struct PoolReference {
addr_src_gpr: powerpc::GPR, addr_src_gpr: powerpc::GPR,
addr_offset: i16, addr_offset: i64,
addr_dst_gpr: Option<powerpc::GPR>, addr_dst_gpr: Option<powerpc::GPR>,
} }
@@ -683,16 +683,20 @@ struct PoolReference {
// If so, return information pertaining to where the instruction is getting that address from and // If so, return information pertaining to where the instruction is getting that address from and
// what it's doing with the address (e.g. copying it into another register, adding an offset, etc). // what it's doing with the address (e.g. copying it into another register, adding an offset, etc).
fn get_pool_reference_for_inst( fn get_pool_reference_for_inst(
opcode: powerpc::Opcode, ins: powerpc::Ins,
simplified: &powerpc::ParsedIns, simplified: &powerpc::ParsedIns,
) -> Option<PoolReference> { ) -> Option<PoolReference> {
use powerpc::{Argument, Opcode}; use powerpc::{Argument, Opcode};
let args = &simplified.args; let args = &simplified.args;
if flow_analysis::guess_data_type_from_load_store_inst_op(opcode).is_some() { if flow_analysis::guess_data_type_from_load_store_inst_op(ins.op).is_some() {
match (args[1], args[2]) { match (args[1], args[2]) {
(Argument::Offset(offset), Argument::GPR(addr_src_gpr)) => { (Argument::Offset(offset), Argument::GPR(addr_src_gpr)) => {
// e.g. lwz. Immediate offset. // e.g. lwz. Immediate offset.
Some(PoolReference { addr_src_gpr, addr_offset: offset.0, addr_dst_gpr: None }) Some(PoolReference {
addr_src_gpr,
addr_offset: offset.0 as i64,
addr_dst_gpr: None,
})
} }
(Argument::GPR(addr_src_gpr), Argument::GPR(_offset_gpr)) => { (Argument::GPR(addr_src_gpr), Argument::GPR(_offset_gpr)) => {
// e.g. lwzx. The offset is in a register and was likely calculated from an index. // e.g. lwzx. The offset is in a register and was likely calculated from an index.
@@ -712,17 +716,51 @@ fn get_pool_reference_for_inst(
// If either of these match, we also want to return the destination register that the // If either of these match, we also want to return the destination register that the
// address is being copied into so that we can detect any future references to that new // address is being copied into so that we can detect any future references to that new
// register as well. // register as well.
match (opcode, args[0], args[1], args[2]) { match (ins.op, args[0], args[1], args[2]) {
( (
// `addi` or `subi`
Opcode::Addi, Opcode::Addi,
Argument::GPR(addr_dst_gpr), Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr), Argument::GPR(addr_src_gpr),
Argument::Simm(simm), Argument::Simm(simm),
) => Some(PoolReference { ) => {
let offset = if simplified.mnemonic == "addi" { simm.0 } else { -simm.0 };
Some(PoolReference {
addr_src_gpr, addr_src_gpr,
addr_offset: simm.0, addr_offset: offset as i64,
addr_dst_gpr: Some(addr_dst_gpr), addr_dst_gpr: Some(addr_dst_gpr),
}), })
}
(
// `addis`
Opcode::Addis,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::Uimm(uimm), // Note: `addis` uses UIMM, unlike `addi`, `subi`, and `subis`
) => {
assert_eq!(simplified.mnemonic, "addis");
let offset = (uimm.0 as i64) << 16;
Some(PoolReference {
addr_src_gpr,
addr_offset: offset,
addr_dst_gpr: Some(addr_dst_gpr),
})
}
(
// `subis`
Opcode::Addis,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::Simm(simm),
) => {
assert_eq!(simplified.mnemonic, "subis");
let offset = (simm.0 as i64) << 16;
Some(PoolReference {
addr_src_gpr,
addr_offset: offset,
addr_dst_gpr: Some(addr_dst_gpr),
})
}
( (
// `mr` or `mr.` // `mr` or `mr.`
Opcode::Or, Opcode::Or,
@@ -777,13 +815,13 @@ fn clear_overwritten_gprs(ins: powerpc::Ins, gpr_pool_relocs: &mut BTreeMap<u8,
// Also, if this instruction is accessing the middle of a symbol instead of the start, we add an // Also, if this instruction is accessing the middle of a symbol instead of the start, we add an
// addend to indicate that. // addend to indicate that.
fn make_fake_pool_reloc( fn make_fake_pool_reloc(
offset: i16, offset: i64,
cur_addr: u32, cur_addr: u32,
pool_reloc: &Relocation, pool_reloc: &Relocation,
symbols: &[Symbol], symbols: &[Symbol],
) -> Option<Relocation> { ) -> Option<Relocation> {
let pool_reloc = resolve_relocation(symbols, pool_reloc); let pool_reloc = resolve_relocation(symbols, pool_reloc);
let offset_from_pool = pool_reloc.relocation.addend + offset as i64; let offset_from_pool = pool_reloc.relocation.addend + offset;
let target_address = pool_reloc.symbol.address.checked_add_signed(offset_from_pool)?; let target_address = pool_reloc.symbol.address.checked_add_signed(offset_from_pool)?;
let target_symbol; let target_symbol;
let addend; let addend;
@@ -946,7 +984,7 @@ fn generate_fake_pool_relocations_for_function(
clear_overwritten_gprs(ins, &mut gpr_pool_relocs); clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
} }
} }
} else if let Some(pool_ref) = get_pool_reference_for_inst(ins.op, &simplified) { } else if let Some(pool_ref) = get_pool_reference_for_inst(ins, &simplified) {
// This instruction doesn't have a real relocation, so it may be a reference to one of // This instruction doesn't have a real relocation, so it may be a reference to one of
// the already-loaded pools. // the already-loaded pools.
if let Some(pool_reloc) = gpr_pool_relocs.get(&pool_ref.addr_src_gpr.0) { if let Some(pool_reloc) = gpr_pool_relocs.get(&pool_ref.addr_src_gpr.0) {
@@ -965,7 +1003,7 @@ fn generate_fake_pool_relocations_for_function(
// with the offset within the .data section of an array variable into r21. // with the offset within the .data section of an array variable into r21.
// Then the body of the loop will `lwzx` one of the array elements from r21. // Then the body of the loop will `lwzx` one of the array elements from r21.
let mut new_reloc = pool_reloc.clone(); let mut new_reloc = pool_reloc.clone();
new_reloc.addend += pool_ref.addr_offset as i64; new_reloc.addend += pool_ref.addr_offset;
gpr_pool_relocs.insert(addr_dst_gpr.0, new_reloc); gpr_pool_relocs.insert(addr_dst_gpr.0, new_reloc);
} else { } else {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs); clear_overwritten_gprs(ins, &mut gpr_pool_relocs);

View File

@@ -1,242 +0,0 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use crate::{diff, obj};
// Protobuf diff types
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs"));
#[cfg(feature = "serde")]
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs"));
impl DiffResult {
pub fn new(
_left: Option<(&obj::Object, &diff::ObjectDiff)>,
_right: Option<(&obj::Object, &diff::ObjectDiff)>,
) -> Self {
Self {
// TODO
// left: left.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
// right: right.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
left: None,
right: None,
}
}
}
// impl ObjectDiff {
// pub fn new(obj: &obj::Object, diff: &diff::ObjectDiff) -> Self {
// Self {
// sections: diff
// .sections
// .iter()
// .enumerate()
// .map(|(i, d)| SectionDiff::new(obj, i, d))
// .collect(),
// }
// }
// }
//
// impl SectionDiff {
// pub fn new(obj: &obj::Object, section_index: usize, section_diff: &diff::SectionDiff) -> Self {
// let section = &obj.sections[section_index];
// let symbols = section_diff.symbols.iter().map(|d| SymbolDiff::new(obj, d)).collect();
// let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
// // TODO: section_diff.reloc_diff
// Self {
// name: section.name.to_string(),
// kind: SectionKind::from(section.kind) as i32,
// size: section.size,
// address: section.address,
// symbols,
// data,
// match_percent: section_diff.match_percent,
// }
// }
// }
//
// impl From<obj::SectionKind> for SectionKind {
// fn from(value: obj::SectionKind) -> Self {
// match value {
// obj::SectionKind::Code => SectionKind::SectionText,
// obj::SectionKind::Data => SectionKind::SectionData,
// obj::SectionKind::Bss => SectionKind::SectionBss,
// // TODO common
// }
// }
// }
//
// impl SymbolDiff {
// pub fn new(object: &obj::Object, symbol_diff: &diff::SymbolDiff) -> Self {
// let symbol = object.symbols[symbol_diff.symbol_index];
// let instructions = symbol_diff
// .instruction_rows
// .iter()
// .map(|ins_diff| InstructionDiff::new(object, ins_diff))
// .collect();
// Self {
// symbol: Some(Symbol::new(symbol)),
// instructions,
// match_percent: symbol_diff.match_percent,
// target: symbol_diff.target_symbol.map(SymbolRef::from),
// }
// }
// }
//
// impl DataDiff {
// pub fn new(_object: &obj::Object, data_diff: &diff::DataDiff) -> Self {
// Self {
// kind: DiffKind::from(data_diff.kind) as i32,
// data: data_diff.data.clone(),
// size: data_diff.len as u64,
// }
// }
// }
//
// impl Symbol {
// pub fn new(value: &ObjSymbol) -> Self {
// Self {
// name: value.name.to_string(),
// demangled_name: value.demangled_name.clone(),
// address: value.address,
// size: value.size,
// flags: symbol_flags(value.flags),
// }
// }
// }
//
// fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
// let mut flags = 0u32;
// if value.0.contains(ObjSymbolFlags::Global) {
// flags |= SymbolFlag::SymbolGlobal as u32;
// }
// if value.0.contains(ObjSymbolFlags::Local) {
// flags |= SymbolFlag::SymbolLocal as u32;
// }
// if value.0.contains(ObjSymbolFlags::Weak) {
// flags |= SymbolFlag::SymbolWeak as u32;
// }
// if value.0.contains(ObjSymbolFlags::Common) {
// flags |= SymbolFlag::SymbolCommon as u32;
// }
// if value.0.contains(ObjSymbolFlags::Hidden) {
// flags |= SymbolFlag::SymbolHidden as u32;
// }
// flags
// }
//
// impl Instruction {
// pub fn new(object: &obj::Object, instruction: &ObjIns) -> Self {
// Self {
// address: instruction.address,
// size: instruction.size as u32,
// opcode: instruction.op as u32,
// mnemonic: instruction.mnemonic.to_string(),
// formatted: instruction.formatted.clone(),
// arguments: instruction.args.iter().map(Argument::new).collect(),
// relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
// branch_dest: instruction.branch_dest,
// line_number: instruction.line,
// original: instruction.orig.clone(),
// }
// }
// }
//
// impl Argument {
// pub fn new(value: &ObjInsArg) -> Self {
// Self {
// value: Some(match value {
// ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
// ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
// ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
// ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
// }),
// }
// }
// }
//
// impl ArgumentValue {
// pub fn new(value: &ObjInsArgValue) -> Self {
// Self {
// value: Some(match value {
// ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
// ObjInsArgValue::Unsigned(v) => argument_value::Value::Unsigned(*v),
// ObjInsArgValue::Opaque(v) => argument_value::Value::Opaque(v.to_string()),
// }),
// }
// }
// }
//
// impl Relocation {
// pub fn new(object: &obj::Object, reloc: &ObjReloc) -> Self {
// Self {
// r#type: match reloc.flags {
// object::RelocationFlags::Elf { r_type } => r_type,
// object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
// object::RelocationFlags::Coff { typ } => typ as u32,
// object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
// _ => unreachable!(),
// },
// type_name: object.arch.display_reloc(reloc.flags).into_owned(),
// target: Some(RelocationTarget {
// symbol: Some(Symbol::new(&reloc.target)),
// addend: reloc.addend,
// }),
// }
// }
// }
//
// impl InstructionDiff {
// pub fn new(object: &obj::Object, instruction_diff: &ObjInsDiff) -> Self {
// Self {
// instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
// diff_kind: DiffKind::from(instruction_diff.kind) as i32,
// branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
// branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
// arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
// }
// }
// }
//
// impl ArgumentDiff {
// pub fn new(value: &Option<ObjInsArgDiff>) -> Self {
// Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
// }
// }
//
// impl From<ObjInsDiffKind> for DiffKind {
// fn from(value: ObjInsDiffKind) -> Self {
// match value {
// ObjInsDiffKind::None => DiffKind::DiffNone,
// ObjInsDiffKind::OpMismatch => DiffKind::DiffOpMismatch,
// ObjInsDiffKind::ArgMismatch => DiffKind::DiffArgMismatch,
// ObjInsDiffKind::Replace => DiffKind::DiffReplace,
// ObjInsDiffKind::Delete => DiffKind::DiffDelete,
// ObjInsDiffKind::Insert => DiffKind::DiffInsert,
// }
// }
// }
//
// impl From<ObjDataDiffKind> for DiffKind {
// fn from(value: ObjDataDiffKind) -> Self {
// match value {
// ObjDataDiffKind::None => DiffKind::DiffNone,
// ObjDataDiffKind::Replace => DiffKind::DiffReplace,
// ObjDataDiffKind::Delete => DiffKind::DiffDelete,
// ObjDataDiffKind::Insert => DiffKind::DiffInsert,
// }
// }
// }
//
// impl InstructionBranchFrom {
// pub fn new(value: &ObjInsBranchFrom) -> Self {
// Self {
// instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
// branch_index: value.branch_idx as u32,
// }
// }
// }
//
// impl InstructionBranchTo {
// pub fn new(value: &ObjInsBranchTo) -> Self {
// Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
// }
// }

View File

@@ -1,3 +1 @@
#[cfg(feature = "any-arch")]
pub mod diff;
pub mod report; pub mod report;

View File

@@ -49,6 +49,7 @@ pub fn run_make(config: &BuildConfig, arg: &Utf8UnixPath) -> BuildStatus {
}; };
#[cfg(windows)] #[cfg(windows)]
let mut command = { let mut command = {
use alloc::borrow::Cow;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
let mut command = if config.selected_wsl_distro.is_some() { let mut command = if config.selected_wsl_distro.is_some() {
@@ -60,13 +61,17 @@ pub fn run_make(config: &BuildConfig, arg: &Utf8UnixPath) -> BuildStatus {
// Strip distro root prefix \\wsl.localhost\{distro} // Strip distro root prefix \\wsl.localhost\{distro}
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro); let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
let cwd = match cwd.strip_prefix(wsl_path_prefix) { let cwd = match cwd.strip_prefix(wsl_path_prefix) {
Ok(new_cwd) => Utf8UnixPath::new("/").join(new_cwd.with_unix_encoding()), // Convert to absolute Unix path
Err(_) => cwd.with_unix_encoding(), Ok(new_cwd) => Cow::Owned(
Utf8UnixPath::new("/").join(new_cwd.with_unix_encoding()).into_string(),
),
// Otherwise, use the Windows path as is
Err(_) => Cow::Borrowed(cwd.as_str()),
}; };
command command
.arg("--cd") .arg("--cd")
.arg(cwd.as_str()) .arg::<&str>(cwd.as_ref())
.arg("-d") .arg("-d")
.arg(distro) .arg(distro)
.arg("--") .arg("--")

View File

@@ -29,6 +29,7 @@ pub fn create_watcher(
modified: Arc<AtomicBool>, modified: Arc<AtomicBool>,
project_dir: &Path, project_dir: &Path,
patterns: GlobSet, patterns: GlobSet,
ignore_patterns: GlobSet,
waker: Waker, waker: Waker,
) -> notify::Result<Watcher> { ) -> notify::Result<Watcher> {
let base_dir = fs::canonicalize(project_dir)?; let base_dir = fs::canonicalize(project_dir)?;
@@ -54,8 +55,8 @@ pub fn create_watcher(
let Ok(path) = path.strip_prefix(&base_dir_clone) else { let Ok(path) = path.strip_prefix(&base_dir_clone) else {
continue; continue;
}; };
if patterns.is_match(path) { if patterns.is_match(path) && !ignore_patterns.is_match(path) {
// log::info!("File modified: {}", path.display()); log::debug!("File modified: {}", path.display());
any_match = true; any_match = true;
} }
} }

View File

@@ -36,6 +36,8 @@ pub struct ProjectConfig {
pub build_target: Option<bool>, pub build_target: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub watch_patterns: Option<Vec<String>>, pub watch_patterns: Option<Vec<String>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub ignore_patterns: Option<Vec<String>>,
#[cfg_attr( #[cfg_attr(
feature = "serde", feature = "serde",
serde(alias = "objects", skip_serializing_if = "Option::is_none") serde(alias = "objects", skip_serializing_if = "Option::is_none")
@@ -66,7 +68,18 @@ impl ProjectConfig {
.map(|s| Glob::new(s)) .map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()? .collect::<Result<Vec<Glob>, globset::Error>>()?
} else { } else {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() default_watch_patterns()
})
}
pub fn build_ignore_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
Ok(if let Some(ignore_patterns) = &self.ignore_patterns {
ignore_patterns
.iter()
.map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?
} else {
default_ignore_patterns()
}) })
} }
} }
@@ -191,14 +204,21 @@ pub struct ScratchConfig {
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"]; pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm", "*.c", "*.cc", "*.cp", "*.cpp", "*.cxx", "*.c++", "*.h", "*.hh", "*.hp", "*.hpp", "*.hxx",
"*.inc", "*.py", "*.yml", "*.txt", "*.json", "*.h++", "*.pch", "*.pch++", "*.inc", "*.s", "*.S", "*.asm", "*.py", "*.yml", "*.txt",
"*.json",
]; ];
pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &["build/**/*"];
pub fn default_watch_patterns() -> Vec<Glob> { pub fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
} }
pub fn default_ignore_patterns() -> Vec<Glob> {
DEFAULT_IGNORE_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
#[cfg(feature = "std")] #[cfg(feature = "std")]
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo { pub struct ProjectConfigInfo {

View File

@@ -496,6 +496,14 @@ fn diff_instruction(
result.right_args_diff.push(InstructionArgDiffIndex::new(b_diff)); result.right_args_diff.push(InstructionArgDiffIndex::new(b_diff));
} }
} }
if result.kind == InstructionDiffKind::None
&& left_resolved.code.len() != right_resolved.code.len()
{
// If everything else matches but the raw code length differs (e.g. x86 instructions
// with same disassembly but different encoding), mark as op mismatch
result.kind = InstructionDiffKind::OpMismatch;
state.diff_score += PENALTY_REG_DIFF;
}
return Ok(result); return Ok(result);
} }

View File

@@ -35,6 +35,20 @@ pub fn diff_bss_symbol(
)) ))
} }
pub fn symbol_name_matches(left_name: &str, right_name: &str) -> bool {
// Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = left_name.split_once('$') {
if !suffix.chars().all(char::is_numeric) {
return false;
}
right_name
.split_once('$')
.is_some_and(|(p, s)| p == prefix && s.chars().all(char::is_numeric))
} else {
left_name == right_name
}
}
fn reloc_eq( fn reloc_eq(
left_obj: &Object, left_obj: &Object,
right_obj: &Object, right_obj: &Object,
@@ -45,8 +59,8 @@ fn reloc_eq(
return false; return false;
} }
let symbol_name_addend_matches = let symbol_name_addend_matches = symbol_name_matches(&left.symbol.name, &right.symbol.name)
left.symbol.name == right.symbol.name && left.relocation.addend == right.relocation.addend; && left.relocation.addend == right.relocation.addend;
match (left.symbol.section, right.symbol.section) { match (left.symbol.section, right.symbol.section) {
(Some(sl), Some(sr)) => { (Some(sl), Some(sr)) => {
// Match if section and name+addend or address match // Match if section and name+addend or address match
@@ -127,6 +141,28 @@ fn diff_data_relocs_for_range<'left, 'right>(
diffs diffs
} }
pub fn no_diff_data_section(obj: &Object, section_idx: usize) -> Result<SectionDiff> {
let section = &obj.sections[section_idx];
let len = section.data.len();
let data = &section.data[0..len];
let data_diff =
vec![DataDiff { data: data.to_vec(), kind: DataDiffKind::None, len, ..Default::default() }];
let mut reloc_diffs = Vec::new();
for reloc in section.relocations.iter() {
let reloc_len = obj.arch.data_reloc_size(reloc.flags);
let range = reloc.address as usize..reloc.address as usize + reloc_len;
reloc_diffs.push(DataRelocationDiff {
reloc: reloc.clone(),
kind: DataDiffKind::None,
range,
});
}
Ok(SectionDiff { match_percent: Some(0.0), data_diff, reloc_diff: reloc_diffs })
}
/// Compare the data sections of two object files. /// Compare the data sections of two object files.
pub fn diff_data_section( pub fn diff_data_section(
left_obj: &Object, left_obj: &Object,
@@ -415,6 +451,10 @@ pub fn diff_generic_section(
)) ))
} }
pub fn no_diff_bss_section() -> Result<SectionDiff> {
Ok(SectionDiff { match_percent: Some(0.0), data_diff: vec![], reloc_diff: vec![] })
}
/// Compare the addresses and sizes of each symbol in the BSS sections. /// Compare the addresses and sizes of each symbol in the BSS sections.
pub fn diff_bss_section( pub fn diff_bss_section(
left_obj: &Object, left_obj: &Object,

View File

@@ -13,7 +13,7 @@ use crate::{
code::{diff_code, no_diff_code}, code::{diff_code, no_diff_code},
data::{ data::{
diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol, diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol,
diff_generic_section, diff_generic_section, no_diff_bss_section, no_diff_data_section, symbol_name_matches,
}, },
}, },
obj::{InstructionRef, Object, Relocation, SectionKind, Symbol, SymbolFlag}, obj::{InstructionRef, Object, Relocation, SectionKind, Symbol, SymbolFlag},
@@ -288,12 +288,12 @@ pub fn diff_objs(
} }
for section_match in section_matches { for section_match in section_matches {
if let SectionMatch { match section_match {
SectionMatch {
left: Some(left_section_idx), left: Some(left_section_idx),
right: Some(right_section_idx), right: Some(right_section_idx),
section_kind, section_kind,
} = section_match } => {
{
let (left_obj, left_out) = left.as_mut().unwrap(); let (left_obj, left_out) = left.as_mut().unwrap();
let (right_obj, right_out) = right.as_mut().unwrap(); let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind { match section_kind {
@@ -336,6 +336,38 @@ pub fn diff_objs(
SectionKind::Unknown => unreachable!(), SectionKind::Unknown => unreachable!(),
} }
} }
SectionMatch { left: Some(left_section_idx), right: None, section_kind } => {
let (left_obj, left_out) = left.as_mut().unwrap();
match section_kind {
SectionKind::Code => {}
SectionKind::Data => {
left_out.sections[left_section_idx] =
no_diff_data_section(left_obj, left_section_idx)?;
}
SectionKind::Bss | SectionKind::Common => {
left_out.sections[left_section_idx] = no_diff_bss_section()?;
}
SectionKind::Unknown => unreachable!(),
}
}
SectionMatch { left: None, right: Some(right_section_idx), section_kind } => {
let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind {
SectionKind::Code => {}
SectionKind::Data => {
right_out.sections[right_section_idx] =
no_diff_data_section(right_obj, right_section_idx)?;
}
SectionKind::Bss | SectionKind::Common => {
right_out.sections[right_section_idx] = no_diff_bss_section()?;
}
SectionKind::Unknown => unreachable!(),
}
}
SectionMatch { left: None, right: None, .. } => {
// Should not happen
}
}
} }
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) = if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
@@ -543,6 +575,9 @@ fn matching_symbols(
&mut matches, &mut matches,
)?; )?;
} }
// Do two passes for nameless literals. The first only pairs up perfect matches to ensure
// those are correct first, while the second pass catches near matches.
for fuzzy_literals in [false, true] {
for (symbol_idx, symbol) in left.symbols.iter().enumerate() { for (symbol_idx, symbol) in left.symbols.iter().enumerate() {
if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) { if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) {
continue; continue;
@@ -556,17 +591,22 @@ fn matching_symbols(
} }
let symbol_match = SymbolMatch { let symbol_match = SymbolMatch {
left: Some(symbol_idx), left: Some(symbol_idx),
right: find_symbol(right, left, symbol, Some(&right_used)), right: find_symbol(right, left, symbol_idx, Some(&right_used), fuzzy_literals),
prev: find_symbol(prev, left, symbol, None), prev: find_symbol(prev, left, symbol_idx, None, fuzzy_literals),
section_kind, section_kind,
}; };
matches.push(symbol_match); matches.push(symbol_match);
if let Some(right) = symbol_match.right { if let Some(right) = symbol_match.right {
left_used.insert(symbol_idx);
right_used.insert(right); right_used.insert(right);
} }
} }
} }
}
if let Some(right) = right { if let Some(right) = right {
// Do two passes for nameless literals. The first only pairs up perfect matches to ensure
// those are correct first, while the second pass catches near matches.
for fuzzy_literals in [false, true] {
for (symbol_idx, symbol) in right.symbols.iter().enumerate() { for (symbol_idx, symbol) in right.symbols.iter().enumerate() {
if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) { if symbol.size == 0 || symbol.flags.contains(SymbolFlag::Ignored) {
continue; continue;
@@ -578,12 +618,17 @@ fn matching_symbols(
if right_used.contains(&symbol_idx) { if right_used.contains(&symbol_idx) {
continue; continue;
} }
matches.push(SymbolMatch { let symbol_match = SymbolMatch {
left: None, left: None,
right: Some(symbol_idx), right: Some(symbol_idx),
prev: find_symbol(prev, right, symbol, None), prev: find_symbol(prev, right, symbol_idx, None, fuzzy_literals),
section_kind, section_kind,
}); };
matches.push(symbol_match);
if symbol_match.prev.is_some() {
right_used.insert(symbol_idx);
}
}
} }
} }
Ok(matches) Ok(matches)
@@ -605,7 +650,10 @@ where
fn symbol_section<'obj>(obj: &'obj Object, symbol: &Symbol) -> Option<(&'obj str, SectionKind)> { fn symbol_section<'obj>(obj: &'obj Object, symbol: &Symbol) -> Option<(&'obj str, SectionKind)> {
if let Some(section) = symbol.section.and_then(|section_idx| obj.sections.get(section_idx)) { if let Some(section) = symbol.section.and_then(|section_idx| obj.sections.get(section_idx)) {
Some((section.name.as_str(), section.kind)) // Match x86 .rdata$r against .rdata$rs
let section_name =
section.name.split_once('$').map_or(section.name.as_str(), |(prefix, _)| prefix);
Some((section_name, section.kind))
} else if symbol.flags.contains(SymbolFlag::Common) { } else if symbol.flags.contains(SymbolFlag::Common) {
Some((".comm", SectionKind::Common)) Some((".comm", SectionKind::Common))
} else { } else {
@@ -621,52 +669,94 @@ fn symbol_section_kind(obj: &Object, symbol: &Symbol) -> SectionKind {
} }
} }
/// Check if a symbol is a compiler-generated literal like @1234.
fn is_symbol_compiler_generated_literal(symbol: &Symbol) -> bool {
if !symbol.name.starts_with('@') {
return false;
}
if !symbol.name[1..].chars().all(char::is_numeric) {
// Exclude @stringBase0, @GUARD@, etc.
return false;
}
true
}
fn find_symbol( fn find_symbol(
obj: Option<&Object>, obj: Option<&Object>,
in_obj: &Object, in_obj: &Object,
in_symbol: &Symbol, in_symbol_idx: usize,
used: Option<&BTreeSet<usize>>, used: Option<&BTreeSet<usize>>,
fuzzy_literals: bool,
) -> Option<usize> { ) -> Option<usize> {
let in_symbol = &in_obj.symbols[in_symbol_idx];
let obj = obj?; let obj = obj?;
let (section_name, section_kind) = symbol_section(in_obj, in_symbol)?; let (section_name, section_kind) = symbol_section(in_obj, in_symbol)?;
// Match compiler-generated symbols against each other (e.g. @251 -> @60)
// If they are in the same section and have the same value
if is_symbol_compiler_generated_literal(in_symbol)
&& matches!(section_kind, SectionKind::Data | SectionKind::Bss)
{
let mut closest_match_symbol_idx = None;
let mut closest_match_percent = 0.0;
for (symbol_idx, symbol) in unmatched_symbols(obj, used) {
let Some(section_index) = symbol.section else {
continue;
};
if obj.sections[section_index].name != section_name {
continue;
}
if !is_symbol_compiler_generated_literal(symbol) {
continue;
}
match section_kind {
SectionKind::Data => {
// For data, pick the first symbol with exactly matching bytes and relocations.
// If no symbols match exactly, and `fuzzy_literals` is true, pick the closest
// plausible match instead.
if let Ok((left_diff, _right_diff)) =
diff_data_symbol(in_obj, obj, in_symbol_idx, symbol_idx)
&& let Some(match_percent) = left_diff.match_percent
&& (match_percent == 100.0
|| (fuzzy_literals
&& match_percent >= 50.0
&& match_percent > closest_match_percent))
{
closest_match_symbol_idx = Some(symbol_idx);
closest_match_percent = match_percent;
if match_percent == 100.0 {
break;
}
}
}
SectionKind::Bss => {
// For BSS, pick the first symbol that has the exact matching size.
if in_symbol.size == symbol.size {
closest_match_symbol_idx = Some(symbol_idx);
break;
}
}
_ => unreachable!(),
}
}
return closest_match_symbol_idx;
}
// Try to find an exact name match // Try to find an exact name match
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| { if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| {
symbol.name == in_symbol.name && symbol_section_kind(obj, symbol) == section_kind symbol.name == in_symbol.name && symbol_section_kind(obj, symbol) == section_kind
}) { }) {
return Some(symbol_idx); return Some(symbol_idx);
} }
// Match compiler-generated symbols against each other (e.g. @251 -> @60)
// If they are at the same address in the same section
if in_symbol.name.starts_with('@')
&& matches!(section_kind, SectionKind::Data | SectionKind::Bss)
&& let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| {
let Some(section_index) = symbol.section else {
return false;
};
symbol.name.starts_with('@')
&& symbol.address == in_symbol.address
&& obj.sections[section_index].name == section_name
})
{
return Some(symbol_idx);
}
// Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = in_symbol.name.split_once('$') {
if !suffix.chars().all(char::is_numeric) {
return None;
}
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|&(_, symbol)| { if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|&(_, symbol)| {
if let Some((p, s)) = symbol.name.split_once('$') { symbol_name_matches(&in_symbol.name, &symbol.name)
prefix == p
&& s.chars().all(char::is_numeric)
&& symbol_section_kind(obj, symbol) == section_kind && symbol_section_kind(obj, symbol) == section_kind
} else { && symbol_section(obj, symbol).is_some_and(|(name, _)| name == section_name)
false
}
}) { }) {
return Some(symbol_idx); return Some(symbol_idx);
} }
}
None None
} }

View File

@@ -170,13 +170,6 @@ pub enum JobResult {
CreateScratch(Option<Box<CreateScratchResult>>), CreateScratch(Option<Box<CreateScratchResult>>),
} }
fn should_cancel(rx: &Receiver<()>) -> bool {
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => true,
Err(_) => false,
}
}
fn start_job( fn start_job(
waker: Waker, waker: Waker,
title: &str, title: &str,
@@ -203,7 +196,6 @@ fn start_job(
} }
}); });
let id = JOB_ID.fetch_add(1, Ordering::Relaxed); let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
// log::info!("Started job {}", id); TODO
JobState { id, kind, handle: Some(handle), context, cancel: tx } JobState { id, kind, handle: Some(handle), context, cancel: tx }
} }
@@ -228,3 +220,10 @@ fn update_status(
context.waker.wake_by_ref(); context.waker.wake_by_ref();
Ok(()) Ok(())
} }
fn should_cancel(rx: &Receiver<()>) -> bool {
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => true,
Err(_) => false,
}
}

View File

@@ -130,7 +130,7 @@ fn map_symbols(
/// When inferring a symbol's size, we ignore symbols that start with specific prefixes. They are /// When inferring a symbol's size, we ignore symbols that start with specific prefixes. They are
/// usually emitted as branch targets and do not represent the start of a function or object. /// usually emitted as branch targets and do not represent the start of a function or object.
fn is_local_label(symbol: &Symbol) -> bool { fn is_local_label(symbol: &Symbol) -> bool {
const LABEL_PREFIXES: &[&str] = &[".L", "LAB_"]; const LABEL_PREFIXES: &[&str] = &[".L", "LAB_", "switchD_"];
symbol.size == 0 symbol.size == 0
&& symbol.flags.contains(SymbolFlag::Local) && symbol.flags.contains(SymbolFlag::Local)
&& LABEL_PREFIXES.iter().any(|p| symbol.name.starts_with(p)) && LABEL_PREFIXES.iter().any(|p| symbol.name.starts_with(p))

View File

@@ -25,6 +25,7 @@ wsl = []
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
argp = "0.4"
cfg-if = "1.0" cfg-if = "1.0"
const_format = "0.2" const_format = "0.2"
cwdemangle = "1.0" cwdemangle = "1.0"

View File

@@ -16,8 +16,8 @@ use globset::Glob;
use objdiff_core::{ use objdiff_core::{
build::watcher::{Watcher, create_watcher}, build::watcher::{Watcher, create_watcher},
config::{ config::{
DEFAULT_WATCH_PATTERNS, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, build_globset,
build_globset, default_watch_patterns, path::platform_path_serde_option, default_ignore_patterns, default_watch_patterns, path::platform_path_serde_option,
save_project_config, save_project_config,
}, },
diff::DiffObjConfig, diff::DiffObjConfig,
@@ -219,6 +219,8 @@ pub struct AppConfig {
#[serde(default = "default_watch_patterns")] #[serde(default = "default_watch_patterns")]
pub watch_patterns: Vec<Glob>, pub watch_patterns: Vec<Glob>,
#[serde(default)] #[serde(default)]
pub ignore_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<String>, pub recent_projects: Vec<String>,
#[serde(default)] #[serde(default)]
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
@@ -239,7 +241,8 @@ impl Default for AppConfig {
build_target: false, build_target: false,
rebuild_on_changes: true, rebuild_on_changes: true,
auto_update_check: true, auto_update_check: true,
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(), watch_patterns: default_watch_patterns(),
ignore_patterns: default_ignore_patterns(),
recent_projects: vec![], recent_projects: vec![],
diff_obj_config: Default::default(), diff_obj_config: Default::default(),
} }
@@ -431,6 +434,7 @@ impl App {
app_path: Option<PathBuf>, app_path: Option<PathBuf>,
graphics_config: GraphicsConfig, graphics_config: GraphicsConfig,
graphics_config_path: Option<PathBuf>, graphics_config_path: Option<PathBuf>,
project_dir: Option<Utf8PlatformPathBuf>,
) -> Self { ) -> Self {
// Load previous app state (if any). // Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work. // Note that you must enable the `persistence` feature for this to work.
@@ -440,7 +444,17 @@ impl App {
app.appearance = appearance; app.appearance = appearance;
} }
if let Some(config) = deserialize_config(storage) { if let Some(config) = deserialize_config(storage) {
let mut state = AppState { config, ..Default::default() }; let state = AppState { config, ..Default::default() };
app.state = Arc::new(RwLock::new(state));
}
}
{
let mut state = app.state.write().unwrap();
if let Some(project_dir) = project_dir
&& state.config.project_dir.as_ref().is_none_or(|p| *p != project_dir)
{
state.set_project_dir(project_dir);
}
if state.config.project_dir.is_some() { if state.config.project_dir.is_some() {
state.config_change = true; state.config_change = true;
state.watcher_change = true; state.watcher_change = true;
@@ -449,8 +463,6 @@ impl App {
state.queue_build = true; state.queue_build = true;
} }
app.view_state.config_state.queue_check_update = state.config.auto_update_check; app.view_state.config_state.queue_check_update = state.config.auto_update_check;
app.state = Arc::new(RwLock::new(state));
}
} }
app.appearance.init_fonts(&cc.egui_ctx); app.appearance.init_fonts(&cc.egui_ctx);
app.appearance.utc_offset = utc_offset; app.appearance.utc_offset = utc_offset;
@@ -551,11 +563,17 @@ impl App {
if let Some(project_dir) = &state.config.project_dir { if let Some(project_dir) = &state.config.project_dir {
match build_globset(&state.config.watch_patterns) match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
.and_then(|globset| { .and_then(|patterns| {
build_globset(&state.config.ignore_patterns)
.map(|ignore_patterns| (patterns, ignore_patterns))
.map_err(anyhow::Error::new)
})
.and_then(|(patterns, ignore_patterns)| {
create_watcher( create_watcher(
self.modified.clone(), self.modified.clone(),
project_dir.as_ref(), project_dir.as_ref(),
globset, patterns,
ignore_patterns,
egui_waker(ctx), egui_waker(ctx),
) )
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)

View File

@@ -0,0 +1,63 @@
// Originally from https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c
//! Extend `argp` to be better integrated with the `cargo` ecosystem
//!
//! For now, this only adds a --version/-V option which causes early-exit.
use std::ffi::OsStr;
use argp::{EarlyExit, FromArgs, TopLevelCommand, parser::ParseGlobalOptions};
struct ArgsOrVersion<T>(T)
where T: FromArgs;
impl<T> TopLevelCommand for ArgsOrVersion<T> where T: FromArgs {}
impl<T> FromArgs for ArgsOrVersion<T>
where T: FromArgs
{
fn _from_args(
command_name: &[&str],
args: &[&OsStr],
parent: Option<&mut dyn ParseGlobalOptions>,
) -> Result<Self, EarlyExit> {
/// Also use argp for catching `--version`-only invocations
#[derive(FromArgs)]
struct Version {
/// Print version information and exit.
#[argp(switch, short = 'V')]
pub version: bool,
}
match Version::from_args(command_name, args) {
Ok(v) => {
if v.version {
println!(
"{} {}",
command_name.first().unwrap_or(&""),
env!("CARGO_PKG_VERSION"),
);
std::process::exit(0);
} else {
// Pass through empty arguments
T::_from_args(command_name, args, parent).map(Self)
}
}
Err(exit) => match exit {
EarlyExit::Help(_help) => {
// TODO: Chain help info from Version
// For now, we just put the switch on T as well
T::from_args(command_name, &["--help"]).map(Self)
}
EarlyExit::Err(_) => T::_from_args(command_name, args, parent).map(Self),
},
}
}
}
/// Create a `FromArgs` type from the current processs `env::args`.
///
/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested.
/// Error messages will be printed to stderr, and `--help` output to stdout.
pub fn from_env<T>() -> T
where T: TopLevelCommand {
argp::parse_args_or_exit::<ArgsOrVersion<T>>(argp::DEFAULT).0
}

View File

@@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use globset::Glob; use globset::Glob;
use objdiff_core::config::{DEFAULT_WATCH_PATTERNS, try_project_config}; use objdiff_core::config::{default_ignore_patterns, default_watch_patterns, try_project_config};
use typed_path::{Utf8UnixComponent, Utf8UnixPath}; use typed_path::{Utf8UnixComponent, Utf8UnixPath};
use crate::app::{AppState, ObjectConfig}; use crate::app::{AppState, ObjectConfig};
@@ -96,8 +96,15 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
.map(|s| Glob::new(s)) .map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?; .collect::<Result<Vec<Glob>, globset::Error>>()?;
} else { } else {
state.config.watch_patterns = state.config.watch_patterns = default_watch_patterns();
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); }
if let Some(ignore_patterns) = &project_config.ignore_patterns {
state.config.ignore_patterns = ignore_patterns
.iter()
.map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?;
} else {
state.config.ignore_patterns = default_ignore_patterns();
} }
state.watcher_change = true; state.watcher_change = true;
state.objects = project_config state.objects = project_config

View File

@@ -3,6 +3,7 @@
mod app; mod app;
mod app_config; mod app_config;
mod argp_version;
mod config; mod config;
mod fonts; mod fonts;
mod hotkeys; mod hotkeys;
@@ -11,19 +12,83 @@ mod update;
mod views; mod views;
use std::{ use std::{
ffi::OsStr,
fmt::Display,
path::PathBuf, path::PathBuf,
process::ExitCode, process::ExitCode,
rc::Rc, rc::Rc,
str::FromStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use anyhow::{Result, ensure}; use anyhow::{Result, ensure};
use argp::{FromArgValue, FromArgs};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use objdiff_core::config::path::check_path_buf;
use time::UtcOffset; use time::UtcOffset;
use tracing_subscriber::EnvFilter; use tracing_subscriber::{EnvFilter, filter::LevelFilter};
use typed_path::Utf8PlatformPathBuf;
use crate::views::graphics::{GraphicsBackend, GraphicsConfig, load_graphics_config}; use crate::views::graphics::{GraphicsBackend, GraphicsConfig, load_graphics_config};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl FromStr for LogLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"error" => Self::Error,
"warn" => Self::Warn,
"info" => Self::Info,
"debug" => Self::Debug,
"trace" => Self::Trace,
_ => return Err(()),
})
}
}
impl Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
})
}
}
impl FromArgValue for LogLevel {
fn from_arg_value(value: &OsStr) -> Result<Self, String> {
String::from_arg_value(value)
.and_then(|s| Self::from_str(&s).map_err(|_| "Invalid log level".to_string()))
}
}
#[derive(FromArgs, PartialEq, Debug)]
/// A local diffing tool for decompilation projects.
struct TopLevel {
#[argp(option, short = 'L')]
/// Minimum logging level. (Default: info)
/// Possible values: error, warn, info, debug, trace
log_level: Option<LogLevel>,
#[argp(option, short = 'p')]
/// Path to the project directory.
project_dir: Option<PathBuf>,
/// Print version information and exit.
#[argp(switch, short = 'V')]
version: bool,
}
fn load_icon() -> Result<egui::IconData> { fn load_icon() -> Result<egui::IconData> {
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").as_ref()); let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").as_ref());
let mut reader = decoder.read_info()?; let mut reader = decoder.read_info()?;
@@ -38,23 +103,63 @@ fn load_icon() -> Result<egui::IconData> {
const APP_NAME: &str = "objdiff"; const APP_NAME: &str = "objdiff";
fn main() -> ExitCode { fn main() -> ExitCode {
// Log to stdout (if you run with `RUST_LOG=debug`). let args: TopLevel = argp_version::from_env();
tracing_subscriber::fmt() let builder = tracing_subscriber::fmt();
if let Some(level) = args.log_level {
builder
.with_max_level(match level {
LogLevel::Error => LevelFilter::ERROR,
LogLevel::Warn => LevelFilter::WARN,
LogLevel::Info => LevelFilter::INFO,
LogLevel::Debug => LevelFilter::DEBUG,
LogLevel::Trace => LevelFilter::TRACE,
})
.init();
} else {
builder
.with_env_filter( .with_env_filter(
EnvFilter::builder() EnvFilter::builder()
// Default to info level // Default to info level
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) .with_default_directive(LevelFilter::INFO.into())
.from_env_lossy() .from_env_lossy()
// This module is noisy at info level // This module is noisy at info level
.add_directive("wgpu_core::device::resource=warn".parse().unwrap()), .add_directive("wgpu_core::device::resource=warn".parse().unwrap()),
) )
.init(); .init();
}
// Because localtime_r is unsound in multithreaded apps, // Because localtime_r is unsound in multithreaded apps,
// we must call this before initializing eframe. // we must call this before initializing eframe.
// https://github.com/time-rs/time/issues/293 // https://github.com/time-rs/time/issues/293
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC); let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
// Resolve project directory if provided
let project_dir = if let Some(path) = args.project_dir {
match path.canonicalize() {
Ok(path) => {
// Ensure the path is a directory
if path.is_dir() {
match check_path_buf(path) {
Ok(path) => Some(path),
Err(e) => {
log::error!("Failed to convert project directory to UTF-8 path: {}", e);
None
}
}
} else {
log::error!("Project directory is not a directory: {}", path.display());
None
}
}
Err(e) => {
log::error!("Failed to canonicalize project directory: {}", e);
None
}
}
} else {
None
};
let app_path = std::env::current_exe().ok(); let app_path = std::env::current_exe().ok();
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None)); let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let mut native_options = eframe::NativeOptions { let mut native_options = eframe::NativeOptions {
@@ -113,6 +218,7 @@ fn main() -> ExitCode {
app_path.clone(), app_path.clone(),
graphics_config.clone(), graphics_config.clone(),
graphics_config_path.clone(), graphics_config_path.clone(),
project_dir.clone(),
) { ) {
eframe_error = Some(e); eframe_error = Some(e);
} }
@@ -139,6 +245,7 @@ fn main() -> ExitCode {
app_path.clone(), app_path.clone(),
graphics_config.clone(), graphics_config.clone(),
graphics_config_path.clone(), graphics_config_path.clone(),
project_dir.clone(),
) { ) {
eframe_error = Some(e); eframe_error = Some(e);
} else { } else {
@@ -161,6 +268,7 @@ fn main() -> ExitCode {
app_path, app_path,
graphics_config, graphics_config,
graphics_config_path, graphics_config_path,
project_dir,
) { ) {
eframe_error = Some(e); eframe_error = Some(e);
} else { } else {
@@ -204,6 +312,7 @@ fn run_eframe(
app_path: Option<PathBuf>, app_path: Option<PathBuf>,
graphics_config: GraphicsConfig, graphics_config: GraphicsConfig,
graphics_config_path: Option<PathBuf>, graphics_config_path: Option<PathBuf>,
project_dir: Option<Utf8PlatformPathBuf>,
) -> Result<(), eframe::Error> { ) -> Result<(), eframe::Error> {
eframe::run_native( eframe::run_native(
APP_NAME, APP_NAME,
@@ -216,6 +325,7 @@ fn run_eframe(
app_path, app_path,
graphics_config, graphics_config,
graphics_config_path, graphics_config_path,
project_dir,
))) )))
}), }),
) )

View File

@@ -11,6 +11,7 @@ pub struct Appearance {
pub ui_font: FontId, pub ui_font: FontId,
pub code_font: FontId, pub code_font: FontId,
pub diff_colors: Vec<Color32>, pub diff_colors: Vec<Color32>,
pub diff_bg_color: Option<Color32>,
pub theme: egui::Theme, pub theme: egui::Theme,
// Applied by theme // Applied by theme
@@ -67,6 +68,7 @@ impl Default for Appearance {
replace_color: Color32::LIGHT_BLUE, replace_color: Color32::LIGHT_BLUE,
insert_color: Color32::GREEN, insert_color: Color32::GREEN,
delete_color: Color32::from_rgb(200, 40, 41), delete_color: Color32::from_rgb(200, 40, 41),
diff_bg_color: None,
utc_offset: UtcOffset::UTC, utc_offset: UtcOffset::UTC,
fonts: FontState::default(), fonts: FontState::default(),
next_ui_font: None, next_ui_font: None,
@@ -103,6 +105,9 @@ impl Appearance {
match self.theme { match self.theme {
egui::Theme::Dark => { egui::Theme::Dark => {
style.visuals = egui::Visuals::dark(); style.visuals = egui::Visuals::dark();
if let Some(diff_bg_color) = self.diff_bg_color {
style.visuals.faint_bg_color = diff_bg_color;
}
self.text_color = Color32::GRAY; self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::LIGHT_GRAY; self.emphasized_text_color = Color32::LIGHT_GRAY;
self.deemphasized_text_color = Color32::DARK_GRAY; self.deemphasized_text_color = Color32::DARK_GRAY;
@@ -114,6 +119,9 @@ impl Appearance {
} }
egui::Theme::Light => { egui::Theme::Light => {
style.visuals = egui::Visuals::light(); style.visuals = egui::Visuals::light();
if let Some(diff_bg_color) = self.diff_bg_color {
style.visuals.faint_bg_color = diff_bg_color;
}
self.text_color = Color32::GRAY; self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::DARK_GRAY; self.emphasized_text_color = Color32::DARK_GRAY;
self.deemphasized_text_color = Color32::LIGHT_GRAY; self.deemphasized_text_color = Color32::LIGHT_GRAY;
@@ -294,6 +302,21 @@ pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut
appearance, appearance,
); );
ui.separator(); ui.separator();
ui.horizontal(|ui| {
ui.label("Diff fill color:");
let mut diff_bg_color =
appearance.diff_bg_color.unwrap_or_else(|| match appearance.theme {
egui::Theme::Dark => egui::Visuals::dark().faint_bg_color,
egui::Theme::Light => egui::Visuals::light().faint_bg_color,
});
if ui.color_edit_button_srgba(&mut diff_bg_color).changed() {
appearance.diff_bg_color = Some(diff_bg_color);
}
if ui.button("Reset").clicked() {
appearance.diff_bg_color = None;
}
});
ui.separator();
ui.label("Diff colors:"); ui.label("Diff colors:");
if ui.button("Reset").clicked() { if ui.button("Reset").clicked() {
appearance.diff_colors = DEFAULT_COLOR_ROTATION.to_vec(); appearance.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();

View File

@@ -10,7 +10,7 @@ use egui::{
}; };
use globset::Glob; use globset::Glob;
use objdiff_core::{ use objdiff_core::{
config::{DEFAULT_WATCH_PATTERNS, path::check_path_buf}, config::{default_ignore_patterns, default_watch_patterns, path::check_path_buf},
diff::{ diff::{
CONFIG_GROUPS, ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind, CONFIG_GROUPS, ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind,
ConfigPropertyValue, ConfigPropertyValue,
@@ -41,6 +41,7 @@ pub struct ConfigViewState {
pub build_running: bool, pub build_running: bool,
pub queue_build: bool, pub queue_build: bool,
pub watch_pattern_text: String, pub watch_pattern_text: String,
pub ignore_pattern_text: String,
pub object_search: String, pub object_search: String,
pub filter_diffable: bool, pub filter_diffable: bool,
pub filter_incomplete: bool, pub filter_incomplete: bool,
@@ -790,20 +791,49 @@ fn split_obj_config_ui(
state.watcher_change = true; state.watcher_change = true;
}; };
state.watcher_change |= patterns_ui(
ui,
"File patterns",
&mut state.config.watch_patterns,
&mut config_state.watch_pattern_text,
appearance,
state.project_config_info.is_some(),
default_watch_patterns,
);
state.watcher_change |= patterns_ui(
ui,
"Ignore patterns",
&mut state.config.ignore_patterns,
&mut config_state.ignore_pattern_text,
appearance,
state.project_config_info.is_some(),
default_ignore_patterns,
);
}
fn patterns_ui(
ui: &mut egui::Ui,
text: &str,
patterns: &mut Vec<Glob>,
pattern_text: &mut String,
appearance: &Appearance,
has_project_config: bool,
on_reset: impl FnOnce() -> Vec<Glob>,
) -> bool {
let mut change = false;
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color)); ui.label(RichText::new(text).color(appearance.text_color));
if ui if ui
.add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset")) .add_enabled(!has_project_config, egui::Button::new("Reset"))
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
state.config.watch_patterns = *patterns = on_reset();
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); change = true;
state.watcher_change = true;
} }
}); });
let mut remove_at: Option<usize> = None; let mut remove_at: Option<usize> = None;
for (idx, glob) in state.config.watch_patterns.iter().enumerate() { for (idx, glob) in patterns.iter().enumerate() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label( ui.label(
RichText::new(glob.to_string()) RichText::new(glob.to_string())
@@ -811,7 +841,7 @@ fn split_obj_config_ui(
.family(FontFamily::Monospace), .family(FontFamily::Monospace),
); );
if ui if ui
.add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small()) .add_enabled(!has_project_config, egui::Button::new("-").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
{ {
@@ -820,26 +850,27 @@ fn split_obj_config_ui(
}); });
} }
if let Some(idx) = remove_at { if let Some(idx) = remove_at {
state.config.watch_patterns.remove(idx); patterns.remove(idx);
state.watcher_change = true; change = true;
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add_enabled( ui.add_enabled(
state.project_config_info.is_none(), !has_project_config,
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0), egui::TextEdit::singleline(pattern_text).desired_width(100.0),
) )
.on_disabled_hover_text(CONFIG_DISABLED_TEXT); .on_disabled_hover_text(CONFIG_DISABLED_TEXT);
if ui if ui
.add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small()) .add_enabled(!has_project_config, egui::Button::new("+").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT) .on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked() .clicked()
&& let Ok(glob) = Glob::new(&config_state.watch_pattern_text) && let Ok(glob) = Glob::new(pattern_text)
{ {
state.config.watch_patterns.push(glob); patterns.push(glob);
state.watcher_change = true; change = true;
config_state.watch_pattern_text.clear(); pattern_text.clear();
} }
}); });
change
} }
pub fn arch_config_window( pub fn arch_config_window(

View File

@@ -619,9 +619,7 @@ fn symbol_label_ui(
.font(appearance.code_font.clone()) .font(appearance.code_font.clone())
.color(appearance.highlight_color), .color(appearance.highlight_color),
) )
.selectable(false) .show_tooltip_when_elided(false)
// TODO .show_tooltip_when_elided(false)
// https://github.com/emilk/egui/commit/071e090e2b2601e5ed4726a63a753188503dfaf2
.ui(ui) .ui(ui)
.on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, ctx, symbol_idx, appearance)) .on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, ctx, symbol_idx, appearance))
.context_menu(|ui| { .context_menu(|ui| {

View File

@@ -1,8 +1,7 @@
use core::any::Any;
use egui::ScrollArea; use egui::ScrollArea;
use objdiff_core::{ use objdiff_core::{arch::ppc::ExceptionInfo, obj::Object};
arch::ppc::ExceptionInfo,
obj::{Object, Symbol},
};
use crate::views::{appearance::Appearance, function_diff::FunctionDiffContext}; use crate::views::{appearance::Appearance, function_diff::FunctionDiffContext};
@@ -26,19 +25,19 @@ fn decode_extab(extab: &ExceptionInfo) -> String {
text text
} }
fn find_extab_entry<'a>(_obj: &'a Object, _symbol: &Symbol) -> Option<&'a ExceptionInfo> { fn find_extab_entry(obj: &Object, symbol_index: usize) -> Option<&ExceptionInfo> {
// TODO (obj.arch.as_ref() as &dyn Any)
// obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)) .downcast_ref::<objdiff_core::arch::ppc::ArchPpc>()
None .and_then(|ppc| ppc.extab_for_symbol(symbol_index))
} }
fn extab_text_ui( fn extab_text_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ctx: FunctionDiffContext<'_>, ctx: FunctionDiffContext<'_>,
symbol: &Symbol, symbol_index: usize,
appearance: &Appearance, appearance: &Appearance,
) -> Option<()> { ) -> Option<()> {
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) { if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol_index) {
let text = decode_extab(extab_entry); let text = decode_extab(extab_entry);
ui.colored_label(appearance.replace_color, &text); ui.colored_label(appearance.replace_color, &text);
return Some(()); return Some(());
@@ -58,10 +57,8 @@ pub(crate) fn extab_ui(
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if let Some(symbol) = if let Some(symbol_index) = ctx.symbol_ref {
ctx.symbol_ref.and_then(|symbol_ref| ctx.obj.symbols.get(symbol_ref)) extab_text_ui(ui, ctx, symbol_index, appearance);
{
extab_text_ui(ui, ctx, symbol, appearance);
} }
}); });
}); });

View File

@@ -142,6 +142,11 @@ impl DiffViewState {
JobResult::ObjDiff(result) => { JobResult::ObjDiff(result) => {
self.build = take(result); self.build = take(result);
// Clear reload flag so that we don't reload the view immediately
if let Ok(mut state) = state.write() {
state.queue_reload = false;
}
// TODO: where should this go? // TODO: where should this go?
if let Some(result) = self.post_build_nav.take() { if let Some(result) = self.post_build_nav.take() {
self.current_view = result.view; self.current_view = result.view;

View File

@@ -0,0 +1,6 @@
[build]
target = "wasm32-wasip2"
[unstable]
build-std = ["panic_abort", "core", "alloc"]
build-std-features = ["compiler-builtins-mem"]

View File

@@ -17,12 +17,13 @@ build = "build.rs"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[features] [features]
default = ["std"] default = []
std = ["objdiff-core/std"] std = ["objdiff-core/std"]
[dependencies] [dependencies]
log = { version = "0.4", default-features = false } log = { version = "0.4", default-features = false }
regex = { version = "1.11", default-features = false, features = ["unicode-case"] } regex = { version = "1.11", default-features = false, features = ["unicode-case"] }
wit-bindgen = { version = "0.44", default-features = false, features = ["macros"] }
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
[dependencies.objdiff-core] [dependencies.objdiff-core]
@@ -33,8 +34,5 @@ features = ["arm", "arm64", "mips", "ppc", "superh", "x86", "dwarf"]
[target.'cfg(target_family = "wasm")'.dependencies] [target.'cfg(target_family = "wasm")'.dependencies]
talc = { version = "4.4", default-features = false, features = ["lock_api"] } talc = { version = "4.4", default-features = false, features = ["lock_api"] }
[target.'cfg(target_os = "wasi")'.dependencies]
wit-bindgen = { version = "0.43", default-features = false, features = ["macros"] }
[build-dependencies] [build-dependencies]
wit-deps = "0.5" wit-deps = "0.5"

View File

@@ -1,12 +1,12 @@
{ {
"name": "objdiff-wasm", "name": "objdiff-wasm",
"version": "3.0.0-beta.13", "version": "3.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "objdiff-wasm", "name": "objdiff-wasm",
"version": "3.0.0-beta.13", "version": "3.0.1",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.3", "@biomejs/biome": "^1.9.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "objdiff-wasm", "name": "objdiff-wasm",
"version": "3.0.0-beta.13", "version": "3.0.1",
"description": "A local diffing tool for decompilation projects.", "description": "A local diffing tool for decompilation projects.",
"author": { "author": {
"name": "Luke Street", "name": "Luke Street",
@@ -19,8 +19,8 @@
"types": "dist/objdiff.d.ts", "types": "dist/objdiff.d.ts",
"scripts": { "scripts": {
"build": "npm run build:wasm && npm run build:transpile && npm run build:lib", "build": "npm run build:wasm && npm run build:transpile && npm run build:lib",
"build:wasm": "cargo +nightly -Zbuild-std=panic_abort,core,alloc -Zbuild-std-features=compiler-builtins-mem build --target wasm32-wasip2 --release --no-default-features", "build:wasm": "cargo build --profile release-min --no-default-features",
"build:transpile": "jco transpile ../target/wasm32-wasip2/release/objdiff_wasm.wasm --no-nodejs-compat --no-wasi-shim --no-namespaced-exports --map wasi:logging/logging=./wasi-logging.js --optimize -o pkg --name objdiff", "build:transpile": "jco transpile ../target/wasm32-wasip2/release-min/objdiff_wasm.wasm --no-nodejs-compat --no-wasi-shim --no-namespaced-exports --map wasi:logging/logging=./wasi-logging.js --optimize -o pkg --name objdiff",
"build:lib": "rslib build" "build:lib": "rslib build"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"
components = ["rust-src"]
targets = ["wasm32-wasip2"]

View File

@@ -1,3 +1,4 @@
#![allow(clippy::derivable_impls)]
use alloc::{ use alloc::{
format, format,
rc::{Rc, Weak}, rc::{Rc, Weak},
@@ -223,7 +224,7 @@ impl GuestDisplay for Component {
let symbol_display = from_symbol_ref(symbol_ref); let symbol_display = from_symbol_ref(symbol_ref);
diff::display::symbol_context(obj, symbol_display.symbol as usize) diff::display::symbol_context(obj, symbol_display.symbol as usize)
.into_iter() .into_iter()
.map(|item| ContextItem::from(item)) .map(ContextItem::from)
.collect() .collect()
} }
@@ -235,7 +236,7 @@ impl GuestDisplay for Component {
let symbol_display = from_symbol_ref(symbol_ref); let symbol_display = from_symbol_ref(symbol_ref);
diff::display::symbol_hover(obj, symbol_display.symbol as usize, addend, override_color) diff::display::symbol_hover(obj, symbol_display.symbol as usize, addend, override_color)
.into_iter() .into_iter()
.map(|item| HoverItem::from(item)) .map(HoverItem::from)
.collect() .collect()
} }
@@ -282,7 +283,7 @@ impl GuestDisplay for Component {
}; };
diff::display::instruction_context(obj, resolved, &ins) diff::display::instruction_context(obj, resolved, &ins)
.into_iter() .into_iter()
.map(|item| ContextItem::from(item)) .map(ContextItem::from)
.collect() .collect()
} }
@@ -331,7 +332,7 @@ impl GuestDisplay for Component {
}; };
diff::display::instruction_hover(obj, resolved, &ins) diff::display::instruction_hover(obj, resolved, &ins)
.into_iter() .into_iter()
.map(|item| HoverItem::from(item)) .map(HoverItem::from)
.collect() .collect()
} }
} }
@@ -527,9 +528,7 @@ impl GuestObjectDiff for ResourceObjectDiff {
fn get_symbol(&self, symbol_ref: SymbolRef) -> Option<SymbolInfo> { fn get_symbol(&self, symbol_ref: SymbolRef) -> Option<SymbolInfo> {
let obj = self.0.as_ref(); let obj = self.0.as_ref();
let symbol_display = from_symbol_ref(symbol_ref); let symbol_display = from_symbol_ref(symbol_ref);
let Some(symbol) = obj.symbols.get(symbol_display.symbol) else { let symbol = obj.symbols.get(symbol_display.symbol)?;
return None;
};
Some(SymbolInfo { Some(SymbolInfo {
id: to_symbol_ref(symbol_display), id: to_symbol_ref(symbol_display),
name: symbol.name.clone(), name: symbol.name.clone(),

View File

@@ -1,9 +1,7 @@
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc; extern crate alloc;
#[cfg(target_os = "wasi")]
mod api; mod api;
#[cfg(target_os = "wasi")]
mod logging; mod logging;
#[cfg(all(target_os = "wasi", not(feature = "std")))] #[cfg(all(target_os = "wasi", not(feature = "std")))]