Skip to content

Implement "timeout" support for Rust #78193

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

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/rust/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ int main(void)
return 0;
}

/* On most arches, panic is entirely macros resulting in some kind of inline assembly. Create this
* wrapper so the Rust panic handler can call the same kind of panic.
*/
void rust_panic_wrap(void)
{
k_panic();
}

#endif
3 changes: 3 additions & 0 deletions lib/rust/zephyr-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ fn main() -> Result<()> {
.derive_copy(false)
.allowlist_function("k_.*")
.allowlist_function("gpio_.*")
.allowlist_function("sys_.*")
// Deprecated
.blocklist_function("sys_clock_timeout_end_calc")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
Expand Down
3 changes: 3 additions & 0 deletions lib/rust/zephyr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Functionality for Rust-based applications that run on Zephyr.
[dependencies]
zephyr-sys = { version = "0.1.0", path = "../zephyr-sys" }

[dependencies.fugit]
version = "0.3.7"

# These are needed at build time.
# Whether these need to be vendored is an open question. They are not
# used by the core Zephyr tree, but are needed by zephyr applications.
Expand Down
18 changes: 16 additions & 2 deletions lib/rust/zephyr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#![no_std]
#![allow(unexpected_cfgs)]

pub mod sys;
pub mod time;

// Bring in the generated kconfig module
include!(concat!(env!("OUT_DIR"), "/kconfig.rs"));

Expand All @@ -24,8 +27,19 @@ use core::panic::PanicInfo;

/// Override rust's panic. This simplistic initial version just hangs in a loop.
#[panic_handler]
fn panic(_ :&PanicInfo) -> ! {
loop {
fn panic(info :&PanicInfo) -> ! {
#[cfg(CONFIG_PRINTK)]
{
printkln!("panic: {}", info);
}
let _ = info;
Comment on lines +30 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you can call the argument _info and use it in the printkln!.

Suggested change
fn panic(info :&PanicInfo) -> ! {
#[cfg(CONFIG_PRINTK)]
{
printkln!("panic: {}", info);
}
let _ = info;
fn panic(_info :&PanicInfo) -> ! {
#[cfg(CONFIG_PRINTK)]
{
printkln!("panic: {}", _info);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is there a typical way this is done? Both seem awkward.


// Call into the wrapper for the system panic function.
unsafe {
extern "C" {
fn rust_panic_wrap() -> !;
}
rust_panic_wrap();
}
}

Expand Down
18 changes: 18 additions & 0 deletions lib/rust/zephyr/src/sys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) 2024 Linaro LTD
// SPDX-License-Identifier: Apache-2.0

//! Zephyr 'sys' module.
//!
//! The `zephyr-sys` crate contains the direct C bindings to the Zephyr API. All of these are
//! unsafe.
//!
//! This module `zephyr::sys` contains thin wrappers to these C bindings, that can be used without
//! unsafe, but as unchanged as possible.

use zephyr_sys::k_timeout_t;

// These two constants are not able to be captured by bindgen. It is unlikely that these values
// would change in the Zephyr headers, but there will be an explicit test to make sure they are
// correct.
pub const K_FOREVER: k_timeout_t = k_timeout_t { ticks: -1 };
pub const K_NO_WAIT: k_timeout_t = k_timeout_t { ticks: 0 };
135 changes: 135 additions & 0 deletions lib/rust/zephyr/src/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) 2024 Linaro LTD
// SPDX-License-Identifier: Apache-2.0

//! Time types designed for Zephyr, inspired by `std::time`.
//!
//! In `std`, there are two primary time types: `Duration`, representing a span of time, and
//! `Instant`, representing a specific point in time. Both have nanosecond precision, which is
//! well-suited to more powerful machines. However, on embedded systems like Zephyr, this precision
//! can lead to performance issues, often requiring divisions whenever timeouts are used.
//!
//! In the Rust embedded ecosystem, the `fugit` crate is commonly used for handling time. It
//! provides both `Duration` and `Instant` types, but with parameters that allow the representation
//! to match the time slice used, enabling compile-time conversion and storing time directly as
//! tick counts.
//!
//! Zephyr manages time in terms of system tick intervals, derived from
//! `sys_clock_hw_cycles_per_sec()`. This model aligns well with `fugit`, especially when the
//! types are properly parameterized.
//!
//! It's important to note that Rust’s `std::Instant` requires time to be monotonically increasing.
//!
//! Zephyr’s `sys/time_units.h` provides a variety of optimized macros for manipulating time
//! values, converting between human-readable units and ticks, and minimizing divisions (especially
//! by non-constant values). Similarly, the `fugit` crate offers constructors that aim to result
//! in constants when possible, avoiding costly division operations.

use zephyr_sys::{k_timeout_t, k_ticks_t};

use core::fmt::Debug;

// The system ticks, is mostly a constant, but there are some boards that use a dynamic tick
// frequency, and thus need to read this at runtime.
#[cfg(CONFIG_TIMER_READS_ITS_FREQUENCY_AT_RUNTIME)]
compile_error!("Rust does not (yet) support dynamic frequency timer");

// Given the above not defined, the system time base comes from a kconfig.
/// The system time base. The system clock has this many ticks per second.
pub const SYS_FREQUENCY: u32 = crate::kconfig::CONFIG_SYS_CLOCK_TICKS_PER_SEC as u32;

/// Zephyr can be configured for either 64-bit or 32-bit time values. Use the appropriate type
/// internally to match. This should end up the same size as `k_ticks_t`, but unsigned instead of
/// signed.
#[cfg(CONFIG_TIMEOUT_64BIT)]
pub type Tick = u64;
#[cfg(not(CONFIG_TIMEOUT_64BIT))]
pub type Tick = u32;

/// Duration appropriate for Zephyr calls that expect `k_timeout_t`. The result will be a time
/// interval from "now" (when the call is made).
pub type Duration = fugit::Duration<Tick, 1, SYS_FREQUENCY>;

/// An Instant appropriate for Zephyr calls that expect a `k_timeout_t`. The result will be an
/// absolute time in terms of system ticks.
#[cfg(CONFIG_TIMEOUT_64BIT)]
pub type Instant = fugit::Instant<Tick, 1, SYS_FREQUENCY>;

// The Zephyr `k_timeout_t` represents several different types of intervals, based on the range of
// the value. It is a signed number of the same size as the Tick here, which effectively means it
// is one bit less.
//
// 0: K_NO_WAIT: indicates the operation should not wait.
// 1 .. Max: indicates a duration in ticks of a delay from "now".
// -1: K_FOREVER: a time that never expires.
// MIN .. -2: A wait for an absolute amount of ticks from the start of the system.
//
// The absolute time offset is only implemented when time is a 64-bit value. This also means that
// "Instant" isn't available when time is defined as a 32-bit value.

// Wrapper around the timeout type, so we can implement From/Info.
pub struct Timeout(pub k_timeout_t);

// `From` allows methods to take a time of various types and convert it into a Zephyr timeout.
impl From<Duration> for Timeout {
fn from(value: Duration) -> Timeout {
let ticks: k_ticks_t = checked_cast(value.ticks());
debug_assert_ne!(ticks, crate::sys::K_FOREVER.ticks);
debug_assert_ne!(ticks, crate::sys::K_NO_WAIT.ticks);
Timeout(k_timeout_t { ticks })
}
}

#[cfg(CONFIG_TIMEOUT_64BIT)]
impl From<Instant> for Timeout {
fn from(value: Instant) -> Timeout {
let ticks: k_ticks_t = checked_cast(value.ticks());
debug_assert_ne!(ticks, crate::sys::K_FOREVER.ticks);
debug_assert_ne!(ticks, crate::sys::K_NO_WAIT.ticks);
Timeout(k_timeout_t { ticks: -1 - 1 - ticks })
Copy link
Contributor

@ithinuel ithinuel Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overflow checks be done after the offset (-1 -1) is applied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on how valuable having a single point of failure is. Integer overflow on the subtraction will also panic when debug assertions are enabled.

}
}

/// A sleep that waits forever. This is its own type, that is `Into<Timeout>` and can be used
/// anywhere a timeout is needed.
pub struct Forever;

impl From<Forever> for Timeout {
fn from(_value: Forever) -> Timeout {
Timeout(crate::sys::K_FOREVER)
}
}

/// A sleep that doesn't ever wait. This is its own type, that is `Info<Timeout>` and can be used
/// anywhere a timeout is needed.
pub struct NoWait;

impl From<NoWait> for Timeout {
fn from(_valued: NoWait) -> Timeout {
Timeout(crate::sys::K_NO_WAIT)
}
}

/// Put the current thread to sleep, for the given duration. Uses `k_sleep` for the actual sleep.
/// Returns a duration roughly representing the remaining amount of time if the sleep was woken.
pub fn sleep<T>(timeout: T) -> Duration
where T: Into<Timeout>,
{
let timeout: Timeout = timeout.into();
let rest = unsafe { crate::raw::k_sleep(timeout.0) };
Duration::millis(rest as Tick)
}

/// Convert from the Tick time type, which is unsigned, to the `k_ticks_t` type. When debug
/// assertions are enabled, it will panic on overflow.
fn checked_cast<I, O>(tick: I) -> O
where
I: TryInto<O>,
I::Error: Debug,
O: Default,
{
if cfg!(debug_assertions) {
tick.try_into().expect("Overflow in time conversion")
} else {
tick.try_into().unwrap_or(O::default())
}
}
10 changes: 10 additions & 0 deletions tests/lib/rust/time/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(time_rust)

target_sources(app PRIVATE src/times.c)

rust_cargo_application()
16 changes: 16 additions & 0 deletions tests/lib/rust/time/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) 2024 Linaro LTD
# SPDX-License-Identifier: Apache-2.0

[package]
# This must be rustapp for now.
name = "rustapp"
version = "0.1.0"
edition = "2021"
description = "Tests of time"
license = "Apache-2.0 or MIT"

[lib]
crate-type = ["staticlib"]

[dependencies]
zephyr = "0.1.0"
5 changes: 5 additions & 0 deletions tests/lib/rust/time/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright (c) 2024 Linaro LTD
# SPDX-License-Identifier: Apache-2.0

CONFIG_RUST=y
CONFIG_MAIN_STACK_SIZE=2048
86 changes: 86 additions & 0 deletions tests/lib/rust/time/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) 2024 Linaro LTD
// SPDX-License-Identifier: Apache-2.0

#![no_std]

use core::ffi::{
c_char,
CStr,
};

use zephyr::printkln;
use zephyr::time::{Duration, Instant, Tick, Timeout};
use zephyr::raw::k_timeout_t;

#[no_mangle]
extern "C" fn rust_main() {
printkln!("Tick frequency: {}", zephyr::time::SYS_FREQUENCY);
check_conversions();
printkln!("All tests passed");
}

/// Verify that the conversions are correct.
fn check_conversions() {
let mut index = 0;
loop {
// The entry returns is always valid, so is a valid reference.
let entry = unsafe { &*get_time_entry(index) };
if entry.name.is_null() {
break;
}
let name = unsafe {
CStr::from_ptr(entry.name).to_str().expect("Invalid C string")
};
printkln!("Testing: {}", name);

// The units must match the enum in the C code.
match entry.units {
// UNIT_FOREVER
0 => {
assert_eq!(entry.value.ticks, zephyr::sys::K_FOREVER.ticks);
}
// UNIT_NO_WAIT
1 => {
assert_eq!(entry.value.ticks, zephyr::sys::K_NO_WAIT.ticks);
}
// UNIT_DUR_MS
2 => {
let value = Duration::millis_at_least(entry.uvalue as Tick);
let value: Timeout = value.into();
assert_eq!(entry.value.ticks, value.0.ticks);
}
// UNIT_INST_MS
3 => {
let base = Instant::from_ticks(0);
let value = Duration::millis_at_least(entry.uvalue as Tick);
let value = base + value;
let value: Timeout = value.into();
let c_value = unsafe { ms_to_abs_timeout(entry.uvalue) };
if c_value.ticks != value.0.ticks {
printkln!("Mismatch C: {}, Rust: {}",
c_value.ticks, value.0.ticks);
}
assert_eq!(c_value.ticks, value.0.ticks);
}
_ => {
panic!("Invalid unit enum");
}
}

index += 1;
}
}

/// The time entry information.
#[repr(C)]
struct TimeEntry {
name: *const c_char,
units: u32,
uvalue: i64,
value: k_timeout_t,
}

extern "C" {
fn get_time_entry(index: usize) -> *const TimeEntry;
fn ms_to_abs_timeout(ms: i64) -> k_timeout_t;
}
Loading
Loading