rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.82k stars 1.55k forks source link

Proposal: Std: Compile-time reflection. #2743

Open tema3210 opened 4 years ago

tema3210 commented 4 years ago

Goal is to have ability to generate descriptive struct by macro(from std).

Reason is that in lot of crates there are impementations of compile time reflection not compatible with each other, so many developers just reinventing wheel.

Proposed features:

  1. The Reflector trait in std.
  2. Implementation of derive macro.
  3. Unsafe fn's to acess fields by name.
  4. (Optional) Mechanism to store metadata of/for fields.

P.S I won't propose implementation, because i can't imagine brige between compile time generics and runtime logic.

daboross commented 4 years ago

How different would your proposed syntax and features be from https://github.com/dtolnay/reflect?

tema3210 commented 4 years ago

How different would your proposed syntax and features be from https://github.com/dtolnay/reflect?

Well, issue should be named "Compile-time introspection". My mistake.

Consider an example: `

[derive(Reflection)]

struct MyStruct {
a: u64, b: i32, //etc } `

This should generate a static hashmap with string key naming the field and value storing the offset, (optional) metadata, attach this static data to struct via fn returning refence to hashmap.

Value in hashmap should store offset: u64 and (optional) metadata String(or parsed one, HashMap<String,String>). Consider a trait ReflectField with asociated types and methods returning offset, metadata.

So acessor function should look like:

unsafe fn GetField<S,F>(trg: &mut S,desc: ReflectField<F>)->(&mut F,HashMap<String,String>) {//impl}

Benefit from adding it in Std is that required reflection package which is present in most crates connected with SERialization (and DEserialization) will gone, instead of them all there will be one standart reflection package.

SOF3 commented 4 years ago

Why should field accessors be unsafe? It is not apparent to me that accessing fields directly would lead to undefined behavior.

tema3210 commented 4 years ago

@SOF3 because the struct whose field we want to change is taken by mut ref, it means that all introspection related things must be done in the same context, to avoid thing like changing field of unexisting struct. Internally this should work like "we take mut ref to struct we are changing, then simply adding offset and returning mut ref to field inside of struct", so gotten ref must not outlive mut ref to struct nor struct itself. Alternatively, we can use getter and setter patterns

tema3210 commented 4 years ago

Alternatively, we can use getter\setter pattern to create interface like pub fn get<'a,T: Reflectable ,D>(trg: &'a mut T,name: String, data &'a mut D)->bool. (set has exactly the same signature) No unsafe in case of getter\setter pattern(lifetimes is gurantee of safety).

Internally we would need to check type match as well as existance of field. We return bool to say whether data been transferred.

UPD: Reflectable trait: pub trait Reflectable{ fn getFieldsVault()->&'static HashMap<String,Descriptor>; } Descriptor struct: pub struct Descriptor{ offset: isize, type_id: TypeId, }

daboross commented 4 years ago

Sorry I didn't reply to your initial reply!

I think this proposal has promise, but I'm unsure whether it should be implemented as an RFC first and foremost.

Something like #[derive(Reflection)] struct MyStruct { a: u64, b: i32, //etc } like you describe, could definitely be implemented in an external crate. Derives are stable, and using something like https://docs.rs/field-offset/0.1.1/field_offset/macro.offset_of.html or the proposed std offset_of! macro I think the metadata could definitely be provided.

If this was implemented as a separate crate, then it could be tested and refined - and then potentially added to std if that seemed useful. Or it could just live as an external crate, and be used as one?


The other thing you mention, replacing other kinds of reflection (like that in serde), sounds nice but also not necessarily like a positive? Serde-style reflection based on derive macros makes code which can be optimized per-struct. It's super fast, and I don't see how we could achieve that same speed if figuring out the fields of a struct required a method call.

This proposal could be added in addition to other methods of compile-time reflection, but would it really be a good replacement for most others? If it requires calling a method and traversing a data structure, I can't see that being as fast as code which already knows all the fields.

tema3210 commented 4 years ago

Sorry I didn't reply to your initial reply!

I think this proposal has promise, but I'm unsure whether it should be implemented as an RFC first and foremost.

Something like #[derive(Reflection)] struct MyStruct { a: u64, b: i32, //etc } like you describe, could definitely be implemented in an external crate. Derives are stable, and using something like https://docs.rs/field-offset/0.1.1/field_offset/macro.offset_of.html or the proposed std offset_of! macro I think the metadata could definitely be provided.

If this was implemented as a separate crate, then it could be tested and refined - and then potentially added to std if that seemed useful. Or it could just live as an external crate, and be used as one?

The other thing you mention, replacing other kinds of reflection (like that in serde), sounds nice but also not necessarily like a positive? Serde-style reflection based on derive macros makes code which can be optimized per-struct. It's super fast, and I don't see how we could achieve that same speed if figuring out the fields of a struct required a method call.

This proposal could be added in addition to other methods of compile-time reflection, but would it really be a good replacement for most others? If it requires calling a method and traversing a data structure, I can't see that being as fast as code which already knows all the fields.

I have implementation which still need to have a macro to create Descriptor struct, requires the RFC 1849 to be implemented(type checking is done with TypeId::of<>()), addtitionally inside there a static HashMap which must be createn in compile time, i.e it's const item. I think, one day this interface will be able to work with static strings just like they were const generic items.

tema3210 commented 4 years ago

I have interface + realization(not tested): lib.rs.zip Also i haven't implemented macros yet.

tema3210 commented 4 years ago

This proposal could be added in addition to other methods of compile-time reflection, but would it really be a good replacement for most others? If it requires calling a method and traversing a data structure, I can't see that being as fast as code which already knows all the fields.

We can implement it in a way suitable for const evaluating. Actually, some macro assembled const functions to check whether field is present, if yes, which offset it has, which TypeId?, etc. Many things can be done at compile time, we should use this. However, my approach is heavily based on const-fn s, which are not stable at the moment of writing.

daboross commented 4 years ago

For anyone who comes across this, it's worth noting that a lot of discussion of a similar feature happened in https://internals.rust-lang.org/t/pre-rfc-runtime-reflection/11039. I'm not 100% sure whether that's a direct continuation of this issue or not.

I don't have anything to add beyond linking that thread, but I think the issues raised in the last few posts there, especially the ones by haohou and idubrov, should definitely be resolved before we can move forward on this (or not).

jxsl13 commented 3 years ago

Go uses reflections for creating a JSON string from any defined struct. Solely of attributes that are publicly accessible. I like that. Opt-in reflections would be nice.

tema3210 commented 3 years ago

Go uses reflections for creating a JSON string from any defined struct. Solely of attributes that are publicly accessible. I like that. Opt-in reflections would be nice.

It will be waste of resources, since go's reflection is used to do ADT, introspection for SerDe analogs, etc. The primary use case in rust is to simplify and unify introspection. Also, due to generics introspection of everything will bloat binaries with metadata; And it is unclear how to handle generic structs (sadly, my signatures don't handle it at all).