5 releases

new 0.1.0-rc5 Apr 7, 2025

#597 in Procedural macros

34 downloads per month
Used in seal-the-deal

Zlib OR MIT OR Apache-2.0

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.

Repository Latest version Documentation MSRV unsafe forbidden License CI no_std compatible

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.
  • 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 unsafety reasons).

    • if this is really deemed to be a problem, just further encapsulate the whole thing in a helper private module:

      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 for Trait.

  • Whilst both properly-Trait-bounded generic parameters and dyn Traits 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