Skip to content

Commit 7ebb78a

Browse files
authored
Add a style guideline for conditional compilation (bytecodealliance#10144)
* Add a style guideline for conditional compilation I've been doing a fair amount of work recently that's touched on `#[cfg]` in many ways throughout Wasmtime. I've personally got opinions about how to structure everything as well, and these opinions are not always obvious to others or discerned from just reading snippets. To assist with this I figured it would be nice to have a style guideline for Wasmtime's conditionally compiled code explaining at least at a high level what's going on and some rough basic principles. I've attempted to give this a stab and have added a page to the contributing documentation about the style guidelines for conditional compilation. I'm sure I've forgotten something here but my hope is that we can evolve this over time. * Fix typo * Fix code examples
1 parent 10eda1c commit 7ebb78a

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,6 @@
6565
- [Maintainer Guidelines](./contributing-maintainer-guidelines.md)
6666
- [Code Review](./contributing-code-review.md)
6767
- [Release Process](./contributing-release-process.md)
68+
- [Conditional Compilation](./contributing-conditional-compilation.md)
6869
- [Governance](./contributing-governance.md)
6970
- [Code of Conduct](./contributing-coc.md)
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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

Comments
 (0)