Skip to content

Unsafe derives and attributes #3715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented Oct 22, 2024

Allow declaring proc macro attributes and derive macros as unsafe, and
requiring unsafe to invoke them.

Rendered

@joshtriplett joshtriplett added T-lang Relevant to the language team, which will review and decide on the RFC. A-macros Macro related proposals and issues A-proc-macros Proc macro related proposals & ideas labels Oct 22, 2024
@clarfonthey
Copy link

Just my 2¢, but I think that the shorthand for derive(..., unsafe(Trait), ...) is a bit unprecedented.

You have to separate out derive traits any time there's some different requirement, e.g. cfg_attr(feature = "serde", derive(Serialize, Deserialize)), and by keeping unsafe at the top level you can easily grep for #!?\[unsafe\( whereas unsafe( will catch any call to method_unsafe(.

Sure, it's likely it won't make a difference, but I think that only having to check attributes at the top level for unsafe to verify safety is best.

@carbotaniuman
Copy link

The greppability is already broken by things like #[cfg_attr(all(), unsafe(no_mangle)], and can be restored by just grepping unsafe( I think putting the unsafe with the trait makes syntactic sense as it discharges the safety obligation of each trait derive.

@GnomedDev

This comment was marked as outdated.

@Noratrieb

This comment was marked as outdated.

@GnomedDev

This comment was marked as outdated.

@clarfonthey
Copy link

The greppability is already broken by things like #[cfg_attr(all(), unsafe(no_mangle)], and can be restored by just grepping unsafe( I think putting the unsafe with the trait makes syntactic sense as it discharges the safety obligation of each trait derive.

I feel kind of silly for literally alluding to this point in my post and missing it somehow. You're right and I retract my original claim.

@joshtriplett
Copy link
Member Author

We had a @rust-lang/lang design meeting today on the set of macro RFCs. I've updated the RFC to incorporate the feedback from that design meeting.

Per the feedback in that meeting, I'm starting an FCP to start letting people register consensus for this RFC.

@rfcbot merge

@rfcbot
Copy link
Collaborator

rfcbot commented Nov 21, 2024

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Nov 21, 2024
@joshtriplett joshtriplett added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Mar 11, 2025
@joshtriplett
Copy link
Member Author

@rust-lang/lang I've updated this to match the consensus syntax of derive(unsafe(UnsafeTrait)), and provided all the points of rationale that I remember from the discussion. Please feel free to post further suggestions if I missed any points of rationale.

@carbotaniuman
Copy link

One thing I thought of was that it might make sense to make a derive maybe-unsafe and branch between whether unsafe was provided, although it might be better to just spell that out as 2 separate derives... The use case for that would be something where the derive can check the preconditions using static_asserts or similar, and one where you can promise the preconditions hold if they aren't checkable.

@RalfJung
Copy link
Member

One thing I thought of was that it might make sense to make a derive maybe-unsafe and branch between whether unsafe was provided

I think that would be a big mistake. The presence or absence of unsafe should never affect the resulting generated code, it should only affect whether the code is accepted or not.

@RalfJung
Copy link
Member

@rust-lang/lang I've updated this to match the consensus syntax of derive(unsafe(UnsafeTrait)), and provided all the points of rationale that I remember from the discussion. Please feel free to post further suggestions if I missed any points of rationale.

In terms of the mental model I find derive(unsafe(Trait)) a bit odd -- this looks like the trait is called unsafe(Trait). In the grammar of attributes (well, in its simplified form in my head), unsafe($attr) is also an $attr, and derive($trait) is an $attr, but to fit the proposed scheme into that model we have to do an ad-hoc extension of the grammar to allow unsafe inside derive.

OTOH, the RFC does make some good points in favor of "inner unsafe", so I am a bit torn here.

Use phrasings that can't be read as implying SAFETY comments are
unusual. The previous phrasing could be interpreted as "in the unusual
case where you're writing SAFETY comments ...".
@moulins
Copy link

moulins commented Mar 20, 2025

it just occurred to me that we may want to mark derive macro helper attributes unsafe instead of (or in addition to) the derive macro:

[...snip]

I'd go even further: the same attribute could be either safe or unsafe, depending on other attributes of the same macro, so there should be a way to accept both #[unsafe(debug_deref)] and #[debug_deref].

@joshtriplett
Copy link
Member Author

@moulins

it just occurred to me that we may want to mark derive macro helper attributes unsafe instead of (or in addition to) the derive macro:
[...snip]

I'd go even further: the same attribute could be either safe or unsafe, depending on other attributes of the same macro, so there should be a way to accept both #[unsafe(debug_deref)] and #[debug_deref].

We could certainly do that, but I think it's reasonable for that to be future work. This proposal introduces always-unsafe derives and always-unsafe attributes. In the future, we could have an RFC for sometimes-unsafe derives and sometimes-unsafe attributes, which would require additional definition and documentation.

@joshtriplett
Copy link
Member Author

@tmandry facet-rs/facet#80 is a good example of rationale for this.

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Apr 16, 2025

@rfcbot reviewed

I am happy with the syntax and I think you should write unsafe(Trait) in both the def and use site.

I like the idea of unsafe attributes.

I agree that facet-rs/facet#80 would be a nice thing to include in the motivation.

@traviscross
Copy link
Contributor

@rfcbot resolve pick-def-site-name

We discussed this in the lang call today and aligned around the def-site syntax that matches the use-site syntax.

We also talked about expanding on the motivation section of the document, so as to document more of this for posterity, and @joshtriplett is working on making this update.

@rfcbot rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Apr 16, 2025
@rfcbot
Copy link
Collaborator

rfcbot commented Apr 16, 2025

