2
votes

Say we want to have objects implementations switched at runtime, we'd do something like this:

pub trait Methods {
    fn func(&self);
}

pub struct Methods_0;
impl Methods for Methods_0 {
    fn func(&self) {
        println!("foo");
    }
}

pub struct Methods_1;
impl Methods for Methods_1 {
    fn func(&self) {
        println!("bar");
    }
}

pub struct Object<'a> { //'
    methods: &'a (Methods + 'a),
}

fn main() {
    let methods: [&Methods; 2] = [&Methods_0, &Methods_1];
    let mut obj = Object { methods: methods[0] };
    obj.methods.func();
    obj.methods = methods[1];
    obj.methods.func();
}

Now, what if there are hundreds of such implementations? E.g. imagine implementations of cards for collectible card game where every card does something completely different and is hard to generalize; or imagine implementations for opcodes for a huge state machine. Sure you can argue that a different design pattern can be used -- but that's not the point of this question...

Wonder if there is any way for these Impl structs to somehow "register" themselves so they can be looked up later by a factory method? I would be happy to end up with a magical macro or even a plugin to accomplish that.

Say, in D you can use templates to register the implementations -- and if you can't for some reason, you can always inspect modules at compile-time and generate new code via mixins; there are also user-defined attributes that can help in this. In Python, you would normally use a metaclass so that every time a new child class is created, a ref to it is stored in the metaclass's registry which allows you to look up implementations by name or parameter; this can also be done via decorators if implementations are simple functions.

Ideally, in the example above you would be able to create Object as

Object::new(0)

where the value 0 is only known at runtime and it would magically return you an Object { methods: &Methods_0 }, and the body of new() would not have the implementations hard-coded like so "methods: [&Methods; 2] = [&Methods_0, &Methods_1]", instead it should be somehow inferred automatically.

1
As a tiny nit, you will need to return a Box<Method> instead of &Method. There's no safe space for the &Method to point to (unless you have a single 'static instance). - Shepmaster
Would your current solution be acceptable if you just didn't have to maintain the array in your main method? Do you have any direction for how a specific implementation would be picked? It seems like you'd have a map of "Orc" => OrcMethods, but at that point, you might as well call OrcMethods directly. - Shepmaster
Thanks @Shepmaster, you're certainly right about the Box. - aldanor
The problem here is that "Orc" is only known at runtime -- so we can't call OrcMethods directly. - aldanor
So you're not going to accept an answer that says “use [fn()]”? - Chris Morgan

1 Answers

1
votes

So, this is probably extremely buggy, but it works as a proof of concept.

It is possible to use Cargo's code generation support to make the introspection at compile-time, by parsing (not exactly parsing in this case, but you get the idea) the present implementations, and generating the boilerplate necessary to make Object::new() work.

The code is pretty convoluted and has no error handling whatsoever, but works.

Tested on rustc 1.0.0-dev (2c0535421 2015-02-05 15:22:48 +0000) (See on github)

src/main.rs:

pub mod implementations;
mod generated_glue {
    include!(concat!(env!("OUT_DIR"), "/generated_glue.rs"));
}

use generated_glue::Object;

pub trait Methods {
    fn func(&self);
}

pub struct Methods_2;
impl Methods for Methods_2 {
    fn func(&self) {
        println!("baz");
    }
}

fn main() {
    Object::new(2).func();
}

src/implementations.rs:

use super::Methods;

pub struct Methods_0;
impl Methods for Methods_0 {
    fn func(&self) {
        println!("foo");
    }
}

pub struct Methods_1;
impl Methods for Methods_1 {
    fn func(&self) {
        println!("bar");
    }

}

build.rs:

#![feature(core, unicode, path, io, env)]

use std::env;
use std::old_io::{fs, File, BufferedReader};
use std::collections::HashMap;

fn main() {
    let target_dir      = Path::new(env::var_string("OUT_DIR").unwrap());
    let mut target_file = File::create(&target_dir.join("generated_glue.rs")).unwrap();

    let source_code_path = Path::new(file!()).join_many(&["..", "src/"]);

    let source_files = fs::readdir(&source_code_path).unwrap().into_iter()
        .filter(|path| {
            match path.str_components().last() {
                Some(Some(filename))  => filename.split('.').last() == Some("rs"),
                _                     => false
            }
        });

    let mut implementations = HashMap::new();

    for source_file_path in source_files {
        let relative_path = source_file_path.path_relative_from(&source_code_path).unwrap();
        let source_file_name = relative_path.as_str().unwrap();

        implementations.insert(source_file_name.to_string(), vec![]);
        let mut file_implementations = &mut implementations[*source_file_name];

        let mut source_file = BufferedReader::new(File::open(&source_file_path).unwrap());

        for line in source_file.lines() {
            let line_str = match line {
                Ok(line_str) => line_str,
                Err(_)       => break,
            };

            if line_str.starts_with("impl Methods for Methods_") {
                const PREFIX_LEN: usize = 25;

                let number_len = line_str[PREFIX_LEN..].chars().take_while(|chr| {
                    chr.is_digit(10)
                }).count();

                let number: i32 = line_str[PREFIX_LEN..(PREFIX_LEN + number_len)].parse().unwrap();
                file_implementations.push(number);
            }
        }
    }

    writeln!(&mut target_file, "use super::Methods;").unwrap();

    for (source_file_name, impls) in &implementations {
        let module_name = match source_file_name.split('.').next() {
            Some("main") => "super",
            Some(name)   => name,
            None         => panic!(),
        };

        for impl_number in impls {
            writeln!(&mut target_file, "use {}::Methods_{};", module_name, impl_number).unwrap();
        }
    }

    let all_impls = implementations.values().flat_map(|impls| impls.iter());

    writeln!(&mut target_file, "
pub struct Object;

impl Object {{
    pub fn new(impl_number: i32) -> Box<Methods + 'static> {{
        match impl_number {{
    ").unwrap();

    for impl_number in all_impls {
        writeln!(&mut target_file,
"           {} => Box::new(Methods_{}),", impl_number, impl_number).unwrap();
    }

    writeln!(&mut target_file, "
           _ => panic!(\"Unknown impl number: {{}}\", impl_number),
        }}
    }}
}}").unwrap();
}

The generated code:

use super::Methods;
use super::Methods_2;
use implementations::Methods_0;
use implementations::Methods_1;

pub struct Object;

impl Object {
    pub fn new(impl_number: i32) -> Box<Methods + 'static> {
        match impl_number {

           2 => Box::new(Methods_2),
           0 => Box::new(Methods_0),
           1 => Box::new(Methods_1),

           _ => panic!("Unknown impl number: {}", impl_number),
        }
    }
}