Skip to content

macros: add support for exhaustive attribute - #5390

Open
dybucc wants to merge 3 commits into
rust-lang:mainfrom
dybucc:non_exhaustive-macro-extension
Open

macros: add support for exhaustive attribute#5390
dybucc wants to merge 3 commits into
rust-lang:mainfrom
dybucc:non_exhaustive-macro-extension

Conversation

@dybucc

@dybucc dybucc commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Description

Attempts to take the first steps in closing #4080. This should
allow us to add a private field for each of the records declared within
s_with_default or s_no_extra_traits_with_default.

This should in turn enforce those records to be initialized field-by-field in
downstream crates, while avoiding the lint that currently pops up when using a
struct marked non_exhaustive in FFI contexts.

Checklist

  • Relevant tests in libc-test/semver have been updated
  • Commit messages permalink to headers for added or changed API
  • Placeholder or unstable values like *LAST or *MAX have the standard
    doc comment
  • Tested locally (cargo test -p libc-test --target mytarget); especially
    relevant for platforms that may not be checked in CI

@rustbot label +stable-nominated

@dybucc dybucc changed the title macros: add support for exhaustive attr macros: add support for exhaustive attribute Aug 8, 2026
@dybucc
dybucc force-pushed the non_exhaustive-macro-extension branch from 8c61548 to 5a738cb Compare August 8, 2026 08:06
@rustbot rustbot added the stable-nominated This PR should be considered for cherry-pick to libc's stable release branch label Aug 8, 2026
@dybucc
dybucc force-pushed the non_exhaustive-macro-extension branch from 5a738cb to f11b352 Compare August 8, 2026 08:13
@tgross35

Copy link
Copy Markdown
Member

Could you base this on top of #5372? It looks like there may be some overlap. Feel free to leave a review there as well, if you have any suggestions.

Comment thread src/macros.rs Outdated
Comment thread src/macros.rs Outdated
Comment thread src/lib.rs Outdated
@dybucc
dybucc force-pushed the non_exhaustive-macro-extension branch from f11b352 to f38fc87 Compare August 29, 2026 10:24
@rustbot

This comment has been minimized.

@dybucc

dybucc commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

@rustbot author

@rustbot

rustbot commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@dybucc

dybucc commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

I think it's done. I've created a new patchset based off of #5372 1,
but I haven't yet dropped the commits in this patch. Once that PR
merges, I'll drop the ones in this patch and cherry pick the ones from
the above patch.

@rustbot ready

Footnotes

  1. https://github.com/dybucc/libc/commits/feature/default-macro-cfg/

@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@tgross35 tgross35 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@rustbot author

Needs the updates after #4080. Would you mind also renaming s_with_default! to s2! and similar for s_no_extra_traits_with_default? Since with this PR they'll be doing more than just adding Default.

Please also include this in their doc comments.

View changes since this review

@rustbot

This comment has been minimized.

@dybucc

dybucc commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@dybucc
dybucc force-pushed the non_exhaustive-macro-extension branch 2 times, most recently from e2d5adb to a58d229 Compare September 1, 2026 12:44
@dybucc
dybucc force-pushed the non_exhaustive-macro-extension branch from a58d229 to d492384 Compare September 1, 2026 12:45

@tgross35 tgross35 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks pretty great to me! Nothing major here

View changes since this review

Comment thread src/macros.rs Outdated
Comment on lines +408 to +432
/// Emit a struct with the given derive attributes plus a generated `Default` impl.
/// Emit a struct with the given derive attributes plus a generated `Default`
/// impl. Ensure that the record has an additional private field added to
/// replicate `#[non_exhaustive]`, unless it is annotated with `#[exhaustive]`.
///
/// Fields default to `Default::default()`. A field whose default can't be derived must carry
/// `#[custom_default(EXPR)]` as its *first* attribute, and `EXPR` is used instead.
/// Fields default to `Default::default()`. A field whose default can't be
/// derived must carry `#[custom_default(EXPR)]` as its *first* attribute, and
/// `EXPR` is used instead.
///
/// This works by scanning each field for `#[custom_default]` attributes. If one exists, the
/// attribute's contents are added to `processed_field_defaults` and will be used in the expansion
/// for `Default`. If it does not exist, `Default::default()` is used instead. In either case, the
/// field is added to `processed_fields` with `#[custom_default]` stripped if necessary, and
/// This works by scanning each field for `#[custom_default]` attributes. If one
/// exists, the attribute's contents are added to `processed_field_defaults` and
/// will be used in the expansion for `Default`. If it does not exist,
/// `Default::default()` is used instead. In either case, the field is added to
/// `processed_fields` with `#[custom_default]` stripped if necessary, and
/// `struct_with_default` is invoked again with the remaining fields.
///
/// Attributes are split into `cfg_attrs` and `other_attrs` before the fields are scanned. Both
/// go on the struct, but only the `cfg`s are repeated on the `Default` impl. A `cfg` decides
/// whether the type exists at all, so without it a configured-out struct leaves an impl behind
/// referring to a type that isn't there.
/// Attributes are split into `cfg_attrs` and `other_attrs` before the fields
/// are scanned. Both go on the struct, but only the `cfg`s are repeated on the
/// `Default` impl. A `cfg` decides whether the type exists at all, so without
/// it a configured-out struct leaves an impl behind referring to a type that
/// isn't there.
///
/// Out of `other_attrs`, we scan for `#[exhaustive]`. If found, we remove it
/// but take into account that the record should be expanded _without_ an
/// additional private field. The scan for both `cfg` attributes and the
/// (non-existent) `exhaustive` attribute is done in one linear pass.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like some autoformatting got you, seems like this was rewrapped from 100 to 80.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nay, this one was fully intentional. I decided to go for this one
because I saw some other areas of libc that don't fully comply with the
80 columns for comments, and 100 columns for doc comments style guide.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What style guide is that? If it suggests doc comments are 100 then this moves away from that.

