rust - 如何将“元数据”分配给特征?
问题描述
我有 2 个宏。第一个是special_trait
用于 trait 声明的属性宏,第二个useful_macro
是用于此类 trait。
也就是说,用户代码将编写:
#[special_trait]
pub trait MyTrait{}
// meanwhile, in a different file...
use some_mod::MyTrait;
useful_macro!(MyTrait);
现在,special_trait
宏需要以可以使用它MyTrait
的方式分配一些元数据。useful_macro
这可能吗?如何?
可能但次优的解决方案
我突然想到,我可以要求所有用户代码指定特征的完整路径,而不是依赖use
:
#[special_trait]
pub trait MyTrait{}
// meanwhile, in a different file...
useful_macro!(some_mod::MyTrait);
然后,special_trait
只需要定义一个pub const MyTrait_METADATA: i32 = 42
,并且useful_macro
可以找到这个元数据 const 的路径,因为它有完整的some_mod::MyTrait
路径,并且只需要更改最后一段:some_mod::MyTrait_METADATA
。
但是,禁止use
和要求完整路径似乎很卑鄙,如果有更好的方法,我不想这样做。
我可以将元数据常量与特征关联起来,使任何对特征具有“访问权限”的宏也可以找到元数据吗?
解决方案
Rocket v4 也有同样的问题:
当在根以外的模块内声明路由时,您可能会在挂载时发现意外错误:
mod other { #[get("/world")] pub fn world() -> &'static str { "Hello, world!" } } #[get("/hello")] pub fn hello() -> &'static str { "Hello, outside world!" } use other::world; fn main() { // error[E0425]: cannot find value `static_rocket_route_info_for_world` > in this scope rocket::ignite().mount("/hello", routes![hello, world]); }
这是因为路线!宏将路由名称隐式转换为 Rocket 代码生成生成的结构名称。解决方案是使用命名空间路径来引用路由:
rocket::ignite().mount("/hello", routes![hello, other::world]);
在 Rocket v5(目前只有一个候选版本)中,这种情况不再发生。例如,使用 Rocket v5 编译:
#[macro_use]
extern crate rocket;
mod module {
#[get("/bar")]
pub fn route() -> &'static str {
"Hello, world!"
}
}
use module::route;
fn main() {
rocket::build().mount("/foo", routes![route]);
}
在上面运行cargo-expand
时,我们看到 Rocket 生成了这样的东西(由我缩写):
#[macro_use]
extern crate rocket;
mod module {
pub fn route() -> &'static str {
"Hello, world!"
}
#[doc(hidden)]
#[allow(non_camel_case_types)]
/// Rocket code generated proxy structure.
pub struct route {}
/// Rocket code generated proxy static conversion implementations.
impl route {
#[allow(non_snake_case, unreachable_patterns, unreachable_code)]
fn into_info(self) -> ::rocket::route::StaticInfo {
// ...
}
// ...
}
// ...
}
// ...
应用于函数的get
属性宏构造一个与函数同名的新结构。该结构包含元数据(或者,更准确地说,包含一个into_info()
返回具有正确元数据的结构的函数——尽管这更多地是 Rocket 使用的实现的细节)。
这是因为函数声明存在于 Value Namespace 中,而 struct 声明存在于 Type Namespace中。use
声明同时导入.
让我们将此应用于您的示例:您的特征声明位于类型命名空间中,就像结构一样。因此,虽然您不能让special_trait
宏声明与特征具有相同名称的结构,但您可以让该宏声明一个返回包含元数据的结构的同名函数。然后可以调用此函数useful_macro!
来访问特征的元数据。因此,例如,元数据结构可能如下所示:
struct TraitMetadata {
name: String
}
然后,您的宏可以扩展如下:
mod other {
#[special_trait]
pub trait MyTrait{}
}
use some_mod::MyTrait;
fn main() {
useful_macro!(MyTrait);
}
对此:
mod other {
pub trait MyTrait{}
pub fn MyTrait() -> TraitMetadata {
TraitMetadata {
name: "MyTrait".to_string()
}
}
}
use other::MyTrait;
fn main() {
do_something_with_trait_metadata(MyTrait());
}
这种设计只有一个问题:如果用户声明了一个与 trait 同名的函数(或存在于值命名空间中的任何其他内容),则此操作将失败。然而:
- 在惯用的 Rust 中,函数名称是
snake_case
trait 名称CamelCase
,因此如果用户使用惯用标识符,他将永远不会拥有与 trait 使用的名称相同的函数。 - 即使用户使用非惯用名称,对特征和函数使用相同的标识符也只是自找麻烦。我怀疑任何人(好吧,除了宏作者之外的任何人)都会这样做。
因此,这可能导致冲突的唯一现实方式是另一个宏作者也在使用此构造将元数据附加到特征,并且用户将您的属性宏和另一个宏应用于同一特征。在我看来,这是一个极少发生的极端案例,不值得支持。