1

I have a Rust project where I have the following structure:

|- src
|  |- main.rs
|  |- handler.rs
|  |- modules
|  |  |- handler1.rs
|  |  |- handler2.rs
|  |  |  ...

With pseudo-code like this:

//handler.rs
trait Handler {
    fn handle(message: str) -> bool;
}

// handler1.rs
struct Handler1;
impl Handler for Handler1 {
    fn handle(message: str) -> bool {
        //...
    }
}

//handler2.rs
struct Handler2;
impl Handler for Handler2 {
    fn handle(message: str) -> bool {
        //...
    }
}

//main.rs
const handlers: Vec<Handler> = vec![Box::new(Handler1 {}), Box::new(Handler2 {})];

fn handleMessage(myMessage: str) {
    for handler in handlers.iter() {
        if (handler.handle(myMessage)) {
            break;
        };
    }
}

I'd like to automate the creation of this handlers constant from the project's structure. There are 2 approaches which I think could work:

  1. Using macros in each module, something like this(?):

//handler2.rs
struct Handler2;
impl Handler for Handler2 {
    fn handle(message: str) -> bool {
        //...
    }
}
registerHandler!{Handler2}

  1. Perhaps there's a way to scan all modules for implementations of Handler trait at build time? (Or scanning for some attribute marked implementations?)

I have very little idea how to do either, is there a standard approach to this kind of problem?

P.S. in case it was obvious, new to rust, so apologies for any big mistakes! I suspect this is non-trivial, but any resources on the matter / standard approaches would be greatly helpful.

