Solid reflection in rust

I think rust is a very interesting. In many aspects it is better than other langs . I think a company could be almost ready to pass to create projects in rust lang but there is still a strong limit caused by reflection absense. The most important frameworks , library in enterprise world software are using reflection . It is mandatary in the modern software to use reflection. I think the first priority for adding new feature in language should be in this sense . I opened this topic for triggering rust developers and designers to give much more effort in this direction.

1 Like

This is pretty thoroughly covered in existing discussions, most recently Pre-RFC: Runtime reflection

IIRC the conclusions in that thread boiled down to "compile-time reflection is already mostly feasible with macro libraries, and there's no motivation for runtime reflection; the reasons Java/C#/etc have runtime reflection simply don't apply to Rust."

Perhaps most interesting was this post from an "enterprise" user of Rust who basically whipped up their own compile-time reflection stuff in-house and yet agrees for many reasons that it shouldn't be a core language feature:

In particular, the suggestions for improvement that post does make:

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)

are things actively being discussed by the community under other subject headers like "layout guarantees", since they're far more low-level than and would unlock a lot more use cases than just reflection code.

16 Likes

Reflection not embedded in language sounds good . Reflection implemented in a crate is a good idea . Reflection at compile time?uhmm i dont think is sufficient in the modern software . There are many cases where you need to add behaviour, features not at compile time but when the application is running. To handle many customizable features in application on demand-time is the tipical need in enterprise platforms . I dont think also at compile time you can add reflection if you apply it on already compiled crate. I m worried, rust with this choise will block its diffusion . So it will remain smart alternative for c++ , nothing else.

1 Like

This appears to be talking about plugins, which are a totally separate thing from runtime reflection. I believe Java/C#/etc typically use runtime reflection for plugins, but this is just another example of "those languages' reasons don't apply to Rust".

See Plugins in Rust · Michael-F-Bryan for details.

If plugins are not what you're trying to ask for, then please provide way more detail so we can understand you.

After compilation, there's no guarantee the type you want to reflect on even exists any more, so it's not clear what "runtime reflection" would even mean (without some opt-in component at compile-time, which then might as well just provide compile-time reflection instead).

So this is most likely a deeply confused statement and we're again going to need way more detail about what you're looking for to offer any meaningful response.

2 Likes

Please do some research. Many real-world use cases are covered by compile-time reflection in a better, more principled way than what's possible in other languages. Serde, Diesel, derive-able traits in general. Your abstract "it won't be good enough" argument is readily contradicted by their existence.

Furthermore, if you prefer working in C++ and not using Rust for this reason or another, that is perfectly fine.

11 Likes

Non only plugins , instrumentation for example is very used in modern languages , it permits to add bevaviour to the compiled code at runtime , native code generation based on specific struct defined by a runtime application(not tell me to write rust code in a file and then to compile it because it is not a serious solution ),....How do you solve it?

Again, you need to be far more specific and detailed for us to tell what you're trying to say.

I could make some additional guesses about what "add behavior ... at runtime" means and point out that there are loads of sanitizers and profilers and other tools which basically do that at the assembly level and thus work just as well on Rust as on C/C++. But again, I simply have no idea if that's what you're talking about or not. Help us help you.

instrumentation and code generation are not concept invented by me . They are known and used for many decades . I think you have to study a bit about it. To be critical in general ok but you need also to evaluate and weight the fact if thousands of engineers used a solution ... maybe there are cases where it is necessary

I think the combo of macros and dylibs should be enough to suit any need that other languages would use reflection for... so it's not clear what you feel is lacking. You just say that "instrumentation and code generation are important" but you don't explain why Rust doesn't solve these issues.

2 Likes

if you think rust can solve , explain you how to do this. Instrumentation and code generation is known. I cant expain computer science concepts here. There are books and manuals

The thing other people are trying to express is: it’s possible to instrument code and generate code at compile time in Rust, via its macro system. In particular, many of the things that use reflection in C♯ and other languages not only can be done but are done in Rust—via procedural macros! Are you familiar with all the kinds of things that Rust’s macros can do? If so, can you give a specific example of a thing you would like to be able to do but find that you cannot? Because “code generation” and “instrumentation” are already things you can do in Rust—just in a different way than you would in other languages. People here understand the broad strokes; it’s the details that matter. :slight_smile:

12 Likes

yes you can deassembly a struct in memory , inject a method on the fly in the struct or modify a method already present. In this way you can change the normal bevaviour of a struct . The code generation is used to generate dinamically struct defined at runtime time. Suppose you want create a struct with specific attributes based on a user form for handling with high performance dynamic structure. This concepts are used also for AOP programming. The fact is at compile time you can use a library defined by others developers , you havent the source code or you have but for you it is a black box . When you work in a big project you cant imagine to modify external libraries you dont know But it is not important for you . You can just add a additional feature to logic without touch the code . You work using formal logic for modify the behaviour. Instrumentation is used also for patch modules (also multiple module in the same time). For example you have a complex application using many external libraries using a common mutex. You can change the mutex behavour without touching the logic of libraries. Many framework use code generation for create optimized code based on your configuration read at runtime. Yes the generic idea to use the power of rust macro for reflection is very intelligent. But it is not the solution for all.

I think you underestimate the power of macros. In particular, procedural macros are basically just normal Rust code that you can do anything with (read files, talk the the network), if you want to dynamically load in things modules, that's what dylibs are for. Everything else can be done in build scripts (which I see as light weight codegen), which is also just ordinary Rust code.

This is a bad example, because you don't need reflection for this at all. This is just pulling in a library. (see parking_lot for an example of decoupling the mutex implementation from the usage). But you can't change the implementations of mutexes used in upstream libraries, as that could undermine the safety of those libraries who depend on their implementations for safety.