In any case, I'd prefer to leave as-is.

Comment thread src/new/helenos/time.rs
pub struct timespec {
pub tv_sec: time_t,
pub tv_nsec: c_long,
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you mark this one #[exhaustive]? timespec shouldn't ever get new fields.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same thing for the structs in this file that don't have private fields. We can probably remove them at some point, feel free to add a FIXME, but we should keep the breakage smaller.

Do this in the first commit (adding non-exhaustive/exhaustive) to avoid changing this in one patch then restoring behavior after.

Comment thread src/macros.rs
cfg_attrs: { }
other_attrs: { }
remaining_attrs: { $($attrs)* $(#$attr)* }
found_exhaustive: { false }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: either call this found_exhaustive_attr, or needs_non_exhaustive_field and flip the logic

Comment thread src/macros.rs
Comment on lines +667 to +697
(
found_exhaustive: { false },
expansion: { decl },
body: { $(#[$attr:meta])* $vis:vis $name:ident { $($field:tt)* } }
) => {
$(#[$attr])*
$vis struct $name { $($field)* __non_exhaustive: () }
};
(
found_exhaustive: { false },
expansion: { default_impl },
body: { $($field_default:tt)* }
) => {
Self { $($field_default)* __non_exhaustive: () }
};

(
found_exhaustive: { true },
expansion: { decl },
body: { $(#[$attr:meta])* $vis:vis $name:ident { $($field:tt)* } }
) => {
$(#[$attr])*
$vis struct $name { $($field)* }
};
(
found_exhaustive: { true },
expansion: { default_impl },
body: { $($field_default:tt)* }
) => {
Self { $($field_default)* }
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: match simple statements without the braces so it doesn't look like something is getting grouped

found_exhaustive: true,
expansion: default_impl,

Comment thread src/macros.rs
Comment on lines +633 to +657
finalize_exhaustiveness! {
found_exhaustive: { $found_exhaustive },
expansion: { decl },
body: {
$($other_attrs)*
$($cfg_attrs)*
$vis $name { $($processed_fields)* }
}
}

$($cfg_attrs)*
// The impl names the type and its fields, which warns if either is deprecated.
// The impl names the type and its fields, which warns if either is
// deprecated.
#[allow(deprecated)]
impl ::core::default::Default for $name {
// Field attributes (`#[cfg]`, doc comments) get forwarded to the initializer too.
// Docs are harmless there but trip the lint, so silence it.
// Field attributes (`#[cfg]`, doc comments) get forwarded to the
// initializer too. Docs are harmless there but trip the lint, so
// silence it.
#[allow(unused_doc_comments)]
fn default() -> Self {
Self { $($processed_field_defaults)* }
finalize_exhaustiveness! {
found_exhaustive: { $found_exhaustive },
expansion: { default_impl },
body: { $($processed_field_defaults)* }
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rather than use the same finalize_exhaustiveness macro for the expansion and default impl, split into to macros like emit_struct_definition and emit_struct_default_body to make things a bit easier to follow.

- Modify `struct_with_default` to automatically add a private field to
  all records such that they are always built field-by-field in
  downstream crates.

- Add support for `exhaustive` custom attribute to opt out of having the
  record have a `__non_exhaustive` field added to it.

- Annotate records without private fields under
  `src/new/helenos/time.rs` and `src/new/linux_uapi/linux/can.rs` with
  `exhaustive` attribute.
Add tests to ensure two things about the private field added to enforce
non-exhaustiveness in records declared within `s_with_default` and
`s_no_extra_traits_with_default`.

- Ensure the field is added only if the `exhaustive` attribute is not
  found while munching the annotated attributes of the record item.

- Ensure the `exhaustive` attribute also works when it appears between
  other sets of attributes.
- Rename `s_with_default`, `s_no_extra_traits_with_default` and
  `struct_with_default` to `s2`, `s_no_extra_traits2`, and
  `custom_struct`. These macros now both add a `Default` impl and a
  private field to enforce non-exhaustiveness.

- Rewrap comments and doc comments to follow 80 and 100 columns guides.
@dybucc
dybucc force-pushed the non_exhaustive-macro-extension branch from ccf62b2 to dc74109 Compare September 8, 2026 16:33
@rustbot

rustbot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

O-bsd O-freebsd O-linux S-waiting-on-author stable-nominated This PR should be considered for cherry-pick to libc's stable release branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants