├── .gitignore ├── sonde-test ├── .gitignore ├── providerB.d ├── providerA.d ├── build.rs ├── Cargo.toml ├── scripts │ └── testing.d └── src │ └── main.rs ├── src ├── d │ ├── mod.rs │ ├── ast.rs │ └── parser.rs ├── lib.rs └── builder.rs ├── image └── logo.jpg ├── Cargo.toml ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /sonde-test/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /src/d/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ast; 2 | pub mod parser; 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod d; 3 | 4 | pub use builder::Builder; 5 | -------------------------------------------------------------------------------- /image/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmerio/sonde-rs/HEAD/image/logo.jpg -------------------------------------------------------------------------------- /sonde-test/providerB.d: -------------------------------------------------------------------------------- 1 | provider Salut { 2 | probe le_monde(); 3 | probe toi(); 4 | probe moi(); 5 | }; -------------------------------------------------------------------------------- /sonde-test/providerA.d: -------------------------------------------------------------------------------- 1 | provider Hello { 2 | probe world(); 3 | probe you(char*, int); 4 | probe me(); 5 | probe you__me(); 6 | }; -------------------------------------------------------------------------------- /sonde-test/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | sonde::Builder::new() 3 | .file("./providerA.d") 4 | .file("./providerB.d") 5 | .compile(); 6 | } 7 | -------------------------------------------------------------------------------- /sonde-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sonde-test" 3 | version = "0.1.0" 4 | authors = ["Ivan Enderlin "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [build-dependencies] 9 | sonde = { path = "../" } -------------------------------------------------------------------------------- /sonde-test/scripts/testing.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | 3 | #pragma D option quiet 4 | 5 | BEGIN 6 | { 7 | printf("[trace] Starting…\n"); 8 | } 9 | 10 | Hello*:::you 11 | { 12 | printf("[trace] who=`%s`\n", stringof(copyin(arg0, arg1))) 13 | } 14 | 15 | END 16 | { 17 | printf("[trace] Ending…\n"); 18 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sonde" 3 | version = "0.1.1" 4 | description = "A library to compile USDT probes into a Rust library" 5 | authors = ["Ivan Enderlin "] 6 | repository = "https://github.com/Hywan/sonde-rs" 7 | license = "MIT" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | nom = "^6.1" 12 | cc = "^1.0" 13 | tempfile = "^3.2" -------------------------------------------------------------------------------- /sonde-test/src/main.rs: -------------------------------------------------------------------------------- 1 | mod tracing { 2 | #![allow(unused)] 3 | 4 | include!(env!("SONDE_RUST_API_FILE")); 5 | } 6 | 7 | fn main() { 8 | { 9 | let who = std::ffi::CString::new("Gordon").unwrap(); 10 | tracing::hello::you(who.as_ptr() as *mut _, who.as_bytes().len() as _); 11 | } 12 | 13 | println!("Hello, World!"); 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-present Wasmer, Inc. and its affiliates. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the 13 | distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/d/ast.rs: -------------------------------------------------------------------------------- 1 | pub trait Names { 2 | fn name(&self) -> &str; 3 | 4 | fn safe_name(&self) -> String { 5 | self.name().replace("__", "_") 6 | } 7 | 8 | fn name_for_c_macro(&self) -> String { 9 | self.safe_name().to_uppercase() 10 | } 11 | 12 | fn name_for_c(&self) -> String { 13 | self.safe_name().to_lowercase() 14 | } 15 | 16 | fn name_for_rust(&self) -> String { 17 | self.safe_name().to_lowercase() 18 | } 19 | } 20 | 21 | /// Contains `provider` blocks from a `.d` file. 22 | #[derive(Debug, PartialEq)] 23 | pub struct Script { 24 | pub providers: Vec, 25 | } 26 | 27 | /// Describes a `provider` block. 28 | #[derive(Debug, PartialEq)] 29 | pub struct Provider { 30 | /// The provider's name. 31 | pub name: String, 32 | 33 | /// The probes defined inside the the block. 34 | pub probes: Vec, 35 | } 36 | 37 | impl Names for Provider { 38 | fn name(&self) -> &str { 39 | &self.name 40 | } 41 | } 42 | 43 | /// Describes a `probe`. 44 | #[derive(Debug, PartialEq)] 45 | pub struct Probe { 46 | /// THe probe's name. 47 | pub name: String, 48 | 49 | /// The probe's arguments. 50 | pub arguments: Vec, 51 | } 52 | 53 | impl Names for Probe { 54 | fn name(&self) -> &str { 55 | &self.name 56 | } 57 | } 58 | 59 | impl Probe { 60 | pub fn arguments_for_c(&self) -> String { 61 | self.arguments 62 | .iter() 63 | .enumerate() 64 | .map(|(nth, argument)| format!("{ty} arg{nth}", ty = argument, nth = nth,)) 65 | .collect::>() 66 | .join(", ") 67 | } 68 | 69 | pub fn arguments_for_c_from_rust(&self) -> String { 70 | self.arguments 71 | .iter() 72 | .enumerate() 73 | .map(|(nth, argument_ty)| { 74 | let number_of_pointers = argument_ty.chars().filter(|c| *c == '*').count(); 75 | let ty = match argument_ty.trim_end_matches(|c| c == ' ' || c == '*') { 76 | "char" => "c_char", 77 | "short" => "c_short", 78 | "int" => "c_int", 79 | "long" => "c_long", 80 | "long long" => "c_longlong", 81 | "int8_t" => "i8", 82 | "int16_t" => "i16", 83 | "int32_t" => "i32", 84 | "int64_t" => "i64", 85 | "intptr_t" => "isize", 86 | "uint8_t" => "u8", 87 | "uint16_t" => "u16", 88 | "uint32_t" => "u32", 89 | "uint64_t" => "u64", 90 | "uintptr_t" => "usize", 91 | "float" => "c_float", 92 | "double" => "c_double", 93 | t => panic!("D type `{}` isn't supported yet", t), 94 | }; 95 | 96 | format!( 97 | "arg{nth}: {ptr}{ty}", 98 | ty = ty, 99 | ptr = "*mut ".repeat(number_of_pointers), 100 | nth = nth 101 | ) 102 | }) 103 | .collect::>() 104 | .join(", ") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | sonde 4 |

5 | 6 | [![crates.io](https://img.shields.io/crates/v/sonde)](https://crates.io/crates/sonde) 7 | [![documentation](https://img.shields.io/badge/doc-sonde-green)](https://docs.rs/sonde) 8 | 9 | `sonde` is a library to compile USDT probes into a Rust library, and 10 | to generate a friendly Rust idiomatic API around it. 11 | 12 | [Userland Statically Defined Tracing][usdt] probes (USDT for short) is 13 | a technique inherited from [DTrace] (see [OpenDtrace] to learn 14 | more). It allows user to define statically tracing probes in their own 15 | application; while they are traditionally declared in the kernel. 16 | 17 | USDT probes can be naturally consumed with DTrace, but also with 18 | [eBPF] (`bcc`, `bpftrace`…). 19 | 20 | ## Lightweight probes by design 21 | 22 | USDT probes for libraries and executables are defined in an ELF 23 | section in the corresponding application binary. A probe is translated 24 | into a `nop` instruction, and its metadata are stored in the ELF's 25 | `.note.stapstd` section. When registering a probe, USDT tool (like 26 | `dtrace`, `bcc`, `bpftrace` etc.) will read the ELF section, and 27 | instrument the instruction from `nop` to `breakpoint`, and after that, 28 | the attached tracing event is run. After deregistering the probe, USDT 29 | will restore the `nop` instruction from `breakpoint`. 30 | 31 | The overhead of using USDT probes is almost zero when no tool is 32 | listening the probes, otherwise a tiny overhead can be noticed. 33 | 34 | ## The workflow 35 | 36 | Everything is automated. `dtrace` must be present on the system at 37 | compile-time though. Let's imagine the following `sonde-test` 38 | fictitious project: 39 | 40 | ``` 41 | /sonde-test 42 | ├── src 43 | │ ├── main.rs 44 | ├── build.rs 45 | ├── Cargo.toml 46 | ├── provider.d 47 | ``` 48 | 49 | Start with the obvious thing: let's add the following lines to the 50 | `Cargo.toml` file: 51 | 52 | ```toml 53 | [build-dependencies] 54 | sonde = "0.1" 55 | ``` 56 | 57 | Now, let's see what is in the `provider.d` file. It's _not_ a `sonde` 58 | specific vendor format, it's the canonical way to declare USDT probes 59 | (see [Scripting][scripting])! 60 | 61 | ```d 62 | provider hello { 63 | probe world(); 64 | probe you(char*, int); 65 | }; 66 | ``` 67 | 68 | It describes a probe provider, `hello`, with two probes: 69 | 70 | 1. `world`, 71 | 2. `you` with 2 arguments: `char*` and `int`. 72 | 73 | Be careful, D types aren't the same as C types, even if they look like 74 | the same. 75 | 76 | At this step, one needs to play with `dtrace -s` to compile the probes 77 | into systemtrap headers or an object file, but forget about that, 78 | `sonde` got you covered. Let's see what's in the `build.rs` script: 79 | 80 | ```rust 81 | fn main() { 82 | sonde::Builder::new() 83 | .file("./provider.d") 84 | .compile(); 85 | } 86 | ``` 87 | 88 | That's all. That's the minimum one needs to write to make it 89 | work. 90 | 91 | Ultimately, we want to fire this probe from our code. Let's see what's 92 | inside `src/main.rs` then: 93 | 94 | ```rust 95 | // Include the friendly Rust idiomatic API automatically generated by 96 | // `sonde`, inside a dedicated module, e.g. `tracing`. 97 | mod tracing { 98 | include!(env!("SONDE_RUST_API_FILE")); 99 | } 100 | 101 | fn main() { 102 | tracing::hello::world(); 103 | 104 | println!("Hello, World!"); 105 | } 106 | ``` 107 | 108 | What can we see here? The `tracing` module contains a `hello` module, 109 | corresponding to the `hello` provider. And this module contains a 110 | `world` function, corresponding to the `world` probe. Nice! 111 | 112 |
113 | See what's contained by the file pointed by SONDE_RUST_API_FILE: 114 | 115 | ```rust 116 | /// Bindings from Rust to the C FFI small library that calls the 117 | /// probes. 118 | 119 | use std::os::raw::*; 120 | 121 | extern "C" { 122 | #[doc(hidden)] 123 | fn hello_probe_world(); 124 | 125 | #[doc(hidden)] 126 | fn hello_probe_you(arg0: *mut c_char, arg1: c_int); 127 | } 128 | 129 | /// Probes for the `hello` provider. 130 | pub mod r#hello { 131 | use std::os::raw::*; 132 | 133 | /// Call the `world` probe of the `hello` provider. 134 | pub fn r#world() { 135 | unsafe { super::hello_probe_world() }; 136 | } 137 | 138 | /// Call the `you` probe of the `hello` provider. 139 | pub fn r#you(arg0: *mut c_char, arg1: c_int) { 140 | unsafe { super::hello_probe_you(arg0, arg1) }; 141 | } 142 | } 143 | ``` 144 | 145 |
146 | 147 | Let's see it in action: 148 | 149 | ```sh 150 | $ cargo build --release 151 | $ sudo dtrace -l -c ./target/release/sonde-test | rg sonde-test 152 | 123456 hello98765 sonde-test hello_probe_world world 153 | ``` 154 | 155 | Neat! Our `sonde-test` binary contains a `world` probe from the 156 | `hello` provider! 157 | 158 | ```sh 159 | $ # Let's execute `sonde-test` as usual. 160 | $ ./target/release/sonde-test 161 | Hello, World! 162 | $ 163 | $ # Now, let's execute it with `dtrace` (or any other tracing tool). 164 | $ # Let's listen the `world` probe and prints `gotcha!` when it's executed. 165 | $ sudo dtrace -n 'hello*:::world { printf("gotcha!\n"); }' -q -c ./target/release/sonde-test 166 | Hello, World! 167 | gotcha! 168 | ``` 169 | 170 | Eh, it works! Let's try with the `you` probe now: 171 | 172 | ```rust 173 | fn main() { 174 | { 175 | let who = std::ffi::CString::new("Gordon").unwrap(); 176 | tracing::hello::you(who.as_ptr() as *mut _, who.as_bytes().len() as _); 177 | } 178 | 179 | println!("Hello, World!"); 180 | } 181 | ``` 182 | 183 | Time to show off: 184 | 185 | ```sh 186 | $ cargo build --release 187 | $ sudo dtrace -n 'hello*:::you { printf("who=`%s`\n", stringof(copyin(arg0, arg1))); }' -q -c ./target/release/sonde-test 188 | Hello, World! 189 | who=`Gordon` 190 | ``` 191 | 192 | Successfully reading a string from Rust inside a USDT probe! 193 | 194 | With `sonde`, you can add as many probes inside your Rust library or 195 | binary as you need by simply editing your canonical `.d` file. 196 | 197 | Bonus: `sonde` generates documentation for your probes 198 | automatically. Run `cargo doc --open` to check. 199 | 200 | ## Possible limitations 201 | 202 | ### Types 203 | 204 | DTrace has its own type system (close to C) (see [Data Types and 205 | Sizes][data-types]). `sonde` tries to map it to the Rust system as 206 | much as possible, but it's possible that some types could not 207 | match. The following types are supported: 208 | 209 | | Type Name in D | Type Name in Rust | 210 | |-|-| 211 | | `char` | `std::os::raw::c_char` | 212 | | `short` | `std::os::raw::c_short` | 213 | | `int` | `std::os::raw::c_int` | 214 | | `long` | `std::os::raw::c_long` | 215 | | `long long` | `std::os::raw::c_longlong` | 216 | | `int8_t` | `i8` | 217 | | `int16_t` | `i16` | 218 | | `int32_t` | `i32` | 219 | | `int64_t` | `i64` | 220 | | `intptr_t` | `isize` | 221 | | `uint8_t` | `u8` | 222 | | `uint16_t` | `u16` | 223 | | `uint32_t` | `u32` | 224 | | `uint64_t` | `u64` | 225 | | `uintptr_t` | `usize` | 226 | | `float` | `std::os::raw::c_float` | 227 | | `double` | `std::os::raw::c_double` | 228 | | `T*` | `*mut T` | 229 | | `T**` | `*mut *mut T` (and so on) | 230 | 231 | ### Parser 232 | 233 | The `.d` files are parsed by `sonde`. For the moment, only the 234 | `provider` blocks are parsed, which declare the `probe`s. All the 235 | pragma (`#pragma`) directives are ignored for the moment. 236 | 237 | ## License 238 | 239 | `BSD-3-Clause`, see `LICENSE.md`. 240 | 241 | 242 | [usdt]: https://illumos.org/books/dtrace/chp-usdt.html 243 | [DTrace]: https://en.wikipedia.org/wiki/DTrace 244 | [OpenDtrace]: https://github.com/opendtrace/opendtrace 245 | [eBPF]: http://www.brendangregg.com/blog/2019-01-01/learn-ebpf-tracing.html 246 | [data-types]: https://illumos.org/books/dtrace/chp-typeopexpr.html#chp-typeopexpr-2 247 | [`std::os::raw`]: https://doc.rust-lang.org/std/os/raw/index.html 248 | [scripting]: https://illumos.org/books/dtrace/chp-script.html#chp-script 249 | -------------------------------------------------------------------------------- /src/d/parser.rs: -------------------------------------------------------------------------------- 1 | //! [The original grammar can be found 2 | //! here](https://github.com/opendtrace/opendtrace/blob/master/lib/libdtrace/common/dt_grammar.y). This 3 | //! parser re-implements the `provider_definition` rule (with its 4 | //! children, `provider_probe_list`, `provider_probe`, `function` 5 | //! etc.). 6 | 7 | use super::ast::*; 8 | use nom::{ 9 | bytes::complete::{tag, take_until, take_while}, 10 | character::{complete::char, is_alphanumeric}, 11 | combinator::map, 12 | error::{ParseError, VerboseError}, 13 | multi::{many0, separated_list0}, 14 | sequence::{delimited, preceded, terminated, tuple}, 15 | IResult, 16 | }; 17 | 18 | // Canonicalization of a `$parser`, i.e. remove the whitespace before it. 19 | macro_rules! canon { 20 | ($parser:expr) => { 21 | preceded(ws, $parser) 22 | }; 23 | } 24 | 25 | /// Parse whitespaces. 26 | fn ws<'i, E: ParseError<&'i str>>(input: &'i str) -> IResult<&'i str, &'i str, E> { 27 | let chars = "\t\r\n "; 28 | 29 | take_while(move |c| chars.contains(c))(input) 30 | } 31 | 32 | /// Parse a name. 33 | fn name<'i, E: ParseError<&'i str>>(input: &'i str) -> IResult<&'i str, &'i str, E> { 34 | take_while(|c| is_alphanumeric(c as u8) || c == '-' || c == '_')(input) 35 | } 36 | 37 | /// Parse a type. That's super generic. It doesn't validate anything specifically. 38 | /// 39 | /// Note: This is incomplete for the moment. See the 40 | /// `parameter_type_list` from the official grammar (see module's 41 | /// documentation). 42 | fn ty<'i, E: ParseError<&'i str>>(input: &'i str) -> IResult<&'i str, &'i str, E> { 43 | let chars = ",)"; 44 | 45 | take_while(move |c| !chars.contains(c))(input) 46 | } 47 | 48 | /// Parse a `probe`. 49 | fn probe<'i, E: ParseError<&'i str>>(input: &'i str) -> IResult<&'i str, Probe, E> { 50 | map( 51 | tuple(( 52 | preceded(tag("probe"), canon!(name)), 53 | delimited( 54 | canon!(char('(')), 55 | separated_list0(char(','), canon!(ty)), 56 | canon!(terminated(char(')'), canon!(char(';')))), 57 | ), 58 | )), 59 | |(name, arguments)| Probe { 60 | name: name.into(), 61 | arguments: arguments 62 | .iter() 63 | .filter_map(|argument| { 64 | let argument = argument.trim(); 65 | 66 | if argument.is_empty() { 67 | None 68 | } else { 69 | Some(argument.to_string()) 70 | } 71 | }) 72 | .collect(), 73 | }, 74 | )(input) 75 | } 76 | 77 | /// Parse a `provider`. 78 | fn provider<'i, E: ParseError<&'i str>>(input: &'i str) -> IResult<&'i str, Provider, E> { 79 | map( 80 | tuple(( 81 | preceded(tag("provider"), canon!(name)), 82 | delimited( 83 | canon!(char('{')), 84 | many0(canon!(probe)), 85 | canon!(terminated(char('}'), canon!(char(';')))), 86 | ), 87 | )), 88 | |(name, probes)| Provider { 89 | name: name.into(), 90 | probes, 91 | }, 92 | )(input) 93 | } 94 | 95 | /// Parse a script. It collects only the `provider` blocks, nothing else. 96 | fn script<'i, E: ParseError<&'i str>>(mut input: &'i str) -> IResult<&'i str, Script, E> { 97 | let mut script = Script { providers: vec![] }; 98 | 99 | loop { 100 | match take_until::<_, _, E>("provider")(input) { 101 | Ok((input_next, _)) => { 102 | let (input_next, output) = provider(input_next)?; 103 | 104 | script.providers.push(output); 105 | 106 | input = input_next; 107 | } 108 | 109 | _ => return Ok(("", script)), 110 | } 111 | } 112 | } 113 | 114 | /// Parse a `.d` file and return a [`Script`] value. 115 | pub fn parse<'i>(input: &'i str) -> Result { 116 | match script::>(input) { 117 | Ok((_, output)) => Ok(output), 118 | Err(e) => Err(e.to_string()), 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | 126 | #[test] 127 | fn test_name() { 128 | assert_eq!(name::<()>("foobar"), Ok(("", "foobar"))); 129 | } 130 | 131 | #[test] 132 | fn test_type() { 133 | assert_eq!(ty::<()>("char"), Ok(("", "char"))); 134 | assert_eq!(ty::<()>("short"), Ok(("", "short"))); 135 | assert_eq!(ty::<()>("int"), Ok(("", "int"))); 136 | assert_eq!(ty::<()>("long"), Ok(("", "long"))); 137 | assert_eq!(ty::<()>("long long"), Ok(("", "long long"))); 138 | assert_eq!(ty::<()>("uint32_t"), Ok(("", "uint32_t"))); 139 | assert_eq!(ty::<()>("char *"), Ok(("", "char *"))); 140 | assert_eq!(ty::<()>("foo bar *,"), Ok((",", "foo bar *"))); 141 | assert_eq!(ty::<()>("foo bar *)"), Ok((")", "foo bar *"))); 142 | } 143 | 144 | #[test] 145 | fn test_probe_with_zero_argument() { 146 | assert_eq!( 147 | probe::<()>("probe abc();"), 148 | Ok(( 149 | "", 150 | Probe { 151 | name: "abc".to_string(), 152 | arguments: vec![], 153 | } 154 | )) 155 | ); 156 | } 157 | 158 | #[test] 159 | fn test_probe_with_one_argument() { 160 | assert_eq!( 161 | probe::<()>("probe abc ( char * ) ;"), 162 | Ok(( 163 | "", 164 | Probe { 165 | name: "abc".to_string(), 166 | arguments: vec!["char *".to_string()], 167 | } 168 | )) 169 | ); 170 | } 171 | 172 | #[test] 173 | fn test_probe_with_many_argument() { 174 | assert_eq!( 175 | probe::<()>("probe abc ( char *, uint8_t ) ;"), 176 | Ok(( 177 | "", 178 | Probe { 179 | name: "abc".to_string(), 180 | arguments: vec!["char *".to_string(), "uint8_t".to_string()], 181 | } 182 | )) 183 | ); 184 | } 185 | 186 | #[test] 187 | fn test_empty_provider() { 188 | assert_eq!( 189 | provider::<()>("provider foobar { } ;"), 190 | Ok(( 191 | "", 192 | Provider { 193 | name: "foobar".to_string(), 194 | probes: vec![] 195 | } 196 | )) 197 | ); 198 | } 199 | 200 | #[test] 201 | fn test_provider() { 202 | assert_eq!( 203 | provider::<()>( 204 | "provider foobar { 205 | probe abc(char*, int); 206 | probe def(string); 207 | };" 208 | ), 209 | Ok(( 210 | "", 211 | Provider { 212 | name: "foobar".to_string(), 213 | probes: vec![ 214 | Probe { 215 | name: "abc".to_string(), 216 | arguments: vec!["char*".to_string(), "int".to_string()], 217 | }, 218 | Probe { 219 | name: "def".to_string(), 220 | arguments: vec!["string".to_string()], 221 | } 222 | ] 223 | } 224 | )) 225 | ); 226 | } 227 | 228 | #[test] 229 | fn test_script() { 230 | assert_eq!( 231 | script::<()>( 232 | "provider foobar { 233 | probe abc(char*, int); 234 | probe def(string); 235 | }; 236 | 237 | provider hopla { 238 | probe xyz(); 239 | };" 240 | ), 241 | Ok(( 242 | "", 243 | Script { 244 | providers: vec![ 245 | Provider { 246 | name: "foobar".to_string(), 247 | probes: vec![ 248 | Probe { 249 | name: "abc".to_string(), 250 | arguments: vec!["char*".to_string(), "int".to_string()], 251 | }, 252 | Probe { 253 | name: "def".to_string(), 254 | arguments: vec!["string".to_string()], 255 | } 256 | ] 257 | }, 258 | Provider { 259 | name: "hopla".to_string(), 260 | probes: vec![Probe { 261 | name: "xyz".to_string(), 262 | arguments: vec![] 263 | }], 264 | }, 265 | ] 266 | } 267 | )) 268 | ); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::d::{self, ast::Names}; 2 | use std::{ 3 | env, 4 | fs::{read_to_string, File}, 5 | io::prelude::*, 6 | path::{Path, PathBuf}, 7 | process::Command, 8 | }; 9 | 10 | const SONDE_RUST_API_FILE_ENV_NAME: &str = "SONDE_RUST_API_FILE"; 11 | 12 | #[derive(Default)] 13 | pub struct Builder { 14 | d_files: Vec, 15 | keep_h_file: bool, 16 | keep_c_file: bool, 17 | } 18 | 19 | impl Builder { 20 | pub fn new() -> Self { 21 | Self { 22 | ..Default::default() 23 | } 24 | } 25 | 26 | pub fn file

(&mut self, path: P) -> &mut Self 27 | where 28 | P: AsRef, 29 | { 30 | self.d_files.push(path.as_ref().to_path_buf()); 31 | 32 | self 33 | } 34 | 35 | pub fn files

(&mut self, paths: P) -> &mut Self 36 | where 37 | P: IntoIterator, 38 | P::Item: AsRef, 39 | { 40 | for path in paths.into_iter() { 41 | self.file(path); 42 | } 43 | 44 | self 45 | } 46 | 47 | pub fn keep_h_file(&mut self, keep: bool) -> &mut Self { 48 | self.keep_h_file = keep; 49 | 50 | self 51 | } 52 | 53 | pub fn keep_c_file(&mut self, keep: bool) -> &mut Self { 54 | self.keep_c_file = keep; 55 | 56 | self 57 | } 58 | 59 | pub fn compile(&self) { 60 | let out_dir = env::var("OUT_DIR") 61 | .map_err(|_| "The Cargo `OUT_DIR` variable is missing") 62 | .unwrap(); 63 | let mut contents = String::new(); 64 | let mut providers = Vec::with_capacity(self.d_files.len()); 65 | 66 | // Tell Cargo to rerun the build script if one of the `.d` files has changed. 67 | { 68 | for d_file in &self.d_files { 69 | println!( 70 | "cargo:rerun-if-changed={file}", 71 | file = d_file.as_path().display() 72 | ); 73 | } 74 | } 75 | 76 | // Collect all contents of the `.d` files, and parse the declared providers. 77 | { 78 | for d_file in &self.d_files { 79 | let content = read_to_string(d_file).unwrap(); 80 | contents.push_str(&content); 81 | 82 | let script = d::parser::parse(&content).unwrap(); 83 | 84 | for provider in script.providers { 85 | providers.push(provider); 86 | } 87 | } 88 | } 89 | 90 | // Let's get a unique `.h` file from the `.d` files. 91 | let h_file = tempfile::Builder::new() 92 | .prefix("sonde-") 93 | .suffix(".h") 94 | .tempfile_in(&out_dir) 95 | .unwrap(); 96 | 97 | let h_file_name = h_file.path(); 98 | 99 | { 100 | let mut d_file = tempfile::Builder::new() 101 | .prefix("sonde-") 102 | .suffix(".d") 103 | .tempfile_in(&out_dir) 104 | .unwrap(); 105 | d_file.write_all(contents.as_bytes()).unwrap(); 106 | 107 | Command::new("dtrace") 108 | .arg("-arch") 109 | .arg(match env::var("CARGO_CFG_TARGET_ARCH").unwrap().as_str() { 110 | "aarch64" => "arm64", 111 | arch => arch, 112 | }) 113 | .arg("-o") 114 | .arg(h_file_name.as_os_str()) 115 | .arg("-h") 116 | .arg("-s") 117 | .arg(&d_file.path().as_os_str()) 118 | .status() 119 | .unwrap(); 120 | } 121 | 122 | // Generate the FFI `.c` file. The probes are defined behind C 123 | // macros; they can't be call from Rust, so we need to wrap 124 | // them inside C functions. 125 | let mut ffi_file = tempfile::Builder::new() 126 | .prefix("sonde-ffi") 127 | .suffix(".c") 128 | .tempfile_in(&out_dir) 129 | .unwrap(); 130 | 131 | { 132 | let ffi = format!( 133 | r#"#include {header_file:?} 134 | 135 | {wrappers}"#, 136 | header_file = h_file_name, 137 | wrappers = providers 138 | .iter() 139 | .map(|provider| { 140 | provider 141 | .probes 142 | .iter() 143 | .map(|probe| { 144 | format!( 145 | r#" 146 | void {prefix}_probe_{suffix}({arguments}) {{ 147 | {macro_prefix}_{macro_suffix}({argument_names}); 148 | }} 149 | "#, 150 | prefix = provider.name_for_c(), 151 | suffix = probe.name_for_c(), 152 | macro_prefix = provider.name_for_c_macro(), 153 | macro_suffix = probe.name_for_c_macro(), 154 | arguments = probe.arguments_for_c(), 155 | argument_names = probe 156 | .arguments 157 | .iter() 158 | .enumerate() 159 | .map(|(nth, _)| { format!("arg{nth}", nth = nth) }) 160 | .collect::>() 161 | .join(", ") 162 | ) 163 | }) 164 | .collect::>() 165 | .join("") 166 | }) 167 | .collect::>() 168 | .join("\n") 169 | ); 170 | 171 | ffi_file.write_all(ffi.as_bytes()).unwrap(); 172 | } 173 | 174 | // Let's compile the FFI `.c` file to a `.a` file. 175 | { 176 | cc::Build::new().file(&ffi_file).compile("sonde-ffi"); 177 | } 178 | 179 | // Finally, let's generate the nice API for Rust. 180 | let mut rs_path = PathBuf::new(); 181 | rs_path.push(&out_dir); 182 | rs_path.push("sonde.rs"); 183 | let mut rs_file = File::create(&rs_path).unwrap(); 184 | 185 | { 186 | let rs = format!( 187 | r#"/// Bindings from Rust to the C FFI small library that calls the 188 | /// probes. 189 | 190 | #[allow(unused)] 191 | use std::os::raw::*; 192 | 193 | extern "C" {{ 194 | {externs} 195 | }} 196 | 197 | {wrappers} 198 | "#, 199 | externs = providers 200 | .iter() 201 | .map(|provider| { 202 | provider 203 | .probes 204 | .iter() 205 | .map(|probe| { 206 | format!( 207 | r#" #[doc(hidden)] 208 | fn {ffi_prefix}_probe_{ffi_suffix}({arguments});"#, 209 | ffi_prefix = provider.name_for_c(), 210 | ffi_suffix = probe.name_for_c(), 211 | arguments = probe.arguments_for_c_from_rust(), 212 | ) 213 | }) 214 | .collect::>() 215 | .join("\n\n") 216 | }) 217 | .collect::>() 218 | .join("\n\n"), 219 | wrappers = providers 220 | .iter() 221 | .map(|provider| { 222 | format!( 223 | r#"/// Probes for the `{provider_name}` provider. 224 | pub mod r#{provider_name} {{ 225 | #[allow(unused)] 226 | use std::os::raw::*; 227 | 228 | {probes} 229 | }}"#, 230 | provider_name = provider.name_for_rust(), 231 | probes = provider 232 | .probes 233 | .iter() 234 | .map(|probe| { 235 | format!( 236 | r#" /// Call the `{probe_name}` probe of the `{provider_name}` provider. 237 | pub fn r#{probe_name}({arguments}) {{ 238 | unsafe {{ super::{ffi_prefix}_probe_{ffi_suffix}({argument_names}) }}; 239 | }}"#, 240 | provider_name = provider.name_for_rust(), 241 | probe_name = probe.name_for_rust(), 242 | ffi_prefix = provider.name_for_c(), 243 | ffi_suffix = probe.name_for_c(), 244 | arguments = probe.arguments_for_c_from_rust(), 245 | argument_names = probe 246 | .arguments 247 | .iter() 248 | .enumerate() 249 | .map(|(nth, _)| { format!("arg{nth}", nth = nth) }) 250 | .collect::>() 251 | .join(", ") 252 | ) 253 | }) 254 | .collect::>() 255 | .join("\n\n") 256 | ) 257 | }) 258 | .collect::>() 259 | .join("\n\n") 260 | ); 261 | 262 | println!( 263 | "cargo:rustc-env={name}={value}", 264 | name = SONDE_RUST_API_FILE_ENV_NAME, 265 | value = rs_path.as_path().display(), 266 | ); 267 | 268 | rs_file.write_all(rs.as_bytes()).unwrap(); 269 | } 270 | 271 | if self.keep_h_file { 272 | h_file.keep().unwrap(); 273 | } 274 | 275 | if self.keep_c_file { 276 | ffi_file.keep().unwrap(); 277 | } 278 | } 279 | } 280 | --------------------------------------------------------------------------------