- jpegai_decode::{decode, decode_with_stages} load the bundle's networks
(load_networks, under acc64) beside its scale path and run the hyper decoders,
context stages and synthesis through rec_y, rec_uv, syn_y and syn_uv;
decode_loaded(data, icci, norm, scale path, networks, observe) decodes with
both loaded once. The direct networks are reached only from
jpegai_decode::legacy (Front::Direct, Back::Direct, test-only, legacy_int).
- The loaders no longer panic: Weights::from_json, IntNet::from_json and
IcciNet::from_json return Result<_, BadMember>; a mistyped member is
model_mismatch. IntNet keeps only well-formed tensors, so a member §13.6.1 reads
that is mistyped is missing, as the lowering of the bytes refuses it.
- The neural sink lowers and loads the networks from their eight files' bytes
through the store of loaded sets (keyed by their listing, at acc64), parses only
the eICCI net besides, and reports a refusal (model_mismatch, field_range)
without falling back: a_refused_exact_decode_does_not_fall_back gains a
non-integer network weight and a mistyped eICCI field.
- The browser crate's decodeJpegAiFull reports a mistyped member.
- Goldens, unchanged (build host, release): jpegai_cross_platform's nine pinned
hashes, the Synthesis stage d56695726bb6ebed… among them; the decoded RGB
SHA-256 dbc494a5a670914e… (jpegai_reconstruction_binding with
WAI_JPEGAI_EXPECT_SHA256, jpegai_e2e_full: max|Δ| = 1 on 4.446%).
- Memory (jpegai_memory, reference kernels): with the scale path and the networks
loaded beforehand the decode holds 72,752,387 bytes, the direct decode
175,465,380; the networks take 89,727,913 to load and hold 55,530,900.
- Also green: clippy's disallowed-methods guard and jpegai_decode_symbols, the
sink's three dispatch tests, wai-web's tests and its wasm32 build; include/wai.h
unchanged.
- CHANGELOG: the M7b entry.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
243 lines
12 KiB
Rust
243 lines
12 KiB
Rust
//! The JPEG AI exact decoder's executable imports no C math-library function, and
|
|
//! defines none but the exactly specified ones.
|
|
//!
|
|
//! [`wai::jpegai_decode::decode`] is meant to give the same bytes on every platform
|
|
//! because its float stages use only IEEE-754 basic operations, with `exp` and rounding
|
|
//! built from them in `wai::float_repro`. A math-library function on that path (`exp`,
|
|
//! `log`, `pow`, `fmod`, ...) would tie the output to whichever library a platform
|
|
//! links. The pinned-output tests need not notice: the decoder rounds its float stages,
|
|
//! and a last-bit difference can vanish in the rounding on the platforms measured.
|
|
//!
|
|
//! This test checks the linked executable rather than the source, so the way the call
|
|
//! is spelled does not matter: a method call, `f64::exp(x)`, a function path, a float
|
|
//! `%` (which compiles to `fmod`), or a call made in another module or crate all leave a
|
|
//! symbol. It is a program (`harness = false` in Cargo.toml, so none of the test
|
|
//! harness's own code is in the executable) that takes the addresses of the exact
|
|
//! decoder's public entry points and of the loaders for its weights. The linker keeps
|
|
//! those and everything they reach: rustc links every executable with unreferenced
|
|
//! sections removed (`--gc-sections`, `-dead_strip`). The program then lists its own
|
|
//! executable's symbols with `nm` and fails on:
|
|
//! - any C math-library function the executable imports, which is a call into the
|
|
//! platform's math library;
|
|
//! - any it defines, as Rust's compiler builtins do for some functions on x86_64 Linux,
|
|
//! except the round-to-integral functions (`ceil`, `floor`, `trunc`, `round`,
|
|
//! `roundeven`, `rint`, `nearbyint`, and their `f`/`l` forms).
|
|
//!
|
|
//! A definition shows that the linker kept a function, not that anything calls it. On
|
|
//! x86_64 Linux, GNU ld (2.40, debug build) kept the compiler builtins' `floor` in this
|
|
//! executable although nothing in it referred to `floor`: the only reference had been
|
|
//! in a section the linker removed as unused, from code the decoder does not reach. In
|
|
//! a minimal program, GNU ld kept such an unused definition only when the C math library
|
|
//! was also linked, which other code in the crate's object files can cause. The same
|
|
//! build linked with LLD held no `floor`. So a definition of a function whose result is
|
|
//! the same whatever computes it does not fail the test: IEEE 754 fixes the
|
|
//! round-to-integral results bit for bit (`rint` and `nearbyint` in the current rounding
|
|
//! direction, which the decoder checks is the default). The program prints the ones it
|
|
//! allows. Every other math function, defined or imported, still fails.
|
|
//!
|
|
//! Also allowed: `sqrt`, `fabs` and `copysign` and their `f`/`l` forms, defined or
|
|
//! imported. IEEE 754 fixes their results bit for bit, like the other basic operations,
|
|
//! so the decode may use them however the target compiles them.
|
|
//!
|
|
//! Two controls keep an incomplete listing from passing. The executable must show the
|
|
//! decoder's own `jpegai_decode::decode` symbol, so the decode was linked and the
|
|
//! listing includes the executable's own symbols; and it must import `malloc` (Rust's
|
|
//! allocator), so the listing includes imports.
|
|
//!
|
|
//! Scope:
|
|
//! - Unix targets, with `nm` on the PATH (it ships with the linker toolchain these
|
|
//! targets link with). On other targets (Windows, wasm32) the program prints
|
|
//! `SKIPPED` and checks nothing.
|
|
//! - It sees calls, not instructions. A rounding the target does with an instruction
|
|
//! (AArch64 `frinta`) leaves no symbol. `exp` and the other transcendentals have no
|
|
//! instruction on these targets and always leave one.
|
|
//! - It does not enforce, where the compiler builtins define the rounding functions
|
|
//! (x86_64 Linux), that the decoder's roundings come from `float_repro`; clippy does
|
|
//! that in the decode-path modules. A rounding call that slips past it cannot change
|
|
//! the output, for the reason above.
|
|
//! - It checks the build it is part of (the profile and features `cargo test` was run
|
|
//! with). CI runs it in the debug profile.
|
|
//! - `clippy.toml` (`disallowed-methods`) is the source-level guard for the same rule.
|
|
//! It names the offending line, but sees only method calls, in the modules that deny
|
|
//! the lint.
|
|
|
|
use std::hint::black_box;
|
|
|
|
use wai::jpegai_decode::{self, DecoderWeights, NormTables, Refusal, Rgb, Stage};
|
|
use wai::jpegai_icci::IcciNet;
|
|
use wai::jpegai_synthesis::{IntNet, Weights};
|
|
|
|
type Decode = fn(&[u8], &DecoderWeights, &NormTables) -> Result<Rgb, Refusal>;
|
|
type DecodeWithStages =
|
|
fn(&[u8], &DecoderWeights, &NormTables, &mut dyn FnMut(Stage, &[&[f32]])) -> Result<Rgb, Refusal>;
|
|
type Check = fn(&[u8]) -> Result<(), &'static str>;
|
|
|
|
/// Take the address of every entry point a sink uses to decode, through `black_box`,
|
|
/// so the compiler must emit them and the linker must keep them and what they call.
|
|
/// Nothing is run.
|
|
fn link_the_decoder() {
|
|
black_box(jpegai_decode::decode as Decode);
|
|
black_box(jpegai_decode::decode_with_stages as DecodeWithStages);
|
|
black_box(jpegai_decode::check_scope as Check);
|
|
black_box(jpegai_decode::check_codestream as Check);
|
|
black_box(jpegai_decode::fp_env_is_default as fn() -> bool);
|
|
black_box(Weights::from_json as fn(&serde_json::Value) -> Result<Weights, wai::jpegai_synthesis::BadMember>);
|
|
black_box(IntNet::from_json as fn(&serde_json::Value) -> Result<IntNet, wai::jpegai_synthesis::BadMember>);
|
|
black_box(IcciNet::from_json as fn(&serde_json::Value) -> Result<IcciNet, wai::jpegai_synthesis::BadMember>);
|
|
}
|
|
|
|
/// C99 `<math.h>` functions, besides the allowed ones. Each is also checked with the
|
|
/// `f` and `l` suffixes.
|
|
const MATH_FUNCTIONS: [&str; 66] = [
|
|
"acos", "acosh", "asin", "asinh", "atan", "atan2", "atanh", "cbrt", "ceil", "cos",
|
|
"cosh", "erf", "erfc", "exp", "exp10", "exp2", "expm1", "fdim", "floor", "fma", "fmax",
|
|
"fmin", "fmod", "frexp", "gamma", "hypot", "ilogb", "j0", "j1", "jn", "ldexp",
|
|
"lgamma", "llrint", "llround", "log", "log10", "log1p", "log2", "logb", "lrint",
|
|
"lround", "modf", "nearbyint", "nextafter", "nexttoward", "pow", "pow10", "remainder",
|
|
"remquo", "rint", "round", "roundeven", "scalb", "scalbln", "scalbn", "sin", "sincos",
|
|
"sinh", "tan", "tanh", "tgamma", "trunc", "y0", "y1", "yn", "significand",
|
|
];
|
|
|
|
/// Other names under which a math function reaches an executable: the integer-power
|
|
/// helpers from the compiler's runtime library (`powi`), and platform variants.
|
|
const OTHER_MATH_SYMBOLS: [&str; 8] = [
|
|
"__powisf2", "__powidf2", "__powitf2", "__sincos_stret", "__sincosf_stret",
|
|
"__exp10", "__exp10f", "__sinpi",
|
|
];
|
|
|
|
/// A symbol name as `nm` prints it, reduced to the C name: without the version
|
|
/// (`exp@GLIBC_2.29`, `exp@@GLIBC_2.29`) and, on targets whose vendor is `apple`, without the leading
|
|
/// underscore Mach-O adds to C names.
|
|
fn c_name(sym: &str) -> &str {
|
|
let s = sym.split('@').next().unwrap_or(sym);
|
|
if cfg!(target_vendor = "apple") { s.strip_prefix('_').unwrap_or(s) } else { s }
|
|
}
|
|
|
|
/// True if `name` (a C name) is a math-library function this test checks.
|
|
fn is_math_function(name: &str) -> bool {
|
|
let base = name.strip_suffix("_finite").map(|b| b.strip_prefix("__").unwrap_or(b)).unwrap_or(name);
|
|
OTHER_MATH_SYMBOLS.contains(&name)
|
|
|| MATH_FUNCTIONS.iter().any(|f| {
|
|
base == *f
|
|
|| base.strip_suffix('f') == Some(f)
|
|
|| base.strip_suffix('l') == Some(f)
|
|
})
|
|
}
|
|
|
|
/// The round-to-integral functions, which IEEE 754 fixes bit for bit. A definition of
|
|
/// one of these is allowed (see the module doc); an import is not.
|
|
const ROUND_TO_INTEGRAL: [&str; 7] = ["ceil", "floor", "trunc", "round", "roundeven", "rint", "nearbyint"];
|
|
|
|
fn is_round_to_integral(name: &str) -> bool {
|
|
ROUND_TO_INTEGRAL.iter().any(|f| {
|
|
name == *f || name.strip_suffix('f') == Some(f) || name.strip_suffix('l') == Some(f)
|
|
})
|
|
}
|
|
|
|
/// `nm -P` type letters for a symbol the executable does not define: `U`, and the
|
|
/// lower-case weak forms.
|
|
fn is_import(kind: &str) -> bool {
|
|
matches!(kind, "U" | "w" | "v")
|
|
}
|
|
|
|
/// True if the symbol `sym` of `nm` type `kind` fails the test.
|
|
fn forbidden(sym: &str, kind: &str) -> bool {
|
|
let name = c_name(sym);
|
|
is_math_function(name) && (is_import(kind) || !is_round_to_integral(name))
|
|
}
|
|
|
|
/// The classifier recognises the forms `nm` prints, and lets the allowed and unrelated
|
|
/// names through, before it is trusted with the executable.
|
|
fn check_the_classifier() {
|
|
let apple = cfg!(target_vendor = "apple");
|
|
let c = |s: &'static str| -> String { if apple { format!("_{s}") } else { s.to_string() } };
|
|
let exp = if apple { "_exp".to_string() } else { "exp@GLIBC_2.29".to_string() };
|
|
for (sym, kind, want) in [
|
|
(exp, "U", true),
|
|
(if apple { c("fmodf") } else { "fmodf@@GLIBC_2.38".to_string() }, "U", true),
|
|
(c("fmod"), "W", true),
|
|
(c("exp"), "T", true),
|
|
(c("roundf"), "U", true),
|
|
(if apple { c("floor") } else { "floor@GLIBC_2.2.5".to_string() }, "U", true),
|
|
(c("floor"), "w", true),
|
|
(c("floor"), "W", false),
|
|
(c("roundf"), "W", false),
|
|
(c("rint"), "T", false),
|
|
(c("lround"), "W", true),
|
|
(c("__powidf2"), "T", true),
|
|
(if apple { c("__exp_finite") } else { "__exp_finite@GLIBC_2.15".to_string() }, "U", true),
|
|
(if apple { c("sqrt") } else { "sqrt@GLIBC_2.2.5".to_string() }, "U", false),
|
|
(c("fabsf"), "W", false),
|
|
(c("copysign"), "T", false),
|
|
(if apple { c("malloc") } else { "malloc@GLIBC_2.2.5".to_string() }, "U", false),
|
|
("_ZN3wai8float_repro3exp17h0123456789abcdefE".to_string(), "T", false),
|
|
(c("expand_macro"), "T", false),
|
|
] {
|
|
assert_eq!(forbidden(&sym, kind), want, "classifier on {sym:?} ({kind})");
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn main() {
|
|
link_the_decoder();
|
|
check_the_classifier();
|
|
|
|
let exe = std::env::current_exe().expect("path of this test executable");
|
|
let out = std::process::Command::new("nm")
|
|
.arg("-P")
|
|
.arg(&exe)
|
|
.output()
|
|
.unwrap_or_else(|e| panic!("cannot run `nm` ({e}); it ships with the linker toolchain \
|
|
(binutils, or the Xcode command line tools)"));
|
|
assert!(out.status.success(), "nm -P {} failed: {}", exe.display(),
|
|
String::from_utf8_lossy(&out.stderr));
|
|
let listing = String::from_utf8_lossy(&out.stdout);
|
|
|
|
// `nm -P`: one symbol per line, `name type [value [size]]`.
|
|
let symbols: Vec<(&str, &str)> = listing
|
|
.lines()
|
|
.filter_map(|l| {
|
|
let mut f = l.split_whitespace();
|
|
Some((f.next()?, f.next()?))
|
|
})
|
|
.collect();
|
|
|
|
let decoder_linked = symbols.iter().any(|(s, _)| s.contains("13jpegai_decode6decode"));
|
|
let imports_listed = symbols.iter().any(|(s, t)| *t == "U" && c_name(s) == "malloc");
|
|
assert!(decoder_linked,
|
|
"{}: no `jpegai_decode::decode` symbol among {} listed, so this check cannot say \
|
|
what the decoder links (a stripped executable?)", exe.display(), symbols.len());
|
|
assert!(imports_listed,
|
|
"{}: `malloc` is not listed as an import among {} symbols, so the listing does not \
|
|
show imports", exe.display(), symbols.len());
|
|
|
|
let describe = |(s, t): &(&str, &str)| {
|
|
format!("{s} ({})", if is_import(t) { "imported" } else { "defined here" })
|
|
};
|
|
let found: Vec<String> = symbols.iter().filter(|(s, t)| forbidden(s, t)).map(describe).collect();
|
|
assert!(found.is_empty(),
|
|
"the JPEG AI exact decoder's executable holds C math-library functions: {}. Something \
|
|
the decode reaches calls the platform math library; use wai::float_repro or basic \
|
|
operations instead", found.join(", "));
|
|
let allowed: Vec<String> = symbols
|
|
.iter()
|
|
.filter(|(s, t)| is_math_function(c_name(s)) && !forbidden(s, t))
|
|
.map(describe)
|
|
.collect();
|
|
let allowed = if allowed.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!(" (allowed, exactly specified: {})", allowed.join(", "))
|
|
};
|
|
eprintln!("jpegai_decode_symbols: {} symbols in {}; the decoder is linked; no C \
|
|
math-library function the test forbids among them{allowed}",
|
|
symbols.len(), exe.display());
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn main() {
|
|
link_the_decoder();
|
|
check_the_classifier();
|
|
eprintln!("jpegai_decode_symbols: SKIPPED, not a pass: this check reads the executable's \
|
|
symbols with `nm`, which it runs on unix targets only");
|
|
}
|