usdt 0.1.12

Dust your Rust with USDT probes
Documentation
# `usdt`

Dust your Rust with USDT probes.

## Overview

`usdt` exposes statically-defined DTrace probes to Rust code. Users write a provider definition
as usual, in a D language script. The provider's probes may then be compiled into Rust code that
fires the probes.

There are two mechanisms for converting the D probe definitions into Rust: a build.rs script and
a procedural macro. The generated code is interchangeable, so it's simply the preference of the
consuming developer that dictates which method is used. The build-time and macro versions are
shown in the example crates `probe-test-build` and `probe-test-macro`, respectively.

> Note: This crate uses inline assembly to work its magic. As such a nightly Rust toolchain is
required, and the functionality is hidden behind the `"asm"` feature flag. A nightly toolchain
can be installed with `rustup toolchain install nightly`.

## Example

The `probe-test-build` binary crate in this package implements a complete example, using the
build-time code generation.

The starting point is a D script, called `"test.d"`. It looks like:

```d
provider test {
	probe start(uint8_t);
	probe stop(char*, uint8_t);
};
```

This script defines a single provider, `test`, with two probes, `start` and `stop`,
with a different set of arguments. (Numeric primitive types and `&str`s are currently
supported.)

This provider definition must be converted into Rust code, which can be done in a simple
build script:


```rust
use usdt::Builder;

fn main() {
	Builder::new("test.d").build().unwrap();
}
```

This generates a file in the directory `OUT_DIR` which contains the generated Rust macros
that fire the probes. Unless it is changed, this file is named the same as the provider
definition file, so `test.rs` in this case.

Using the probes in Rust code looks like the following, which is in `probe-test-build/src/main.rs`.

```rust
//! An example using the `usdt` crate, generating the probes via a build script.
#![feature(asm)]

use std::thread::sleep;
use std::time::Duration;

use usdt::register_probes;

// Include the Rust implementation generated by the build script.
include!(concat!(env!("OUT_DIR"), "/test.rs"));

fn main() {
    let duration = Duration::from_secs(1);
    let mut counter: u8 = 0;

    // NOTE: One _must_ call this function in order to actually register the probes with DTrace.
    // Without this, it won't be possible to list, enable, or see the probes via `dtrace(1)`.
    register_probes().unwrap();

    loop {
        // Call the "start" probe which accepts a u8.
        test_start!(|| (counter));

        // Do some work.
        sleep(duration);

        // Call the "stop" probe, which accepts a &str and a u8.
        test_stop!(|| ("the probe has fired", counter));

        counter = counter.wrapping_add(1);
    }
}
```

Note that the `#![feature(asm)]` attribute is required. One can also see that the Rust code
is included directly using the `include!` macro. The probe definitions are converted into Rust
macros, named by the provider and probe. In our case, the first probe is converted into a macro
`test_start!`.

> IMPORTANT: It's important to note that the application _must_ call `usdt::register_probes()`
in order to actually register the probe points with DTrace. Failing to do this will not impact
the application's functionality, but it will be impossible to list, enable, or otherwise see the
probes with the `dtrace(1)` tool without this.

We can see that this is hooked up with DTrace by running the example and listing the expected
probes by name.

```bash
$ cargo +nightly run --features asm
```

And in another terminal, list the matching probes with:

```bash
$ sudo dtrace -l -n test*:::
   ID   PROVIDER            MODULE                          FUNCTION NAME
 2865  test14314  probe-test-build _ZN16probe_test_build4main17h906db832bb52ab01E [probe_test_build::main::h906db832bb52ab01] start
 2866  test14314  probe-test-build _ZN16probe_test_build4main17h906db832bb52ab01E [probe_test_build::main::h906db832bb52ab01] stop
 ```

## Probe arguments

One can see that the probe macros are called with closures, rather than with the probe
arguments directly. This has two purposes.

First, it indicates that the probe arguments may not be evaluated. DTrace generates
"is-enabled" probes for defined probe, which is a simple way to check if the probe has
currently been enabled. The arguments are only unpacked if the probe is enabled, and
so users _must not_ rely on side-effects. The closure helps indicate this.

The second point of this is efficiency. Again, the arguments are not evaluated if the
probe is not enabled. The closure is only evaluated internally _after_ the probe is
verified to be enabled, which avoid the unnecessary work of argument marshalling if
the probe is disabled.

## Procedural macro version

The procedural macro version of this crate can be seen in the `probe-test-macro` example,
which is nearly identical to the above example. However, there is no build.rs script,
so in place of the `include!` macro, one finds the procedural macro:

```rust
dtrace_provider!("test.d");
```

This macro generates the same macros as seen above, but does at the time the source
itself is compiled. This may be easier for some use cases, as there is no build script.
However, procedural macros have downsides. It can be difficult to understand their
internals, especially when things fail. Additionally, the macro is run on every compile,
even if the provider definition is unchanged. This may be negligible for small provider
definitions, but users may see a noticeable increase in compile times when many probes
are defined.

## A note about registration

Note that the `usdt::register_probes()` function is called at the top of main in the above
example. This method is required to actually register the probes with the DTrace kernel
module. This presents a quandary for library developers who wish to instrument their
code, as consumers of their library may forget to (or choose not to) call this function.
There are potential workarounds to this problem (init-sections, other magic), but each
comes with significant tradeoffs. As such the current recommendation is:

> Library developers are encouraged to re-export the `usdt::register_probes` (or a
function calling it), and document to their users that this function should be called to
guarantee that probes are registered.