Fleshing out libstd "scenarios"

So, I’ve been thinking a bit more about this proposal, and I think there is a way to extend it to give stronger, statically checkable, inter-crate guarantees that address some of the concerns I’ve seen about the proposal as-is.

I’m not sure how feasible, or even internally consistent these idea are, but here is what I got:

  • A crate can define any number of scenarios by name.
  • Scenarios can be transparent aliases for combinations of other scenarios.
  • Said combinations would be logical formulas with the operators any() and all() from #[cfg], but additionally also support either() to express exclusivity.
  • Scenarios defined in different crates that share the same name are distinct from each other.
  • Scenarios can be imported from an extern crate to use them in more than one crate.
  • Every single item in a crate gets “tagged” as belonging to one or more scenarios.
  • To make this more manageable, every crate can declare an “implicit scenario” that gets applied to all items automatically.
  • To make this backwards compatible, the implicit scenario defaults to one defined in std, which represents the currently in the community assumed “desktop” scenario of being cross platform compatible for the major platforms linux, windows and MacOS.
  • Scenario-tagged items transitively check that their definition does not access items for scenarios more constrained than them self.
  • A crate has to tag itself with all scenario combinations it supports, with a warning lint that enforces it in a backwards compatible way by checking all items in the crate. This also the serves as a mechanism for enabling scenarios, by supporting more than the default one.
  • The cargo repo would start rejecting new versions of crates that emit that warning, similar to the “*” version dependency check. This ensures gradual transition to the new system.
  • #[cfg] also gains the properties of a scenario tag, and would be checked in the same way. Enabling them per compiler flag would probably have to implicitly define their scenario, with a warning lint demanding explicitness.
  • The compiler would start remembering #[cfg]d items in some fashion, so that the scenario checker can reason about their existence for the crate-wide check.
  • Different #[cfg] flags on items with the same name would cause a “virtual” scenario tag expressing the constraint “either of these needs to be defined”.

This seems like a somewhat complex system, but by picking the right defaults it should be manageable. (And has to be ignore-able anyway, for backwards compatibility)

Examples

Std

// [crate std]

// Import scenarios.
// Attribute behaves like macro_import for selecting either all or a subset.
#[scenario_import]
extern crate core;

// Declare scenarios "atoms", that might correspond to cfg flags
#![declare_scenario(thread)]
#![declare_scenario(unix)]
#![declare_scenario(windows)]
#![declare_scenario(floats)]
// ...

// Declare scenario aliases
#![declare_scenario(desktop, all(either(unix, windows), thread, /* ... */)))]
#![declare_scenario(emscripten, all(floats, core, /* ... */))))]

// Require explicit annotations for all items in std
#![no_implicit_scenario]

// Restrict the std library to a number of scenarios.
// These check the crate assuming only one of them is active at any given time.
#![crate_scenario(desktop)]
#![crate_scenario(all(desktop, unix))]
#![crate_scenario(all(desktop, windows))]
#![crate_scenario(emscripten)]

// ...

impl File {
    #[scenario(desktop)]
    pub fn new() -> Self;

    #[scenario(all(desktop, unix))]
    pub fn as_raw_fd(&self) -> c_int {
        self.fd
    }

    #[scenario(all(desktop, windows))]
    pub fn as_raw_handle(&self) -> *mut HANDLE {
        self.handle
    }
}


Crate with additional features (optional functionality)

// [crate foo]

// Implicit:
// #![scenario_import] extern crate std;
// #![implicit_scenario(desktop)]
// #![crate_scenario(desktop)]
// #[scenario(desktop)] on each item

#![declare_scenario(extra)]

// Implicitly translated as
// #![crate_scenario(and(desktop, extra))]
// because of the implicit desktop scenario
#![crate_scenario(extra)]

// Implicit translated as #[scenario(all(desktop, extra))]
#[cfg(extra)]
fn feature() {
    // ...
}

