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

BOOL/BOOLEAN templates are incorrect #624

Closed
AaronRobinsonMSFT opened this issue Jul 27, 2022 · 26 comments · Fixed by #645
Closed

BOOL/BOOLEAN templates are incorrect #624

AaronRobinsonMSFT opened this issue Jul 27, 2022 · 26 comments · Fixed by #645
Assignees
Labels
bug Something isn't working partner

Comments

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Jul 27, 2022

The BOOL and BOOLEAN template constructors taking a C# bool are invalid.

internal unsafe BOOL(bool value) => this.Value = *(sbyte*)&value;

internal unsafe BOOLEAN(bool value) => this.Value = *(byte*)&value;

The BOOL template should be, the BOOLEAN is similar.

internal unsafe BOOL(bool value) => this.Value = value ? 1 : 0;

The notion of simply blitting the lowest bits of a bool instance is undefined. It also makes this usage troublesome if a user defines SkipLocalsInitAttribute, which with the current implementation leave the upper bits of Value in an undefined state thus making a .NET false possibly be non-zero.

@AaronRobinsonMSFT AaronRobinsonMSFT added the bug Something isn't working label Jul 27, 2022
@AaronRobinsonMSFT AaronRobinsonMSFT changed the title BOOL template is incorrect BOOL/BOOLEAN templates are incorrect Jul 27, 2022
@JeremyKuhne
Copy link
Member

making a .NET false possibly be non-zero.

Wow, that's a good caveat to know. Is that just with the default value, or even if you then assign false?

@jnm2
Copy link
Contributor

jnm2 commented Jul 27, 2022

Also, comparisons in different contexts don't operate the same on bool values that aren't identical to true or false: dotnet/roslyn#24865 (comment)

@AaronRobinsonMSFT
Copy link
Member Author

making a .NET false possibly be non-zero.

Wow, that's a good caveat to know. Is that just with the default value, or even if you then assign false?

With the current implementation, even if a .NET false value was set, it would only set the least significant byte in the Value field. This means that if the higher 3 bytes are non-zero due to not being explicitly initialized to zero then only the lowest byte would be set to the assumed value of 0.

elachlan added a commit to elachlan/CsWin32 that referenced this issue Jul 27, 2022
@elachlan
Copy link
Contributor

This documentation comment is in the winforms code base.
https://github.com/dotnet/winforms/blob/d46ad2e8dc76248739d9ae22b28b399a6a6b299e/src/System.Windows.Forms.Primitives/src/Interop/Interop.BOOL.cs#L9-L17

Does that make this implicit operator incorrect as well?

public static unsafe implicit operator bool(BOOL value)
{
sbyte v = checked((sbyte)value.Value);
return *(bool*)&v;
}

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Jul 28, 2022

Does that make this implicit operator incorrect as well?

@elachlan Good lord, I didn't even pay attention to that. Yes, that is very suspect. The Windows BOOL is typically defined to be 0 is false and true is non-zero. There are some Win32 APIs that do questionable things with checking flags - blech.

@elachlan
Copy link
Contributor

@AaronRobinsonMSFT , do we just go with this?

public static unsafe implicit operator bool(BOOL value)
{
return this.value != 0 ? true : false;
}

@AaronRobinsonMSFT
Copy link
Member Author

@AaronRobinsonMSFT , do we just go with this?

Sure or, even simpler - return this.value != 0;

elachlan added a commit to elachlan/CsWin32 that referenced this issue Jul 28, 2022
@elachlan
Copy link
Contributor

@AaronRobinsonMSFT the BOOLEAN class has a byte value backing field.

internal unsafe BOOLEAN(bool value) => this.Value = *(byte*)&value;
public static unsafe implicit operator bool(BOOLEAN value)
{
byte v = checked((byte)value.Value);
return *(bool*)&v;
}

The tests seem strange:
https://github.com/microsoft/CsWin32/blob/ab63c9ad139a77a43ad331f7334d38799d6125e2/test/GenerationSandbox.Tests/BooleanTests.cs

It looks like BOOLEAN is supposed to not equal when different values are supplied:

[Fact]
public void BOOLEANEqualsComparesExactValue()
{
BOOLEAN b1 = new BOOLEAN(1);
BOOLEAN b2 = new BOOLEAN(2);
Assert.Equal(b1, b1);
Assert.NotEqual(b1, b2);
}

But then, when comparing a native bool value. It is supposed to equal? I think this allows the byte value to be passed to a native bool and then get the value back again.

[Theory]
[InlineData(3)]
[InlineData(0xff)]
public void NotLossyConversionBetweenBoolAndBOOLEAN(byte ordinal)
{
BOOLEAN nativeBool = new BOOLEAN(ordinal);
bool managedBool = nativeBool;
BOOLEAN roundtrippedNativeBool = managedBool;
Assert.Equal(nativeBool, roundtrippedNativeBool);
}
[Theory]
[InlineData(3)]
[InlineData(0xff)]
public void NotLossyConversionBetweenBoolAndBOOLEAN_Ctors(byte ordinal)
{
BOOLEAN nativeBool = new BOOLEAN(ordinal);
bool managedBool = nativeBool;
BOOLEAN roundtrippedNativeBool = new BOOLEAN(managedBool);
Assert.Equal(nativeBool, roundtrippedNativeBool);
}

So the changes to BOOLEAN should not be made according to the tests.

@elachlan
Copy link
Contributor

I think the same tests exist for BOOL as well.

@elachlan
Copy link
Contributor

The documentation on the datatypes doesn't say anything about preserving the underlying values.
https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types

@AArnott have you got any guidance?

@elachlan
Copy link
Contributor

CC: @RussKie

@AaronRobinsonMSFT
Copy link
Member Author

But then, when comparing a native bool value. It is supposed to equal? I think this allows the byte value to be passed to a native bool and then get the value back again.

That is an odd test. All Win32 boolean based types normalize to a "true" or "false" state. As long as we are in managed code the actual value does matter beyond 0 and non-zero. All non-zero are equal from a boolean type's perspective.

@elachlan
Copy link
Contributor

elachlan commented Jul 28, 2022

I think its because some Win32APIs return BOOL, but indicate -1 for an error state.
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage
#339
#362
#363

Thank git and its version history for me being able to track that down.

@AaronRobinsonMSFT
Copy link
Member Author

I think its because some Win32APIs return BOOL, but indicate -1 for an error state. https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage #339 #362 #363

Thank git and its version history for me being able to track that down.

This is news to me - thanks for tracking it down. That code/tests likely warrant a comment as it isn't intuitive and is likely limited to a narrow set of APIs.

This doesn't change the marshal to unmanaged logic though. Basically, going from C# bool to Win32 BOOL, the correct logic is BOOL res = value ? 1 : 0;

@elachlan
Copy link
Contributor

@AArnott, can you explain why BOOL/BOOLEAN to bool conversion is not lossy (keeping the underlying value)?

@AArnott
Copy link
Member

AArnott commented Jul 28, 2022

Because of what you found in #624 (comment). We allow implicit conversions across the bool types, and implicit conversions should not be lossy. So either we make BOOL and BOOLEAN convert to bool only through explicit conversions (which would be a terrible experience) or we can preserve the data so that -1 or other hidden things that are totally allowed in native code don't get lost in translation.

It sounds like we don't have a bug at this point because we in fact don't yet set SkipLocalsInit, but when we do for #595, we should revisit this and find a safe way to update this code.

@jnm2
Copy link
Contributor

jnm2 commented Jul 28, 2022

Instead of lossy implicit conversions to bool, what if you instead have an explicit conversion to bool but also declare operator true and operator false?

@AaronRobinsonMSFT
Copy link
Member Author

AaronRobinsonMSFT commented Jul 28, 2022

It sounds like we don't have a bug at this point because we in fact don't yet set SkipLocalsInit

Aren't these types injected into the user's project as source?

@AArnott
Copy link
Member

AArnott commented Jul 28, 2022

Aren't these types injected into the user's project as source?

Yes. Are you calling out that SkipLocalsInit can be applied assembly-wide, thus the generated code can misbehave?

what if you instead have an explicit conversion to bool but also declare operator true and operator false?

That's a very interesting idea. I didn't know about operator true. Thanks for sharing.

I haven't deeply studied the problem yet, but in passing it seems like the problem is a risk that we read more bytes than are guaranteed to be initialized. Isn't that solvable without changing the fundamental idea of preserving the underlying value (the part of it that's guaranteed to be initialized)?

@AaronRobinsonMSFT
Copy link
Member Author

Yes. Are you calling out that SkipLocalsInit can be applied assembly-wide, thus the generated code can misbehave?

Yes. I can accept that data coming from an unmanaged context may not want to be normalized but going in the other direction the code should really just be this.value = v ? 1 : 0;

@AArnott
Copy link
Member

AArnott commented Jul 28, 2022

What value is there in not being lossy in one conversion but being lossy in the other? Since C# makes extracting the non-binary value from a bool very difficult, it seems to me the value in being non-lossy is primarily in being able to jump across BOOL -> bool -> BOOL and having the original value.

@AaronRobinsonMSFT
Copy link
Member Author

BOOL -> bool -> BOOL and having the original value.

I don't see how that would work in practice. It would mean the bool, which is always passed by value, maintains the interop binary value which isn't guarantee nor a good idea in practice. It is basically assuming the runtime will define its bool ABI to be what the interop layer decided is appropriate.

@elachlan
Copy link
Contributor

@stephentoub, sorry to snipe you like this. Could you please chime in on how we should be handling interop BOOL/BOOLEAN to bool conversion or blitting? Should bool ever have an underlying value other than true or false?

@AArnott
Copy link
Member

AArnott commented Jul 30, 2022

It is basically assuming the runtime will define its bool ABI to be what the interop layer decided is appropriate.

The runtime cannot change the memory layout of bool at this point, can it? We expect it to be 1 byte long, and we may copy 1 byte into that memory. C# emits IL for bool conditions that treat any non-zero value as truthy, just as C does. I'm struggling to see how it's problematic then to copy the value from BOOL or BOOLEAN into bool in an implicit conversion so that another implicit conversion can change it back. And this isn't me just trying to be clever... I had to work this out to resolve another issue (linked above).

@elachlan
Copy link
Contributor

elachlan commented Aug 2, 2022

Do we just initialize the bytes ourselves when doing the conversion?

@JeremyKuhne
Copy link
Member

@AArnott this checked conversion is a hard blocker for WinForms. We're getting back BOOL values from Windows APIs that overflow. I've tried providing true and false operators, but they don't take precedence unfortunately. :(

It's way too risky for us to have checked operations with BOOL. We're probably going to have to shelve working on our porting exercise until we can get this resolved.

@AArnott AArnott self-assigned this Aug 9, 2022
AArnott added a commit that referenced this issue Aug 9, 2022
This means it has to be a lossy conversion, since `BOOL` is 4 bytes and `bool` is 1 byte. Generally .NET recommends avoiding lossy implicity operators (though explicit is ok). But for a type like this, it seems like super-high value for folks to be able to author `if (!SomeBOOL())` and have that work without casts.

Fixes #624
AArnott added a commit that referenced this issue Aug 9, 2022
This means it has to be a lossy conversion, since `BOOL` is 4 bytes and `bool` is 1 byte. Generally .NET recommends avoiding lossy implicity operators (though explicit is ok). But for a type like this, it seems like super-high value for folks to be able to author `if (!SomeBOOL())` and have that work without casts.

Fixes #624
AArnott added a commit that referenced this issue Aug 9, 2022
This means it has to be a lossy conversion, since `BOOL` is 4 bytes and `bool` is 1 byte. Generally .NET recommends avoiding lossy implicity operators (though explicit is ok). But for a type like this, it seems like super-high value for folks to be able to author `if (!SomeBOOL())` and have that work without casts.

Fixes #624
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working partner
Projects
None yet
5 participants