Help in understaning Rust compiler behavior

I have the following code:

unsafe extern "system" fn window_proc(
    hwnd: HWND,
    msg: u32,
    w_param: WPARAM,
    l_param: LPARAM,
) -> LRESULT {
    match msg {
        WM_DESTROY => {
            PostQuitMessage(0);
            0
        }
        _ => DefWindowProcW(hwnd, msg, w_param, l_param),
    }
}

The Rust compiler issued the warning for the code as:

164 |         WM_DESTROY => {
    |         ---------- matches any value
...
168 |         _ => DefWindowProcW(hwnd, msg, w_param, l_param),
    |         ^ no value can reach this

This messages was a bit cryptic for me? Why WM_DESTROY matches any value? So I added a debug printout and found that WM_DESTROY simply isn't defined. My code stopped be a compilable. After adding a definition of WM_DESTROY , the warning went away. So my question was why

  1. No any error message that WM_DESTROY isn't defined
  2. Which value does Rust assume for the WM_DESTROY when it isn't defined?

The topic mostly for satisfying the curiosity.

If it's not a const, you're just binding to any value, analogous to let WM_DESTROY = ....

2 Likes

You should also have gotten a warning about giving a variable an uppercase name.

3 Likes

Correct, I had such warning but allowed an uppercase name because it is Windows stuff from the previous century.

1 Like

It seems like you misunderstood the warning. The warning is about defining a variable binding with an uppercase name, not about using a const with an uppercase name. If you had an actual const called WM_DESTROY in scope, there would be no warning. As @quinedot already said your code is equivalent to

unsafe extern "system" fn window_proc(
    hwnd: HWND,
    msg: u32,
    w_param: WPARAM,
    l_param: LPARAM,
) -> LRESULT {
    let WM_DESTROY = msg;
    PostQuitMessage(0);
    0
}

which in turn is equivalent to

unsafe extern "system" fn window_proc(
    hwnd: HWND,
    msg: u32,
    w_param: WPARAM,
    l_param: LPARAM,
) -> LRESULT {
    let my_var = msg;
    PostQuitMessage(0);
    0
}

the fix would be to do something like the following

use windows_sys::Win32::UI::WindowsAndMessaging::WM_DESTROY;
// ^----- add this line to import WM_DESTROY.

unsafe extern "system" fn window_proc(
    hwnd: HWND,
    msg: u32,
    w_param: WPARAM,
    l_param: LPARAM,
) -> LRESULT {
    match msg {
        WM_DESTROY => {
            PostQuitMessage(0);
            0
        }
        _ => DefWindowProcW(hwnd, msg, w_param, l_param),
    }
}
1 Like

Thanks, I do not need a fix, I am just trying to understand the match behavior. People blame C switch , but Rust match isn't less awkward. What is the value of such behavior? I can only imagine that

match msg {
        WM_DESTROY if msg == 2 => {
            println!{"Msg {WM_DESTROY} issued."}
            PostQuitMessage(0);
            0
        }
        _ => DefWindowProcW(hwnd, msg, w_param, l_param),
    }

But still I see more confusions than a clear value. Maybe you have a better example I can start using after?

It's less of a designed-in behaviour and more of a consequence of other decisions.

A match arm takes a series of patterns, and evaluates whichever body follows from the first pattern in the list that accepts the value (msg, in your case). "Pattern" is a very broad category - it includes literal values (2), but also compound values, and, critically, placeholders.

Placeholders are useful: they let the body of the match arm operate on parts of the matched value. Unfortunately, they share syntax with other kinds of language item: the token WM_DESTROY may be a placeholder, but it may also be an existing variable, a constant, a type name, a trait name, a function name, or any of a few other things. The distinction rests on what's in scope at that point: if WM_DESTROY is a new symbol, then it's treated as a placeholder and defines a new symbol for the resulting variable binding, while if it's a symbol already in scope, then it is that symbol.

This is a known hazard with match statements and other pattern-matching operations, which is why clippy has a lint for it, but it's pretty damned near impossible to fix without seriously hampering the ergonomics of pattern-matching for at least some common purposes, and without breaking a lot of existing code.

As for utility, a pattern consisting of a bare placeholder is useful for catch-all patterns, usually at the end of a match:

match msg {
  // imagine several arms here for various window messages
  WindowsAndMessaging::WM_DESTROY => { todo!("destroy window"); },
  other => {
    eprintln!("Ignoring unnexpected window message: {other:#?}");
  }
}

While you might not leave that code in place permanently, a catch-all arm like that lets you see what your program is actually doing during development, and might be crucial to figuring out how the documentation lines up with reality. WM_DESTROY, when interpreted as a placeholder, has exactly the same "purpose," only in your program, it's being interpreted that way by accident rather than by intent.

2 Likes

See this extensive thread on this question

You've already used unconditional binding countless times yourself. When I say it's analagous to

let variable = ...

it is because the same mechanism, patterns, are used for

with the primary distinction being which positions do or don't require irrefutable patterns.

So approximately every time you've used let or for or taken a function parameter, you've used an unconditional binding.

Additionally if you've ever done something like

match result {
    Ok(value) => {}
    Err(err) => {}
}

the bindings (value, err) are also identifier patterns. They're just not at the top level.


The main confusion is that named constants are accepted as a path pattern, but if the path is just a name and that constant doesn't exist, you end up with an identifier pattern instead (which is irrefutable if at the top level). You do generally end up with warnings, but it also still confuses people, so perhaps some day extra syntax will be required for simply named consts by default, const { NAME } or just { NAME } or such.

3 Likes

Yes, it would be clearer if Rust considers such cases as errors instead of warnings currently. It's same level of mistakes I can do in Java declaring a local variable shading a class member with same name. If Rust claimed to be the safest language, it should be such. As currently, I have to consider such warnings as errors and it confuses with other warnings, for example not following the naming convention.

The compiler already gives you three different warnings in the case of match expr { SOME_CONSTANT => {} }.

  • One for the SOME_CONSTANT variable being unused
  • One for the SOME_CONSTANT variable being an uppercase variable name
  • One for any following match arms being unreachable (which prompted your OP)

That should be enough. If you're getting a lot of warnings about SCREAMING_SNAKE_CASE variable names, then silence the lint with #[allow(non_snake_case)]. Don't just leave dozens of warnings in your code, as they will inevitably obscure a real problem.

In general, all warnings should be resolved. (#[allow]ing or ignoring unused stuff is okay for initial development, but you should know which warnings are for dead code and which warnings are potential bugs. Your editor should show all the warnings your code has so you can easily see when new ones appear.)

The policy for warnings given by rustc is that there are not false positives[1]. The style lints are probably the most opinionated, but they can be disabled (as I mentioned above). This means that any warning that cargo check will give should be addressed, whether it means allowing it[2] or fixing it. I think that all the warnings the compiler already gives is enough to stop this from being a problem.

All the warnings I get
   Compiling playground v0.0.1 (/playground)
warning: unreachable pattern
 --> src/main.rs:4:9
  |
3 |         SOME_CONSTANT => {},
  |         ------------- matches any value
4 |         _ => {},
  |         ^ no value can reach this
  |
  = note: `#[warn(unreachable_patterns)]` on by default

warning: unused variable: `SOME_CONSTANT`
 --> src/main.rs:3:9
  |
3 |         SOME_CONSTANT => {},
  |         ^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_SOME_CONSTANT`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: variable `SOME_CONSTANT` should have a snake case name
 --> src/main.rs:3:9
  |
3 |         SOME_CONSTANT => {},
  |         ^^^^^^^^^^^^^ help: convert the identifier to snake case: `some_constant`
  |
  = note: `#[warn(non_snake_case)]` on by default

warning: `playground` (bin "playground") generated 3 warnings

  1. in deny or warn-by-default lints, allow-by-default ones get some slack here ↩︎

  2. like style stuff in certain circumstances ↩︎

1 Like

The problem is that an average developer doesn't concern about that Rust compiler gave enough legit warnings. He/she is more concerned that can do a proper development without much hassle. Otherwise Rust didn't give much benefits over C/C++. Just yesterday's example, my Rust application suddenly stopped compiling. What happened? Rustc did an update I didn't ask about. It considered all my crates invalid after. I even forgot how I compiled them. But now, I need to recover that knowledge. I anticipate your arguing - it's happened because you do not use Cargo. But I will argue that your Cargo does tons of useless work. If I debugged some code and wrapped it in a crate, it should just work regardless my rustc version. Needless to say that Cargo requires to have all code in a source code. However it will accept any C code in binary form. Anyway, my intention just to advocate my Rust usage.

That just doesn't happen. Something must have ran rustup update, although it might not have been you directly.

I do use Cargo, and I love it. I think you misplaced some modifiers in your post, which makes it difficult to understand.

That's exactly to fix the problem you had above with rustc updates invalidating old artifacts. When you get a new rustc, Cargo just rebuilds the dependency tree seamlessly (although it might take a while).

Rust's ABI isn't stable (unlike C), so binaries produced by rustc 1.X can't be used with other code compiled by rustc 1.Y. If you compile Rust while exposing an extern "C" API with only #[repr(C)] structs, you can use the same ABI C does to allow linking to any other rustc version.
Cargo can link to this type of artifact just fine the same way you would link to C.

Also, Cargo uses rustc invocations internally to compile everything. Pass the -v flag to enable verbose output to show them all. It just handles rustc versioning and getting dependencies built for you easily.

IMO, no developer concerned with "[developing] without much hassle" would refuse to use Cargo. I think your argument against Cargo is in the sentence quoted below, but it doesn't make sense to my brain.

The only thing I'm confident in understanding here is that you are saying that "crates" are purely a concept in Cargo. I won't say anything about the rest of this sentence since I can't interpret it. It would be great if you could elaborate.

But rustc also uses crates. The file you pass to rustc to compile is interpreted as the root of the crate, and modules will be found following the paths of mod statements.

A Cargo package is something with a [package] table in Cargo.toml. This term has been historically inflated with "crate", but there's a difference. A package can have multiple crates:

  • src/lib.rs and modules are a crate
  • src/main.rs and modules are a crate
  • every file in src/bin/ is a crate
  • the build.rs script is a crate
  • integration tests in tests/ are crates

Rust can't do anything to make programmers read the warning messages. Rustc does its best to lead the horses programmers to water good coding practices, but you can't make them drink write better code.

1 Like

All your points look legit. However, visit any job board and you will find almost zero job openings requiring Rust. Say more, if you find any job requiring Rust, more likely it will be related to a Blockchain. Why?

Regarding Cargo, I simply do not like it and I do not believe it's required for Rust development. So far, I never used it, and so far I could build whatever I want without any productivity impact.

Finally, this thread was useful for me, because I learned other weakness of Rust and I prepared for it now.

"That just doesn't happen. Something must have ran rustup update, although it might not have been you directly."
You can be right. I didn't remember how I setup Rust on Windows machine. I build rustc from sources on Linux machine, but decided to save time on Windows so did an Internet search - install Rust and then just followed the first link. So after some time, I just typed as usually rustc, but instead of a compilation, it started updating and then did a compilation with tons errors, because all crates became invalid. Windows gave me more surprises with Rust, but they are problem of Windows, not Rust. So I didn't bring them there.