11

I want to use circular queue with different types and lengths in a project which mainly consists of C code. I'm thinking about implementing the circular queue as a C++ template struct. How to expose the fully specialized C++ template struct to C?

// C++ implementation
// queue.hpp
template <typename T, size_t MAX_LEN>
struct Queue {
    T data[MAX_LEN];
    size_t readIdx = 0;
    size_t writeIdx = 0;

    bool PushBack(T);
    bool PopFront(T *);
};
// expected C API
typedef struct {
    int data[8];
    size_t readIdx;
    size_t writeIdx;
} Qint8;
bool Qint8_PushBack(Qint8 *, int);
bool Qint8_PopFront(Qint8 *, int *);
// C code won't access struct members directly
// but should be able to know the struct's size
// e.g. define an instance, or embed the circular queue in anouther struct
extern Qint8 g_qint8;

typedef struct {
    char data[4];
    size_t readIdx;
    size_t writeIdx;
} Qchar4;
bool Qchar4_PushBack(Qchar4 *, char);
bool Qchar4_PopFront(Qchar4 *, char *);
8
  • C code won't access struct members directly but should be able to know the struct's size Offhand, I don't see how that could be done without providing struct instances for C like you've done in your sample. And if you do that, why use templates at all for C++ (DRY) You may have to implement something like extern "C" Qchar4 *createQchar4() and extern "C" void freeQchar4( Qchar4 * ) functions using opaque structs. Commented 2 days ago
  • 10
    TLDR : No. You would need to make an extern "C" API that then in turn will use your fully specialized template. Commented 2 days ago
  • 2
    Can not be done. Since C doesn't have templates, any attempt to do something like this would necessarily fall afoul of the One Definition rule. Commented 2 days ago
  • If you want one, check my github (just check my profile, there's a link in there). Once you initialize the circular queue, you must define what size each value will take. This way, if you want to store an int, char, pointer, etc..., you just need to supply it with the 'sizeof' instruction. Commented 2 days ago
  • It is not clear what you want to achieve. Do you want to implement a C API for Qint8 that uses a Queue<int, 8> under the hood? In order to do that, your C code would have to invoke C++ code, which means your C++ code would have to instantiate Queue<int, 8>. Commented 2 days ago

7 Answers 7

4

I've got an idea: expose the concrete type Qint8 and Qchar4 as memory buffers.

// bridge.h
#pragma once
#include <stdalign.h>
#ifdef __cplusplus
extern "C" {
#endif

#define Qint8Size 48
#define Qint8Align 8
typedef struct {
    alignas(Qint8Align) unsigned char internal[Qint8Size];
} Qint8;
void Qint8_Construct(Qint8 *);
bool Qint8_PushBack(Qint8 *, int);
bool Qint8_PopFront(Qint8 *, int *);
// C code won't access struct members directly
// but should be able to know the struct's size
// e.g. define an instance, or embed the circular queue in anouther struct
extern Qint8 g_qint8;

#define Qchar4Size 24
#define Qchar4Align 8
typedef struct {
    alignas(Qchar4Align) unsigned char internal[Qchar4Size];
} Qchar4;
void Qchar4_Construct(Qchar4 *);
bool Qchar4_PushBack(Qchar4 *, char);
bool Qchar4_PopFront(Qchar4 *, char *);

#ifdef __cplusplus
}
#endif
// bridge.cpp
#include "bridge.h"
#include "queue.hpp"
#include <new>
using T1 = Queue<int, 8>;
static_assert(sizeof(T1) == Qint8Size && sizeof(Qint8) == Qint8Size);
static_assert(alignof(T1) == Qint8Align);

void Qint8_Construct(Qint8 *q) { new(q->internal) T1; }
bool Qint8_PushBack(Qint8 *q, int v) {
    auto p = std::launder(reinterpret_cast<T1 *>(q->internal));
    return p->PushBack(v);
}
bool Qint8_PopFront(Qint8 *q, int *v) {
    auto p = std::launder(reinterpret_cast<T1 *>(q->internal));
    return p->PopFront(v);
}

using T2 = Queue<char, 4>;
static_assert(sizeof(T2) == Qchar4Size && sizeof(Qchar4) == Qchar4Size);
static_assert(alignof(T2) == Qchar4Align);
void Qchar4_Construct(Qchar4 *q) { new(q->internal) T2; }
bool Qchar4_PushBack(Qchar4 *q, char v) {
    auto p = std::launder(reinterpret_cast<T2 *>(q->internal));
    return p->PushBack(v);
}
bool Qchar4_PopFront(Qchar4 *q, char *v) {
    auto p = std::launder(reinterpret_cast<T2 *>(q->internal));
    return p->PopFront(v);
}

This works. The downside is the hardcode of queue size and alignment in the header.

Sign up to request clarification or add additional context in comments.

5 Comments

The downside is the hardcode of queue size and alignment in the header. Definitely an O&M nightmare. It might be better to turn the dependency around and to implement the structs in C and base the C++ templates off those C struct definitions.
What is O&M?
Operations and Maintenance, most likely, but it's still best to spell it out at first use.
The other downside is the need to replicate and appropriately tweak moderately complicated code in both bridge.h and bridge.cpp - e.g. future you [or someone else] adding support of a new Queue size on the "C side". Easy now, when you have all the nitty-gritties of what's needed in mind. Not so easy for future maintainers of the code who don't have those nitty-gritties in mind. Code maintenance is likely to omit something, so introduce (or expose) bugs - and it takes effort to identify and squash them. Nothing worse than spending unplanned hours finding bugs or (if it's urgent) days.
@user4581301 well, it's not as bad as Business Systems Development Method
3

You turn the problem upside down and specialize your template for the one case that is used in the C API like this.

// assume the following lines are drawn in via #include of the C header
typedef struct {
    int data[8];
    size_t readIdx;
    size_t writeIdx;
} Qint8;
void Qint8_Construct(Qint8 *);
bool Qint8_PushBack(Qint8 *, int);
bool Qint8_PopFront(Qint8 *, int *);

// this is the generic template
template <typename T, size_t MAX_LEN>
struct Queue {
    T data[MAX_LEN];
    size_t readIdx = 0;
    size_t writeIdx = 0;

    bool PushBack(T);
    bool PopFront(T *);
};

// this is the special case
template<>
struct Queue<int, 8> : Qint8 {
    Queue() : { Qint8_Construct(this); }
    bool PushBack(int x) { return Qint8_PushBack(this, x); }
    bool PopFront(int *px) { return Qint8_PopFront(this, px); }
};

3 Comments

With this approach, would the logic of Queue<T,MAX_LEN>::PushBack be repeated in Qint8_PushBack and Qchar4_PushBack? (If an error is discovered, it would need to be fixed in multiple locations?)
It would have to be repeated in this simple approach. To avoid that, one possibility is to implement them as function templates and call those from the generic class template, the specialization, and possibly also from the C function implementations (which would then have to live in the C++ code, of course).
be careful with this answer. you should know what slicing, structure size with inheritance, constructors, and possibly many more, to use this answer.
1

You cannot expose a template to C, and any object created in C code will not have member functions. So C code cannot define an instance of your C++ queue. However, you can still leverage C++ templates to avoid code duplication.

Note (for future readers): A convenient way to use C++ objects in C code is to allocate the object dynamically so that the type can be incomplete in the C code. The question specifically rules this out, though.

It might be better to think in terms of function templates instead of struct templates. This is one of the situations where forcing an object-oriented design can be more trouble than it is worth. Implement the functionality with function templates (perhaps inside a namespace to group them), and have the C API call those. The C++ version of the queue could also call these functions, if you still want a C++ version. Admittedly, such a setup does not look great from a C++ perspective, but some amount of compromise is necessary to accommodate C.

With this approach, structures like Qint8 would not be exposed to C; they would be defined in C. The API functions would be declared in C, but defined in C++ where they can utilize the function templates. To get started with these templates, let's have a way to recognize that a structure looks like it could be a queue (QueueLike) and a way to extract the element type and size (QueueInfo).

#include <concepts>
#include <type_traits>
#include <utility>

#include <cstddef> // If you want to force the indices to be `size_t`
#include <limits>  // If you do not want to force the indices to be `size_t`


template <typename Q>
concept QueueLike = 
    requires (Q queue) {
        requires 0 < std::extent_v<decltype(queue.data)>; // is an array

        // If you want to force the indices to be `size_t`:
        {queue.readIdx} -> std::same_as<size_t&>;
        {queue.writeIdx} -> std::same_as<size_t&>;

        // If you do not want to force the indices to be `size_t`:
        // The indices need to be integers of the same type and large enough for the data.
        requires std::is_integral_v<decltype(queue.readIdx)>;
        requires std::same_as<decltype(queue.writeIdx), decltype(queue.readIdx)>;
        requires std::extent_v<decltype(queue.data)>
                 <= std::numeric_limits<decltype(queue.readIdx)>::max();
};

template <QueueLike Q>
struct QueueInfo
{
    using data_type = decltype(std::declval<Q>().data); // for readability

    using element_type = std::remove_extent_t<data_type>;
    static constexpr auto max_len = std::extent_v<data_type>;
};

Given these tools, one can write function templates to implement the queue functionality. These should be minor adaptations of the Queue member functions. Unlike the code in the question, where the template parameters are the type of elements and the maximum length, the template parameter here is the struct that holds the data. The type of the elements and the maximum length are obtained via the QueueInfo template. As examples, here are an implementation of queue initialization and a sketch of an implementation of "push back". (Presumably the details of "push back" are known; if not, that's a separate question.)

template <QueueLike Q>
void InitializeQueue(Q * queue) {
    if ( queue != nullptr )
        queue->readIdx = queue->writeIdx = 0;
}

template <QueueLike Q>
bool PushBackImpl(Q * queue, typename QueueInfo<Q>::element_type value) {
    // Make sure `queue` is non-null and `*queue` is not full before pushing.
    // The maximum length is available as `QueueInfo<Q>::max_len`.
    queue->writeIdx %= QueueInfo<Q>::max_len; // Probably done at some point.
    // Do whatever else is needed.
    return true;
}

Once you have all the templates you need, the functions of the C API become simple wrappers. As a reminder, these definitions need to be in a C++ source file so that the C compiler does not try to process them. You might note that the template arguments have been deduced. This is intentional, as it cuts down on copy-paste errors. (To change the Qint8 functions to Qchar4 versions, only the function names and parameter types need to be changed, not the function bodies.) Also, I added an initialization routine since a C compiler will not invoke C++ constructors.

void Qint8_Initialize(Qint8 * queue) {
    InitializeQueue(queue);
}

bool Qint8_PushBack(Qint8 * queue, int value) {
    return  PushBackImpl(queue, value);
}

bool Qint8_PopFront(Qint8 * queue, int * value)  {
    return  PopFrontImpl(queue, value);
}

Since the project is mainly C code, it might be that the C++ queue is not really needed. If it is, though, it can leverage the above function templates. If the data members are kept public (not the best idea), the implementation could look like the following.

template <typename T, size_t MAX_LEN>
struct Queue {
    T data[MAX_LEN];
    size_t readIdx = 0;
    size_t writeIdx = 0;

    bool PushBack(T value) { return PushBackImpl(this, value); }
    bool PopFront(T * value) { return PopFrontImpl(this, value); }
};

It gets a little more complicated when the data members are private, but the point still stands that there is no code duplication. (Well, the initialization is duplicated, but you could add a constructor that calls InitializeQueue if you want.) You get object-oriented queues for C++ code and procedure-based queues for C, with the logic shared between them.

Comments

1

Most simple approach is to enforce use of heap and hide Qint8 details from C code. This is quite common pattern.
Here is my approach:

Qint8.h

#pragma once

#include <stdalign.h>
#ifdef __cplusplus
extern "C" {
#endif

struct Qint8;
typedef struct Qint8 Qint8;

Qint8* Qint8_New();
void Qint8_Free(Qint8*);

int Qint8_PushBack(Qint8*, int);
int Qint8_PopFront(Qint8*, int*);

#ifdef __cplusplus
}
#endif

Qint8.cpp

#include "Qint8.h"
#include "queue.hpp"

struct Qint8 : Queue <int, 8> {};

extern "C" {
Qint8* Qint8_New() {
    return new Qint8{};
}

void Qint8_Free(Qint8* q) {
    delete q;
}

int Qint8_PushBack(Qint8* q, int x) {
    return q->PushBack(x);
}

int Qint8_PopFront(Qint8* q, int* p) {
    return q->PopFront(p);
}
}

This will work with C++98.

Comments

1

A standard way I expose things to C is to use handles.

A handle is typically a struct containing a single pointer, either to void or to some other anonymous structure.

You then convert it back to the type within the class. You can write machinery on the C++ side to make this typo-free.

Start with this:

struct handle_t { void* ptr; };

then for a given type:

struct foo_handle { handle_t handle; }
foo_handle foo_create();
void foo_destroy( foo_handle );

and add some "methods":

int foo_getHeight( foo_handle );
BOOL foo_eatFood( foo_handle, char const* food );

shared between C and C++.

In C++, we write helper functions and macros:

template<class T>struct tag_t{using type=T;};
constexpr tag_t<T> tag = {};

#define CONNECT_TO_HANDLE( C_HANDLE, ... ) \
  inline tag_t<C_HANDLE> get_handle_for_type( tag_t<__VA_ARGS__> ) { return {}; } \
  inline tag_t<__VA_ARGS__> get_type_for_handle( tag_t<C_HANDLE> ) { return {}; }

template<class T>
auto to_handle( T* obj ) {
  using Handle = decltype( get_handle_for_type(tag<T>) )::type;
  return Handle{ handle_t{ obj } };
}
template<class Handle>
auto from_handle( Handle obj ) {
  void* ptr = obj.handle.ptr;

  using Object = decltype( get_type_for_handle(tag<Handle>) )::type;
  return static_cast<Object*>(ptr);
}

We write our actual template:

template<class T>
struct my_template {
  std::vector<T> data;
  bool eatFood( std::string_view sv ) const {
    bool all = true;
    for (T const& t:data)
      all = all && t.eatFood(sv);
    return all;
  }
  int totalHeight() const {
    int total = 0;
    for (T const& t:data)
      total += t.height();
    return total;
  }
};

now we connect the two:

CONNECT_TO_HANDLE( foo_handle, my_template<int> )

foo_handle foo_create() {
  return to_handle( new my_template<int>{} );
}
void foo_destroy( foo_handle h ) {
  delete from_handle(h);
}

int foo_getHeight( foo_handle h ) {
  auto* ptr = from_handle(h);
  return ptr?ptr->totalHeight():0;
}
BOOL foo_eatFood( foo_handle h, char const* food ) {
  auto* ptr = from_handle(h);
  if (!ptr)
    return FALSE;
  std::string_view sv(food);
  return ptr->eatFood(sv)?TRUE:FALSE;
}

The glue code is standard C/C++ bridging.

The conversion code is type safe ish, in that it refuses to produce a handle if given an unconnected type (and produces the handle of the connected type if it is connected), and will refuse a handle of the wrong type (and will return the connected object type only if it is connected).

You could extend this to value-likes, but this is dangerous; C will copy the bytes blindly, and most C++ types don't support byte-based copies.

Comments

1

You could just use C. The following is a possible implementation that I put together with Visual Studio 2019 and ran the test program as both an x86 application and a 64 bit application.

I suspect if you spent a bit of time with C11 _Generic() you could wrap this and improve compiler diagnostics and error checking. I did make a change in your buffer data structure by putting the array containing the actual circular buffer at the end of the struct. I also require the type of the data items stored in the circular buffer to be a struct of some kind.

I think that you could allocate the queue on the heap and having a variable length data area included in the allocation request. And it seems reasonable you could add the data struct size as an additional data member of the queue header and management data so as to remove the need to pass this value as an argument to the two functions PushBackv() and PullFrontv().

Provide a header file with the following Preprocessor defines to provide a basic domain specific set of queue tools.

#define queuename(tn)  Queue##tn 

#define queue(tn,cnt)    \
  typedef   \
  struct queuename(tn) { \
      size_t  ndxOut;  \
      size_t  ndxIn;   \
      size_t  maxSize;  \
      struct tn array[cnt];    \
   } queuename(tn)[1];

#define queuemake(tn, v, cnt) struct queuename(tn) v[1] = {0, 0, cnt};

#define queuedefinemake(tn,cnt,v)     \
  typedef   \
  struct queuename(tn) { \
      size_t  ndxOut;  \
      size_t  ndxIn;   \
      size_t  maxSize;  \
      struct tn array[cnt];    \
   } queuename(tn)[1];    \
   struct queuename(tn) v[1] = {0, 0, cnt};

// function prototypes for the unspecified argument type functions.
int PushBackv(void* p, void* x, size_t nSize);
int PullFrontv(void* p, void* x, size_t nSize);

Next have a C source file providing the two functions, PushBackv() and PullFrontv().

// using void pointers for flexible interface
int PushBackv (void *p, void *x, size_t nSize) {
    struct QueueStruct {
        size_t  ndxOut;
        size_t  ndxIn;
        size_t  maxSize;
        struct QueueStruct *a;
    } jQueue = *(struct QueueStruct*)p;

    int  iRet = 0;     // assume failure
    size_t tmpIn = jQueue.ndxIn;

    if (++tmpIn >= jQueue.maxSize) tmpIn = 0;
    if (!(tmpIn == jQueue.ndxOut)) {
        unsigned char* cp = (void *)&(((struct QueueStruct*)p)->a);
        cp += nSize * jQueue.ndxIn;
        memcpy(cp, x, nSize);
        ((struct QueueStruct*)p)->ndxIn = tmpIn;
        iRet = 1;     // change to nonfailure
    }

    return iRet;
}

int PullFrontv(void* p, void* x, size_t nSize) {
    struct QueueStruct {
        size_t  ndxOut;
        size_t  ndxIn;
        size_t  maxSize;
        struct QueueStruct* a;
    } jQueue = *(struct QueueStruct*)p;

    int  iRet = 0;     // assume failure

    if (!(jQueue.ndxOut == jQueue.ndxIn)) {
        unsigned char* cp = (void*)&(((struct QueueStruct*)p)->a);
        cp += nSize * jQueue.ndxOut;
        memcpy(x, cp, nSize);
        if (++(jQueue.ndxOut) >= jQueue.maxSize) jQueue.ndxOut = 0;
        ((struct QueueStruct*)p)->ndxOut = jQueue.ndxOut;
        iRet = 1;     // change to nonfailure
    }

    return iRet;
}

and then use them as in the following sample:


// define the struct for the elements of the circular queue
struct Frank {
    int i1;
    int i2;
    char  name[16];
};
typedef struct Frank Frank;      // typedef it to make it all easier to use.


int main() {
    queuedefinemake(Frank, 20, MyFrank2);
    Frank jj = { 0 };

    int pushOps = 0, pullOps = 0;     // counters for number of operations of each type.

    {
        Frank kk;
        printf("check initial condition (should be 0) %d\n", PullFrontv(MyFrank2, &kk, sizeof(kk)));
    }

    printf("start pushback.\n");
    for (int i = 0; i < 30; i++) {
        // make up some values for our testing.
        ++jj.i1;
        jj.i2 = jj.i1 * 100;
        sprintf(jj.name, "i1 %d", jj.i1);
        if (!PushBackv(MyFrank2, &jj, sizeof(jj))) break;
        ++pushOps;

        if (i % 2) {
            Frank kk = { 0 };
            if (PullFrontv(MyFrank2, &kk, sizeof(kk))) {
                ++pullOps;
                printf("    kk = %d  %d %s\n", kk.i1, kk.i2, kk.name);
            }
        }
    }

    printf("\nstart pullfront.\n");
    for (int i = 0; i < 30; i++) {
        Frank kk = { 0 }, *pkk = &kk;
        if (!PullFrontv(MyFrank2, pkk, sizeof(*pkk))) break;
        ++pullOps;
        printf("    kk = %d  %d %s\n", kk.i1, kk.i2, kk.name);
    }

    {
        Frank kk;
        printf("check final condition (should be 0) %d\n", PullFrontv(MyFrank2, &kk, sizeof(kk)));
        printf("   pushOps = %d  pullOps = %d\n", pushOps, pullOps);
        printf("   MyQueue ndxOut %d  ndxIn %d  maxSize %d ", (int)MyFrank2->ndxOut, (int)MyFrank2->ndxIn, (int)MyFrank2->maxSize);
    }

    return 0;
}

Comments

0

A possiblity could be to enhance the GCC compiler (compiling your code) with your open source compiler plugin. Old code from bismon project could be inspirational.

Another approach might be to generate some C++ glue code.

The generated C++ glue code would contain extern "C" functions.

Look into RefPerSys or the moc generator from Qt for inspiration. See also the GPP project, or GNU m4 or carburetta.

On Linux systems you can generate temporary code even at runtime (and compile it with system(3) invoking a g++ compilation) and use dlopen(3) and dlsym(3) maybe with dladdr(3) to use it at runtime. Empirically this can be done quickly enough to be compatible with human interaction.

Consider coding your C++ code generator using tools like ANTLR or bisoncpp.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.