Search code examples
visual-studio-2019cl

VS2019 and _NO_CRT_STDIO_INLINE, how to explain this weirdo?


Please check my short code below.

pwrapper.h

#include <stdio.h>
#include <stdarg.h>

extern"C" int mm_printfA(const char *fmt, ...);
extern"C" int mm_printfW(const wchar_t *fmt, ...);

pwrapper.cpp

#include "pwrapper.h"

int mm_printfA(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);

    int ret = vprintf(fmt, args);

    va_end(args);
    return ret;
}

int mm_printfW(const wchar_t *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    
    int ret = vwprintf(fmt, args);
    
    va_end(args);
    return ret;
}

main.cpp

#include "pwrapper.h"

// cl /MT /D _NO_CRT_STDIO_INLINE main.cpp pwrapper.cpp

void main()
{
    mm_printfA("What is %d?\n", 123);
}

#if 0
void usedull()
{
    vprintf(NULL, NULL);
    vwprintf(NULL, NULL);
}
#endif

For some reason, I need to compile it with _NO_CRT_STDIO_INLINE, like this:

cl /MT /D _NO_CRT_STDIO_INLINE main.cpp pwrapper.cpp

But link stage fails saying unresolved external symbol vwprintf and vprintf .

enter image description here

A very weird workaround I find out is: Enable the usedull() function body -- although never be called, and, link through pwrapper.lib, using bb.bat below:

@setlocal EnableDelayedExpansion
@set CFLAGS=/D _NO_CRT_STDIO_INLINE

cl /nologo /c /MT %CFLAGS% pwrapper.cpp
@if errorlevel 1 exit /b 4

lib /nologo  /out:pwrapper.lib pwrapper.obj
@if errorlevel 1 exit /b 4

cl /nologo /c /MT  main.cpp
@if errorlevel 1 exit /b 4

link /nologo main.obj pwrapper.lib
@if errorlevel 1 exit /b 4

Well, this really works, but why?

enter image description here

This is not a pleasant workaround, because each exe project needs to include a "useless" usedull() function. So, is there any better way?

I really can't tell why this workaround works, an explanation of it is very welcome.

==== Some Clarification ====

There were two main.cpp in my original post. Let me name them separately for later reference in case someone would bother to answer this weird question.

  • main.0.cpp refers to the one without usedull().
  • main.1.cpp refers to the one with usedull().

In this question, I use VC++ headers and libs for application(not for kernel), and

  • I compile main.0.cpp and main.1.cpp without _NO_CRT_STDIO_INLINE.
  • I always compile pwrapper.cpp with _NO_CRT_STDIO_INLINE.

Whether having pwrapper.obj go through pwrapper.lib produce the same result in this issue.


Solution

  • In short:

    • compiling pwrapper.cpp with -D _NO_CRT_STDIO_INLINE tells the compiler you are going to provide your own implementation of vprintf and vwprintf at link time, and
    • compiling main.cpp without -D _NO_CRT_STDIO_INLINE tells the compiler to include implementations of vprintf and vwprintf which are used at link time to satisfy both the references from usedull and mm_printfA/mm_printfW

    so, this particular combination works to resolve all undefined symbols at link time. See below for more discussion however.

    Discussion

    In stdio.h, vprintf (which I'll focus on, but vwprintf is configured in the same way) is defined like so:

    _Check_return_opt_
    _CRT_STDIO_INLINE int __CRTDECL vprintf(
        _In_z_ _Printf_format_string_ char const* const _Format,
                                      va_list           _ArgList
        )
    #if defined _NO_CRT_STDIO_INLINE
    ;
    #else
    {
        return _vfprintf_l(stdout, _Format, NULL, _ArgList);
    }
    #endif
    

    Note that

    • if _NO_CRT_STDIO_INLINE is defined, this becomes a forward declaration
    • whereas if it is not defined, the full body is included in the compilation of the including translation unit.

    Additionally, in corecrt_stdio_config.h whether _NO_CRT_STDIO_INLINE is defined determines the value of _CRT_STDIO_INLINE; if it is defined, _CRT_STDIO_INLINE is defined as empty, otherwise it is defined as __inline.

    Putting these together,

    • if _NO_CRT_STDIO_INLINE is not defined, these functions will be candidates for inline expansion,
    • otherwise a separate implementation of that function will need to be provided at link time.

    Default Compilation (no /O1, /O2, no _NO_CRT_STDIO_INLINE)

    The above works with the specific compile and link invocations you are using, as without optimization the compiler will simply include the function body in the compilation of main.1.obj. You can see this using dumpbin; running dumpbin -symbols main.1.obj | find "| vprintf" prints:

    01D 00000000 SECT8  notype ()    External     | vprintf
    

    showing that main.1.obj provides vprintf as an externally available symbol.

    Checking pwrapper.obj, we get:

    00A 00000000 UNDEF  notype ()    External     | vprintf
    

    showing that vprintf is undefined in this object file, and will need to be provided at link time.

    Optimisation for Inline Expansion

    However, if we change the optimisation option for inline expansion, we get different results. Using even the first level of optimisation (-Ob1, included in -O1 and -O2) like so:

    cl -c -Ob1 main.1.cpp
    

    causes the compiler to incorporate the body of vprintf directly into usedull, and remove the separate implementation of vprintf, which can be confirmed using dumpbin. So, as you would now expect, attempting to link main.1.obj and pwrapper.obj together will once again give your original error:

    pwrapper.obj : error LNK2019: unresolved external symbol vwprintf referenced in function mm_printfW
    pwrapper.obj : error LNK2019: unresolved external symbol vprintf referenced in function mm_printfA
    main.exe : fatal error LNK1120: 2 unresolved externals
    

    Multiple Implementations?

    So, following on from that it is apparent that compiling both files with -D _NO_CRT_STDIO_INLINE will fail as there will be no implementations of the relevant methods. What about if both are compiled without this definition?

    If we check the object files, both have defined symbols for vprintf:

    01D 00000000 SECT8  notype ()    External     | vprintf
    

    and:

    01A 00000000 SECT7  notype ()    External     | vprintf
    

    which under normal circumstances would result in errors due both to multiple definitions of a symbol and violations of the One Definition Rule. However, when performing inline expansion, the compiler and linker have your back. As per 2:

    Rather than expand an inline function defined in a header file, the compiler may create it as a callable function in more than one translation unit. The compiler marks the generated function for the linker to prevent one-definition-rule (ODR) violations.