5个不稳定版本

0.3.0 2023年7月27日
0.3.0-alpha.12023年7月25日
0.2.1 2023年6月11日
0.2.0 2023年2月9日
0.1.0 2023年2月5日

#562过程宏

Download history 29/week @ 2024-04-07 86/week @ 2024-04-14 22/week @ 2024-04-21 41/week @ 2024-04-28 440/week @ 2024-05-05 38/week @ 2024-05-12 104/week @ 2024-05-19 103/week @ 2024-05-26 93/week @ 2024-06-02 123/week @ 2024-06-09 33/week @ 2024-06-16 86/week @ 2024-06-23 30/week @ 2024-06-30 161/week @ 2024-07-07 179/week @ 2024-07-14 166/week @ 2024-07-21

每月536次下载
9 个crates中使用(通过 portrait-codegen

Apache-2.0

26KB
514

portrait

GitHub actions crates.io crates.io docs.rs GitHub GitHub

用默认值、委托等方式填充 impl-trait 块。

动机

Rust 特征支持提供的方法,这对于向后兼容性和实现编码效率非常出色。然而,它们也带来了一些限制

  • 如果关联函数的返回类型是关联类型,就没有合理的实现方式。
  • 如果一个特征包含许多高度相似的相关函数,编写默认值会涉及大量的样板代码。但是,用户只能通过过程宏为每个方法提供一个默认实现。

使用 portrait,默认实现是在 impl 级别粒度提供的,而不是特征级别。

用法

首先,使用 #[portrait::make] 属性创建要实现的特征的画像

#[portrait::make]
trait FooBar {
  // ...
}

部分实现特征,其余的留给 #[portrait::fill] 属性

#[portrait::fill(portrait::default)]
impl FooBar {}

portrait::default 部分是“填充宏”的路径,它实际上是填充 impl 块的项目。语法类似于 #[derive(path::to::Macro)]

如果不同模块中有实现,需要手动将用于特征项的导入传递给 make 属性

#[portrait::make(import(
  foo, bar::*,
  // same syntax as use statements
))]
trait FooBar {...}

如果填充属性因未定义的错误而失败,关于 foo_bar_portrait,请手动导入它以及 FooBar 特性;#[portrait::make] 属性在 FooBar 特性相同的模块中生成这个新模块。

提供的填充器

portrait 提供以下填充宏

  • default:通过委托到 Default::default()Default 是 const-unstable 并需要 nightly,以及 #![feature(const_default_impls)])实现每个缺失的方法和常量。
  • delegate:将每个缺失的方法、常量和类型代理到表达式(通常是 self.field)或实现相同特性的其他类型。
  • log:使用方法参数调用一个类似 format! 的宏。

这是如何工作的

Rust 宏在编译链的早期阶段被调用。因此,属性宏只能访问它们应用的项目字面表示,并且跨项目派生不是直接可能的。大多数宏通过尝试生成无论不可访问的信息如何都能工作的代码来避免这个问题,例如,Default derive 宏通过调用 Default::default() 来工作,而不检查实际字段类型是否实现了 Default(因为编译器会在稍后的阶段进行检查)。

遗憾的是,这种方法在 portrait 的用例中不起作用,其中属性宏需要编译时(过程宏运行时)访问 impl 块中引用的特性的项目;唯一可用的信息是特性的路径(甚至可以将其重命名为不同的标识符)。

portrait 通过要求特性提前以标记流的形式导出其信息(其“肖像”)来应对这一挑战。通过 #[portrait::make] 属性,派生了一个具有相同标识符的 声明性 宏,其中包含特性项。然后,impl 块上的 (#[portrait::fill]) 属性将输入传递给声明性宏,该宏随后将它们转发到实际的属性实现(例如 #[portrait::make]),包括特性项。

现在实际属性已经可以访问特性和用户实现,但这并不是故事的结束。特性项是在特性定义的范围内编写的,但属性宏输出是在实现定义的范围内。最明显的影响是特性模块中的导入对实现输出不起作用。为了避免由于特性模块的变化而频繁更新实现者,#[portrait::make] 属性也导出一个模块,该模块包含特性中使用的导入,这些导入将自动导入到实现块中。

事实证明,由于当前编译器的限制,即使导入的类型是公共的,私有导入实际上也不能公开重新导出,因此自动扫描特性项以查找重新导出路径变得不切实际(预定义类型也需要特殊处理,因为它们不是模块的一部分)。因此,不可避免地将异构作用域的问题暴露给用户:要么通过导入手动对所有所需的重新导出进行类型化,要么使导入在更广泛的范围内可见。

另一个困难是模块标识符与特性标识符共享相同的符号空间(因为 module::FooTrait::Foo 无法区分)。因此,包含导入的模块不能与特性一起导入,除非通过其封装模块引用特性,用户必须手动导入/导出这两个符号。

免责声明

portrait 并不是第一个在属性中使用声明性宏的工具。 macro_rules_attribute 也实现了类似的想法,尽管没有涉及生成 macro_rules! 部分的框架。


lib.rs:

开发 portrait 填充宏的框架。

示例

依赖关系

~0.5–1MB
~23K SLoC