Sancarn
  • 2,575
  • 20
  • 45
  • Does this answer your question? [Is there a way to automatically register trait implementors?](/q/75122868/2189130) – kmdreko May 06 '23 at 21:17
  • @kmdreko Kinda yeah, so one could probably use `ctor` to build the array of handlers at the start of runtime... If i understand this correctly... – Sancarn May 06 '23 at 21:56
  • Yes. If you can make all your `Handler`s statically then `linkme` might be simpler, but otherwise yeah `ctor` is the way to go. – kmdreko May 06 '23 at 22:13
  • @kmdreko So... slight problem, this has just moved the problem. 1. I still have to define `mod Handler1` etc. to actually get these to compile in it seems? 2. The constructors are running but I'm not entirely sure how to create a public static mutable collection of things which implement a trait... I did get `pub const PEOPLE: Vec<&'static dyn Person> = vec![];` but it seems none of my ctors are actually able to mutate this... – Sancarn May 06 '23 at 23:20
  • Sounds like you want [How do I create a global, mutable singleton?](/q/27791532/2189130) Using `const` with a `Vec` doesn't really make sense, especially since you intend to mutate it. And yes, you still have to include the `mod` declaration (that's how you tell the compiler what to compile). – kmdreko May 06 '23 at 23:30
  • You may also be interested in [How do I conditionally import modules and add instances of struct to vec!, only when the module (and struct) exists?](/q/74858663/2189130) which tries to do the importing and prep-work automatically through a procedural macro, but I would personally not recommend it. – kmdreko May 06 '23 at 23:37
  • After many hours frustratingly trying to get ctor to work I gave up and resorted to using `build.rs` script to [generate the module](https://github.com/sancarn/rust_experiment-distributed_handlers) on the fly. Realistically this is likely the best methodology too as you don't have to do any maintenance whatsoever, as long as all files are in the correct directory. Would add an answer, but can't... – Sancarn May 07 '23 at 02:22
  • 1
    I've reopened your question. – kmdreko May 07 '23 at 02:28

2 Answers2

0

By far the best solution with the minimum maintenance required and what I finally settled with is as follows:

|- build.rs
|- cargo.toml
|- src
|  |- main.rs
|  |- handler.rs
|  |- handlers.rs  //generated
|  |- handlers
|  |  |- handler1.rs
|  |  |- handler2.rs

handler.rs contains the Handler trait:

trait Handler {
    fn handle(message: str) -> bool;
}

handlers.rs is generated at build time via build.rs:

mod handler1; mod handler2; //...
use crate::handler::*;
use handler1::Handler1; use handler2::Handler2; //...
pub fn get_handlers() -> Vec<Box<dyn Handler>> {
  return vec![Box::new(Handler1), Box::new(Handler2)];
}

cargo.toml must have the build entry supplied:

[package]
build = "build.rs"
edition = "2021"
name = "distributed_handlers"
#...

and finally build.rs looks like this:

use std::{env, fs};
macro_rules! print {
    ($($tokens: tt)*) => {
        println!("cargo:warning={}", format!($($tokens)*))
    }
}

const TEMPLATE: &str = r#"
// Generated bindings to modules
$[mods]
use crate::handler::*;
$[uses]
pub fn get_handlers() -> Vec<Box<dyn Handler>> {
    $[inits]
}
"#;

fn main() {
    let cur = env::current_dir().unwrap();
    let path = String::from(cur.to_string_lossy());
    let mut mods = String::from("");
    let mut uses = String::from("");
    let mut inits: Vec<String> = vec![];
    for entry in fs::read_dir(path.clone() + "/src/handlers").unwrap() {
        let entry: fs::DirEntry = entry.unwrap();
        let name: String = entry.file_name().into_string().unwrap();
        let name: String = String::from(name.split_at(name.len() - 3).0);
        let mut proper: String = name.clone();
        let proper: String = format!("{}{proper}", proper.remove(0).to_uppercase());
        mods.push_str(&String::from(format!("mod {};", name)));
        uses.push_str(&String::from(format!("use {}::{};", name, proper)));
        inits.push(format!("Box::new({})", proper));
    }
    let inits = format!("return vec![{}];", inits.join(","));
    let mut template = String::from(TEMPLATE);
    template = template.replace("$[mods]", &mods);
    template = template.replace("$[uses]", &uses);
    template = template.replace("$[inits]", &inits);
    let _ = fs::write(path.clone() + "/src/handlers.rs", template.clone());
    for s in template.split("\n") {
        print!("{s}");
    }
    print!("Written to {path}/src/handlers.rs")
}

The above code assumes your module is named the same (just different casing) from the struct / handler itself. A full example is on github.


Edit: I refactored the build script, maybe it's a little more readable now:

let current_dir = env::current_dir().unwrap();
let path = current_dir.to_string_lossy().to_string() + "/src/people";
let (mods, uses, inits) = fs::read_dir(path.clone())
    .unwrap()
    .map(|entry| {
        let entry = entry.unwrap();
        let name = entry.file_name().into_string().unwrap();
        let name = String::from(name.split_at(name.len() - 3).0);
        let proper = name.chars().next().unwrap().to_uppercase().to_string() + &name[1..];
        (
            format!("mod {name};\n"),
            format!("use {name}::{proper};\n"),
            format!("Box::new({proper})"),
        )
    })
    .fold(
        (String::new(), String::new(), Vec::new()),
        |(mut mods, mut uses, mut inits), (m, u, i)| {
            mods.push_str(&m);
            uses.push_str(&u);
            inits.push(i);
            (mods, uses, inits)
        },
    );
let inits = inits.join(",");
let template = TEMPLATE
    .replace("$[mods]", &mods)
    .replace("$[uses]", &uses)
    .replace("$[inits]", &inits);
let _ = fs::write(
    current_dir.to_string_lossy().to_string() + "/src/people.rs",
    template.clone(),
);
Sancarn
  • 2,575
  • 20
  • 45
0

This is a case which is easily solved by virtual calls.

You just need to do add dyn to make a box/reference which can do virtual calls.

let handlers: Vec<Box<dyn Handler>> = vec![Box::new(Handler1 {}), Box::new(Handler2 {})];

Note that you are not required to store boxes. You may want to store references instead:

let h1 = Handler1 {};
let h2 = Handler2 {};
let handlers: &[&dyn Handler] = &[&h1, &h2];

If you want to avoid virtual calls, there is an option with recursive data structure. This option potentially provides best runtime performance (because it allows compiler to inline handler implementations and unroll loops) but can cause longer compile times.

struct HandlerSet<H1, H2>{
   h1: H1,
   h2: H2,
}

impl<H1: Handler, H2: Handler> Handler for HandlerSet<H1, H2> {
   fn handle(&self, message: &str) -> bool {
      // Chaining handlers happens here.
      self.h1.handle(message) || self.h2.handle(message)
   }
}

impl<H1, H2> HandlerSet<H1, H2> {
   const fn new(h1: H1, h2: H2)->Self{
      Self{h1, h2}
   }
   const fn join(self, h3: H3)->HandlerSet<Self, H3>{
      HandlerSet{h1: self, h2: h3}
   }
}


const HANDLERS: HandlerSet = HandlerSet::new(handler1, handler2).join(handler3).join(handler4);

fn handleMessage(myMessage: &str) {
    HANDLERS.handle(myMessage);
}

I would recomment to go with dynamic dispatch if your handlers are relatively slow or this code performance is not critical, otherwise HandlerSet approach is better.

  • Interesting approach, but the question was less about how to handle many handlers, but rather about how to autonymously add all handlers in the `modules` folder to the handler queue. :) Perhaps with CTOR you could join new handlers to the handler set? – Sancarn May 08 '23 at 14:11