Crate with reduced features (emscripten compatibility)

// [crate bar]

// Replace the default "desktop" scenario for this crate
// Implies #![crate_scenario(emscripten)] and
// #[scneario(emscripten)] on all items
#![implicit_scenario(emscripten)]

// Dependency is checked to support the emscripten scenario
extern crate dependency;

// ...

Crate that abstracts over different platforms

// [crate cross_plattform]

// Implicit:
// #![scenario_import] extern crate std;
// #![implicit_scenario(desktop)]
// #![crate_scenario(desktop)]
// #[scenario(desktop)] on each item

// Implicitly:
//    #[scenario(all(desktop, windows))]
// == #[scenario(all(all(either(unix, windows), thread, ...), windows))]
// == #[scenario(all(windows, thread, ...))]
#[cfg(windows)]
fn cross_platform_fn() {
    // ...
}

// Implicitly:
//    #[scenario(all(desktop, unix))]
// == #[scenario(all(all(either(unix, windows), thread, ...), windows))]
// == #[scenario(all(unix, thread, ...))]
#[cfg(unix)]
fn cross_platform_fn() {
    // ...
}

// Both above implicitly define
//    #[scenario(either(all(unix, thread, ...), all(windows, thread, ...)))]
// == #[scenario(all(either(windows, unix), thread, ...)]
// == #[scenario(desktop]
// for the item `cross_platform_fn`

// Implicit #[scenario(desktop)], so check ok.
fn cross_platform_fn_2() {
    cross_platform_fn()
}

Crate that causes various errors


// ERROR: Crate implicitly declares itself as belonging to the desktop scenario,
//        but some of its items are only available for the all(desktop, unix) scenario.
// NOTE:  Consider adding #![crate_scenario(unix)] to optionally support the unix scenario.

// ERROR: Access to item b is only enabled for scenario unix,
//        but a implicitly declare itself for the "desktop" scenario
//        which does not imply unix
// NOTE:  Consider adding #[scenario(unix)] to constrain a() to the unix scenario
// NOTE:  Consider adding a implementation of b for the windows scenario
//        to fulfill the desktop scenario
fn a() {
    b();
}

#[cfg(unix)]
fn b() {
}


Winapi families

This is based on the constraints given here.

// [crate winapi]

#![declare_scenario(WINAPI_FAMILY_PC_APP)]
#![declare_scenario(WINAPI_FAMILY_PHONE_APP)]
#![declare_scenario(WINAPI_FAMILY_SYSTEM)]
#![declare_scenario(WINAPI_FAMILY_SERVER)]
#![declare_scenario(WINAPI_FAMILY_DESKTOP_APP)]

#![crate_scenario(either(WINAPI_FAMILY_PC_APP,
                         WINAPI_FAMILY_PHONE_APP,
                         WINAPI_FAMILY_SYSTEM,
                         WINAPI_FAMILY_SERVER,
                         WINAPI_FAMILY_DESKTOP_APP))]
// [crate winapi_consuming_lib]

#[scenario_import]
extern crate winapi;

#![crate_scenario(either(WINAPI_FAMILY_PHONE_APP, WINAPI_FAMILY_DESKTOP_APP))]

#[cfg(WINAPI_FAMILY_PHONE_APP)]
fn cross_platform_fn() {}

#[cfg(WINAPI_FAMILY_DESKTOP_APP)]
fn cross_platform_fn() {}

// [crate binary]

#[scenario_import]
extern crate winapi;
extern crate winapi_consuming_lib;

#![crate_scenario(WINAPI_FAMILY_PHONE_APP)]

What makes this example a bit tricky is that this scenario system only checks for validity, but can not trigger configured builds. That is cargos domain, so the top-level selection of the family would probably have to happen with a cargo feature that gets passed through to all upstream crates up to winapi itself. (Not sure if the current feature system makes this feasible though).

1 Like