1

Since we shouldn't pass anything else than Plain Old Data-structure[1] across a plug-in boundary, I came up with to following idea in order to pass an object :

  • expose all the public method in the plugin "C" interface, and on the application side, wrap the plugin in an object. (See the following example)

My question is : Is there a better way to do this ? [EDIT] See my edit below with a probably better solution using standard-layout object.

Here is a toy example illustrating the idea :

I want to pass a Writer across the boundary :

class Writer{
     Writer();
     virtual void write(std::string) = 0;
     ~Writer(){}
};

However, we know that it shouldn't be done directly because of compatibility issue. The idea is to expose the Writer's interface as free functions in the plugin :

// plugin

extern "C"{
   Writer* create_writer(){
        return new PluginWriterImpl{}; 
   }

   void write(Writer* this_ , const char* str){
        this_->write(std::string{str});
   }

   void delete_writer(Writer* this_){
        delete this_;
   }
}

and to wrap all those function call in a wrapper object on the application side :

// app

class WriterWrapper : public Writer{
private:
      Writer* the_plugin_writer; //object being wrapped
public:
      WriterWrapper() : the_plugin_writer{ create_writer() } 
      {}

      void write(std::string str) override{
          write(the_plugin_writer,  str.c_str() );
      }

      ~WriterWrapper(){
          delete_writer(the_plugin_writer);
      }
};

This leads to lots of forwarding function. Nothing else than POD cross the boundary, and the application doesn't know about the fact that the current Writer's implementation comes from a plugin.

[1] For binary compatibility issues. For more information, you can see this related SO question : c++ plugin : Is it ok to pass polymorphic objects?


[EDIT] It seems that we could pass standard-layout across the boundary. If so, would such a solution be correct ? (And could it be simplified ?)

We want to pass a Writer across the boundary :

class Writer{
     Writer();
     virtual void write(std::string) = 0;
     ~Writer(){}
};

So we will pass a standard-layout object form the plugin to the app, and wrap it on the application side.

// plugin.h
struct PluginWriter{
    void write(const char* str);
};

-

// plugin_impl.cpp 

#include "plugin.h"

extern "C"{
    PluginWriter* create_writer();
    void delete_writer(PluginWriter* pw);
}

void PluginWriter::write(const char* str){
    // . . .
}

-

// app
#include "plugin.h"

class WriterWrapper : public Writer{
private:
      PluginWriter* the_plugin_writer; //object being wrapped
public:
      WriterWrapper() : the_plugin_writer{ create_writer() } 
      {}

      void write(std::string str) override{
          the_plugin_writer->write( str.c_str() );
      }

      ~WriterWrapper(){
          delete_writer(the_plugin_writer);
      }
};

However, I fear that the linker will complain while compiling the app because of the : #include plugin.h

Community
  • 1
  • 1
