Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Declarative per-task configuration #302

Closed
mx-shift opened this issue Dec 6, 2021 · 4 comments
Closed

Declarative per-task configuration #302

mx-shift opened this issue Dec 6, 2021 · 4 comments

Comments

@mx-shift
Copy link
Contributor

mx-shift commented Dec 6, 2021

Fairly often, tasks need some form of configuration for a given application (target device, target board, etc). A few examples are user_leds, dependencies on other tasks, and i2c topology.

Previous approaches

user_leds currently manages with cfg_if! blocks that look at cfg(target_board). While this provides static knowledge of the board configuration at compile-time (good), it also requires modifying user_leds' source for every new board. Discoverability is OK-ish in that user_leds will just fail to compile on an unsupported target board but the error messages will be unhelpful (use of undeclared variable) even though it points you in the correct general direction.

Inter-task dependencies originally used an enum called userlib::Task that was generated by userlib's build.rs. Again, this provided static knowledge at compile time. Discoverability was poor as the enum only existed in generated code. Searching for Task would turn up lots of usages but no declaration which made figuring out what variants existed difficult. Further, the variant needed to be Rust identifiers but they were generated from app.toml which was more relaxed so looking in app.toml only got you close to the actual variant name. Lastly, the calling task needed to name the specific variant to use at compile-time. This led to the same problem as user_leds where each task would have cfg_if! blocks to handle per-target, per-board, or per-application configuration.

userlib::task_slot was introduced to address some of userlib::Task's limitations. task_slot allows the calling task to declare a generic dependency on another task without naming the specific dependency. This improves discoverability since the task_slot macro is invoked in the calling task and it introduces a clearly named static variable in the module where the macro is invoked. Configuration of the target task for a slot is moved to app.toml which feels natural for per-application configuration details. Unfortunately, task_slot's implementation is more dynamic and requires intentionally preventing some compile-time optimizations (constant propagation on task_slot static variable contents). For the limited use case of task IDs used in IPCs, the impact on code size and performance is minimal.

I2C topology is another case that has recently received some attention with #238. build.rs is once again used to generate code from app.toml. Just as with userlib::Task, this provides poor discoverability but good static knowledge at compile-time while also allowing per-application configuration. Unlike userlib::Task, each task that wishes to use i2c topology information is required to write their own build.rs that invokes build_i2c::codegen().

Suggested approach

Combine the declarative nature of userlib::task_slot with the compile-time knowledge of build.rs codegen. How? proc_macro. Consider the use case of user_leds. If there was a proc_macro named task_config!, user_leds could declare a dependency on application-specific configuration data like so:

task_config! {
    led: PinMapping,
    foobar: bool,
}

and then in app.toml, the configuration would be supplied via the same struct-like key/vals:

[tasks.user_leds.config]
foobar = false

[tasks.user_leds.config.led]
...

The task_config! proc_macro would read app.toml, extract the config information, and generate a struct w/ initialization such as:

struct Config {
  led: PinMapping,
  foobar: bool,
};

let config: Config = Config {
  led: PinMapping{...},
  foobar: false,
};
@cbiffle
Copy link
Collaborator

cbiffle commented Feb 1, 2022

FWIW - tried this, but, proc-macros cannot currently get the output dir location to generate code / generate the right include! line. Just in case someone wants to try this, be aware that you may hit this hurdle.

@mkeeter
Copy link
Collaborator

mkeeter commented Mar 8, 2022

I made an attempt and got something running in the task-config-macro branch. The fun parts are in the new task-config crate.

It looks like this in the task:

task_config::task_config! {
    user_leds, // config block name
    count: usize,
    leds: &'static [(drv_stm32xx_sys_api::PinSet, bool)],
}

And looks like this in app.toml:

[config.user_leds]
count = 4
leds = [
    ["drv_stm32xx_sys_api::Port::C.pin(6)", true],
    ["drv_stm32xx_sys_api::Port::I.pin(8)", false],
    ["drv_stm32xx_sys_api::Port::I.pin(9)", false],
    ["drv_stm32xx_sys_api::Port::I.pin(10)", false],
    ["drv_stm32xx_sys_api::Port::I.pin(11)", false],
]

(only implemented for app/gimletlet/app.toml right now)

This is closely modeled after Rick's suggestion, so it just generates a single const TASK_CONFIG: Config = Config { ... }. Do folks have opinions on whether it's worth pushing forward?

@mkeeter
Copy link
Collaborator

mkeeter commented Mar 21, 2022

Draft PR in #466

@mkeeter
Copy link
Collaborator

mkeeter commented May 8, 2022

Merged!

@mkeeter mkeeter closed this as completed May 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants