valgrind-requests 1.1.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.

Core client request APIs are no_std compatible. The std feature, which implies alloc, is enabled by default.

Features

  • act (default): Enables actual execution of client requests when running under Valgrind. Implies stubs.
  • stubs: Enables the same public API surface and build-time code generation, but all client requests compile to no-ops.
  • alloc: Enables allocation-backed convenience APIs, such as formatting macros and owned C string helpers.
  • std (default): Enables standard library support and implies alloc.

Formatting convenience macros such as valgrind_printf! and valgrind_println! require the alloc feature because they allocate owned C strings. Allocation-free alternatives are valgrind_print or valgrind_print_backtrace.

Installation

[dependencies]
valgrind-requests = "1.1"

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.1", default-features = false, features = ["stubs"] }

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

The stubs compile down to nothing and your production code is as performant as without any annotations. If your production code uses the formatting convenience macros, enable both stubs and alloc with features = ["stubs", "alloc"].

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);
}

no_std Example

Disable default features and re-enable either stubs or act:

valgrind-requests = { version = "1.1", default-features = false, features = ["act"] }

The core client request functions are no_std compatible and can be used as usual. In allocation-free environments, you can use allocation-free print functions, for example:

valgrind_requests::valgrind_print(c"running under Valgrind\n");

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 allocation-free valgrind_print and valgrind_print_backtrace functions live in the crate root. The alloc-backed 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 also live in the crate root.

Platform support

If possible, client requests execute with zero indirection and the same overhead as the original Valgrind C macros usable even in high performance code. On Valgrind-supported platforms for which zero-indirection isn't implemented by us, a native C FFI binding is used which introduces at least an additional frame on the stack and the costs for the function call. That means all targets covered by Valgrind are also covered by valgrind-requests. Targets not supported by Valgrind produce a compile error.

Target Zero-indirection Notes
x86_64/linux yes except the x32 ABI
x86_64/android yes except the x32 ABI
x86_64/freebsd yes -
x86_64/macos yes the versions supported by Valgrind and LouisBrunner/valgrind-macos
x86_64/windows+gnu yes -
x86_64/solaris yes -
x86/linux yes -
x86/android yes -
x86/freebsd yes -
x86/macos yes the versions supported by Valgrind and LouisBrunner/valgrind-macos
x86/windows+gnu yes -
x86/solaris yes -
arm/linux yes -
arm/android yes -
aarch64/linux yes -
aarch64/android yes -
aarch64/freebsd yes -
aarch64/macos yes LouisBrunner/valgrind-macos
riscv64/linux yes -
s390x/linux yes -
powerpc/linux yes rust >= 1.95.0
powerpc64/linux yes rust >= 1.95.0
powerpc64le/linux yes rust >= 1.95.0
mips32/linux no no rust inline assembly available
mips64/linux no no rust inline assembly available
nanomips/linux no no zero-indirection planned
x86/windows+msvc no no zero-indirection planned

To disable the native C FFI binding as fallback you can set the environment variable VALGRIND_REQUESTS_STRATEGY=strict (possible values are: strict, fallback).

License

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