1

I have defined an interface using C++ abstract class. I want to instantiate it multiple times for different variants of my project (variants can be hardware variants or OS variants). My goal is to create shared libraries for each variant and use the right one for a given project without having to re-compile anything.

I order to retrieve the required instance for a given variant, I am using a public function. In the cpp file of each variant I am defining a function to return the instance for that variant. In the main code I am making an extern declaration of this function. I intend to call a method of the interface using the returned object.

// Interface.hpp
class Interface {
    public:
        virtual void routine() = 0;
};

// variantA.cpp
class variantA : public Interface {
    public:
        void routine() {
            printf( "We are in variantA\n" );
        }
} variant;

Interface& getVariant() {
    return variant;
}

// variantB.cpp
class variantB : public Interface {
    public:
        void routine() {
            printf( "We are in variantB\n" );
        }
} variant;

Interface& getVariant() {
    return variant;
}

// main.cpp
#include "Interface.hpp"

extern Interface& getVariant();

int main() {
    Interface& interface = getVariant();
    interface.routine();
}
// build.sh
g++ -c -fPIC -O0 variantA.cpp
g++ -c -fPIC -O0 variantB.cpp
g++ variantA.o -shared -o libvariantA.so
g++ variantB.o -shared -o libvariantB.so

ln -s libvariantA.so libvariant.so

g++ -L. -lvariant main.cpp -o main

Main question: Is my method is theoretically correct? Is it a good idea to do it this way? Or, are there any obvious pitfalls to this method? If so, can anyone please recommend me a better approach?

Second question: I could probably figure it out myself with some trials. But still I am posting this because this could potentially render the approach as unusable. The problem is the compilation error:

In function `main':
main.cpp:(.text+0x9): undefined reference to `getVariant()'
collect2: error: ld returned 1 exit status
DJ1411
  • 15
  • 4
  • `extern Interface& getVariant();` should probably go in header. – Jarod42 Oct 15 '19 at 03:01
  • If variants are representing different hardware, I would go for link-time polymorphism. Where your build system chooses which implementation should be picked. – Guillaume Racicot Oct 15 '19 at 03:03
  • For you unresolved symbol, order is important, see [why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc](https://stackoverflow.com/questions/45135/why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc) – Jarod42 Oct 15 '19 at 03:04
  • thanks @Jarod42, this solves my compilation problem – DJ1411 Oct 15 '19 at 07:17

2 Answers2

0

Nope. That seems to be a bad idea and not how everyone else does C++ shared libs. If they are different hardware or OS variants then the libraries may have to be a different format. One library with the same API compiled for each variant is the normal approach.

John3136
  • 28,809
  • 4
  • 51
  • 69
  • Could you please elaborate your answer? I dont think I quite understood the problem with my approach and what your proposed solution is. – DJ1411 Oct 15 '19 at 09:47
  • In simple terms, each OS or hardware platform may need a different format library and so having a single library just doesn't work. You need the same code compiled for each platform. – John3136 Oct 15 '19 at 21:43
0

First don't do that with the declaration before usage, that belongs in the interface header, that is what exactly a header is good for.

Problem

But I fear we got an XY problem here:

I want to instantiate it multiple times for different variants of my project (variants can be hardware variants or OS variants). My goal is to create shared libraries for each variant and use the right one for a given project without having to re-compile anything.

That's not possible: See you usually can not compile libvarianta for hardware or OS variant a while compiled libvariantb for hardware or OS variant b and use it all together in one program which links to a and b simultaneously.

The problem is that there is a reason why you usually would have to compile every time again: Different hardware and different operating systems usually need different machine code. Even the printf call you do translates to different machine code for different compilers or operating systems. That's what the C++ standard and reference means with what the runtime and implementation is.

Also you could often not even call the function because when using shared linking you got into problems of the ABI (not API), which also means that even for the same Microsoft compiler Version for the same architecture you have to compile it once for the debug and once for the release build (unless further precaution are followed).

Most libraries even have different high level code compiling on different platforms by using the preprocessor.

Solution

So you would have to compile still all needed different versions for your libraries and the consumer of your library, which would need a complex logic in order to know which binary to load. In another words: You would have to find the one shared library binary which you would have gotten if you simply compiled them together anyway. So it would be easier to simply static link it and use one API, no need for an abstract class or interfaces,at least run time wise. You might need to do different things at compile time, since you might have no portable way to do things. Then you can use either preprocessor macros or compile time polymorphism. That fits with the recommendation John3136 has given you.

Answer

So NO, shared linking would not solve the problem i.e. achieve what your try to do as far as I understand your question. It would simply create more complex problems. Shared linking is not portable nor even a thing according to pure standard C++. However there are some tricks how different operating system try to achieve ABI stability (like COM objects on windows) but this still would not solve the problem if your code needs to target different hardware or runtimes.

Superlokkus
  • 4,731
  • 1
  • 25
  • 57
  • Notice than OP build both .so, but only link with "active" variant. – Jarod42 Oct 15 '19 at 22:58
  • 1
    Thanks @Superlokkus for the detailed explanation. It helped me a lot to understand the problem. Hardware or OS compatibility issue is clearly understood. I learned about ABI, which was a new thing for me. And I agree that my solution wont work in this scenario. In future, I will try to avoid the XY problem. – DJ1411 Oct 16 '19 at 04:11
  • 1
    Just so that my understanding is complete, I would like to ask a follow up question, if you don't mind. Although my solution wont work for HW or OS variant, I have the feeling it could work for software Feature variants for the same target. e.g. I want to package my product with feature X+Y and another variant with features X+Z. In that case, I could compile my main binary and shared libs for both variants. While shipping I could use (main+libvariantA.so) for customerA and (main+libvariantB.so) for customerB. Do you think that could work? – DJ1411 Oct 16 '19 at 04:23
  • Yes that is indeed one of the main use cases in practice for shared libraries, since you don't want to give customers binaries containing fuctionality they didn't pay for, and could use by simply changing a jump in your main binaries assembly (cracking(. That also the reason you see the PIMPL pattern ( https://en.cppreference.com/w/cpp/language/pimpl ) so oftem. Boost DLL has a nice site for this plugin approach, and also lets you load your dyn libs in a portable way: https://www.boost.org/doc/libs/1_71_0/doc/html/boost_dll/tutorial.html#boost_dll.tutorial.plugin_basics – Superlokkus Oct 16 '19 at 09:28
  • Also I recommend using CMake for building and structuring your project, it also lets your automatically build your libraries shared in a portable way and helps your with your internal dependency management. My productive project template is: https://github.com/Superlokkus/spielwiese – Superlokkus Oct 16 '19 at 09:41
  • And don't forget to accept the answer if it helped you ;-) – Superlokkus Oct 16 '19 at 11:44
  • 1
    thanks @Superlokkus. That completes my understanding. And of course it answers my question :-) – DJ1411 Oct 16 '19 at 13:24
  • I don't know if you get notified but be aware of my recent edit. Somehow preprocessor got autocorrect to processor. Of course I meant the https://en.cppreference.com/w/cpp/preprocessor . But don't take the preprocessor as a good style, in modern C++ `constexpr` and SFINAE ( https://en.cppreference.com/w/cpp/language/sfinae ) are better, faster safer alternatives (altough the later one is only suitable for intermediate and beyond skill levels) – Superlokkus Oct 16 '19 at 13:56