Repository: SCons/scons

Issue:
MSVC: Configure test failure on non-AMD64 hosts with default VS 2015+ target architecture
The generated  c source file for test ```conf.CheckFunc('printf')``` in ```test\Configure\config-h.py``` produces a linker error due to an unresolved ```printf``` symbol for non-```AMD64``` Windows hosts using the default target architecture for VS2015 and later.

The crux of the problem is using ```printf``` without an explicit ```#include <stdio.h>``` for VS2015 and later with a target architecture that is not ```AMD64```.

For VS2015 and later on non-```AMD64``` hosts and using the default target architecture, the ```config-h.py``` test fails due to ```printf```:
```
STDOUT =========================================================================
2c2
< Checking for C function printf()... yes
---
> Checking for C function printf()... no
```

It appears that starting with VS2015, ```printf``` is only exported from the ```AMD64``` libraries.  The MSVC ```<stdio.h>``` header generates inline functions that invoke a ```printf``` compatible function that is exported from the architecture specific libraries.  Without inclusion of ```<stdio.h>```, the test will fail for VS2015+ when the target architecture is not ```AMD64```.

Test environments:
* Windows 10 x86 (VMWare) Host with VS2019 BuildTools (default target architecture is ```'x86'```)
* Windows 11 ARM64 Host with VS2022 Community installed (WIP branch: default target architecture is ```'ARM64'```)

See below for additional details.


Discussion:
Research details, notes, and comments.

**MSVC Libraries with ```printf``` or ```_printf``` exported**

* The libraries searched below should be comprehensive when no compiler or linker options are specified.
  * VC6-VS2013: ***all architectures***
  * VS2015-VS2022: ***```AMD64``` architectures only***
* ```dumpbin /LINKERMEMBER <library>``` (non-default installation paths):

    ```
    libcmt.txt
        _printf C:\Software\MSVS-1998-60-Pro\VC98\Lib\LIBCMT.LIB
        _printf C:\Software\MSVS-2002-70-Pro\Vc7\lib\libcmt.lib
        _printf C:\Software\MSVS-2003-71-Pro\Vc7\lib\libcmt.lib
        _printf C:\Software\MSVS-2005-80-Pro\VC\lib\libcmt.lib
         printf C:\Software\MSVS-2005-80-Pro\VC\lib\amd64\libcmt.lib
        _printf C:\Software\MSVS-2008-90-Pro\VC\lib\libcmt.lib
         printf C:\Software\MSVS-2008-90-Pro\VC\lib\amd64\libcmt.lib
         printf C:\Software\MSVS-2008-90-Pro\VC\lib\ia64\libcmt.lib
        _printf C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib\libcmt.lib
         printf C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib\amd64\libcmt.lib
         printf C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\lib\ia64\libcmt.lib
        _printf C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\lib\libcmt.lib
         printf C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\lib\amd64\libcmt.lib
         printf C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\lib\arm\libcmt.lib
        _printf C:\Software\MSVS-2013-120-Com\VC\lib\libcmt.lib
         printf C:\Software\MSVS-2013-120-Com\VC\lib\amd64\libcmt.lib
         printf C:\Software\MSVS-2013-120-Com\VC\lib\arm\libcmt.lib
         printf C:\Program Files (x86)\Microsoft SDKs\Windows\v5.0\Lib\IA64\libcmt.lib
         printf C:\Program Files\Microsoft Platform SDK for Windows Server 2003 R2\Lib\AMD64\libcmt.lib
         printf C:\Program Files\Microsoft Platform SDK for Windows Server 2003 R2\Lib\IA64\libcmt.lib
         printf C:\Program Files\Microsoft Platform SDK\Lib\AMD64\libcmt.lib
         printf C:\Program Files\Microsoft Platform SDK\Lib\IA64\libcmt.lib
        _printf C:\Program Files\Microsoft SDKs\Windows\v6.0\VC\LIB\libcmt.lib
         printf C:\Program Files\Microsoft SDKs\Windows\v6.0\VC\LIB\IA64\libcmt.lib
         printf C:\Program Files\Microsoft SDKs\Windows\v6.0\VC\LIB\x64\libcmt.lib
    oldnames.txt
    kernel32.txt
    runtimeobject.txt
    libvcruntime.txt
    libucrt.txt
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10150.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10586.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.14393.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.15063.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.16299.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.17134.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.17763.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.20348.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22000.0\ucrt\x64\libucrt.lib
         printf C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\ucrt\x64\libucrt.lib
    uuid.txt
    ```

