Search code examples
rustconditional-compilation

Sharing data between a library module and its test module


I have a pathfinding algorithm library, and a test module that graphically displays every step the algorithm takes. The idea is that one should be able to conditionally compile either a test for the algorithm, or the library. So the algorithm object should remain lean and not store nor return any data that wouldn't be needed outside of testing.

At the moment I'm using a text file as intermediate storage, writing the steps into it whenever they happen. The test module read them from there when the search is done.

The test module is a child of the algorithm module.

Is there a better intermediate storage? Some way to get a static mutable vector perhaps? I also read something about task local storage but it's not documented well.

Edit:

Unfortunately, something like this doesn't seem to work:

pub struct JumpPointSearch {
    closed_set: Vec<Node>,
    open_set: PriorityQueue<Node>,
    #[cfg(demo)] steps: Vec<(Point2<uint>,Point2<uint>)>
}

impl JumpPointSearch {
    pub fn new() -> JumpPointSearch {
        if cfg!(demo) {
            JumpPointSearch {
                closed_set: Vec::with_capacity(40),
                open_set: PriorityQueue::with_capacity(10),
                steps: Vec::new()
            }
        } else {
            JumpPointSearch { // error: missing field: `steps`
                closed_set: Vec::with_capacity(40),
                open_set: PriorityQueue::with_capacity(10),
            }
        }
    }
}

This also doesn't work:

pub struct JumpPointSearch {
    closed_set: Vec<Node>,
    open_set: PriorityQueue<Node>,
    #[cfg(demo)] steps: Vec<(Point2<uint>,Point2<uint>)>
}

impl JumpPointSearch {
    pub fn new() -> JumpPointSearch {
        JumpPointSearch {
            closed_set: Vec::with_capacity(40),
            open_set: PriorityQueue::with_capacity(10),
            #[cfg(demo)] steps: Vec::new() 
            // error: expected ident, found `#`
        }
    }
}

Solution

  • For conditional definitions like this, you need to use the attribute form, not the macro. That is

    #[cfg(demo)]
    pub fn new() -> JumpPointSearch {
        JumpPointSearch {
            closed_set: Vec::with_capacity(40),
            open_set: PriorityQueue::with_capacity(10),
            steps: Vec::new()
        }
    }
    #[cfg(not(demo))]
    pub fn new() -> JumpPointSearch {
        JumpPointSearch {
            closed_set: Vec::with_capacity(40),
            open_set: PriorityQueue::with_capacity(10),
        }
    }
    

    The cfg! macro just expands to true or false depending if the configuration matches, that is, neither branch is eliminated, and type checking (etc.) still happens for both, which is why it doesn't work: one of the struct initialisations doesn't match the definition.


    However, I think there's higher-level approachs to this, they're both pretty similar, but the gist is to have the JumpPointSearch implementation the same always, and to just have the steps field change.

    True generics

    struct ReleaseSteps;
    
    struct DebugSteps {
        steps: Vec<(Point2<uint>, Point2<uint>)>
    }
    
    trait Step {
        fn register_step(&mut self, step: (Point<uint>, Point<uint>));
    }
    
    impl Step for ReleaseSteps {
        #[inline(always)] // ensure that this is always 0 overhead
        fn register_step(&mut self, step: (Point<uint>, Point<uint>)) {}
    }
    
    impl Step for DebugSteps {
        fn register_step(&mut self, step: (Point<uint>, Point<uint>)) {
            self.steps.push(step)
        }
    }
    
    struct JumpPointSearch<St> {
        closed_set: Vec<Node>,
        open_set: PriorityQueue<Node>,
        steps: St
    }
    
    impl<St: Step> JumpPointSearch<St> {
        fn new(step: St) -> JumpPointSearch<St> { ... }
    }
    

    Then, whenever you would've updated steps before, just call register_step: if it's in "release mode", that will be ignored, and if it's in "debug mode", it will be registered.

    This has the unfortunate downside of requiring generics etc. (could be ameliorated with careful use of; - cfg e.g. #[cfg(demo)] impl JumpPointSearch<DebugSteps> { fn new() -> JumpPointSearch<DebugSteps> { ... } } + the corresponding #[cfg(not(demo))] with ReleaseSteps. - default type parameters, e.g. struct JumpPointSearch<St = ReleaseSteps> { ... } (these are still experimental)

    This approach allows you to easily extend the diagnostics by providing more hooks in the Steps trait (e.g. you could provide an option to record even more expensive-to-compute information, and have three levels of Step: release, debug and super-debug).

    cfg "generics"

    The other option is to use the types defined in the previous section with cfg to select DebugSteps or ReleaseSteps in a fixed way, that is:

    #[cfg(demo)]
    use Steps = DebugSteps;
    #[cfg(not(demo))]
    use Steps = ReleaseSteps;
    
    struct JumpPointSearch {
        closed_set: Vec<Node>,
        open_set: PriorityQueue<Node>,
        steps: Steps
    }
    

    If you don't need the extensibility and are happy just making a fixed selection at compile time, this is my recommended approach: you don't need many #[cfg]s at all (just the two above, and possibly 2 more, depending on how you handle extracting data from the steps field) and you don't have to throw a lot of generics around.

    Two small points:

    • using use like that allows Steps::new() to work, and means you can write Steps whereever you need the type. (The two other possibilities have issues: using the struct fields, i.e. #[cfg(demo)] steps: DebugSteps, fails both; and using #[cfg(demo)] type Steps = DebugSteps; doesn't (yet) allow Steps::new().)
    • you can also drop the Steps trait for this one, and just impl the method directly, that is: impl ReleaseSteps { fn new() -> ReleaseSteps { ... } fn register_steps(...) {} } and similarly for DebugSteps.