Note: Rust may not do things in the exact same way, but that's fine. There are other ways to solve the same problems.

You can do something similar with dylibs, (see the plugin article that was linked earlier).

This can't be done, all structs must be defined at compile time. Instead you could use something like HashMap<String, Box<dyn Any>>, for a dynamically defined struct. (or a wrapper around this). This is similar to how other langauges internally represent structs. (maybe swap Box for another smart pointer depending on how you use it).

This is done with proc-macros/build scripts

I don't know what your getting at here.

3 Likes

You are not yet understanding :frowning:

You can do something similar with dylibs, (see the plugin article that was linked earlier). I dont think is the same , in your case read on the fly it seems you are loading dinamically a library , ie, it is a code already defined , in my case the code is written at runtime , not compile time.

i found finally a library doing it . A developer understanding exists ,ahahaah https://docs.rs/reflect/0.0.7/reflect/ but it is not solid yet

1 Like

I'm confused,the library doesn't do anything at runtime:

Once again the reflection API is just a means for defining a procedural macro. Despite what it may look like below, everything written here executes at compile time. The reflect library spits out generated code in an output TokenStream that is compiled into the macro user's crate. This token stream contains no vestiges of runtime reflection.

5 Likes

This seems to be a lot of the disconnect here: Rust is really not designed for working with binary/obfuscated-source dependencies, and for good reason. Java allows you to do a lot by runtime decompiling library bytecode, even if the vendor intended not to provide them in readable form; I believe a lot of the Rust ABI stability pushback is to prevent being stuck developing in "black box" scenarios as a supported use-case.

1 Like

Even more fundamentally, Rust deeply and pervasively relies on zero-cost abstractions to achieve most of the high-level properties that made us care about the language in the first place ("fast, reliable, productive: pick three."). But you simply cannot do that while preserving all of the type information that Java/C#/etc bytecode does, which is critical to doing things like runtime reflection against precompiled libraries you can’t change the source of.

(this is what I was getting at earlier when I mentioned that types aren’t even guaranteed to exist at runtime; many types can’t be zero-cost abstractions if we forbade the optimizer from optimizing away their existence)

8 Likes

It add extra info in struct data permitting to read metadata associated to struct at runtime. The final result is that you can make reflection at runtime the data struct. But code generation is more complex to do in rust, you have to pass trough the llvm, anyway it is possible . The instrumentation is the most complex task to do in rust. There are 2 ways or implementing a runtime bytecode engine or a online engine similar to macro engine but a lot more powerful than the current does

For a person who wrote a LOT of C#, I understand perfectly what you're asking. However, Rust is different enough from C#/Java that what you're looking for may not make sense, and that has caused a lot of the confusion.

In my experience, reflection is commonly used to do the following:

Dynamic instantiation

Instantiate a new object (type in Rust) in a late-bind manner. For example, given a text string containing the type's full path name, find the type in any loaded library/assembly, and create an instance of it, optionally choosing a constructor method and passing parameters. Notice that there is no compile-time knowledge of what that class may be or whether it resides in the current program or is something that a user wrote as an add-on (usually it is the latter).

This is extremely useful for highly-configurable software (aka "enterprise" software) because you can basically extend the functionalities of the core system by 1) writing a plugin module/assembly/library, 2) load it, 3) create an object based on some text name, 4) as long as that object implements some system interface..., 5) hook it up to the system via said interface. The system does not even need know about these extensions; it only works with objects implementing a standard interface.

I believe this is reasonably covered by Rust using dynlib and having user types implement a standard trait. Rust cannot instantiate a type directly given its name, but it can do it via a factory function that each plugin library must export. That function can, given a name, create a trait object of the required type. In fact this is how Windows's COM does it to allow itself to interop with C#.

Dynamic discovery of behaviors via metadata

When faced with a type, determine what behaviors it supports by examining the set of attributes, methods, properties and/or interfaces it supports. This set can be obtained via reflection API's. Invoke supported behaviors by calling such methods/properties/interfaces, passing parameters.

Combined with dynamic instantiation, this becomes a complete system of late-bound extensions. Again, this is extremely flexible for highly-configurable "enterprise" software.

Rust doesn't have the same concept. In fact, as mentioned, the functions in question may not even exist in the compiled binary -- they may have been inlined away. You can still achieve exactly the same thing by providing a metadata function that all external library/assembly/module must export. In fact, this is how Windows RT does it via the wmd files to expose its API to C# reflection, but you must be prepared to write a lot of wrappers and shims to convert a metadata call to an actual function call. Usually these are machine-generated.

Dynamic extension of behaviors

Some languages allow extension of compiled behaviors. Most don't, though. For instance, AOP is really hard to do in C# because it is a compiled language and there is no monkey-patching; people do AOP via rewriting the MSIL instructions.

Still, some behaviors can be lazy-added. For example, "extension methods" in C# allow users to add methods to a "black-boxed" library type without knowing the internal implementation. These extension implementations have no access to the internals of the type, and they are there only to mimic a method. One can easily define a normal function to do exactly the same thing, but you can't build a "fluent" API via function calls, can you?

However, you'll find that Rust actually supports extension methods in a major way -- in fact, all of Rust's type methods are actually "extension methods" in the C# manner. It is so powerful that Rust actually has to manually prohibit users from doing it (i.e. the rule that you cannot extend a type with a trait both not defined in the same crate); and it also has to disallow users from impl-ing a type that is not defined in the same crate.

This restriction in functionality is to prevent exactly the same issues plaguing C#'s extension methods -- when the library updates with new methods, it breaks user code due to method conflicts. Rust has a simple way to get around this: the newtype pattern.

Dependency injection

All that easy reflection power combines to give us DI containers and IOC systems, very popular among highly-configurable enterprise software.

29 Likes