Using external(/user defined) struct in a library

Hello,

I'm developing a simulation program that is separated into a library (with the core functionalities) and a binary (to provide parameters and collect/process data). In the public interface of the library I would like to give access to monitor the very fine details of the current state, and allow to collect 'any' data in a user defined struct. So if later I (or other users) need other results of the simulation, it's enought to modify the binary.

The goal would be to allow the user of the library to define some struct(s) (to collect data and maybe interact with the simulation in a limited way) that has functions which are called at well defined points during the simulation (e.g. after every event or step, and at the end of the simulation). Something like:

//binary
pub struct Collections {
    data_field1 : DataType1,
    ...
}

impl Collections {
    pub fn end_of_event_collect(&mut self,  event: Event) {...}
}

//libraray
pub struct Event {...}
impl Event {
    pub fn get_data1(&self) -> DataType1 {...}
    fn new(...) -> Event {...}
    fn process(&mut self, ...) {...}
}
struct EventManager {
    collections: Collections,
    ...
}
impl EventManager {
    fn run_events (&mut self, number_of_events: u32) {
        for _ in 0..number_of_events {
            let event = Event::new(...);
            event.process(...);
            self.collections.end_of_event_collect(event);
        }
    }
}

So far I could think of 3 solutions:

  1. Separate the library into a core and user module, and put the data collection in the user module. But if later the users would like to write some different versions of the user module (for different applications), than the core should be copied for each of it. (Although currently the functionality of the program is limited, it might be extended later.)

  2. Propagate every event (and/or step) to the binary (e.g. with messages) so the library don't have to know about the Collections. This way the usage of the library would be more complicated, and might cause some serious overhead.

  3. Declare a Collections trait in the library, and use a trait object to collect the data inside the library. This seems to be the best solution from these, but still has some incoviniences. E.g. the user has to deal with the downcasting in the binary. And the (trait) object has to be created in the binary (than propaget through(/register in) the library).

So I'm asking if there might be any better solution(s)?

Solution:
I sticked with the trait objects. (3. option)

A solution I used before was to have the user code implement a trait with an associated "data collection" data type for their "system", as well as a method to collect said data. This can enable avoiding runtime dispatch as you would have with a trait object.

I've also used a system where I collect f64 values with string tags (interned) again using a trait method:

    /// Collect some data for the current state of the system if we
    /// want to do so.
    fn data_to_collect(&self, _iter: u64) -> Vec<(Interned, f64)> {
        Vec::new()
    }

For context see GitHub.

2 Likes

Hmm, the usage of this trait would be something like this?

//binary
struct MySystem {...}
impl System on MySystem {...}

fn main() {
    let my_system=MySystem{...};
    /* do some stuff with my_system */
    let data = my_system.data_to_collect(...);
}

Well, presumably the trait would be used by your library, so it would look more like:

trait CanCollect {
  type Collected;
  fn collect(&self) -> Self::Collected;
}

struct AlgorithmData<T: CanCollect> {
  data: T,
  collected: Vec<T::Collected>,
}
impl<T: CanCollect> AlgorithmData<T> {
  fn move(&mut self) {
    ...
    self.collected.push(self.data.collect());
  }
}

Add I recall there was not complication in dealing with the trait bounds...

1 Like

Not sure if this works for me. This way the description of the system should be done in the binary, and in the library you just apply some general methods on it. (E.g. some Monte-Carlo energy minimization of the given system.) So other functions should be defined as part of the trait, that can be called on AlgorithmData.data. (E.g. calculate_energy(&self) to calculate the current energy of the system.) Or am I wrong?

In my case there is a 'well defined' (up to some parameters) and complicated system to simulate, the deposition of aerosol particles in the human lung. The library is a concrete modell of this system.