In the conversion to 64-bit Windows, why were some parameters not upgraded to SIZE_T?

Raymond Chen

Raymond

James wonders

why many functions kept DWORD for parameter
lengths instead of upgrading to SIZE_T or
DWORD_PTR
.

When updating the interfaces for 64-bit Windows,
there were a few guiding principles.
Here are two of them.

  • Don’t change an interface unless you really need to.
  • Do you really need to?

Changing an interface causes all sorts of problems when porting.
For example, if you change the parameters to a COM interface,
then you introduce a breaking change in everybody who implements it.
Consider this hypothetical interface:

// namedobject.idl
interface INamedObject : IUnknown
{
    HRESULT GetName([out, string, sizeof(cchBuf)] LPWSTR pszBuf,
                    [in] DWORD cchBuf);
};

And here’s a hypothetical implementation:

// contoso.cpp
class CContosoBasicNamedObject : public INamedObject
{
    ...
    HRESULT GetName(LPWSTR pszBuf, DWORD cchBuf)
    {
        return StringCchPrintfW(pszBuf, cchBuf, L"Contoso");
    }
    ...
};

Okay, now it’s time to 64-bit-ize this puppy.
So you do the natural thing: Grow the
DWORD parameter
to DWORD_PTR.
Since
DWORD_PTR maps to DWORD
on 32-bit systems, this is a backward-compatible change.

// namedobject.idl
interface INamedObject : IUnknown
{
    HRESULT GetName([out, string, sizeof(cchBuf)] LPWSTR pszBuf,
                    [in] DWORD_PTR cchBuf);
};

Then you recompile the entire operating system and find that
the compiler complains,
“Cannot instantiate abstract class: CContosoBasicNamedObject.”
Oh, right, that’s because the
INamed­Object::Get­Name method
in the implementation no longer matches the method in the
base class,
so the method in the base class is not overridden.
Fortunately, you have access to the source code for
contoso.cpp, and you can apply the appropriate fix:

// contoso.cpp
class CBasicNamedObject : public INamedObject
{
    ...
    HRESULT GetName(LPWSTR pszBuf, DWORD_PTR cchBuf)
    {
        return StringCchPrintfW(pszBuf, cchBuf, L"Basic");
    }
    ...
};

Yay, everything works again.
A breaking change led to a compiler error, which led you
to the fix.
The only consequence (so far) is that the number of
“things in code being ported from 32-bit Windows to 64-bit Windows
needs to watch out for”
has been incremented by one.
Of course,
too much of this incrementing, and the list of things becomes
so long that developers are going to throw up their hands and say
“Porting is too much work, screw it.”
Don’t forget,

the number of breaking API changes in the
conversion from 16-bit to 32-bit Windows was only 117
.

You think you fixed the problem, but you didn’t.
Because there’s another class elsewhere in the Contoso project.

class CSecureNamedObject : public CBasicNamedObject
{
    ...
    HRESULT GetName(LPWSTR pszBuf, DWORD cchBuf)
    {
        if (IsAccessAllowed())
        {
            return CBasicNamedObject::GetName(pszBuf, cchBuf);
        }
        else
        {
            return E_ACCESSDENIED:
        }
    }
}

The compiler did not raise an error on
CSecure­Named­Object because that
class is not abstract.
The
INamed­Object::Get­Name method
from the INamed­Object interface
is implemented by CBasic­Named­Object.
All abstract methods have been implemented,
so no “instantiating abstract class” error.

On the other hand, the CSecure­Named­Object
method wanted to override the base method,
but since its parameter list didn’t match,
it ended up creating a separate method rather than an override.
(The override pseudo-keyword not yet having been standardized.)
As a result, when somebody calls the
INamed­Object::Get­Name method
on your
CSecure­Named­Object,
they don’t get the one with the security check,
but rather the one from
CBasic­Named­Object.
Result: Security check bypassed.

These are the worst types of breaking changes:
The ones where the compiler doesn’t tell you that something is wrong.
Your code compiles,
it even basically runs,
but it doesn’t run correctly.
Now, sure, the example I gave would have been uncovered
in security testing,
but I chose that just for drama.
Go ahead and substitute something much more subtle.
Like say,

invalidating the entire desktop when you pass
NULL to
Invalidate­Rect
.

Okay, so let’s look back at those principles.
Do we really need to change this interface?
The only case where expanding to
SIZE_T would make a difference is if
an object had a name longer than 2 billion characters.
Is that a realistic end-user scenario?
Not really.
Therefore, don’t change it.

Remember,
you want to make it easier for people to port
their program to 64-bit Windows, not harder.
The goal is make customers happy, not
create the world’s most architecturally
pure operating system
.
And customers aren’t happy when the operating system
can’t run their programs
(because every time the vendor try to port it, they keep stumbling
over random subtle behavior changes that break their program).

Raymond Chen
Raymond Chen

Follow Raymond   

0 comments

Comments are closed.