3

I know that flexible array member is not part of the C++11 standard.

So what is the correct way of interoperating with C code that return, or accept as argument, structs with flexible array member, from C++11?

Should I write a shim that maps the flexible array member from the C struct to a pointer in C++?

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
Kal
  • 1,707
  • 15
  • 29
  • Why do you single out C++11, as though this C feature was somehow part of other C++ standards? – Nicol Bolas May 08 '17 at 02:47
  • @NicolBolas I know that flexible array member is not part of any of other versions of C++ standards (C++98, 14, 17) either, but I mention C++11 specifically because my project uses C++11. This is just in case someone wants to give me an answer that is idiomatic in C++14 or C++17, but not doable in C++11. – Kal May 08 '17 at 02:53
  • I assume by flexible you mean variable, i.e. variable length arrays, i.e. VLAs. That said, VLAs as member variables are not even part of the C standard, because indeed they can be a tricky beast. It's a gcc extension. I think you've wandered into a nasty corner here. – Nir Friedman May 08 '17 at 03:06
  • 2
    @NirFriedman, no, flexible array members are a different feature from VLAs. And *both* are indeed standard C features. – John Bollinger May 08 '17 at 03:07
  • You could just define the size of the array to 1 and allocate whatever size you want. – Liran Funaro May 08 '17 at 11:36
  • 2
    @LiranFunaro, whereas that *is* the approach that inspired flexible array members, (1) it is and always has been non-conforming in both C and C++, and its use produces undefined behavior when accessing the member array outside its declared bounds; (2) given an existing C struct with a FAM, the alternative with a 1-element array member is not guaranteed to have that member laid out correspondingly; (3) treating an object of one type as if it were a different, incompatible (in C's sense of the term) type violates the strict aliasing rule, also producing UB. – John Bollinger May 08 '17 at 12:54

3 Answers3

4

As far as I am aware, standard C++ won't even accept the declaration of a struct with a flexible array member. With that being the case, I see no alternative but to write wrapper functions (in C), unless the structure type containing the FAM can be opaque to your C++ code. I'm uncertain whether a wrapper is the kind of shim you had in mind.

Before we go further, however, I should point out that the problem is substantially different if your C functions accept and return pointers to structures with a flexible array member than if they pass and return the actual structures. I'll assume that they do work with pointers to these structures, for otherwise there seems no point to having the FAM in the first place.

I guess that given a C declaration such as

struct foo {
    int count;
    my_type fam[];
};

I would represent the same data in C++ as

struct cpp_foo {
    int count;
    my_type *fam;
};

, which of course can be handled by C, as well. Be aware that you cannot successfully cast between these, because arrays are not pointers.

Given a C function

struct foo *do_something(struct foo *input);

the needed wrapper might then look like this:

struct cpp_foo *do_something_wrap(struct cpp_foo *input) {
    struct cpp_foo *cpp_output = NULL;

    // Prepare the input structure
    size_t fam_size = input->count * sizeof(*input->fam);
    struct foo *temp = malloc(sizeof(*temp) + fam_size);

    if (!temp) {
        // handle allocation error ...
    } else {
        struct foo *output;

        temp->count = input->count;
        memcpy(temp->fam, input->fam, fam_size);

        // call the function
        output = do_something(temp);

        if (output) {
            // Create a copy of the output in C++ flavor
            cpp_output = malloc(sizeof(*cpp_output));
            if (!cpp_output) {
                // handle allocation error
            } else {
                fam_size = output->count * sizeof(output->fam[0])
                cpp_output->fam = malloc(fam_size);
                if (!cpp_output) // handle allocation error

                memcpy(cpp_output->fam, output->fam, fam_size);

                // Supposing that we are responsible for the output object ...
                free(output);
            }
        } // else cpp_output is already NULL

        free(temp);
    }

    return cpp_output;
}

Naturally, if you have several functions to wrap then you probably want to write reusable conversion functions to simplify it.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • 1
    That wouldn't have the same performance. The C code would not need to read another pointer while the C++ code does. – Liran Funaro May 08 '17 at 11:36
  • 2
    @LiranFunaro, *of course* it won't have the same performance. You're taking a performance hit as soon as you accept the need to use a wrapper in the first place. The question asks what approach to take, and the answer is "write wrapper functions in C", as I said. The example presented is intended to strictly conform to the language standard. Depending on the (undisclosed) circumstances, there may be alternative implementations that are superior in one way or another for a specific application, but that's quite beside the point. – John Bollinger May 08 '17 at 12:06
2

There's a trick used by Windows by setting the flexible array member to have size 1 (because the Win32 API has been developed long before the feature ever went into C99, let alone C++)

struct foo {
    int count;
    my_type fam[1];
};

If you're allowed to change the C version then use the same struct in both C and C++. In case you can't change the C version then you'll need to redefine the struct in C++. You still have to change the C++ code when the C struct was modified, but at least it'll compile fine

See also

phuclv
  • 37,963
  • 15
  • 156
  • 475
1

As flexible array members cannot to exposed to C++ (my_type fam[]; is not a valid C++ member), we'll have to define your own type.

Luckily C linkage functions don't have symbols that depend on their arguments. So we can either modify the definition of foo within shared headers, or define our own and don't include their headers.

This is a struct that is likely to be compatible layout-wise. Note that you should never declare these on the stack in C++:

struct foo {
  int count;
  #ifndef __cplusplus
  my_type fam[];
  #else
  my_type do_not_use_fam_placeholder;
  my_type* fam() {
    return &do_not_use_fam_placeholder;
  }
  my_type const* fam() const {
    return &do_not_use_fam_placeholder;
  }
  #endif
};

This relies upon the binary layout of the foo structure in C to be the prefix members, followed by the flexible array member's elements, and no additional packing or alignment be done. It also requires that the flexible array member never be empty.

I would use this+1 but that runs into alignment issues if there is padding between count and fam.

Use of memcpy or memmov or the like on foo is not advised. In general, creating a foo on the C++ side isn't a good idea. If you have to, you could do something like this:

struct foo_header {
  int count;
};

foo* create_foo_in_cpp(int count) {
  std::size_t bytes = sizeof(foo)+sizeof(my_type)*(count-1);
  foo* r = (foo*)malloc(bytes);
  ::new((void*)r) foo_header{count};
  for (int i = 0; i < count; ++i)
    ::new( (void*)(r->fam()+i) my_type();
  return r;
};

which constructs every object in question in C++. C++'s object existence rules are more strict than C's; merely taking some POD memory and interpreting it as a POD is not a valid action in C++, while it is in C. The news above will be optimized to noops at runtime, but are required by C++ to declare that the memory in question should be treated as objects of that type under strict reading of the standard.

Now, there are some standard issues (defects) with manually per-element constructing the elements of an array, and layout-compatibility between arrays and elements, so you'll have to trust somewhat that the ABI of the C++ compiler and the C code is compatible (or check it).

In general, all interop between C and C++ is undefined by the C++ standard (other than some parts of the standard C library which C++ incorporates; even here, there is no mandate that C++ use the same C library). You must understand how your particular implementation of C and C++ interoprate.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524