|
| 1 | +# Conditional Compilation in Wasmtime |
| 2 | + |
| 3 | +The `wasmtime` crate and workspace is both quite configurable in terms of |
| 4 | +runtime configuration (e.g. `Config::*`) and compile-time configuration (Cargo |
| 5 | +features). Wasmtime also wants to take advantage of native hardware features on |
| 6 | +specific CPUs and operating systems to implement optimizations for executing |
| 7 | +WebAssembly. This overall leads to the state where the source code for Wasmtime |
| 8 | +has quite a lot of `#[cfg]` directives and is trying to wrangle the |
| 9 | +combinatorial explosion of: |
| 10 | + |
| 11 | +1. All possible CPU architectures that Wasmtime (or Rust) supports. |
| 12 | +2. All possible operating systems that Wasmtime (or Rust) supports. |
| 13 | +3. All possible feature combinations of the `wasmtime` crate. |
| 14 | + |
| 15 | +Like any open source project one of the goals of Wasmtime is to have readable |
| 16 | +and understandable code and to that effect we ideally don't have `#[cfg]` |
| 17 | +everywhere throughout the codebase in confusing combinations. The goal of this |
| 18 | +document is to explain the various guidelines we have for conditional |
| 19 | +compilation in Rust and some recommended styles for working with `#[cfg]` in a |
| 20 | +maintainable and scalable manner. |
| 21 | + |
| 22 | +## Rust's `#[cfg]` attribute |
| 23 | + |
| 24 | +If you haven't worked with Rust much before or if you'd like a refresher, Rust's |
| 25 | +main ability to handle conditional compilation is the `#[cfg]` attribute. This |
| 26 | +is semantically and structurally different from `#ifdef` in C/C++ and gives rise |
| 27 | +to alternative patterns which look quite different as well. |
| 28 | + |
| 29 | +One of the more common conditional compilation attributes in Rust is |
| 30 | +`#[cfg(test)]` which enables including a module or a piece of code only when |
| 31 | +compiled with `cargo test` (or `rustc`'s `--test` flag). There are many other |
| 32 | +directives you can put in `#[cfg]`, however, for example: |
| 33 | + |
| 34 | +* `#[cfg(target_arch = "...")]` - this can be used to detect the architecture |
| 35 | + that the code was compiled for. |
| 36 | +* `#[cfg(target_os = "...")]` - this can be used to detect the operating system |
| 37 | + that the code was compiled for. |
| 38 | +* `#[cfg(feature = "...")]` - these correspond to [Cargo features][cargo] and |
| 39 | + are enabled when depending on crates in `Cargo.toml`. |
| 40 | +* `#[cfg(has_foo)]` - completely custom directives can be emitted by build |
| 41 | + scripts such as `crates/wasmtime/build.rs`. |
| 42 | + |
| 43 | +[cargo]: https://doc.rust-lang.org/cargo/reference/features.html |
| 44 | + |
| 45 | +To explore built-in `#[cfg]` directives you can use `rustc --print cfg` for your |
| 46 | +host target. This also supports `rustc --print cfg --target ...`. |
| 47 | + |
| 48 | +Finally, `#[cfg]` directive support internal "functions" such as `all(...)`, |
| 49 | +`any(...)`, and `not(..)`. |
| 50 | + |
| 51 | +Attributes in Rust can be applied to syntactic items in Rust, not fragments of |
| 52 | +lexical tokens like C/C++. This means that conditional compilation happens at |
| 53 | +the AST level rather than the lexical level. For example: |
| 54 | + |
| 55 | +```rust,ignore |
| 56 | +#[cfg(foo)] |
| 57 | +fn the_function() { /* ... */ } |
| 58 | +
|
| 59 | +#[cfg(not(foo))] |
| 60 | +fn the_function() { /* ... */ } |
| 61 | +``` |
| 62 | + |
| 63 | +This can additionally be applied to entire expressions in Rust too: |
| 64 | + |
| 65 | +```rust,ignore |
| 66 | +fn the_function() { |
| 67 | + #[cfg(foo)] |
| 68 | + { |
| 69 | + // ... |
| 70 | + } |
| 71 | + #[cfg(not(foo))] |
| 72 | + { |
| 73 | + // ... |
| 74 | + } |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +The Rust compiler doesn't type-check or analyze anything in |
| 79 | +conditionally-omitted code. It is only required to be syntactically valid. |
| 80 | + |
| 81 | +## Hazards with `#[cfg]` |
| 82 | + |
| 83 | +Conditional compilation in any language can get hairy quickly and Rust is no |
| 84 | +exception. The venerable "`#ifdef` soup" one might have seen in C/C++ is very |
| 85 | +much possible to have in Rust too in the sense that it won't look the same but |
| 86 | +it'll still taste just as bad. In that sense it's worth going over some of the |
| 87 | +downsides of `#[cfg]` in Rust and some hazards to watch out for. |
| 88 | + |
| 89 | +**Unused Imports** |
| 90 | + |
| 91 | +Conditional compilation can be great for quickly excluding an entire function in |
| 92 | +one line, but this might have ramifications if that function was the only use of |
| 93 | +an imported type for example: |
| 94 | + |
| 95 | +```rust,ignore |
| 96 | +use std::ptr::NonNull; //~ WARNING: unused import when `foo` is turned off |
| 97 | +
|
| 98 | +#[cfg(foo)] |
| 99 | +fn my_function() -> NonNull<u8> { |
| 100 | + // ... |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +**Repetitive Attributes** |
| 105 | + |
| 106 | +Enabling a Cargo feature can add features to existing types which means it can |
| 107 | +lead to repetitive `#[cfg]` annotations such as: |
| 108 | + |
| 109 | +```rust,ignore |
| 110 | +#[cfg(feature = "async")] |
| 111 | +use std::future::Future; |
| 112 | +
|
| 113 | +impl<T> Store<T> { |
| 114 | + #[cfg(feature = "async")] |
| 115 | + async fn some_new_async_api(&mut self) { |
| 116 | + // ... |
| 117 | + } |
| 118 | +
|
| 119 | + #[cfg(feature = "async")] |
| 120 | + async fn some_other_new_async_api(&mut self) { |
| 121 | + // ... |
| 122 | + } |
| 123 | +} |
| 124 | +
|
| 125 | +#[cfg(feature = "async")] |
| 126 | +struct SomeAsyncHelperType { |
| 127 | + // ... |
| 128 | +} |
| 129 | +
|
| 130 | +#[cfg(feature = "async")] |
| 131 | +impl SomeAsyncHelperType { |
| 132 | + // ... |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +**Boilerplate throughout an implementation** |
| 137 | + |
| 138 | +In addition to being repetitive when defining conditionally compiled code |
| 139 | +there's also a risk of being quite repetitive when using conditionally compiled |
| 140 | +code as well. In its most basic form any usage of a conditionally compiled piece |
| 141 | +of code must additionally be gated as well. |
| 142 | + |
| 143 | +```rust,ignore |
| 144 | +#[cfg(feature = "gc")] |
| 145 | +fn gc() { |
| 146 | + // ... |
| 147 | +} |
| 148 | +
|
| 149 | +fn call_wasm() { |
| 150 | + #[cfg(feature = "gc")] |
| 151 | + gc(); |
| 152 | +
|
| 153 | + // do the call ... |
| 154 | +
|
| 155 | + #[cfg(feature = "gc")] |
| 156 | + gc(); |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +**Interactions with ecosystem tooling** |
| 161 | + |
| 162 | +Conditionally compiled code does not always interact well with ecosystem tooling |
| 163 | +in Rust. An example of this is the `cfg_if!` macro where `rustfmt` is unable to |
| 164 | +format the contents of the macro. If there are conditionally defined modules in |
| 165 | +the macro then it means `rustfmt` won't format any modules internally in the |
| 166 | +macro either. Not a great experience! |
| 167 | + |
| 168 | +Here neither `gc.rs` nor `gc_disabled.rs` will be formatted by `cargo fmt`. |
| 169 | + |
| 170 | +```rust,ignore |
| 171 | +cfg_if::cfg_if! { |
| 172 | + if #[cfg(feature = "gc")] { |
| 173 | + mod gc; |
| 174 | + use gc::*; |
| 175 | + } else { |
| 176 | + mod gc_disabled; |
| 177 | + use gc_disabled::*; |
| 178 | + } |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +**Combinatorial explosion in testing complexity** |
| 183 | + |
| 184 | +Each crate feature can be turned on and off. Wasmtime supports a range of |
| 185 | +platforms and architectures. It's practically infeasible to test every single |
| 186 | +possible combination of these. This means that inevitably there are going to be |
| 187 | +untested configurations as well as bugs within these configurations. |
| 188 | + |
| 189 | +## Conditional Compilation Style Guide |
| 190 | + |
| 191 | +With some of the basics out of the way, this is intended to document the rough |
| 192 | +current state of Wasmtime and some various principles for writing conditionally |
| 193 | +compiled code. Much of these are meant to address some of the hazards above. |
| 194 | +These guidelines are not always religiously followed throughout Wasmtime's |
| 195 | +repository but PRs to improve things are always welcome! |
| 196 | + |
| 197 | +The main takeaway is that the main goal is **to minimize the number of `#[cfg]` |
| 198 | +attributes necessary in the repository**. Conditional compilation is required no |
| 199 | +matter what so this number can never be zero, but that doesn't mean every other |
| 200 | +line should have `#[cfg]` on it. Otherwise these guidelines need to be applied |
| 201 | +with some understanding that each one is fallible. There's no always-right |
| 202 | +answer unfortunately and style will still differ from person to person. |
| 203 | + |
| 204 | +1. **Separate files** - try to put conditionally compiled code into separate |
| 205 | + files. By placing `#[cfg]` at the module level you can drastically cut down |
| 206 | + on annotations by removing the entire module at once. An example of this is |
| 207 | + that Wasmtime's internal `runtime` module is [feature gated][file-gate] at |
| 208 | + the top-level. |
| 209 | + |
| 210 | +2. **Only `#[cfg]` definitions, not uses** - try to only use `#[cfg]` when a |
| 211 | + type or function is defined, not when it's used. Functions and types can be |
| 212 | + used all over the place and putting a `#[cfg]` everywhere can be quite |
| 213 | + annoying an brittle to maintain. |
| 214 | + |
| 215 | + * This isn't a problem if a use-site is already contained in a |
| 216 | + `#[cfg]` item, such as a module. This can be assisted by lifting `#[cfg]` |
| 217 | + up "as high as possible". An example of this is Wasmtime's `component` |
| 218 | + module which uses `#[cfg(feature = "component-model")]` at the root of all |
| 219 | + component-related functionality. That means that conditionally included |
| 220 | + dependencies used within `component::*` don't need extra `#[cfg]` annotations. |
| 221 | + |
| 222 | + * Another common pattern for this is to conditionally define a "dummy" shim |
| 223 | + interface. The real implementation would live in `foo.rs` while the dummy |
| 224 | + implementation would live in `foo_disabled.rs`. That means that "foo" is |
| 225 | + always available but the dummy implementation doesn't do anything. This |
| 226 | + makes heavy use of zero-sized-types (e.g. `struct Foo;`) and uninhabited |
| 227 | + types (e.g. `enum Foo {}`) to ensure there is no runtime overhead. An |
| 228 | + example of this is [`shared_memory.rs`][dummy-enabled] and |
| 229 | + [`shared_memory_disabled.rs`][dummy-disabled] where the disabled version |
| 230 | + returns an error on construction and otherwise has trivial implementations |
| 231 | + of each method. |
| 232 | + |
| 233 | +3. **Off-by-default code should be trivial** - described above it's not possible |
| 234 | + to test Wasmtime in every possible configuration of `#[cfg]`, so to help |
| 235 | + reduce the risk of lurking bugs try to ensure that all off-by-default code is |
| 236 | + trivially correct-by-construction. For "dummy" shims described above this |
| 237 | + means that methods do nothing or return an error. If off-by-default code is |
| 238 | + nontrivial then it should have a dedicated CI job to ensure that all |
| 239 | + conditionally compiled parts are tested one way or another. |
| 240 | + |
| 241 | +4. **Absolute paths are useful, but noisy** - described above it's easy to get |
| 242 | + into a situation where a conditionally compiled piece of code is the only |
| 243 | + users of a `use` statement. One easy fix is to remove the `use` and use the |
| 244 | + fully qualified path (e.g. `param: core::ptr::NonNull`) in the function |
| 245 | + instead. This reduces the `#[cfg]` to one, just the function in question, as |
| 246 | + opposed to one on the function and one on the `use`. Beware though that this |
| 247 | + can make function signatures very long very quickly, so if that ends up |
| 248 | + happening one of the above points may help instead. |
| 249 | + |
| 250 | +5. **Use `#[cfg]` for anything that requires a new runtime dependency** - one of |
| 251 | + the primary use cases for `#[cfg]` in Wasmtime is to conditionally remove |
| 252 | + dependencies at runtime on pieces of functionality. For example if the |
| 253 | + `async` feature is disabled then stack switching is not necessary to |
| 254 | + implement. This is a lynchpin of Wasmtime's portability story where we don't |
| 255 | + guarantee all features compile on all platforms, but the "major" features |
| 256 | + should compile on all platforms. An example of this is that `threads` |
| 257 | + requires the standard library, but `runtime` does not. |
| 258 | + |
| 259 | +6. **Don't use `#[cfg]` for compiler features** - in contrast to the previous |
| 260 | + point it's generally not necessary to plumb `#[cfg]` features to Wasmtime's |
| 261 | + integration with Cranelift. The runtime size or runtime features required to |
| 262 | + compile WebAssembly code is generally much larger than just running code |
| 263 | + itself. This means that conditionally compiled compiler features can just add |
| 264 | + lots of boilerplate to manage internally without much benefit. Ideally |
| 265 | + `#[cfg]` is only use for WebAssembly runtime features, not compilation of |
| 266 | + WebAssembly features. |
| 267 | + |
| 268 | +Note that it's intentional that these guidelines are not 100% comprehensive. |
| 269 | +Additionally they're not hard-and-fast rules in the sense that they're checked |
| 270 | +in CI somewhere. Instead try to follow them if you can, but if you have any |
| 271 | +questions or feel that `#[cfg]` is overwhelming feel free to reach out on Zulip |
| 272 | +or on GitHub. |
| 273 | + |
| 274 | +[file-gate]: https://github.com/bytecodealliance/wasmtime/blob/24620d9ff4cfd3a2a5f681181119eb8b0edaeab5/crates/wasmtime/src/lib.rs#L380-L383 |
| 275 | +[high-gate]: https://github.com/bytecodealliance/wasmtime/blob/24620d9ff4cfd3a2a5f681181119eb8b0edaeab5/crates/wasmtime/src/runtime.rs#L55-L56 |
| 276 | +[dummy-enabled]: https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasmtime/src/runtime/vm/memory/shared_memory.rs |
| 277 | +[dummy-disabled]: https://github.com/bytecodealliance/wasmtime/blob/24620d9ff4cfd3a2a5f681181119eb8b0edaeab5/crates/wasmtime/src/runtime/vm/memory/shared_memory_disabled.rs |
0 commit comments