minicov/
lib.rs

1//! This crate provides code coverage and profile-guided optimization (PGO) support
2//! for `no_std` and embedded programs.
3//!
4//! This is done through a modified version of the LLVM profiling runtime (normally
5//! part of compiler-rt) from which all dependencies on libc have been removed.
6//!
7//! All types of instrumentation using the LLVM profiling runtime are supported:
8//! - Rust code coverage with `-Cinstrument-coverage`.
9//! - Rust profile-guided optimization with `-Cprofile-generate`.
10//! - Clang code coverage with `-fprofile-instr-generate -fcoverage-mapping`.
11//! - Clang profile-guided optimization with `-fprofile-instr-generate`.
12//! - Clang LLVM IR profile-guided optimization with `-fprofile-generate`.
13//!
14//! Note that to profile both C and Rust code at the same time you must use Clang
15//! with the same LLVM version as the LLVM used by rustc. You can pass these flags
16//! to C code compiled with the `cc` crates using [environment variables].
17//!
18//! [environment variables]: https://github.com/rust-lang/cc-rs#external-configuration-via-environment-variables
19//!
20//! ## Usage
21//!
22//! Note: This crate requires a recent nightly compiler.
23//!
24//! 1. Ensure that the following environment variables are set up:
25//!
26//! ```sh
27//! export RUSTFLAGS="-Cinstrument-coverage -Zno-profiler-runtime"
28//! ```
29//!
30//! Note that these flags also apply to build-dependencies and proc
31//! macros by default. This can be worked around by explicitly
32//! specifying a target when invoking cargo:
33//!
34//! ```sh
35//! # Applies RUSTFLAGS to everything
36//! cargo build
37//!
38//! # Doesn't apply RUSTFLAGS to build dependencies and proc macros
39//! cargo build --target x86_64-unknown-linux-gnu
40//! ```
41//!
42//! 2. Add the `minicov` crate as a dependency to your program:
43//!
44//! ```toml
45//! [dependencies]
46//! minicov = "0.3"
47//! ```
48//!
49//! 3. Before your program exits, call `minicov::capture_coverage` with a sink (such
50//!    as `Vec<u8>`) and then dump its contents to a file with the `.profraw` extension:
51//!
52//! ```ignore
53//! fn main() {
54//!     // ...
55//!
56//!     let mut coverage = vec![];
57//!     unsafe {
58//!         // Note that this function is not thread-safe! Use a lock if needed.
59//!         minicov::capture_coverage(&mut coverage).unwrap();
60//!     }
61//!     std::fs::write("output.profraw", coverage).unwrap();
62//! }
63//! ```
64//!
65//! If your program is running on a different system than your build system then
66//! you will need to transfer this file back to your build system.
67//!
68//! Sinks must implement the `CoverageWriter` trait. If the default `alloc` feature
69//! is enabled then an implementation is provided for `Vec<u8>`.
70//!
71//! 4. Use a tool such as [grcov] or llvm-cov to generate a human-readable coverage
72//!    report:
73//!
74//! ```sh
75//! grcov output.profraw -b ./target/debug/my_program -s . -t html -o cov_report
76//! ```
77//!
78//! [grcov]: https://github.com/mozilla/grcov
79//!
80//! ## Profile-guided optimization
81//!
82//! The steps for profile-guided optimzation are similar. The only difference is the
83//! flags passed in `RUSTFLAGS`:
84//!
85//! ```sh
86//! # First run to generate profiling information.
87//! export RUSTFLAGS="-Cprofile-generate -Zno-profiler-runtime"
88//! cargo run --target x86_64-unknown-linux-gnu --release
89//!
90//! # Post-process the profiling information.
91//! # The rust-profdata tool comes from cargo-binutils.
92//! rust-profdata merge -o output.profdata output.profraw
93//!
94//! # Optimized build using PGO. minicov is not needed in this step.
95//! export RUSTFLAGS="-Cprofile-use=output.profdata"
96//! cargo build --target x86_64-unknown-linux-gnu --release
97//! ```
98
99#![no_std]
100#![warn(missing_docs)]
101#![warn(rust_2018_idioms)]
102
103#[cfg(feature = "alloc")]
104extern crate alloc;
105
106#[cfg(feature = "alloc")]
107use alloc::vec::Vec;
108#[cfg(feature = "alloc")]
109use core::alloc::Layout;
110use core::{fmt, slice};
111
112#[allow(non_snake_case)]
113#[repr(C)]
114struct ProfDataIOVec {
115    Data: *mut u8,
116    ElmSize: usize,
117    NumElm: usize,
118    UseZeroPadding: i32,
119}
120
121#[allow(non_snake_case)]
122#[repr(C)]
123struct ProfDataWriter {
124    Write:
125        unsafe extern "C" fn(This: *mut ProfDataWriter, *mut ProfDataIOVec, NumIOVecs: u32) -> u32,
126    WriterCtx: *mut u8,
127}
128
129// Opaque type for our purposes.
130enum VPDataReaderType {}
131
132extern "C" {
133    fn __llvm_profile_reset_counters();
134    fn __llvm_profile_merge_from_buffer(profile: *const u8, size: u64) -> i32;
135    fn __llvm_profile_check_compatibility(profile: *const u8, size: u64) -> i32;
136    fn __llvm_profile_get_version() -> u64;
137    fn lprofWriteData(
138        Writer: *mut ProfDataWriter,
139        VPDataReader: *mut VPDataReaderType,
140        SkipNameDataWrite: i32,
141    ) -> i32;
142    fn lprofGetVPDataReader() -> *mut VPDataReaderType;
143}
144
145const INSTR_PROF_RAW_VERSION: u64 = 10;
146const VARIANT_MASKS_ALL: u64 = 0xffffffff00000000;
147
148// On some target rustc will insert an artificial dependency on the
149// __llvm_profile_runtime symbol to ensure the static initializer from LLVM's
150// profiling runtime is pulled in by the linker. We don't need any runtime
151// initialization so we just provide the symbol here.
152#[no_mangle]
153static __llvm_profile_runtime: u8 = 0;
154
155// Memory allocation functions used by value profiling. If the "alloc" feature
156// is disabled then value profiling will also be disabled.
157#[cfg(feature = "alloc")]
158#[no_mangle]
159unsafe fn minicov_alloc_zeroed(size: usize, align: usize) -> *mut u8 {
160    alloc::alloc::alloc_zeroed(Layout::from_size_align(size, align).unwrap())
161}
162#[cfg(feature = "alloc")]
163#[no_mangle]
164unsafe extern "C" fn minicov_dealloc(ptr: *mut u8, size: usize, align: usize) {
165    alloc::alloc::dealloc(ptr, Layout::from_size_align(size, align).unwrap())
166}
167#[cfg(not(feature = "alloc"))]
168#[no_mangle]
169unsafe fn minicov_alloc_zeroed(_size: usize, _align: usize) -> *mut u8 {
170    core::ptr::null_mut()
171}
172#[cfg(not(feature = "alloc"))]
173#[no_mangle]
174unsafe extern "C" fn minicov_dealloc(_ptr: *mut u8, _size: usize, _align: usize) {}
175
176/// Sink into which coverage data can be written.
177///
178/// A default implementation for `Vec<u8>` is provided,
179pub trait CoverageWriter {
180    /// Writes the given bytes to the sink.
181    ///
182    /// This method should return an error if all bytes could not be written to
183    /// the sink.
184    fn write(&mut self, data: &[u8]) -> Result<(), CoverageWriteError>;
185}
186
187#[cfg(feature = "alloc")]
188impl CoverageWriter for Vec<u8> {
189    fn write(&mut self, data: &[u8]) -> Result<(), CoverageWriteError> {
190        self.extend_from_slice(data);
191        Ok(())
192    }
193}
194
195/// Callback function passed to `lprofWriteData`.
196unsafe extern "C" fn write_callback<Writer: CoverageWriter>(
197    this: *mut ProfDataWriter,
198    iovecs: *mut ProfDataIOVec,
199    num_iovecs: u32,
200) -> u32 {
201    let writer = &mut *((*this).WriterCtx as *mut Writer);
202    for iov in slice::from_raw_parts(iovecs, num_iovecs as usize) {
203        let len = iov.ElmSize * iov.NumElm;
204        if iov.Data.is_null() {
205            // Pad with zero bytes.
206            let zero = [0; 16];
207            let mut remaining = len;
208            while remaining != 0 {
209                let data = &zero[..usize::min(zero.len(), remaining)];
210                if writer.write(data).is_err() {
211                    return 1;
212                }
213                remaining -= data.len();
214            }
215        } else {
216            let data = slice::from_raw_parts(iov.Data, len);
217            if writer.write(data).is_err() {
218                return 1;
219            }
220        }
221    }
222    0
223}
224
225/// Checks that the instrumented binary uses the same profiling data format as
226/// the LLVM profiling runtime.
227fn check_version() {
228    let version = unsafe { __llvm_profile_get_version() & !VARIANT_MASKS_ALL };
229    assert_eq!(
230        version, INSTR_PROF_RAW_VERSION,
231        "Runtime and instrumentation version mismatch"
232    );
233}
234
235/// Captures the coverage data for the current program and writes it into the
236/// given sink.
237///
238/// The data should be saved to a file with the `.profraw` extension, which can
239/// then be processed using the `llvm-profdata` and `llvm-cov` tools.
240///
241/// You should call `reset_coverage` afterwards if you intend to continue
242/// running the program so that future coverage can be merged with the returned
243/// captured coverage.
244///
245/// # Safety
246///
247/// This function is not thread-safe and should not be concurrently called from
248/// multiple threads.
249pub unsafe fn capture_coverage<Writer: CoverageWriter>(
250    writer: &mut Writer,
251) -> Result<(), CoverageWriteError> {
252    check_version();
253
254    let mut prof_writer = ProfDataWriter {
255        Write: write_callback::<Writer>,
256        WriterCtx: writer as *mut Writer as *mut u8,
257    };
258    let res = lprofWriteData(&mut prof_writer, lprofGetVPDataReader(), 0);
259    if res == 0 {
260        Ok(())
261    } else {
262        Err(CoverageWriteError)
263    }
264}
265
266/// Error type returned when trying to merge incompatible coverage data.
267///
268/// This typically happens if the coverage data comes from a different binary.
269#[derive(Copy, Clone, Debug)]
270pub struct IncompatibleCoverageData;
271impl fmt::Display for IncompatibleCoverageData {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        f.write_str("incompatible coverage data")
274    }
275}
276
277/// Error while trying to write coverage data.
278///
279/// This only happens if the `CoverageWriter` implementation returns an error.
280#[derive(Copy, Clone, Debug)]
281pub struct CoverageWriteError;
282impl fmt::Display for CoverageWriteError {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        f.write_str("error while writing coverage data")
285    }
286}
287
288/// Merges previously dumped coverage data into the coverage counters.
289///
290/// This should be called prior to dumping if coverage data from a previous run
291/// already exists and should be merged with instead of overwritten.
292///
293/// # Safety
294///
295/// This function is not thread-safe and should not be concurrently called from
296/// multiple threads.
297pub unsafe fn merge_coverage(data: &[u8]) -> Result<(), IncompatibleCoverageData> {
298    check_version();
299
300    if __llvm_profile_check_compatibility(data.as_ptr(), data.len() as u64) == 0
301        && __llvm_profile_merge_from_buffer(data.as_ptr(), data.len() as u64) == 0
302    {
303        Ok(())
304    } else {
305        Err(IncompatibleCoverageData)
306    }
307}
308
309/// Resets all coverage counters in the program to zero.
310///
311/// This function should be called after a process forks to avoid recording
312/// coverage data for the parent process twice.
313///
314/// You should also call this after calling `capture_coverage` if you intend to
315/// continue running with the intention of merging with the captured coverage
316/// later.
317pub fn reset_coverage() {
318    check_version();
319
320    unsafe {
321        __llvm_profile_reset_counters();
322    }
323}