#sealed #methods #override #final #traits #seal

seal-the-deal

Attribute to use on the trait methods (or associated functions) that you wish to “seal”, a.k.a., render them final

9 releases

new 0.1.1 Apr 8, 2025
0.1.0 Apr 7, 2025

#985 in Rust patterns

Download history 686/week @ 2025-04-02

686 downloads per month

Zlib OR MIT OR Apache-2.0

36KB
112 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 shall 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
    }
}

struct 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
# */

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 Seal<'__> {}
    impl Seal<'_> for () {}
}

pub trait SomeTrait {
    /// Shall always return `42`.
    fn some_method<'sealed>(&self) -> i32
    where
        () : __SomeTraitඞseal_the_deal::Seal<'sealed>,
    {
        42
    }
}

This approach does effectively and nicely seal that method, preventing it from being overridden:

  • since the clause is trivially implemented for for at least one lifetime (in practice all of them), callers are not hindered by it.

    • underspecified lifetime params are fine, they do not cause "inference ambiguity errors";
    • this clause is dyn-compatible!
    • since the method stays 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 skipping it, 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 were really deemed a problem, the user could then 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, imho, the illusion and magic of the pattern.

    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;
       |
    # */
    

Can unsafe code rely on the method never being overridden in any way?

Not for the default #[sealed] case. Indeed, the reason code trying to override such a method currently fails is because the compiler is "not smart enough".

Since the compiler ought to be very much allowed to get smarter, it means this method could end up not being as foolproof in the future.

For a fully, properly airtight, method-sealing tool, do provide the airtight argument to #[sealed]:

#[sealed(airtight)]

Like this:

#[seal_the_deal::with_seals]
pub trait Example {
    #[sealed(airtight)]
    /// This shall always return a pointer valid for reads
    fn valid_pointer() -> *const i32 {
        &42 as &'static i32
    }
}

pub fn sound<T: Example>() -> i32 {
    let ptr: *const i32 = T::valid_pointer();
    unsafe {
        // SAFETY: the default implementation of that function
        // has been sealed in a fully airtight manner.
        *ptr
    }
}

This does break dyn-compatibility, and also makes the attribute default to hiding its shenanigans from the rendered documentation.

See the docs of #[sealed] for more info about its arguments.

Dependencies

~200–630KB
~15K SLoC