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