🔔 This is now entering its final comment period, as per the review above. 🔔

@traviscross traviscross added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. and removed I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. labels Apr 16, 2025
to declare a helper attribute as `unsafe`:

```rust
#[proc_macro_derive(MyDeriveMacro(, attributes(unsafe(dangerous_helper_attr))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[proc_macro_derive(MyDeriveMacro(, attributes(unsafe(dangerous_helper_attr))]
#[proc_macro_derive(MyDeriveMacro, attributes(unsafe(dangerous_helper_attr)))]

@traviscross
Copy link
Contributor

In the meeting today, we also aligned around including unsafe attributes in the normative part of this RFC rather than as future work, and I see now that @joshtriplett has made that change to the document.

@tmandry
Copy link
Member

tmandry commented Apr 18, 2025

I still haven't seen a worked example of where and how this will be used. The conclusion of facet-rs/facet#80 was that it was not a good candidate for an unsafe derive.

Send and Sync were brought up and they seem like decent candidates. (Modulo the detail that they if they were in core they would be built in derives and not proc macros.. but if this concept was useful in core it would probably be useful elsewhere.)

The problem I see is that the ordinary derive logic doesn't make sense for Send and Sync. If I have

struct Ref<T>(*const T);

Then I most likely want my Send impl to look like this:

unsafe impl<T: Sync> Send for Ref<T> {}

We don't have a precedent for a built-in derive that works like this. If we go by the way those derives work today, #[derive(unsafe(Send))] would give you

unsafe impl<T: Send> Send for Ref<T> {}

which would be subtly wrong (albeit in a conservative direction), and worse, hidden from the user.

Overall I'm concerned by combination of the following:

  • Broad, whole-type-behavior safety invariants implied by the use of a #[derive(unsafe(...)]
  • The inherent subtlety of unsafe impls, which might require more detailed knowledge of a type's behavior than a derive macro can have
  • Opaqueness of derive expansions masking potential problems with the above
  • The limited syntax-level knowledge of macros and their inability to see through other types

I think many of these challenges can be overcome, but we should have use cases in mind to guide this and related features. That way we will end up shipping a good user experience (at least for those use cases) instead of a feature that's almost-but-not-quite useful.

@tmandry
Copy link
Member

tmandry commented Apr 18, 2025

@rfcbot concern needs fleshed out use case

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels Apr 18, 2025
@joshtriplett
Copy link
Member Author

joshtriplett commented Apr 27, 2025

@tmandry A few examples of concrete traits that definitely need unsafe derives (safe derives would not work) and don't have any complexity around bounds:

  • TrustedLen and TrustedStep
  • DerefPure
  • pyo3::marker::Ungil (a version with the blanket impl for Send removed)
  • A hypothetical derive for bytes::Buf or similar. (For cases where your type has fields or trivial expressions for current slice, offset, etc.)

but such a derive would be useful when they're stable, serving the function
of an `unsafe impl`.)
- `pyo3::marker::Ungil` in `pyo3`, in place of the current handling of a
blanket impl for any `Send` type.
Copy link
Member

@kennytm kennytm May 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these are the only motivation examples, I don't get why these marker traits TrustedLen, TrustedStep, DerefPure, Ungil require using the #[derive] mechanism over manual unsafe impl.

For the first three the advantages are not having to explicitly write the where bounds, but they have the Iterator or Deref supertraits which cannot be derived so you gotta repeat those bounds anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Literally any unsafe trait that one might wish to derive is a potential example. These were a few samples. If you have other examples of unsafe trait that you'd prefer to have cited instead of or in addition to these, I'd be happy to add them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Literally any unsafe trait that one might wish to derive is a potential example.

Normally if we wish to derive something it is because we know that it is possible to fill out the impl programmatically with the type's definition alone, thus saving effort to produce the boilerplate impl.

If all the derive doing is just implementing a marker trait without anything extra there's certainly no point from the supply-side anyone wish to write a proc-macro for it. At least it should be derivable together with its base trait like #[derive(Clone, Copy)] i.e. #[derive(Iterator, unsafe(TrustedLen))], but you can't & can't derive an Iterator.

Another reason we wish to derive a marker trait is to insert some compile-time checks, such as zerocopy's macros. But zerocopy's derive-macro checks at the same proved the conditions required by the unsafe trait in the first place, making those traits safe to derive even if unsafe to manually implement.

For libstd the public, documented, non-sealed unsafe traits are:

  1. Traits with no obvious derivable impl:
    • GlobalAlloc
    • Allocator
    • Searcher
    • ReverseSearcher
  2. Marker traits
    • TrustedStep
    • TrustedLen
    • DerefPure
    • PinCoerceUnsized
  3. TransmuteFrom
  4. CloneToUninit

Perhaps CloneToUninit is the one most compatible with #[derive]:

#[derive(CloneToUninit)]
struct Packet<Tail: ?Sized> {
    header: Header,
    tail: Tail,
}

but the actual impl can satisfy impl CloneToUninit's safety condition without user's intervention, meaning #[derive(CloneToUninit)] itself is actually safe.

unsafe impl<Tail: CloneToUninit + ?Sized> CloneToUninit for Packet<Tail> {
    unsafe fn clone_to_uninit(&self, dst: *mut u8) {
        self.header.clone_to_uninit(dst.add(offset_of_val!(self, header)));
        self.tail.clone_to_uninit(dst.add(offset_of_val!(self, tail)));
    }
}

So no I don't have any other examples to support this RFC, i.e. I'm still on the side of the concern "needs fleshed out use case".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-macros Macro related proposals and issues A-proc-macros Proc macro related proposals & ideas disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.