5 releases
new 0.1.0-rc5 | Apr 7, 2025 |
---|
#597 in Procedural macros
34 downloads per month
Used in seal-the-deal
10KB
179 lines
::seal-the-deal
Attribute to use on the trait
methods (or associated functions) that you wish to "seal", a.k.a.,
render them final
.
It will be impossible for implementors to override the default implementation of that function.
Example
use ::seal_the_deal::with_seals;
#[with_seals]
trait SomeTrait {
/// Shall always return `42`.
#[sealed]
fn some_method(&self) -> i32 {
42
}
}
Attempting to override the default impl shall result in a compile error:
use ::seal_the_deal::with_seals;
#[with_seals]
trait SomeTrait {
/// Shall always return `42`.
#[sealed]
fn some_method(&self) -> i32 {
42
}
}
enum Evil {}
impl SomeTrait for Evil {
fn some_method(&self) -> i32 {
eprintln!("**manic laughter**");
27
}
}
with:
# /*
error[E0195]: lifetime parameters or bounds on method `some_method` do not match the trait declaration
--> src/_lib.rs:61:19
|
10 | #[sealed]
| ------ lifetimes in impl do not match this method in trait
...
19 | fn some_method(&self) -> i32 {
| ^ lifetimes do not match method in trait
error: aborting due to 1 previous error
# */ compile_error!("");
Implementation a.k.a. the macro secret magic sauce 🧙
Click to show
use ::seal_the_deal::with_seals;
#[with_seals]
pub trait SomeTrait {
/// Shall always return `42`.
#[sealed]
fn some_method(&self) -> i32 {
42
}
}
expands to:
mod __SomeTraitඞseal_the_deal {
pub trait Sealed<'__> {}
impl Sealed<'_> for () {}
}
pub trait SomeTrait {
/// Shall always return `42`.
fn some_method<'seal>(&self) -> i32
where
() : __SomeTraitඞseal_the_deal::Sealed<'seal>,
{
42
}
}
This approach does effectively and nicely seal that method from being overridden:
-
since the clause is trivially implemented for (at least one, in practice, all) lifetime(s), callers are not hindered by it.
- underspecified lifetime params are fine, they do not cause "inference ambiguity errors";
- this clause is
dyn
-compatible! - method being in the actual trait, it remains visible in the docs;
- a
() : Sealed<'seal>
will be visible, which is rather low-noise w.r.t. the semantics involved.
- a
-
and yet the clause appears to be complex/convoluted enough for Rust not to allow "looser" implementations like it sometimes does, hence the "lifetime mismatch" when people attempt to do actual impls.
-
technically-speaking, it is possible for a same-module or submodule-thereof to have enough visibility of
__SomeTraitඞseal_the_deal::Sealed
to be able to repeat, verbatim, the clause. This ought to be fine since:-
there is a
ඞ
in that path!! -
same-crate code is not really the threat/adversarial model, here, but rather, external code (be it for Semver or
unsafe
ty reasons). -
if this is really deemed to be a problem, just further encapsulate the whole thing in a helper private
mod
ule:pub use paranoid::SomeTrait; mod paranoid { use super::*; #[::seal_the_deal::with_seals] pub trait SomeTrait { /// Shall always return `42`. #[sealed] fn some_method(&self) -> i32 { 42 } } }
-
Alternative
Click to show
The usual approach to have a sealed/final
method like that is through a blanket-implemented super
trait
(a.k.a, the "super extension trait").
trait Trait : FinalMethodsOfTrait {
/* non-final methods here */
}
trait FinalMethodsOfTrait {
/// Shall always return `42`.
fn method(&self) -> i32 {
42
}
}
impl<T : ?Sized + Trait> FinalMethodsOfTrait for T {}
But this approach has two problems:
-
documentation-wise, it is ugly: the final
.method()
is no longer discoverable on the page forTrait
. -
Whilst both properly-
Trait
-bounded generic parameters anddyn Trait
s do let one very ergonomically call.method()
in a blissfully oblivious-to-the-super-extension-trait way, it turns out that concrete implementors do not let one perform such ergonomic calls directly: the super extension trait is expected to be in scope for the method call to succeed, totally shattering the magic, in my opinion.mod lib { pub trait Trait : FinalMethodsOfTrait { /* non-final methods here */ } pub trait FinalMethodsOfTrait { /// Shall always return `42`. fn method(&self) -> i32 { 42 } } impl<T : ?Sized + Trait> FinalMethodsOfTrait for T {} } use lib::Trait; struct Foo; impl Trait for Foo {} Foo.method(); // Error, `FinalMethodsOfTrait` not in scope! 😭
Error message:
# /* error[E0599]: no method named `method` found for struct `Foo` in the current scope --> src/_lib.rs:136:5 | 10 | fn method(&self) -> i32 { | ------ the method is available for `Foo` here ... 20 | struct Foo; | ---------- method `method` not found for this struct ... 24 | Foo.method(); // Error, `FinalMethodsOfTrait` not in scope! 😭 | ^^^^^^ method not found in `Foo` | = help: items from traits can only be used if the trait is in scope help: trait `FinalMethodsOfTrait` which provides `method` is implemented but not in scope; perhaps you want to import it | 2 + use lib::FinalMethodsOfTrait; | error: aborting due to 1 previous error # */ compile_error!();
Dependencies
~215–660KB
~16K SLoC