Pre-RFC: Runtime reflection

In our system, we've built something very similar to what is proposed in this RFC. What we have is:

  • we have our own trait-object-based "reflection" API.
  • we've re-built serialization to use that API instead of using serde directly. Exactly the case mentioned in the RFC.
  • we use that API for other purposes, too (for example, validation against dynamic type schemas).

So, this RFC would be down our alley, right? Well, not quite, I don't think so. Here is why.

Based on my experience building this system, my biggest concern is how generalizable reflection, as a pattern, could potentially be?

In our case, we've build very targeted reflection API for the type of data we need to work with. Which among other things, means we don't have to deal with:

  • Which "base" types we could use. For example, serde base types are very limited -- if they work for you, you a good, if they don't -- well, sometimes it's not very easy to work around that limitation. For example, they way serde_json supports "arbitrary precision numbers" (which are not supported by serde data model directly) is a bit hacky right now (in my opinion) and actually gets into way of other serde features. The list of types in this RFC doesn't include "arbitrary precision decimal" (for valid reason, of course), so it will have to be represented as "struct" or something (so we would loose efficiency here).
  • Even though we use dynamic dispatch, we optimized it for exactly use cases we want. Types we work with are very regular, but have some interesting specific features. For example, instead of scanning "type system" for a field with a given name (and keeping some cache around to optimize these lookups), we can "ask" our data types directly via a sigle call with the signature fn get<'a>(&'a self, field_name: &str) -> FieldRef<'a>, where FieldRef would be an enum indicating the "kind" of the field (list? singular object? etc...) and a trait object to work with that "kind" of a field. This actually allowed us to do serialization / deserialization faster than we used to have when we used serde, so I'm not completely on board with implicit assumption that "static typing is always faster". Like yeah, it is faster, but there are nuances, so sometimes it is not (and also there are a lot of cases where performance trade-off is acceptable).
  • Variability in how types are represented in Rust vs what we want over the wire. This is another case where we can do better compared to off the shelf library like serde is that we can do slight adjustments between how we represent types in Rust (so they make sense to application developers) vs how these types are serialized over the wire (how we need JSON to look like). Plus, we can support more "difficult" formats like XML, which are hard to support in a generic library without a tons of attribute annotations (should field be an element? attribute? what about namespaces? etc...)
  • Other optimizations which I think are hard to do in a generic case. For example, we provide mutability API in our "reflection" API and one major assumption we make is that every data type we have is Default, so we can always create an "empty" field, if field wasn't initialized already. That simplifies API / deserialization code by a lot. Simply because you can create an empty "thing" and then fill it later. Without that assumption, you have to accumulate all the fields first and then create data type instance (and I have feeling that this is where we might be getting performance advantage -- since this would not require drop tracking for local variables -- not that I know anything about it!).

So, I think that even though this RFC is close to our use-case (let's call it "large enterprise application") as it ever could be, it's these details that make me think that no, it is not going to work for us.

What would help us? Little bit of an off-topic.

Things that, I think, would help us are smaller building blocks that we can use to create our "reflection" API, such as:

  • compile-time macroses for things like 1) field offsets 2) trait object vtables (yeah... there's a lot behind this "tiny" ask :slight_smile: )
  • maybe, some ABI stability for trait objects & such (so we can have types to be dynamically loadable)
  • probably, other little things that I'm forgetting about...
18 Likes