Newbie to Rust here, though I have spent quite some time in low-level / kernel driver / embedded C and have some light experience with C++11 and newer. I suspect part of my problem will be un-learning old habits.
How would I implement a type-safe method that constructs objects which implement a give trait yet require a generic parameter themselves?
This project in question is a simple MQTT to Prometheus bridge; the goal here is to be able to parse a config file and then call register_metric
on each configured item to return an object which the MQTT side can later use to update the value (via the UpdateFromString
trait) when a publish comes in. Consider (trimmed for brevity):
enum MetricType { IntegerCounter, FloatCounter, IntegerGauge, FloatGauge }
pub trait UpdateFromString {
fn update_from_string(&self, value: String);
}
impl UpdateFromString for prometheus::Counter {
fn update_from_string(&self, value: String) {
self.inc();
}
}
// snip - impl UpdateFromString for other Prometheus types we support
struct PrometheusEndpoint { ... }
impl PrometheusEndpoint
{
fn new(runtime: std::rc::Rc<tokio::runtime::Runtime>) -> Result<PrometheusEndpoint, i32> { ... }
async fn metrics_handler(registry: std::sync::Arc<prometheus::Registry>) -> Result<impl warp::Reply, std::convert::Infallible> { ... }
fn register_metric(&self, name: String, desc: String, mtype: MetricType) -> Box<impl UpdateFromString>
{
let metric: Box<dyn std::any::Any>;
// Instantiate raw metric type
match mtype {
MetricType::IntegerCounter => metric = Box::new(prometheus::IntCounter::new(name, desc).unwrap()),
MetricType::IntegerGauge => metric = Box::new(prometheus::IntGauge::new(name, desc).unwrap()),
MetricType::FloatCounter => metric = Box::new(prometheus::Counter::new(name, desc).unwrap()),
MetricType::FloatGauge => metric = Box::new(prometheus::Gauge::new(name, desc).unwrap()),
_ => panic!("Unknown metric type"),
};
// Register metric with registry
// NOTE: After this completes, this metric can potentially be scraped by the remote Prometheus instance
self.registry.register(metric.clone()).unwrap();
// We implement UpateFromString on the Prometheus base types above so we can just return them directly.
// This is safe since Rust won't let us use any functions not specified via our trait so a container
// object is just redundant.
metric
}
}
I define my own input enum to hide Prometheus from the rest of the application which I think might be part of the problem. If I did this fully statically (i.e. fn register_metric<T>(&self, name: String, desc: String) -> Box<impl UpdateFromString>
) this is pretty trivial but requires leaking the raw Prometheus types to the config parsing logic. It also doesn't limit me to the Prometheus types only.
The other way would be to use dynamic dispatch, which is what I attempted above. That said, I am not sure how to declare the proper types to make the compiler happy. Untimately, it appears to be unhappy that I doesn't know the final storage size at compile time. this SO post seems to indicate that std::any::Any
should work and Box<Any>
makes sense to my C-brain as a type-safe version of C's struct foo myvar = *((struct foo *)malloc(sizeof struct foo))
. Unfortunately, this still gives unknown size at compile time errors; which makes sense given that the Box will have the size of at least the inner type which is now variable.
AFAICT, even a Rc
/ Arc
won't work as that signature would have to be something like Rc<Box<foo>>
which just brings the whole different-sized type thing one level deeper.
So, as stated above, what it a type-safe way to construct and return one of a supported list of types which implement a given trait and accept generic parameters?
Some final notes:
Box
in required by Prometheus when registering new metrics.registry
object is created in new
; the use of Box.clone()
is copy-pasts from the Prometheus examples (though I don't fully appreciate why this is necessary).unsafe
if at all possible.What you want is type erasure. You are partially achieving it by returning a impl Trait
existential type, but to go all the way there you gotta use a dynamically dispatched object, dyn Trait
, behind some kind of indirection. Which, luckily, you already have in the form of Box
.
Here's what I propose your register_metric
should look like:
fn register_metric(&self, name: String, desc: String, mtype: MetricType) -> Box<dyn UpdateFromString>
{
// Instantiate raw metric type
let metric = match mtype {
MetricType::IntegerCounter => Box::new(prometheus::IntCounter::new(name, desc).unwrap()),
MetricType::IntegerGauge => Box::new(prometheus::IntGauge::new(name, desc).unwrap()),
MetricType::FloatCounter => Box::new(prometheus::Counter::new(name, desc).unwrap()),
MetricType::FloatGauge => Box::new(prometheus::Gauge::new(name, desc).unwrap()),
_ => panic!("Unknown metric type"),
};
// Register metric with registry
// NOTE: After this completes, this metric can potentially be scraped by the remote Prometheus instance
self.registry.register(metric.clone()).unwrap();
// We implement UpateFromString on the Prometheus base types above so we can just return them directly.
// This is safe since Rust won't let us use any functions not specified via our trait so a container
// object is just redundant.
metric
}