If you’re going to sit around and do nothing, at least do it right

If you’re going to sit around and do nothing, at least do it right

Sometimes you want the API to do nothing. At the same time, it is important that he does nothing wrong.

For example, Windows has a sophisticated printing infrastructure, but Xbox doesn’t. What should happen if an app tries to print to the Xbox?

It would be wrong if the printing functions were thrown away Not­Supported­Exception. A user-installed app on an Xbox is probably mostly (if not exclusively) tested on a PC, where printing is always available. When running on Xbox, the exception will most likely not be handled and the application will crash. But even if the program tries to catch the exception, it will probably display an Oops message. Something went wrong. Please contact support and report this error code.”

The best way to “support” printing on the Xbox is to have the printing functions run successfully with a message that no printers are installed. When trying to print, the program will ask the user to select a printer and display an empty list. The user realizes that there are no printers and cancels the print request.

To accommodate the situation where the program says “Oh, you don’t have printers installed, let me help you install them”, the printer installation function can immediately return with a result code that means “user canceled the operation”.

The point here is that all printing functions behave in such a way that printing support is fully supported, but printers are constantly mysteriously missing.

It’s also probably worth adding a function that checks if printing works at all. Applications can use this feature to hide the Print button from their UI when running on a system that does not support printing. But at the same time, naive programs that assume that printing works will still behave intelligently: as if you are working with a system that does not have printers, and all attempts to install a printer are unsuccessful.

We call such “doing nothing” behavior “inert”.

The API surface still exists and the functions work according to the specification, but at the same time it does nothing. The important thing here is that it doesn’t do anything according to the documentation and minimizes the possibility of creating problems with existing code.

Another example is the deprecation API, which has a bunch of functions to create widget handles, other functions that accept widget handles, and a function to close widget handles. The API decommissioning team initially proposed making the API inert as follows:

HRESULT CreateWidget(_Out_ HWIDGET* widget)
{
    *widget = nullptr;
    return S_OK;
}

// По документации каждый виджет должен иметь хотя бы один псевдоним,
// так что нам нужно создать один поддельный псевдоним (пустую строку)
HRESULT GetWidgetAliases(
    _Out_writes_to_(capacity, *actual) PWSTR* aliases,
    UINT capacity,
    _Out_ UINT* actual)
{
    *actual = 0;

    RETURN_HR_IF(
        HRESULT_FROM_WIN32(ERROR_MORE_DATA),
        capacity < 1);

    aliases[0] = make_cotaskmem_string_nothrow(L"").release();
    RETURN_IF_NULL_ALLOC(aliases[0]);

    *actual = 1;
    return S_OK;
}

// Инертные виджеты нельзя включать или отключать
HRESULT EnableWidget(HWIDGET widget, BOOL value)
{
    return E_HANDLE;
}

HRESULT Close(HWIDGET widget)
{
    RETURN_HR_IF(E_INVALIDARG, widget != nullptr);
    return S_OK;
}

I pointed out what if Create­Widget will execute successfully, but returning a null pointer will confuse programs. The call succeeded, but I didn’t get a valid handle? I even found test code for the command itself that checked the success of the call against whether the handle was null, not against the return value.

I also pointed out what if Enable­Widget will return “invalid handle”, then this too will cause misunderstanding. The app calls Create­Widget, it completes successfully, it takes this handle (presumably valid) and tries to use it to include the widget, but gets “This handle is invalid” in response. How can this be? “I asked for a widget and you gave it to me and when I showed it to you you said ‘it’s not a widget.’ This API gaslights me!”

I looked through the API documentation and found that when the user cancels the widget creation, the return value should be ERROR_CANCELLED. That is, applications could still handle the possibility that widgets are not created due to conditions beyond their control. So we can take advantage of that: when the app tries to create a widget, we’ll just say, “Nah, the user canceled the operation here. And so it was, yes.”

HRESULT CreateWidget(_Out_ HWIDGET* widget)
{
    *widget = nullptr;
    return HRESULT_FROM_WIN32(ERROR_CANCELLED);
}

HRESULT GetWidgetAliases(
    _Out_writes_to_(capacity, *actual) PWSTR* aliases,
    UINT capacity,
    _Out_ UINT* actual)
{
    *actual = 0;
    return E_HANDLE;
}

HRESULT EnableWidget(HWIDGET widget, BOOL value)
{
    return E_HANDLE;
}

HRESULT Close(HWIDGET widget)
{
    return E_HANDLE;
}

So we created a truly inert surface API.

If you try to create a widget, we’ll tell you that it can’t be done because the user has canceled the action. Since all attempts to create a widget fail, there is no such thing as a valid widget handle, and whenever you try to use it, the handle will be invalid.

In addition, it also eliminates the need to create fake widget aliases. Since there are no widgetsthe application cannot ask the widget for its aliases in any of the valid situations.

Addition: the point here is that the printing API has always existed on desktops, because printing is supported on that platform, and according to the documentation, the “Show me a list of printers” function does not throw an exception. If you want to port the print API to Xbox, how do you do that so that your existing desktop apps still work on Xbox? Inert behavior is completely straightforward: there are no printers on the Xbox. No one expects that in response to the question “How many printers are there?” he was answered “How dare you ask such a thing!”

Another use case for an inert surface API is decommissioning an old API. How do I make the API’s behavior conform to its contract so that it doesn’t do anything useful?

Related posts