Chef Gladiator
Chef Gladiator

Reputation: 1008

Tales from the MSVC extern "C"

[this question has one SO duplicate I can find, but that answer is plain wrong, see the C code below.]

I understand extern "C" does not produce C code in the middle of your C++. It is just a linkage directive.

I have a few of these extern "C" tales to tell, but here is one that bothers me today. This is a completely up-to-date VS2019, and this is the code:

#include <stdint.h>
#include <stdlib.h>

#ifdef __cplusplus
extern "C" {
 #endif

// NOTE: in here it is still C++ code, 
// extern "C" is a linkage directive

typedef struct Test Test;

struct Test { 

/* remove this const and MSVC makes no warning 
   leave it in and MSVC complains, a lot
   GCC or clang could not care less
*/
    const  
        uint32_t x; 
} ;

/*
MSVC throws warning C4190:  'make_Test' has C-linkage specified, 
   but returns UDT 'Test' which is incompatible with C
:  see declaration of 'Test'
*/
inline constexpr Test make_Test(uint32_t x_ )
{
    return Test{ x_ };
}

#ifdef __cplusplus
  }
#endif

int main( void )
{
    constexpr auto test_{ make_Test(42) };
    return test_.x ;
}

Link to the mandatory GODBOLT: https://godbolt.org/z/ecdz1vqhq

That comment about that const is the gist of my question.

MSVC extern "C" is largely (completely?) undocumented. Thus I am unable to tell if, I am breaking some rules in this undocumented zone. Many claim this is some kind of "not fully implemented" C11 in there.

AFAIK having that const for a C11 (or any other C) struct member type is quite OK. And good old GCC could not care less of course. As visible i that GODBOLT on-line.

Is this just a bug in VS2019, or is it me who made a bug?

Update

Even if I move the implementation of make_Test into a separate C file, and explicitly compile it as C, this Warning will stay the same.

About that 'answer' from the same question from before. C can have const struct data members, and of course, C structs can be list initialized when made. See the code below:

// gcc prog.c -Wall -Wextra -std=gnu11 "-Wno-unused-parameter" "-Wno-unused-variable"

#include <stdlib.h>

typedef struct Test { const long x; } Test;

static struct Test make_Test(long x)
{
    struct Test  test_ = { x } ;
    return test_;
 }
 int main(const int argc, const char * argv[])
 {
  struct Test test_ = make_Test(42) ;
   return 42;
 }

Upvotes: 5

Views: 718

Answers (2)

n. m. could be an AI
n. m. could be an AI

Reputation: 119877

There is no requirement in the C++ standard that a C++ implementation come in a matched pair with a C implementation.

There is no requirement in the C++ standard that all constructs that have similar meaning in C and C++ should be binary compatible between a C++ implementation and all, or some, C implementations. There is not even a requirement anywhere that two C implementations on the same platform should be compatible.

To give a very simple example, one can have an implementation (C or C++) with sizeof(long) == 4 and another implementation on the same platform (C or C++) with sizeof(long) == 8, and there is absolutely nothing wrong with that.

Getting back to the specific construct in the question, there is no requirement anywhere that any specific struct, while being completely legal in both C and C++, has the same layout or the same parameter passing convention in a specific C implementation and a specific C++ implementation.

extern "C" helps the programmer produce code that interoperates with C, but the standard cannot guarantee that any specific construct will work, simply because the C++ standard does not govern C implementations.

TL;DR this is not a case of non-conformance to any standard, this is an unfortunate but completely legal incompatibility between a specific C++ implementation and a specific C implementation. Since they both come from the same producer, they know about the incompatibility, and you get a nice warning.

Upvotes: 2

Acorn
Acorn

Reputation: 26066

The x64 calling convention docs explain that UDTs are returned in eax if they are small enough and fit some criteria:

To return a user-defined type by value in RAX, it must have a length of 1, 2, 4, 8, 16, 32, or 64 bits. It must also have no user-defined constructor, destructor, or copy assignment operator; no private or protected non-static data members; no non-static data members of reference type; no base classes; no virtual functions; and no data members that do not also meet these requirements.

While Test is a StandardLayout type (and as such we would expect it to work), the const non-static data member makes the copy assignment operator deleted, which is probably what they mean, even if it says "user-defined". This makes it, by the way, a non TrivialType and therefore not a POD in the C++03 sense.

Similarly, the x86 calling convention docs explain something similar:

Structures that are not PODs will not be returned in registers.

For instance, a function like the following:

Test f(void)
{
    Test test = { 12345 };
    return test;
}

When compiled under x86/x64 C++ mode, Test is considered a non-POD and therefore eax/rax contains the address of the object as the docs lead us to expect.

However, when compiler under x86/x64 C mode, Test is considered a POD (we are compiling C) and therefore you will get the uint32_t value directly in eax.

Therefore, calling f from C won't work, even if we set the language linkage to C, which is why the warning appears.

Upvotes: 4

Related Questions