xref: /aosp_15_r20/external/crosvm/docs/book/src/contributing/style_guide_platform_specific_code.md (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1# Style guide for platform specific code
2
3## Code organization
4
5The crosvm code can heavily interleave platform specific code into platform agnostic code using
6`#[cfg(target_os = "")]`. This is difficult to maintain as
7
8- It reduces readability.
9- Difficult to write/maintain unit tests.
10- Difficult to maintain downstream, proprietary code
11
12To address the above mentioned issue, the style guide provides a way to standardize platform
13specific code layout.
14
15Consider a following example where we have platform independent code, `PrintInner`, which is used by
16platform specific code, `WinPrinter` and `UnixPrinter` to tweak the behavior according to the
17underlying platform. The users of this module, `sys`, get to use an aliased struct called `Printer`
18which exports similar interfaces on both the platforms.
19
20In this scheme `print.rs` contains platform agnostic logic, structures and traits. Different
21platforms, in `linux.rs` and `windows.rs`, implement traits defined in `print.rs`. Finally `sys.rs`
22exports interfaces implemented by platform specific code.
23
24In a more complex library, we may need another layer, `print.rs`, that uses traits and structures
25exported by platform specific code, `linux/print.rs` and `windows/print.rs`, and adds some more
26common logic to it. Following example illustrates the scheme discussed above. Here,
27`Printer.print()` is supposed to print a value of `u32` and print the target os name.
28
29The files that contain platform specific code **only** should live in a directory named `sys/` and
30those files should be conditionally imported in `sys.rs` file. In such a setup, the directory
31structure would look like,
32
33```bash
34$  tree
35.
36├── print.rs
37├── sys
38│   ├── linux
39│   │   └── print.rs
40│   ├── linux.rs
41│   ├── windows
42│   │   └── print.rs
43│   └── windows.rs
44└── sys.rs
45```
46
47File: `print.rs`
48
49```rust
50pub struct PrintInner {
51    pub value: u32,
52}
53
54impl PrintInner {
55    pub fn new(value: u32) -> Self {
56        Self { value }
57    }
58
59    pub fn print(&self) {
60        print!("My value:{} ", self.value);
61    }
62}
63
64// This is useful if you want to
65// * Enforce interface consistency or
66// * Have more than one compiled-in struct to provide the same api.
67//   Say a generic gpu driver and high performance proprietary driver
68//   to coexist in the same namespace.
69pub trait Print {
70    fn print(&self);
71}
72```
73
74File: `sys/windows/print.rs`
75
76```rust
77use crate::print::{Print, PrintInner};
78
79pub struct WinPrinter {
80    inner: PrintInner,
81}
82
83impl WinPrinter {
84    pub fn new(value: u32) -> Self {
85        Self {
86            inner: PrintInner::new(value),
87        }
88    }
89}
90
91impl Print for WinPrinter {
92    fn print(&self) {
93        self.inner.print();
94        println!("from win");
95    }
96}
97```
98
99File: `sys/linux/print.rs`
100
101```rust
102use crate::print::{Print, PrintInner};
103
104pub struct LinuxPrinter {
105    inner: PrintInner,
106}
107
108impl LinuxPrinter {
109    pub fn new(value: u32) -> Self {
110        Self {
111            inner: PrintInner::new(value),
112        }
113    }
114}
115
116impl Print for LinuxPrinter {
117    fn print(&self) {
118        self.inner.print();
119        println!("from linux");
120    }
121}
122```
123
124File: `sys.rs`
125
126```rust
127#[cfg(any(target_os = "android", target_os = "linux"))]
128mod linux;
129
130#[cfg(windows)]
131mod windows;
132
133mod platform {
134    #[cfg(any(target_os = "android", target_os = "linux"))]
135    pub use super::linux::LinuxPrinter as Printer;
136
137    #[cfg(windows)]
138    pub use super::windows::WinPrinter as Printer;
139}
140
141pub use platform::Printer;
142```
143
144## Imports
145
146When conditionally importing and using modules, use
147`cfg(any(target_os = "android", target_os = "linux"))` and `cfg(windows)` for describing the
148platform. Order imports such that common comes first followed by linux and windows dependencies.
149
150```rust
151// All other imports
152
153#[cfg(any(target_os = "android", target_os = "linux"))]
154use {
155  std::x::y,
156  base::a::b::{Foo, Bar},
157  etc::Etc,
158};
159
160#[cfg(windows)]
161use {
162  std::d::b,
163  base::f::{Foo, Bar},
164  etc::{WinEtc as Etc},
165};
166```
167
168## Structure
169
170It is OK to have a few platform specific fields inlined with cfgs. When inlining
171
172- Ensure that all the fields of a particular platform are next to each other.
173- Organize common fields first and then platform specific fields ordered by the target os name i.e.
174  "linux" first and "windows" later.
175
176If the structure has a large set of fields that are platform specific, it is more readable to split
177it into different platform specific structures and have their implementations separate. If
178necessary, consider defining a crate in platform independent and have the platform specific files
179implement parts of those traits.
180
181## Enum
182
183When enums need to have platform specific variants
184
185- Create a new platform specific enum and move all platform specific variants under the new enum
186- Introduce a new variant, which takes a platform specific enum as member, to platform independent
187  enum.
188
189### Do
190
191File: `sys/linux/base.rs`
192
193```rust
194enum MyEnumSys {
195  Unix1,
196}
197
198fn handle_my_enum_impl(e: MyEnumSys) {
199  match e {
200    Unix1 => {..},
201  };
202}
203```
204
205File: `sys/windows/base.rs`
206
207```rust
208enum MyEnumSys {
209  Windows1,
210}
211
212fn handle_my_enum_impl(e: MyEnumSys) {
213  match e {
214    Windows1 => {..},
215  };
216}
217```
218
219File: `base.rs`
220
221```rust
222use sys::MyEnumSys;
223enum MyEnum {
224  Common1,
225  Common2,
226  SysVariants(MyEnumSys),
227}
228
229fn handle_my_enum(e: MyEnum) {
230  match e {
231    Common1 => {..},
232    Common2 => {..},
233    SysVariants(v) => handle_my_enum_impl(v),
234  };
235}
236```
237
238### Don't
239
240File: `base.rs`
241
242```rust
243enum MyEnum {
244  Common1,
245  Common2,
246  #[cfg(target_os = "windows")]
247  Windows1, // We shouldn't have platform-specific variants in a platform-independent enum.
248  #[cfg(any(target_os = "android", target_os = "linux"))]
249  Unix1, // We shouldn't have platform-specific variants in a platform-independent enum.
250}
251
252fn handle_my_enum(e: MyEnum) {
253  match e {
254    Common1 => {..},
255    Common2 => {..},
256    #[cfg(target_os = "windows")]
257    Windows1 => {..}, // We shouldn't have platform-specific match arms in a platform-independent code.
258    #[cfg(any(target_os = "android", target_os = "linux"))]
259    Unix1 => {..}, // We shouldn't have platform-specific match arms in a platform-independent code.
260  };
261}
262```
263
264### Exception: dispatch enums (trait-object like enums) should NOT be split
265
266Dispatch enums (enums which are pretending to be trait objects) should NOT be split as shown above.
267This is because these enums just forward method calls verbatim and don't have any meaningful cross
268platform code. As such, there is no benefit to splitting the enum. Here is an acceptable example:
269
270```rust
271enum MyDispatcher {
272  #[cfg(windows)]
273  WinType(ImplForWindows),
274  #[cfg(unix)]
275  UnixType(ImplForUnix),
276}
277
278impl MyDispatcher {
279  fn foo(&self) {
280    match self {
281        #[cfg(windows)]
282        MyDispatcher::WinType(t) => t.foo(),
283        #[cfg(unix)]
284        MyDispatcher::UnixType(t) => t.foo(),
285    }
286  }
287}
288```
289
290## Errors
291
292Inlining all platform specific error values is ok. This is an exception to the [enum](#enum) to keep
293error handling simple. Organize platform independent errors first and then platform specific errors
294ordered by the target os name i.e. "linux" first and "windows" later.
295
296## Code blocks and functions
297
298If a code block or a function has little platform independent code and the bulk of the code is
299platform specific then carve out platform specific code into a function. If the carved out function
300does most of what the original function was doing and there is no better name for the new function
301then the new function can be named by appending `_impl` to the functions name.
302
303### Do
304
305File: `base.rs`
306
307```rust
308fn my_func() {
309  print!("Hello ");
310  my_func_impl();
311}
312```
313
314File: `sys/linux/base.rs`
315
316```rust
317fn my_func_impl() {
318  println!("linux");
319}
320```
321
322File: `sys/windows/base.rs`
323
324```rust
325fn my_func_impl() {
326  println!("windows");
327}
328```
329
330### Don't
331
332File: `base.rs`
333
334```rust
335fn my_func() {
336  print!("Hello ");
337
338  #[cfg(any(target_os = "android", target_os = "linux"))] {
339    println!("linux"); // We shouldn't have platform-specific code in a platform-independent code block.
340  }
341
342  #[cfg(target_os = "windows")] {
343    println!("windows"); // We shouldn't have platform-specific code in a platform-independent code block.
344  }
345}
346```
347
348## match
349
350With an exception to matching enums, see [enum](#enum), matching for platform specific values can be
351done in the wildcard patter(`_`) arm of the match statement.
352
353### Do
354
355File: `parse.rs`
356
357```rust
358fn parse_args(arg: &str) -> Result<()>{
359  match arg {
360    "path" => {
361      <multiple lines of logic>;
362      Ok(())
363    },
364    _ => parse_args_impl(arg),
365  }
366}
367```
368
369File: `sys/linux/parse.rs`
370
371```rust
372fn parse_args_impl(arg: &str) -> Result<()>{
373  match arg {
374    "fd" => {
375      <multiple lines of logic>;
376      Ok(())
377    },
378    _ => Err(ParseError),
379  }
380}
381```
382
383File: `sys/windows/parse.rs`
384
385```rust
386fn parse_args_impl(arg: &str) -> Result<()>{
387  match arg {
388    "handle" => {
389      <multiple lines of logic>;
390      Ok(())
391    },
392    _ => Err(ParseError),
393  }
394}
395```
396
397### Don't
398
399File: `parse.rs`
400
401```rust
402fn parse_args(arg: &str) -> Result<()>{
403  match arg {
404    "path" => Ok(()),
405    #[cfg(any(target_os = "android", target_os = "linux"))]
406    "fd" => { // We shouldn't have platform-specific match arms in a platform-independent code.
407      <multiple lines of logic>;
408      Ok(())
409    },
410    #[cfg(target_os = "windows")]
411    "handle" => { // We shouldn't have platform-specific match arms in a platform-independent code.
412      <multiple lines of logic>;
413      Ok(())
414    },
415    _ => Err(ParseError),
416  }
417}
418```
419
420## Platform specific symbols
421
422If a platform exports symbols that are specific to the platform only and are not exported by all
423other platforms then those symbols should be made public through a namespace that reflects the name
424of the platform.
425
426File: `sys.rs`
427
428```rust
429cfg_if::cfg_if! {
430    if #[cfg(any(target_os = "android", target_os = "linux"))] {
431        pub mod linux;
432        use linux as platform;
433    } else if #[cfg(windows)] {
434        pub mod windows;
435        use windows as platform;
436    }
437}
438
439pub use platform::print;
440```
441
442File: `linux.rs`
443
444```rust
445fn print() {
446  println!("Hello linux");
447}
448
449fn print_u8(val: u8) {
450  println!("Unix u8:{}", val);
451
452}
453```
454
455File: `windows.rs`
456
457```rust
458fn print() {
459  println!("Hello windows");
460}
461
462fn print_u16(val: u16) {
463  println!("Windows u16:{}", val);
464
465}
466```
467
468The user of the library, say mylib, now has to do something like below which makes it explicit that
469the functions `print_u8` and `print_u16` are platform specific.
470
471```rust
472use mylib::sys::print;
473
474fn my_print() {
475  print();
476
477  #[cfg(any(target_os = "android", target_os = "linux"))]
478  mylib::sys::linux::print_u8(1);
479
480  #[cfg(windows)]
481  mylib::sys::windows::print_u16(1);
482}
483
484```
485