vips-rs

Functionality, correctness and performance review — 2026-10-22

Scope and result

Review covers vips 0.1.0 → 0.2.0 and its sibling vips-sys 0.2.0 → 0.3.0, with libvips 8.18.6 → 8.18.7. Existing local dependency updates are retained. The initial review prepared both versions in Git. The dependency publication and subsequent release preparation are recorded below.

The companion upstream audit records the official release, 57 merged PRs, unchanged stable headers and deferred development APIs. A modern system library can enable v8_18; default builds keep that symbol dependency optional for older distro packages. Optional codec support still depends on how the native library was built.

Confirmed findings addressed

Severity Trigger and prior behavior Resolution and evidence
Critical An owned/static left operand combined with an inner-scope borrowed right operand could produce an image used after the right buffer died. Conditional branches and masks had the same omission. Every lazy image input now carries the output buffer lifetime; compile-fail tests cover secondary and conditional inputs, with a pass case proving the source image handle itself may drop.
Critical VipsRegion could escape its image’s borrowed backing buffer. VipsRegion<'a> preserves that buffer lifetime; C still owns an image reference so the image handle may drop. Compile-fail and native lifetime cases cover both boundaries.
Critical A safe interpolator call accepted an empty output slice or unprepared/out-of-window region. Documentation could not enforce the C memory bounds. VipsInterpolateMethod::call is unsafe and documents output size/alignment, prepared region, and matching method/interpolator requirements; a compile-fail case protects this boundary.
Critical In-place draw operations on from_memory_reference(&[u8]) could modify immutable Rust storage; non-byte formats could also receive misaligned input pointers. Drawing first writes borrowed/shared input into a fresh native memory image; owned input takes the direct path. Constructors validate alignment and checked storage dimensions. A native test verifies the original borrowed bytes remain unchanged.
Major Two initialization paths could call native init independently; VipsInstance::new ignored the C status and consumed its singleton flag on invalid input. Dropping an instance could shut down images still in use. Share vips-sys::helpers’s process-wide initialization state/lock, propagate failures, claim the singleton only after success, and retain global state for the process lifetime. Cold concurrent initialization and image-after-instance-drop tests cover this behavior.
Major Mismatched linear inputs panicked in a public fallible API; very large median sizes overflowed. Return explicit errors for invalid arrays, nonpositive size and checked-square overflow.
Minor Native output allocations were manually copied and freed; the empty encoded-output path could skip freeing a non-null allocation. A single RAII VipsOwnedBuffer owns all native output paths, including errors/empty results. Existing Vec methods use that owner.

Before the fix, a minimal Rust cargo check reproducer successfully compiled a function returning a static lazy addition from a locally borrowed right-hand buffer, and a region escaping a locally borrowed pixel buffer. The new UI tests require rejection. This was a compile-time lifetime verification, not a deliberate runtime use-after-free execution.

Functionality added

Performance changes and measurement

write_to_memory_owned and write_to_buffer_owned eliminate the additional Rust allocation and memcpy performed by their Vec-returning counterparts. They still perform the native evaluation/encoding and native output allocation: this is not a claim of zero-copy decoding or encoding.

from_memory keeps the caller’s Vec allocation and its capacity. The prior into_boxed_slice could shrink/reallocate a Vec with spare capacity. A pointer identity test verifies that moving such a Vec into the native image preserves its data address.

examples/buffer_bench.rs measures export plus destruction in release mode with three warmups and ten ABBA groups (20 samples per mode), reporting medians for 2048×2048×3 raw pixels. It reports a ratio only when the median A1/A2 baseline drift is at most 10%. perf_smoke also forces evaluation before reading elapsed time. Neither example alone measures general service throughput, RSS or tail latency.

Prioritized follow-up work

  1. Representative performance suite: JPEG/PNG/WebP/AVIF inputs, small and large images, forced output, fixed codec versions/thread counts, warm/cold cache, latency percentiles and peak RSS. Compare shrink-on-load thumbnailing with decoded-image resize under equivalent output/quality settings.
  2. Typed load/save options: expose fail_on, page/frame ranges, HDR gainmap scaling and common encoder options without encouraging untyped raw varargs. Default changes such as master gamma behavior require explicit compatibility review.
  3. Safe streaming Source/Target wrappers: model callback ownership, cancellation, backpressure and panic containment before enabling user callbacks across FFI.
  4. Broader platform/runtime coverage: real Windows/vcpkg execution, Linux default-library tests and macOS 8.18 feature tests. Check native codec provenance; missing codecs should return actionable errors, not be described as enabled support.
  5. Module organization: separate drawing, geometry, IO and colour operation implementations once the safety and behavior regressions are covered. Keep this organizational work separate from performance claims.

Validation

Executed with the current stable Rust 1.99.1 toolchain and a source-built libvips 8.18.7. The native build includes Highway 1.4.0, PNG 1.6.59, JPEG 3.2.0, WebP, TIFF, LCMS, libraw 0.22.2 and libultrahdr 2.0.2. Optional HEIF/AVIF, JXL, SVG, PDF, EXR and other absent codecs were not exercised.

The release raw-export benchmark measured medians of 824.042 µs copied and 572.958 µs native-owned for 12,582,912 bytes, ratio 0.695 (about 30.5% less elapsed time in this local microbenchmark). The A1/A2 baseline drift was 1.5%, passing the benchmark’s 10% drift gate. The owned route also avoids one 12 MiB Rust allocation and copy per export. This is a local microbenchmark, not a claim about general codec or service throughput.

The 512×512 perf_smoke fixture measured resize_reduce at 3.174 ms versus plain resize at 0.197 ms in this run. This fixture does not establish a benefit for the extra pre-shrink path; prefer shrink-on-load for encoded sources and benchmark before using resize_reduce on already decoded inputs.

Rust cross-target checks for Windows MSVC and Linux verify compilation and the GLib size assertions. Native Windows/vcpkg execution, full static dependency closure and valid camera-RAW decoding remain unverified; RAW invalid-input handling was exercised. These results predate the publication follow-up below.

Published dependency follow-up

After merging remote main in both repositories, vips-sys 0.3.0 was published through its release workflow. The release tag points to eab1e6e70c5ac953632a331d61c68db91f96d5b3. Official crates.io index metadata confirms the version is not yanked. Windows, Linux, macOS and the Linux vendor static build all passed the dependency’s release checks.

vips now resolves that published package instead of a sibling path dependency. The lockfile pins its registry source and checksum. Normal and release CI use the same registry dependency, avoiding a second checkout and an unpinned sys branch. A local [patch.crates-io] remains available for joint development.

Validation uses the downloaded registry package, whose SHA-256 matches the official index and lockfile. cargo test --all-features --locked passes the nine native image/HDR tests, initialization regressions, six compile-fail cases, four run-pass cases and 23 compile-only doctests. Clippy, documentation with warnings denied, formatting and workflow lint also pass. CI uses --locked to enforce the checked-in dependency resolution.

The vips package dry-run also passes against the registry dependency. That run aborted upload through --dry-run and did not publish a package.

The native build review documents provider selection, incremental builds, static dependency resolution and measurements. Native Windows execution and vendor static linkage are now verified for vips-sys; valid camera-RAW decoding remains unverified.

That dependency-only follow-up did not tag or publish vips 0.2.0.

vips 0.2.0 release preparation

The subsequent release uses tag v0.2.0 and the existing tag-triggered release workflow. Linux and macOS run native tests, Windows runs Clippy against the pre-generated bindings, and the package dry-run must pass before publishing to crates.io. The same workflow creates the GitHub Release from this changelog.