Cyclic dependency on OS implementation

I'm implementing a simple embedded HRT scheduler for Rpi Pico, now that I have the basics working I wanted to refactor it and split the user code portion out of the OS crate. I ended up having a cyclic dependency since the OS uses the app that in turn uses the OS again to request Os services (for example, the app tasks are implemented as infinite loops that need to signal its job-done flag for each loop iteration)

I already saw How to resolve cyclic dependency, and while the solution stated there makes sense and works, I wanted to know if there is a simpler way to do it (maybe wo/ trait objects) or if somebody can hint any improvements. Here it is

Cargo.toml

[workspace]
members = [
    "os",
    "osapi",
    "userapp"
]

./osapi/Cargo.toml

[package]
name = "osapi"
version = "0.1.0"
edition = "2021"

[dependencies]

./userapp/Cargo.toml

[package]
name = "userapp"
version = "0.1.0"
edition = "2021"

[dependencies]
osapi = { path = "../osapi" }

./os/Cargo.toml

[package]
name = "os"
version = "0.1.0"
edition = "2021"

[dependencies]
userapp = { path = "../userapp" }
osapi = { path = "../osapi" }

./osapi/src/lib.rs

pub trait OsOps {
    fn syscall(&self, idx: usize);
}

pub trait UserAppOps {
    fn init(os: &dyn OsOps);
}

./userapp/src/lib.rs

use osapi::OsOps;

pub struct App;

impl osapi::UserAppOps for App {
    fn init(os: &dyn OsOps){
        os.syscall(0);
    }
}

./os/src/main.rs

use osapi::UserAppOps;

fn main() {
    userapp::App::init(&Os);
}

struct Os;

impl osapi::OsOps for Os {
    fn syscall(&self, idx: usize) {
        println!("userapp called a {} syscall", idx);
    }
}

Thanks!

No one? I've been thinking about this approach and all I really want is to get rid of the trait object, so that there is no runtime penalty. I've been trying to think of a different way but couldn't find any

Any help would be really appreciated

Is there something preventing making the function generic?

The separation of the crates seems fine to me. If you wanted to simplify it you could just put everything in osapi inside userapp.

1 Like

IDK what I did when I tried this, but it wasnt compiling, complaining about missing definitions. I gave it a 2nd try and it now works as a charm

./osapi/src/lib.rs

pub trait OsOps {
    fn syscall(&self, idx: usize);
}

pub trait UserAppOps<T> where T: OsOps {
    fn init(os: &'static T) -> Self;
    fn task(&self);
}

./userapp/src/lib.rs

pub struct App<T:'static> where T: osapi::OsOps {
  os: &'static T,
  f1: usize
}

impl<T> osapi::UserAppOps<T> for App<T> where T: osapi::OsOps {
    fn init(os: &'static T) -> Self {
        os.syscall(0);
        Self {
            os: os,
            f1 : 10,
        }
    }

    fn task(&self) {
      println!("Hello from task. f1: {}", self.f1);
      self.os.syscall(1);
      loop { /* Do task stuff */ }
    }
}

./os/src/main.rs

use osapi::UserAppOps;

fn main() {
    let app = userapp::App::init(&OS);
    app.task();
}

struct Os;

static OS: Os = Os;

impl osapi::OsOps for Os {
    fn syscall(&self, idx: usize) {
        println!("userapp called a {} syscall", idx);
    }
}

Thanks for your comment @Heliozoa

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.