c++ plugin : pass object across boundary (emulatin

2019-02-20 11:13发布

问题:

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

回答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.