ExpandEnvironmentStringsA returns a different required buffer length than ExpandEnvironmentStringsW

I was writing some ANSI-only code the other day to handle the case where an environment string expansion could return a buffer larger than MAX_PATH (the test code I was modifying assumed that it could only ever see a MAX_PATH output, I wanted to make it more resilient).

The way the code worked, I defined a wrapper for ExpandEnvironmentStringsA that returned a std:::string and then appended values to that string. But I found that my strings weren’t working correctly – none of the appended values were actually being appended.

Digging in, I discovered that there was an embedded null in the string, which didn’t make any sense to me – where did that come from?

Here’s the wrapper I wrote for the call to ExpandEnvironmentStrings:

 std::string ExpandEnvironmentStringsToString(PCSTR sourceString)
{
    DWORD dwEvSize;
    dwEvSize = ExpandEnvironmentStrings( sourceString, nullptr, 0);
    if (dwEvSize == 0)
    {
        return std::string();
    }
    else
    {
        std::string returnValue(dwEvSize, L'\0');
        dwEvSize = ExpandEnvironmentStrings( sourceString, &returnValue[0], dwEvSize);
        if (dwEvSize == 0)
        {
            return std::string();
        }
        returnValue.resize(dwEvSize-1); // dwEvSize returned by ExpandEnvironmentStrings includes the trailing null, truncate it.
        return returnValue;
    }
}

That code looks like it should be just fine, but there was still that unexpected extra null character.

On a lark, I switched the code to Unicode and tested the version that returned an std::wstring. That worked perfectly – the string converted the string perfectly.

Since ExpandEnvironmentStringsW worked perfectly and ExpandEnvironmentStringsA added an embedded null, I started looking at the return value of ExpandEnvironmentStringsA. It turns out that ExpandEnvironmentStringsA always returned enough space for *two* null characters, not the one character it’s documented as requiring.

Once I figured that out, the solution to my problem was clear. Just change the

     returnValue.resize(dwEvSize-1);

to

     returnValue.resize(dwEvSize-2);

to account for the additional null character.

Just another day in the strange world that is the Win32 API surface Smile.

Comments

  • Anonymous
    November 19, 2015
    I think you have a typo in your 'after' code, as currently is the same as the 'before' code :)

  • Anonymous
    November 19, 2015
    The comment has been removed

  • Anonymous
    November 19, 2015
    Fixed, thanks.

  • Anonymous
    November 20, 2015
    Is it only me or &returnValue[0] expression actually looks a bit too implementation specific?..

  • Anonymous
    November 21, 2015
    @otstrel: Funny, that's the next blog post I want to write :).

  • Anonymous
    November 22, 2015
    Isn't that pre-C++11 way of getting pointer to raw buffer? (Now it is just data() or c_str() - both being const for basic_string)

  • Anonymous
    November 23, 2015
    @Klimax: Because both data() and c_str() return const strings, they can't be modified. &string[0] returns a non const buffer.

  • Anonymous
    November 23, 2015
    Didn't realize it is non-const. Its presence then makes sense. (Didn't need it so far, even when writing wrappers for C-API like HTTP Server...) Another bit learned to day. Thanks.