valgrind-requests 1.0.0

Idiomatic Rust bindings for Valgrind client requests, with zero-indirection execution and zero-cost fallback
Documentation

valgrind-requests

Idiomatic Rust bindings for Valgrind client requests, with zero-indirection execution and zero-cost fallback.

About

valgrind-requests is a standalone crate providing idiomatic Rust bindings for Valgrind's Client Request Mechanism. It is a retake and rewrite from scratch of the abandoned valgrind_request. This implementation is considered complete and covers all Valgrind tools with client requests (Memcheck, Callgrind, Cachegrind, Helgrind, DRD, DHAT) on all platforms which Valgrind supports.

This crate was formerly hardcoded into Gungraun. Gungraun still uses valgrind-requests but this crate was extracted in the hope to be useful outside of the Gungraun crate.

valgrind-requests uses inline assembly for zero-indirection execution on supported platforms - avoiding the function call overhead of a C FFI layer. The stubs feature compiles all client requests to no-ops with zero runtime cost, making it safe to ship in production code without conditional compilation.

Features

  • act (default): Enables actual execution of client requests when running under Valgrind. Implies stubs.
  • stubs: Enables the client request definitions and build-time code generation, but all client requests compile to no-ops.

Installation

[dependencies]
valgrind-requests = "1.0"

or cargo add valgrind-requests.

To use the zero-cost fallback for example if you want to use the client requests for tests or benchmarks and need to make annotations in production code:

[dependencies]
valgrind-requests = { version = "1.0", default-features = false, features = ["stubs"] }

[dev-dependencies]
valgrind-requests = { version = "1.0" }

The stubs compile down to nothing and your production code is as performant as without any annotations.

The client requests require the Valgrind header files. These are typically installed by your distribution's package manager alongside Valgrind and discovered automatically.

If the headers cannot be found, set the VALGRIND_REQUESTS_VALGRIND_INCLUDE environment variable to the include path containing the valgrind/ directory. For cross-compilation, you can use the target-specific variant:

VALGRIND_REQUESTS_X86_64_UNKNOWN_LINUX_GNU_VALGRIND_INCLUDE=/path/to/include cargo build

Quick start

use valgrind_requests::{valgrind, valgrind_println};

fn main() {
    if valgrind::running_on_valgrind() != 0 {
        // This'll print to the log file/logging output of Valgrind
        valgrind_println!("running under valgrind!").unwrap();
    } else {
        println!("not running under valgrind");
    }
}
use valgrind_requests::memcheck::{self, LeakCounts};

#[test]
fn test_memcheck() {
    // Some ffi code which could be responsible for a memory leak

    let LeakCounts {
        dubious,
        leaked,
        reachable,
        suppressed: _,
    } = memcheck::count_leaks();

    assert!(dubious == 0 && leaked == 0 && reachable == 0);
}

Gungraun example

Measure only a specific code region with Callgrind

// my_lib.rs

use valgrind_requests::callgrind;

fn private_func() -> u64 {
    // heavy work

    return 42
}

pub fn some_func() -> u64 {
    // do something which isn't very interesting to benchmark

    // then only measure this code region
    callgrind::start_instrumentation();
    let r = private_func();
    println!("{r}");
    callgrind::stop_instrumentation();

    return r;
}

For example, in a Gungraun benchmark this could be used

use gungraun::prelude::*;
use gungraun::{Callgrind, EntryPoint};

use std::hint::black_box;

use my_lib::some_func;

#[library_benchmark(
    config = LibraryBenchmarkConfig::default()
        // disable the instrumentation at benchmark start (We start manually)
        .tool(Callgrind::with_args(["instr-atstart=no"])
            // disable the default entry point which is the benchmark function
            .entry_point(EntryPoint::None)
        )
)]
fn bench_my_lib() -> u64 {
    black_box(some_func())
}

library_benchmark_group!(name = my_group, benchmarks = bench_my_lib);
main!(library_benchmark_groups = my_group);

Modules

The client requests are organized into modules representing the source header file:

The valgrind_printf!, valgrind_printf_unchecked!, valgrind_println!, valgrind_println_unchecked!, valgrind_printf_backtrace!, valgrind_printf_backtrace_unchecked!, valgrind_println_backtrace!, and valgrind_println_backtrace_unchecked! macros live in the crate root.

Platform support

This is an ongoing work and depends on which platforms inline assembly is stable. Where it is stable, client requests execute with zero indirection - the same overhead as the original Valgrind C macros usable even in high performance code. On other Valgrind-supported platforms, a native C FFI binding is used which introduces at least an additional frame on the stack and the costs for the function call. All targets covered by Valgrind are also covered by valgrind-requests. Targets not supported by Valgrind produce a compile error.

Target Optimized Reason
x86_64/linux yes -
x86_64/freebsd yes -
x86_64/apple+darwin yes -
x86_64/windows+gnu yes -
x86_64/solaris yes -
x86/linux yes -
x86/freebsd yes -
x86/apple+darwin yes -
x86/windows+gnu yes -
x86/solaris yes -
x86/windows+msvc no TBD
arm/linux yes -
aarch64/linux yes -
riscv64/linux yes -
x86_64/windows+msvc no unsupported by Valgrind
s390x/linux no needs MSRV 1.84.0
mips32/linux no unstable inline assembly
mips64/linux no unstable inline assembly
powerpc/linux no needs MSRV 1.95.0
powerpc64/linux no needs MSRV 1.95.0
powerpc64le/linux no needs MSRV 1.95.0
nanomips/linux no Valgrind only

If a platform uses the native C fallback instead of zero-indirection this is shown as a no entry in the Optimized column.

License

Licensed under Apache-2.0 or MIT, at your option.