**Generated c source file for ```conf.CheckFunc('printf')```**

* The generated c source file is:
    ```
    #include <assert.h>

    #ifdef __cplusplus
    extern "C"
    #endif
    char printf(void);

    #if _MSC_VER && !__INTEL_COMPILER
        #pragma function(printf)
    #endif

    int main(void) {
        #if defined (__stub_printf) || defined (__stub___printf)
            #error "printf has a GNU stub, cannot check"
        #else
            printf();
        #endif
        return 0;
    }
   ```
   
   VS2015+ with a non-```AMD64``` default target architecture results in linker error:
   * ```error LNK2019: unresolved external symbol _printf referenced in function _main```
   
**Documentation: 2.1. Building Simple C / C++ Programs**

* Here's the famous "Hello, World!" program in C:
    ```
    int
    main()
    {
        printf("Hello, world!\n");
    }
    ```
    
* And here's how to build it using SCons. Save the code above into hello.c, and enter the following into a file named SConstruct:
    ```
    Program('hello.c')
    ```
    
    ***The example fails to build for VS2015 and later if the default target architecture is not ```'AMD64'```.***  For example, VS2019 on a 32-bit Windows host.
    
* Adding ```#include <stdio.h>``` will build with the default target architecture on non-```AMD64``` Windows hosts with VS2015 and later:
    ```
    #include <stdio.h>
    int
    main()
    {
        printf("Hello, world!\n");
    }
    ```
    
   The ```#include <stdio.h>``` causes the MSVC c preprocessor to generate inline functions for ```printf``` that dispatch to functions that *are* exported from the architecture-specific libraries.

@mwichmann When you have some time, this one should not take too long to resolve one way or the other.

The easiest solutions are to pick a function other than printf or explicitly include the stdio header (which may not be as easy in the test suite).

With c/c++ headers using inline generation, renaming via the preprocessor, and the like, not explicitly including the associated header seems questionable.  Fifteen years ago, probably not an issue.

In this case, printf is supported when the header is included but mostly because it is remapped to a function that is exported.  
Example/test/configure code should be complete according to current language standards, not just "ancient C". We've fixed a bunch of those over time, including your #4413 above. So including a required header is not just fine, it's the right thing to do.  Is it that simple?  I see no problem with picking a different function to use either, I know in past lives I've run into some interesting problems with GCC which remaps some standard library names to a different name so you don't see the expected symbol in a generated binary (in fact, that scenario seems to be present in your research above with the test code punting on gcc)