Julien__
  • 1,962
  • 1
  • 15
  • 25
  • why only pod? Why not standard layout? – Yakk - Adam Nevraumont Feb 13 '15 at 13:24
  • Are you supposed to make a C API, that is compatible with both C and C++? Otherwise I don't see the reason for `extern "C"` and not allowing non-POD types. And also, you're doing it wrong because `Writer` may *not* be a POD type (depending on C++ standard used). – Some programmer dude Feb 13 '15 at 13:24
  • Just because the C call standard is used to make the call doesn't stop you from passing anything you want. As long as plugin and application are both in C++ it should not be a problem. – Galik Feb 13 '15 at 13:27
  • see this related SO question about why we can't pass anything else than POD : http://stackoverflow.com/questions/28489762/c-plugin-is-it-ok-to-pass-polymorphic-objects – Julien__ Feb 13 '15 at 16:53
  • @JoachimPileborg Julien intends to write a DLL to expose an object, but wants binary compatibility, aka possible use of different compilers on the DLL and the app side. Therefore the `extern "C"`: it avoid name mangling issues. – Christophe Feb 13 '15 at 18:22
  • @Yakk see my comment above : standard layout is nice as long as you use the same compiler on both side. Otherwie, there is no guarantee of binary compatibility (see https://www.p6r.com/articles/2014/07/20/binary-compatible-c-interfaces/#Object_Layout ) – Christophe Feb 13 '15 at 18:26
  • @Christophe Okay, but then the OP still can't use the `Writer` class as defined above, at least not with the POD requirement. – Some programmer dude Feb 13 '15 at 18:30
  • 1
    @Christophe As C++11 standard layout types are guaranteed to have a layout compatible with C++03 POD types, standard layout is enough. POD, as a concept, was split in C++11 into at least "layout" and "trivially constructable" because there is no reason for a non-virtual method to change the layout of a class in any way, shape or form. The linked article seems to be talking about C++03 without saying they are. – Yakk - Adam Nevraumont Feb 13 '15 at 18:51
  • @JoachimPileborg the `virtual` problem, isn't it ? – Christophe Feb 13 '15 at 19:00
  • @Yakk Do you have a reference in the standard about this ? A few days ago I still concluded that you could not get the offset of a member granted to be the same when using two different compilers because of too many implementation specifics. – Christophe Feb 13 '15 at 19:04
  • 1
    @chris http://stackoverflow.com/a/6496703/1774667 -- and standard layout are layout compatible with pod types with the same members and order. Transitively, if you trust pod, you must trust standard layout. POD as a concept is almost completely gone in C++11: it was basically split in two. I can track down chapter-and-verse from the standard if you really need it. Note that "Standard Layout" is a term defined in the standard: some types are, and others are not, standard layout. – Yakk - Adam Nevraumont Feb 13 '15 at 19:27
  • 1
    @Christophe For one thing yes, and also C++ pre C++11 could not have destructors in a POD type. – Some programmer dude Feb 13 '15 at 19:46
  • @JoachimPileborg, I don't understand your reluctance with Writer. I'm passing a pointer to Writer across the boundary from the plugin to the app, but I don't use it in the app, and I just give it back to the plugin. I think that it is the same as if it were a `void*` ? So the POD is not `Writer`, but rather the pointer `Writer*` – Julien__ Feb 14 '15 at 00:51
  • @Yakk, I read about standard layout. If I understood well, I could pass a struct with public member function as long as it has no ctor ? Then I could wrap that struct in a proper object on the app side. (I edited my question with an example) – Julien__ Feb 14 '15 at 00:54
  • @Julien__ standard layout allows ctors and dtors. It requires that its contents be standard layout, nothing involved be virtual, and everything ... is public I think? I don't have the standard open right now. – Yakk - Adam Nevraumont Feb 14 '15 at 02:06
  • @Yakk, I don't understand, how can you export standard-layout object ? Where and how do you declare the member methods ? – Julien__ Feb 14 '15 at 10:57
  • @julien__ that is a linking problem. For a cross-compiler library, I would put the glue standard-layout objects as fully inline and exposed in headers. They can call exported C functions, or if you have faith that vtables are compatible between library and client code, virtual methods of some interface. The point is that your API need not be just POD and pointers. – Yakk - Adam Nevraumont Feb 14 '15 at 12:34
  • So the summary: pre C++11 if your members where all public and pod, you had no cotrs or dtors, there was in effect a promise of C compatibility. In C++11 they split the layout guarantee (in memory) from the construction/destruction guarantee (ie, that memcpy is 'safe'). Standard layout is the layout guarantee: trivially constructible is the construction guarantee. In pracfice compilers where (all?) providing that feature anyhow, so it was a freebe. – Yakk - Adam Nevraumont Feb 14 '15 at 14:16

1 Answers1

1

Using a DLL with different compilers (or even languages) on client and on library side requires binary compatiblity (aka ABI).

Whatever is said about standard layout or POD, the C++ standard does not guarantee any binary commpatibility between different compilers. There is no comprehensive implementation independent rule on the layout of class members that could ensure this (see also this SO answer on relative address of data members ).

Of course, fortunately, in practice many different compilers use the same logic in standard layout objects for padding and aligning, using the specific best practices or requirements for CPU architecture (as long as no packing or exotic alignment compiler switch is used). Therefore the use of POD/standard layout is relatively safe (and as Yakk correctly pointed out: "if you trust pod, you must trust standard layout.")

So your code may work. Other alternatives, relying on c++ virtuals to avoid name mangling issues, seem to work cross compiler as well as explained in this article. For the same reason: in practice many compilers use on one specific OS+architecture a recognized approach for constructing their vtables. But again, that's an observation from practice and not an absolute guarantee.

If you want to give a cross-compilar conformity guarantee for your library, then you should rely only on real gurantees and not only usual practice. On MS-Windows, the binary interface standard for objects is COM. Here is a comprenhensive C++ COM tutorial. It might be a little bit old, but no other has so many illustrations to make it understandable.

COM approach is of course heavier than your snippet. But that's the cost of the cross compiler and even cross language compliance garantee it offers.

Community
  • 1
  • 1
Christophe
  • 68,716
  • 7
  • 72
  • 138