I've been trying to come up with a means of generating a C interface for a C++17 project of mine. The project produces an executable that loads plugins on the fly. I played with clang for a while before discovering SWIG, and I'm wondering if SWIG is up to the task, or if there's a trivial amount of work that I can do to make it suitable for this scenario.
Here's my vision of the plugin interface. Suppose the source code of my program looks like this:
header.h
namespace Test {
struct TestStruct {
int Data;
};
class TestClass {
public:
virtual ~TestClass() = default;
void TestMethod(TestStruct&) const;
virtual void TestVirtual(int);
};
}
then the following code should be generated:
api.h
// opaque structs
typedef struct {} Test_TestStruct;
typedef struct {} Test_TestClass;
typedef struct {
void (*Test_TestClass_destructor)(Test_TestClass*);
void (*Test_TestClass_TestVirtual)(Test_TestClass*, int);
} Test_TestClass_vtable;
typedef struct {
Test_TestStruct *(*Test_TestStruct_construct)();
void (*Test_TestStruct_dispose)(Test_TestStruct*);
int *(*Test_TestStruct_get_Data)(Test_TestStruct*);
int *(*Test_TestStruct_set_Data)(Test_TestStruct*, int);
Test_TestClass *(*Test_TestClass_construct)();
Test_TestClass *(*Test_TestClass_construct_derived(const Test_TestClass_vtable*);
void (*Test_TestClass_dispose)(Test_TestClass*);
void (*Test_TestClass_TestMethod)(const Test_TestClass*, Test_TestStruct*);
void (*Test_TestClass_TestVirtual)(Test_TestClass*, int);
} api_interface;
api_host.h
#include "api.h"
void init_api_interface(api_interface&);
api_host.cpp
#include "header.h"
#include "api.h"
// wrapper class
class _derived_TestClass : public Test::TestClass {
public:
_derived_TestClass(const Test_TestClass_vtable &vtable) : _vtable(vtable) {
}
~_derived_TestClass() {
if (_vtable.Test_TestClass_destructor) {
_vtable.Test_TestClass_destructor(reinterpret_cast<Test_TestClass*>(this));
}
}
void TestVirtual(int v) override {
if (_vtable.Test_TestClass_TestVirtual) {
_vtable.Test_TestClass_TestVirtual(reinterpret_cast<Test_TestClass*>(this), v);
} else {
TestClass::TestVirtual(v);
}
}
private:
const Test_TestClass_vtable &_vtable;
};
// wrapper functions
Test_TestStruct *_api_Test_TestStruct_construct() {
return reinterpret_cast<Test_TestStruct*>(new TestStruct());
}
void _api_Test_TestStruct_dispose(Test_TestStruct *p) {
auto *phost = reinterpret_cast<TestStruct*>(p);
delete phost;
}
int *_api_Test_TestStruct_get_Data(Test_TestStruct *p) {
return &reinterpret_cast<TestStruct*>(p)->Data;
}
...
...
// sets the values of all function pointers
void init_api_interface(api_interface &iface) {
iface.Test_TestStruct_construct = _api_Test_TestStruct_construct;
iface.Test_TestStruct_dispose = _api_Test_TestStruct_dispose;
iface.Test_TestStruct_get_Data = _api_Test_TestStruct_get_Data;
...
...
}
When I compile the host program, I compile all these files into an executable, and call init_api_interface()
to initialize the function pointers. When other people compile plugins, they only include api.h
, and compile the files into a dynamic library with a certain exposed function, say init_plugin(const api_interface*)
. When the user loads a plugin, the host program only needs to pass a pointer to the struct to init_plugin
in the dynamic library, and the plugin can set off to use all these functions.
The benefits of using such a scheme is that:
- Plugins compiled using different toolchains than the host program should work fine.
- The list of API functions can be extended without breaking existing plugins, as long as new function pointers are added after existing ones.
- This approach allows full access to routines in the host program, while it's also easy to hide certain aspects.
- It allows plugins to inherit from classes in the host program, which is kinda important for my case.
- Plugin developers don't need the source of the host program.
- It's convenient since the API interface doesn't need to be manually maintained.
Of course, this is just a gist of the approach and many more details need to be considered in practice.
So my questions are:
- Is this kind of plugin interface good practice? Are there existing examples of this approach? Are there better solutions to this problem? Is there any critical drawbacks of this approach that I don't see?
- Can SWIG accomplish this task? If not, can SWIG be modified to do so?
- If SWIG must be modified, which is easier, modifying SWIG or starting from scratch using clang?