c++coopstandard-layout

Wrapping C++ in C: Derived to base conversions


I am wrapping a simple C++ inheritance hierarchy into "object-oriented" C. I'm trying to figure out if there any gotchas in treating the pointers to C++ objects as pointers to opaque C structs. In particular, under what circumstances would the derived-to-base conversion cause problems?

The classes themselves are relatively complex, but the hierarchy is shallow and uses single-inheritance only:

// A base class with lots of important shared functionality
class Base {
    public:
    virtual void someOperation();
    // More operations...

    private:
    // Data...
};

// One of several derived classes
class FirstDerived: public Base {
    public:
    virtual void someOperation();
    // More operations...

    private:
    // More data...
};

// More derived classes of Base..

I am planning on exposing this to C clients via the following, fairly standard object-oriented C:

// An opaque pointers to the types
typedef struct base_t base_t;
typedef struct first_derived_t first_derived_t;

void base_some_operation(base_t* object) {
     Base* base = (Base*) object;
     base->someOperation();
}

first_derived_t* first_derived_create() {
     return (first_derived_t*) new FirstDerived();
}

void first_derived_destroy(first_derived_t* object) {
     FirstDerived* firstDerived = (FirstDerived*) object;
     delete firstDerived;
}

The C clients only pass around pointers to the underlying C++ objects and can only manipulate them via function calls. So the client can finally do something like:

first_derived_t* object = first_derived_create();
base_some_operation((base_t*) object); // Note the derived-to-base cast here
...

and have the virtual call to FirstDerived::someOperation() succeed as expected.

These classes are not standard-layout but do not use multiple or virtual inheritance. Is this guaranteed to work?

Note that I have control over all the code (C++ and the C wrapper), if that matters.


Solution

  • // An opaque pointers to the types
    typedef struct base_t base_t;
    typedef struct first_derived_t first_derived_t;
    
    // **********************//
    // inside C++ stub only. //
    // **********************//
    
    // Ensures you always cast to Base* first, then to void*,
    // then to stub type pointer.  This enforces that you'll
    // get consistent a address in presence of inheritance.
    template<typename T>
    T * get_stub_pointer ( Base * object )
    {
         return reinterpret_cast<T*>(static_cast<void*>(object));
    }
    
    // Recover (intermediate) Base* pointer from stub type.
    Base * get_base_pointer ( void * object )
    {
         return reinterpret_cast<Base*>(object);
    }
    
    // Get derived type pointer validating that it's actually
    // the right type.  Returs null pointer if the type is
    // invalid.  This ensures you can detect invalid use of
    // the stub functions.
    template<typename T>
    T * get_derived_pointer ( void * object )
    {
        return dynamic_cast<T*>(get_base_pointer(object));
    }
    
    // ***********************************//
    // public C exports (stub interface). //
    // ***********************************//
    
    void base_some_operation(base_t* object)
    {
         Base* base = get_base_pointer(object);
         base->someOperation();
    }
    
    first_derived_t* first_derived_create()
    {
         return get_stub_pointer<first_derived_t>(new FirstDerived());
    }
    
    void first_derived_destroy(first_derived_t* object)
    {
         FirstDerived * derived = get_derived_pointer<FirstDerived>(object);
         assert(derived != 0);
    
         delete firstDerived;
    }
    

    This means that you can always perform a cast such as the following.

    first_derived_t* object = first_derived_create();
    base_some_operation((base_t*) object);
    

    This is safe because the base_t* pointer will be cast to void*, then to Base*. This is one step less than what happened before. Notice the order:

    1. FirstDerived*
    2. Base* (via implicit static_cast<Base*>)
    3. void* (via static_cast<void*>)
    4. first_derived_t* (via reinterpret_cast<first_derived_t*>)
    5. base_t* (via (base_t*), which is a C++-style reinterpret_cast<base_t*>)
    6. void* (via implicit static_cast<void*>)
    7. Base* (via reinterpret_cast<Base*>)

    For calls that wrap a FirstDerived method, you get an extra cast:

    1. FirstDerived* (via dynamic_cast<FirstDerived*>)