Resolving patch:
diff --git a/SCons/Conftest.py b/SCons/Conftest.py
--- a/SCons/Conftest.py
+++ b/SCons/Conftest.py
@@ -231,17 +231,22 @@ def _check_empty_program(context, comp, text, language, use_shared: bool = False
         return context.CompileProg(text, suffix)
 
 
-def CheckFunc(context, function_name, header = None, language = None):
+def CheckFunc(context, function_name, header = None, language = None, funcargs = None):
     """
     Configure check for a function "function_name".
     "language" should be "C" or "C++" and is used to select the compiler.
     Default is "C".
     Optional "header" can be defined to define a function prototype, include a
     header file or anything else that comes before main().
+    Optional "funcargs" can be defined to define an argument list for the
+    generated function invocation.
     Sets HAVE_function_name in context.havedict according to the result.
     Note that this uses the current value of compiler and linker flags, make
     sure $CFLAGS, $CPPFLAGS and $LIBS are set correctly.
     Returns an empty string for success, an error message for failure.
+
+    .. versionchanged:: 4.7.0
+       The ``funcargs`` parameter was added.
     """
 
     # Remarks from autoconf:
@@ -274,6 +279,9 @@ def CheckFunc(context, function_name, header = None, language = None):
         context.Display("Cannot check for %s(): %s\n" % (function_name, msg))
         return msg
 
+    if not funcargs:
+        funcargs = ''
+
     text = """
 %(include)s
 #include <assert.h>
@@ -287,14 +295,15 @@ def CheckFunc(context, function_name, header = None, language = None):
 #if defined (__stub_%(name)s) || defined (__stub___%(name)s)
   #error "%(name)s has a GNU stub, cannot check"
 #else
-  %(name)s();
+  %(name)s(%(args)s);
 #endif
 
   return 0;
 }
 """ % { 'name': function_name,
         'include': includetext,
-        'hdr': header }
+        'hdr': header,
+        'args': funcargs}
 
     context.Display("Checking for %s function %s()... " % (lang, function_name))
     ret = context.BuildProg(text, suffix)
diff --git a/SCons/SConf.py b/SCons/SConf.py
--- a/SCons/SConf.py
+++ b/SCons/SConf.py
@@ -1002,8 +1002,8 @@ def SConf(*args, **kw):
         return SCons.Util.Null()
 
 
-def CheckFunc(context, function_name, header = None, language = None) -> bool:
-    res = SCons.Conftest.CheckFunc(context, function_name, header = header, language = language)
+def CheckFunc(context, function_name, header = None, language = None, funcargs = None) -> bool:
+    res = SCons.Conftest.CheckFunc(context, function_name, header = header, language = language, funcargs = funcargs)
     context.did_show_result = 1
     return not res
 
diff --git a/SCons/SConfTests.py b/SCons/SConfTests.py
--- a/SCons/SConfTests.py
+++ b/SCons/SConfTests.py
@@ -671,11 +671,26 @@ def test_CheckFunc(self) -> None:
                                  log_file=self.test.workpath('config.log'))
 
         try:
-            # CheckFunc()
+            # look for function using default heading
             r = sconf.CheckFunc('strcpy')
             assert r, "did not find strcpy"
+            # no default heading, supply dummy signature
             r = sconf.CheckFunc('strcpy', '/* header */ char strcpy();')
             assert r, "did not find strcpy"
+            # ... supply complete signature, and function args
+            r = sconf.CheckFunc('strcpy', header='/* header */ char *strcpy(char *dest, char *src);', funcargs='"", ""')
+            # ... supply standard header for prototype, and function args
+            assert r, "did not find strcpy"
+            r = sconf.CheckFunc('strcpy', header='#include <string.h>', funcargs='"", ""')
+            # also try in C++ mode
+            cpp_header = """\
+#ifdef __cplusplus
+extern "C"
+#endif
+char *strcpy(char *dest, char *src);
+"""
+            r = sconf.CheckFunc('strcpy', header=cpp_header, funcargs='"", ""', language="C++")
+            assert r, "did not find strcpy"
             r = sconf.CheckFunc('hopefullynofunction')
             assert not r, "unexpectedly found hopefullynofunction"
 


Test patch:
diff --git a/test/Configure/config-h.py b/test/Configure/config-h.py
--- a/test/Configure/config-h.py
+++ b/test/Configure/config-h.py
@@ -41,7 +41,7 @@
 import os
 env.AppendENVPath('PATH', os.environ['PATH'])
 conf = Configure(env, config_h = 'config.h')
-r1 = conf.CheckFunc('printf')
+r1 = conf.CheckFunc('printf', header='#include <stdio.h>', funcargs='""')
 r2 = conf.CheckFunc('noFunctionCall')
 r3 = conf.CheckFunc('memmove')
 r4 = conf.CheckType('int')