Skip to content
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

dead_code lint wrongly warns about public repr(C) structs with private fields but no constructors #126169

Open
asomers opened this issue Jun 8, 2024 · 30 comments · May be fixed by #127104
Open
Labels
A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. C-bug Category: This is a bug. L-dead_code Lint: dead_code T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@asomers
Copy link
Contributor

asomers commented Jun 8, 2024

Code

#[repr(C)] // <--
pub struct Foo {
    pub i: i16,
    align: i16
}

Current output

warning: field `align` is never read
 --> src/lib.rs:3:5
  |
1 | pub struct Foo {
  |            --- field in this struct
2 |     pub i: i16,
3 |     align: i16
  |     ^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

Desired output

No warnings, or perhaps a different warning.

Rationale and extra context

This warning is probably triggering because there's no constructor that allows a user to set the align field on struct Foo. However, there is still a valid way for a user to construct it: std::mem::zeroed(). The libc crate contains many structs that must be initialized that way. libc could silence these warnings by #[allow(dead_code)], but that's too broad a brush. It would silence many much more serious warnings. Could we please either revert to the previous behavior, or else create a new lint for this that can be separately silenced?

Other cases

No response

Rust Version

rustc 1.80.0-nightly (804421dff 2024-06-07)
binary: rustc
commit-hash: 804421dff5542c9c7da5c60257b5dbc849719505
commit-date: 2024-06-07
host: x86_64-unknown-freebsd
release: 1.80.0-nightly
LLVM version: 18.1.7

Anything else?

This is a regression since rustc 1.80.0-nightly (f67a1acc0 2024-06-01). That version did not warn in such cases.

Downstream issue: rust-lang/libc#3740

@asomers asomers added A-diagnostics Area: Messages for errors, warnings, and lints T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jun 8, 2024
@fmease fmease added L-dead_code Lint: dead_code A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. T-lang Relevant to the language team, which will review and decide on the PR/issue. C-discussion Category: Discussion or questions that doesn't represent real issues. and removed A-diagnostics Area: Messages for errors, warnings, and lints labels Jun 8, 2024
@fmease
Copy link
Member

fmease commented Jun 8, 2024

However, there is still a valid way for a user to construct it: std::mem::zeroed()

Personally speaking I find this argument quite weak because the private field align is not part of the public API and thus upstream (the author of Foo) is permitted to change the type of align at any time without officially breaking any contact (as an aside, the exact output of size_of, align_of, Debug is not considered to come with any stability guarantees; auto trait leakage is a slightly different story though).

For example, the author of Foo is allowed to change the type of align to NonZeroI16 rendering zeroed::<Foo>() in the user code immediate undefined behavior. However that would be the fault of the downstream user of Foo, they shouldn't've used zeroed on Foo in the first place.

@asomers
Copy link
Contributor Author

asomers commented Jun 8, 2024

@fmease I would agree with all of that if this were all pure Rust code. But it isn't. libc has to represent structs that were defined in C, where zero-initialization is normal. Many of them already have fields used for padding, and the official C documentation requires zero-initialization. Can we please at least use a new name for this lint?

Another case is when the only way to initialize a struct is to first allocate it uninitialized, and then populate it with a C library function or a syscall. To declare such a structure in pure safe Rust, the syscall which initializes it would have to be its constructor. But defining Rusty wrappers for such syscalls is beyond the scope of the libc crate, which is only concerned with structs and function definitions.

@workingjubilee
Copy link
Contributor

kinda thought the dead code group had an unused field sublint already.

@saethlin
Copy link
Member

saethlin commented Jun 8, 2024

The given reproducer is insufficient, compiling literally just this code in a library crate:

pub struct Foo {
    pub i: i16,
    align: i16
}

has been producing warnings for years. I don't know what's different about the libc crate, but the bisection using the entire libc crate is pretty easy and points to #125572.

@saethlin
Copy link
Member

saethlin commented Jun 9, 2024

Ah! The difference is that the libc crate contains a zillion trait impls that aren't derived. This is a minimal reproducer:

pub struct Foo(i16);

impl Clone for Foo {
    fn clone(&self) -> Self {
        Self(self.0)
    }
}

The change was that trait impls don't make the struct live anymore.

Given that, I do not think it makes a whole lot of sense to be reasoning about constructability via mem::zeroed here. Or I think that would be a new level of reasoning for this lint.

@saethlin
Copy link
Member

saethlin commented Jun 9, 2024

Ah! Here's a better reproducer:

#[repr(C)]
pub struct Foo(i16);

I don't know enough about the lint parts of the compiler to see how the diff in the regressing PR caused repr(C) to stop making the field live, but that seems to be the bug.

@fmease fmease added C-bug Category: This is a bug. and removed T-lang Relevant to the language team, which will review and decide on the PR/issue. C-discussion Category: Discussion or questions that doesn't represent real issues. labels Jun 9, 2024
@fmease
Copy link
Member

fmease commented Jun 9, 2024

I would agree with all of that if this were all pure Rust code. But it isn't. libc has to represent structs that were defined in C, where zero-initialization is normal. Many of them already have fields used for padding, and the official C documentation requires zero-initialization. Can we please at least use a new name for this lint?

Right, I was going by Foo being #[repr(Rust)] in the original repro. It actually being #[repr(C)] on the other hand changes the situation. I didn't take a look at the libc issue earlier and didn't see the "recent regression" part ^^'.

cc @mu001999 (#125572).

@fmease fmease changed the title dead_code lint wrongly warns about public structs with private fields but no constructors dead_code lint wrongly warns about public repr(C) structs with private fields but no constructors Jun 9, 2024
@RalfJung
Copy link
Member

For variables we have a way of making them "intentionally unused", IMO something similar would make sense here. If you rename the field to _align, is there still a warning?

IMO we should keep warning for the code as-is. That field is unused, and only in unusual situations does it matter anyway, libc is one of these unusual situations.

@mu001999
Copy link
Contributor

mu001999 commented Jun 10, 2024

Oh, I think this is an issue about unconstructed pub structs, like the warnings in the build failure in the downstream issue. For unused fields, we can just underline them. But for pub structs which don't provide pub constructors, adding a new lint to control dead code analysis on pub structs may be a solution.


IMO, adding pub constructors to these pub types or making all fields public is also an acceptable solution.

@RalfJung
Copy link
Member

RalfJung commented Jun 10, 2024 via email

@glandium
Copy link
Contributor

"The type is constructed exclusively by mem::zeroed" is a pretty unusual scenario, I am not convinced it is worth designing our lints around that.

Would you call "the type is constructed exclusively by FFI" a pretty unusual scenario?

@RalfJung
Copy link
Member

Certainly less so. However, I'd expect FFI types to have all-pub fields since presumably, the same definition exists in C, where there is no such thing as a private field. It is hardly accurate to say that a field is private when, in fact, code written outside the module -- in a different language even -- can construct values of this type with arbitrary field contents.

@glandium
Copy link
Contributor

I, for one, like the fact that you can relieve yourself from arbitrary rust code using the crate being able to break those values by using private fields.

@RalfJung
Copy link
Member

Arbitrary C code can break those values, so what's the point of preventing Rust code from doing so?

@glandium
Copy link
Contributor

glandium commented Jun 12, 2024

If you go there, arbitrary Rust code can break those values too, even with private fields. But the thing is the code on the other side of FFI is not necessarily arbitrary either. And it could also be C++, which has private fields, too.

@RalfJung
Copy link
Member

RalfJung commented Jun 12, 2024 via email

@workingjubilee
Copy link
Contributor

it'd be nice if all these unused field/variant/whatever lints had their own lint so when we want to allow them in a crate because we are pervasively doing that we can just... do that.

@workingjubilee
Copy link
Contributor

allow(dead_code) applying to both unused functions and unused structs is not at all intuitive.

@nathaniel-bennett
Copy link

Could we modify #125572 so that it doesn't lint structs with private fields when repr is used? That may be a good enough indicator that the struct is going to be used for FFI.

@nathaniel-bennett
Copy link

nathaniel-bennett commented Jun 12, 2024

If not, then naming the lint something unique (maybe allow(unconstructed)) would probably be best for libc and similar libraries employing FFI with private struct fields. Having a blanket #![allow(unconstructed)] would sure beat having to either individually tag each struct with allow(dead_code) or else ignore all dead code (#![allow(dead_code)]).

@RalfJung
Copy link
Member

RalfJung commented Jun 12, 2024 via email

@kennykerr
Copy link
Contributor

There are many thousands of public repr(transparent) structs with private fields in the windows crate. Many are created indirectly via the operating system. I'm allowing dead code as a workaround in the short term, but I ideally there would be a more elegant way to handle this. microsoft/windows-rs#3098

@RalfJung
Copy link
Member

RalfJung commented Jun 13, 2024

Given the discussion above, what is the reason for having private fields in these types?

To reiterate: the point of field privacy in Rust is to allow local reasoning. All code reading or writing those fields must be inside the current module. If you expect the struct to be constructed outside the module (e.g. by the OS), clearly that is not the case, so it seems that private fields don't serve much of a purpose?

@ChrisDenton
Copy link
Contributor

ChrisDenton commented Jun 13, 2024

Whether or not this is desirable, this is a big expansion of the dead_code lint in places it didn't affect before. According to the lint policy currently in FCP, a new lint for this case should at least be considered.

@kennykerr
Copy link
Contributor

kennykerr commented Jun 13, 2024

This approach provides an ideal abstraction to model the type-safety and semantic guarantees offered by certain Windows APIs and goes beyond that which can naturally be modeled in the Rust language. Using public fields would make these APIs substantially more dangerous to use since they could very easily be initialized incorrectly.

@RalfJung
Copy link
Member

So the argument is, the field really is private to the Windows OS, ideally not even the Rust module declaring them would have access?

Maybe we need an #[externally_constructed] attribute or so. Though #[allow(unused)] could play the same role, except currently that does not quite work: #126289.

@kennykerr
Copy link
Contributor

Externally constructed is a good way to think about it, but ideally we could just get unused to work in this case and not have to invent a new term.

@ChrisDenton
Copy link
Contributor

Update: the libc crate has also opted to silence these warnings.

@mu001999
Copy link
Contributor

Though #[allow(unused)] could play the same role, except currently that does not quite work: #126289.

#[allow(unused)] works well now.

@Soveu
Copy link
Contributor

Soveu commented Jun 28, 2024

Interestingly, I found some code that technically does an exception for repr(transparent) and repr(C) structs/unions, but this is just a few minutes looking at the code, so I don't understand it yet.

self.repr_unconditionally_treats_fields_as_live =

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. C-bug Category: This is a bug. L-dead_code Lint: dead_code T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet