Need to access Win32 BOOL as a signed integer

I'm experimenting with using Swift to create a graphical Win32 application and right away I've come across an annoying issue. When I import WinSDK.User, functions which normally return BOOL in C (a typedef for int) are returning Bool in Swift. Normally this would be fine but at least one Win32 function, GetMessage is expected to return -1 under certain conditions. Right now I have no way to check for negative BOOL return values in Swift.

Is this mapping of Win32 BOOL to Swift Bool intentional, or just a side-effect of Swift's relationship with Objective-C where BOOL is defined differently? Either way, I think it needs fixing.

I see there's struct WinSDK.WindowBool which is backed by an Int32, but there's no public way to get or set the value without casting to or from Swift Bool first.

5 Likes

If it's really just a few APIs, by far the best course of action would be to write an overlay shim for those APIs that uses Optional or an enum instead. If this is a pervasive pattern in the Windows SDK, then changing how the type is imported might make sense.

4 Likes

The mapping from the Win32 BOOL to Bool is intentional, as otherwise the functions that return BOOL cannot actually be used as a boolean and you need to explicitly compare against > 0 which can also be easy to confuse (at least I've found that it can be something that requires a bit of thought at each site).

I think that @scanon's suggestion for a shim is pretty reasonable, and it should be possible to vend that from the Win32 module.

8 Likes

Is it particularly important for most functions returning BOOL to be used in boolean contexts, though? It might make sense for functions which are named like predicates (e.g. IsWindow), but most functions returning BOOL are actually stating whether or not the function succeeded. Even in C++ I don't tend to use them as boolean values; I usually write code like this:

// Many Win32 functions use 0 to indicate failure, not just BOOL functions.
template <class T> T check(T t) {
    if (t == 0) win32_fatal_error(); // This even works for pointers in C++!
    return t;
}

void do_stuff() {
    // ...
    HWND hwnd = check(CreateWindow(/* ... */));
    // ...
    while (true) {
        MSG msg;
        BOOL result = GetMessage(&msg, NULL, 0, 0);
        if (result < 0) win32_fatal_error();
        if (result == 0) break;
        // ...
    }
    // ...
    check(DestroyWindow(hwnd);
    // ...
}

Win32's a big, weird, largely legacy API that doesn't have the highest quality documentation. While GetMessage is the only instance of a BOOL return type having more than two values I can think of off the top of my head, it's certainly possible there are more. It's also possible for Microsoft to shoehorn new functionality into existing API's that used to only return two values but might return three or more in the future. If more functions like this are discovered after a Swift release, you have to make API-breaking changes to fix it.

Win32's already so far off from being idiomatic in Swift; introducing a limiting semantic change based on assumptions about an API we don't control seems like the wrong call to me. Semantic changing judgement calls for the sake of convenience or idiomaticness can always be made in wrapper libraries or user wrapper functions instead, whereas making those calls in the standard distribution takes those choices away from most users.

To be clear, I don't think these functions need to return Int32 necessarily. They could return WindowsBool for example, and WindowsBool could have a var intValue: Int32 property added to it. Then, if you really want to write code like if SomeFunction(), you could write if SomeFunction().boolValue instead.

Why is this a more desirable solution than wrapping these API in a way that lets them use Bool naturally?

1 Like

Because, unless I'm missing something, returning plain Swift Bool makes it impossible to handle return values other than true and false without going through the steps of importing Win32 C declarations yourself completely separate from what's provided by the standard distribution. Exceptions can of course be made in the standard distribution on a case-by-case basis, but it's theoretically easy to get it wrong and be stuck with poor choices.

I think changing the API is wrong, convenience be damned. If the windows API is returning an integer type, Swift should be converting the API to do exactly the same.

It would be different if Windows was getting the full Objective-C treatment, in which case it sounds like this GetMessage function should return a Swift enum and throw. I'm not familiar enough with windows to know if that coding pattern is consistent enough to justify the work but I doubt it.

Swift should import the original library as truthfully as possible even if that means everyone has to do an integer comparison for every single function. Making Windows libraries pretty should be done with shims as a separate module.

And I'm just gonna throw this here cuz it doesn't warrant a whole post, but thank you very much to everyone who has worked on this. Especially @compnerd I've been following along for years and I'm very excited to see this almost completed! You're all awesome and I really appreciate all your hard work.

3 Likes

In Objective-C land, many methods return NSString, which is then bridged to String in Swift. That's fine 99.9% of the time, but in some scenarios the actual object that was returned matters and the bridged String does not preserve that. So there's an affordance in the language where you can write objcObject.string as NSString and it'll give you the actual NSString object returned by the Objective-C method, bypassing the conversion to String.

Applying the same mechanism here, GetMessage() as CInt could be used when you actually need the integer value. This way you can bypass Bool conversion when needed, but otherwise treat the result as a regular Bool when it does not matter.

2 Likes

I'm saying that the standard distribution should wrap these for you. If there's a really large number of such API, then that's not a great solution, but if it's only a few (or a few tens), it's by far the simplest option.

The interesting thing about this conversion is that Microsoft claims that BOOL is supposed to be TRUE or FALSE.

The fact that GetMessage violates this is definitely troublesome. Adding the shim and overloading the API through the WinSDK module should allow this to be handled properly.

2 Likes

EnableMenuItem is another one of those BOOL-returning function with more than two return values. Looking this up on the internet I couldn't find any other, but it's a bit hard to search for.

2 Likes

After @jbatez pointed out the interesting (but troubling) little detail in GetMessage, I was playing around with it to see if I could de-sugar the return type and get an overload to CInt. Doing so should allow us to have an overload that allows the use of specific functions as both returning Bool and returning CInt.

I did manage to get something which works properly. I think that adding that to the WinSDK module is reasonable and it would be pretty transparent to the user.

I'll note a tradeoff of using plain overloads for this: if the value is stored in a local variable, the client has to explicitly provide the type. That might actually be desirable in this case, but I can think of a few other options:

  • Only provide the CInt overload. (That is, treat the SDK as wrong.) In this case, you could also consider using API notes instead of an overlay method to get the same effect.

  • Make one of the overloads dispreferred by adding a dummy argument with a default value. It's weird that this works but it does.

  • Fix this bad API by returning an enum.

I'm not so familiar with Windows programming, so I'm not sure which choice is best, but I think they should all be considered before you add something to the overlay forevermore.

5 Likes

Thanks @jrose.

I had thought about the only provide CInt interface. I personally think that this isn't too terrible, although I do wonder about the cost in the sense that it beaks a lot of often cited examples. It would break a lot of compatibility with existing snippets, although admittedly those snippets would be C and not Swift code. Perhaps that cost is not too high, but at the same time, the ability to quickly go from MSDN snippets to working code has been extremely helpful and convenient. I think that APINotes would be the more preferable route here.

I do like the preference trick, but, I think that the more interesting case here is to consider if we want to actually change the API behaviour in these cases (aka, your third point). This approach is really the one that I least like.

Returning an enumerated type is one option. I think that given how Swift handles errors, I would actually prefer that the function signature remain as is, but the function becomes throwing. This was the approach I wanted to play with further over what I ended up with. The problem here is really that there is no mapping from the windows error to Error. There is some prior work related to this in NIO which has a custom Error type that allows conversion from Windows Error and errno errors and tracks provenance as well. This really maps well to the construct as the type is being used: TRUE or FALSE and -1 (FALSE) on error.

func GetMessageW(_ lpMsg: LPMSG?, hWnd: HWND?, _ wMsgFilterMin: UINT, _ wMsgFilterMax: UINT) -> Bool throws

This feels the most Swift inspired way to treat the function - and remains the most clear at the site of use. Mapping the Windows error to Error is really the piece that would make this work really well IMO.

I think the point of which is best is really the problem that I have with this too. I couldn't readily convince myself that any one way is strictly better but rather there are differing tradeoffs with the designs. This prevented me from trying to add this immediately to the WinSDK overlay.

2 Likes

I like throwing, but I wonder if people would then expect it to be the actual result from GetLastError, and if you want to put that extra work in. Having an error also doesn't work too well with the recommended message loop in the GetMessage docs either.

Actually, I think that the resulting error should contain the result of GetLastError. Yes, it does not actually work well in terms of error handling, I do wonder how terrible it is to be recursive here.

func RunMessageLoop() {
  do {
    var msg: MSG = MSG()
    while try GetMessageW(&msg, nil, 0, 0) {
      TranslateMessage(&msg)
      DispatchMessage(&msg)
    }
  } catch WindowsError.InvalidParameter {
    // The desktop is gone, I'm outta here
    return
  } catch {
    return RunMessageLoop()
  }
}
1 Like

Optimal design of the API aside, you may be able to work around the importer here by taking advantage of the bridging peephole: if you immediately cast the result of a bridged call back to its original type, Swift gives that special treatment by simply not doing the bridging at all. So, for example, GetMessage(...) as WinSDK.WindowBool. I don't know off-hand if that works for the Bool treatment, but it probably should. Presumably, once you have a WindowBool, there's some way to get the raw UInt32 back out.

4 Likes

Hmm, it would be possible if we made a member publicly accessible:

The underlying CInt is actually not currently accessible.

1 Like

We haven't allowed that for DarwinBoolean, and no one's asked for it, but I suppose we could do both: expose it on WinBool, and add overlay functions for the APIs we know about.