The software engineering implementation/deployment depicted on this example consists (for the sake of simplicity) on a single (do-nothing) shared library and a (stub) dynamic application linking to it just for testing and demonstration. I'll build 2 major versions of the library (libgreeting.so.1 and libgreeting.so.2) the first of them with some different minor versions and releases (for demonstration purposes). For each major version of the library I'll show a backward compatibility tehcnique for avoiding or minimizing the rebuild of the application (myapp-1). Accompanying each new major version of the library there will be (for the sake of simplicity) a new / updated header-file which as I said will provide for backward compatibility with previous major version(s). The header-files shouldn't be modified within minor versions and releases as this would be a clear deviation of whole strategy. For demonstration purposes, the implementation of each interface will consist on multiple source-code files. Besides the interface mechanism the library will be able to "report" its hard-coded versioning information (as a kind of reflection) which will always correspond to the name of the shared library version final binary object, although (again for the sake of simplicity) there will be no provisions to enforce this correspondence.
I can only hope that the software engineering aspects I'll be exploring side-by-side do not hinder the main goals of this post about building and dealing with shared libraries!
So let me start by the implementation of version reflection.
$ cat lib-version.h
1 #if !defined( ACME_LIB_VERSION_H )
2 #define ACME_LIB_VERSION_H
3
4 namespace ACME
5 {
6
7 namespace Library // {{{1
8 {
9
10 struct Version // {{{2
11 {
12 Version( unsigned major, unsigned minor, unsigned release ) :
13 major( major ), minor( minor ), release( release )
14 {
15 }
16
17 const unsigned major;
18 const unsigned minor;
19 const unsigned release;
20 };
21 // }}}2
22
23 }
24 // namespace Library }}}1
25
26 }
27 // namespace ACME
28
29 #endif // ACME_LIB_VERSION_H
30
NOTE
$ cat lib-greeting.hUnfortunately I still don't get a C++17 compiler in order to declare all these namespaces in a more compact way such as:
namespace ACME::Library::Greeting {}
1 #if !defined( ACME_LIB_GREETING_H )
2 #define ACME_LIB_GREETING_H
3
4 // headers {{{1
5
6 // standard headers {{{2
7 #include <string>
8 // }}}2
9
10 // custom headers {{{2
11 #include "lib-version.h"
12 // }}}2
13
14 // }}}1
15
16 namespace ACME
17 {
18
19 namespace Library
20 {
21
22 namespace Greeting // {{{1
23 {
24
25 extern Version version;
26
27 struct Main // {{{2
28 {
29 std::string hello() const;
30 };
31 // }}}2
32
33 }
34 // namespace Greeting }}}1
35
36 }
37 // namespace Library
38
39 }
40 // namespace ACME
41
42 #endif // ACME_LIB_GREETING_H
43
$ cat lib-greeting-main.cpp
1 // headers {{{1
2
3 // standard headers {{{2
4 // }}}2
5
6 // custom headers {{{2
7 #include "lib-greeting.h"
8 // }}}2
9
10 // }}}1
11
12 namespace ACME
13 {
14
15 namespace Library
16 {
17
18 namespace Greeting // {{{1
19 {
20
21 // externals {{{2
22 Version version( 1, 0, 0 );
23 // }}}2
24
25 }
26 // namespace Greeting }}}1
27
28 }
29 // namespace Library
30
31 }
32 // namespace ACME
33
$ cat lib-greeting-hello.cpp
1 // headers {{{1
2
3 // standard headers {{{2
4 #include <sstream>
5 // }}}2
6
7 // custom headers {{{2
8 #include "lib-greeting.h"
9 // }}}2
10
11 // }}}1
12
13 namespace ACME
14 {
15
16 namespace Library
17 {
18
19 namespace Greeting // {{{1
20 {
21
22 std::string Main::hello() const // {{{2
23 {
24 std::ostringstream oss;
25
26 oss
27 << "Hello"
28 << " ("
29 << version.major << "."
30 << version.minor << "."
31 << version.release
32 << ")."
33 << std::ends;
34
35 return oss.str();
36 }
37 // }}}2
38
39 }
40 // namespace Greeting }}}1
41
42 }
43 // namespace Library
44
45 }
46 // namespace ACME
47
The above source-code is the version 1.0.0 of the Greeting shared library. One way of building this code into a shared library using GCC 4.8.2 under Solaris 11.3 is:
$ cd ~/project/greeting/1.0.0
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 -fPIC \
-shared -Wl,-soname=libgreeting.so.1 -o libgreeting.so.1.0.0 \
lib-greeting-main.cpp \
lib-greeting-hello.cpp
NOTE
$ file libgreeting.so.1.0.0I use core2 just because it makes sense to my particular machine.
...: ELF 64-bit LSB dynamic lib AMD64 ..., dynamically linked, ...
$ ldd libgreeting.so.1.0.0
libstdc++.so.6 => /usr/lib/64/libstdc++.so.6
libm.so.2 => /lib/64/libm.so.2
libgcc_s.so.1 => /usr/lib/64/libgcc_s.so.1
libc.so.1 => /lib/64/libc.so.1
NOTE
An alternative way of building the library is to perform separate steps for compilation and linking as follows:The further dynamic linking above (in red) seems natural and is harmless and probably the safest approach. But fact is the application executable process must also link to them as well, so they seem to be an unnecessary stuff duplication.
By looking at GCC(1) man page one may consider using -nostdlib to remove those linkings as well as startup code. By experimentation, at least on this example, I've verified that -nostdlib has a bad side-effect on global data defined on the shared library. In particular, my shared library defines an ACME::Library::Greeting::version object which wasn't being correctly resolved in the myapp-1 executable. Hence I've discarded this option.
Next I've tried with -nodefaultlibs which stripes out the presumed unnecessary linkings but keeps startup code. Hopefully that fixed the global data resolution issues and everything seemed to have worked fine, though I can't guarantee no other hidden issue may remain, such as some exception handling issue or so. But anyway it never seems good practice to let exceptions propagate across modules, isn't it?
When striping libraries, the man page warns about the possibility of introducing undesired side-effects such as to the resolution of memcmp, memset and memcpy which otherwise would be usually resolved by entries in libc. But so far I believe one may successfully use the -nodefaultlibs option though with care. As I said, on the particular example I present on this post I had no issues in spite of being a virtually do-nothing application.
Nevertheless, in order to inspect this possible issue a little further I have intentionally inserted the following probing code at / near ACME::Library::Greeting::Main::hello:34:
char buffer[ 128 ];
::memset( buffer, '\0', sizeof( buffer ) );
::sprintf( buffer, "Oops!\n" );
::printf( "%s", buffer );
If I compile the library with the -g (debugging) option I'll certainly see (under GDB for instance) the explicitly call to memset, although with absolutely no linking issues; that is, it works perfectly.
If on the other hand I compile the library with the -O2 (optimize even more) option I'll be amazingly pleased to see that the call to memset has been successfully inlined by a rep stos of 16 (ecx == 0x10) zeroed 8-bytes (eax == 0) cycles as follows:
0x...df0 <+544>: lea -0x210(%rbp),%rsi
0x...df7 <+551>: mov $0x10,%ecx
0x...dfc <+556>: xor %eax,%eax
0x...dfe <+558>: mov %rsi,%rdi
0x...e01 <+561>: rep stos %rax,%es:(%rdi)
0x...e04 <+564>: mov $0xa21,%edi
0x...e09 <+569>: movl $0x73706f4f,-0x210(%rbp)
0x...e13 <+579>: mov %di,-0x20c(%rbp)
0x...e1a <+586>: lea -0x40d(%rip),%rdi # 0x...a14
0x...e21 <+593>: callq 0x...a90<printf@plt>
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 -fPIC \
-c lib-greeting-main.cpp
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 -fPIC \
-c lib-greeting-hello.cpp
NOTE
$ g++ \Of course, the compilation can combined into a single command:
(which will still generate separated object-files for each source-file)
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 -fPIC -c \
lib-greeting-main.cpp \
lib-greeting-hello.cpp
A lib-greeting-*.cpp in the tail of the above would also work.
-m64 \
-nodefaultlibs -shared \
-Wl,-soname=libgreeting.so.1 -o libgreeting.so.1.0.0 \
lib-greeting-main.o \
lib-greeting-hello.o
Just for curiosity, a variation of the above command would be:
$ MACH=$(g++ -dumpmachine) # same as $MACHTYPE
$ /usr/bin/ld \
-m64 \
-shared \
-Qy -soname=libgreeting.so.1 -o libgreeting.so.1.0.0 \
/usr/lib/amd64/crti.o \
/usr/gcc/4.8/lib/gcc/$MACH/4.8.2/amd64/crtbegin.o \
lib-greeting-main.o \
lib-greeting-hello.o \
/usr/gcc/4.8/lib/gcc/$MACH/4.8.2/amd64/crtend.o \
/usr/lib/amd64/crtn.o
NOTE
An important versioning procedure which should immediately follow the build of the shared library is adjusting or creating the associated symbolic links. For the very first time, when no other versions yet exist, it suffices to create them:By default GCC linking (collect2) uses the Solaris linker /usr/bin/ld. Be aware there's also an /usr/gnu/bin/ld which's another story. If one decides to use the GNU linker instead then one shall do similarly to the when explicitly invoking the default Solaris linker:
$ /usr/gnu/bin/ld \
-m elf_x86_64_sol2
-shared \
-soname=libgreeting.so.1 -o libgreeting.so.1.0.0 \
/usr/lib/amd64/crti.o \
/usr/gcc/4.8/lib/gcc/$MACH/4.8.2/amd64/crtbegin.o \
lib-greeting-main.o \
lib-greeting-hello.o \
/usr/gcc/4.8/lib/gcc/$MACH/4.8.2/amd64/crtend.o \
/usr/lib/amd64/crtn.o
Note that by using the GNU linker on the shared library will require that the executable be also linked with the GNU linker.
$ cd ~/project/myapp/1/lib
$ cp ~/project/greeting/1.0.0/libgreeting.so.1.0.0 .
$ ln -s libgreeting.so.1.0.0 libgreeting.so.1
$ ln -s libgreeting.so.1 libgreeting.so
The application source-code is as follows:
$ cat myapp-main.cpp
1 // headers {{{1
2
3 // standard headers {{{2
4 #include <cstdlib>
5 #include <iostream>
6 // }}}2
7
8 // custom headers {{{2
9 #include "lib-greeting.h"
10 // }}}2
11
12 // }}}1
13
14 // {{{1
15
16 int main() // {{{2
17 {
18 using namespace ACME::Library;
19
20 using Greeting::version;
21
22 std::cout << std::endl;
23 std::cout
24 << "MyApp - Greeting version: "
25 << version.major << "."
26 << version.minor << "."
27 << version.release
28 << std::endl;
29
30 Greeting::Main greeting;
31
32 std::cout << std::endl;
33 std::cout << greeting.hello() << std::endl;
34
35 return EXIT_SUCCESS;
36 }
37 // }}}2
38
39 // }}}1
40
Assuming that the dynamic library will be deployed no a well-known relative path from the binary executable (for instance on a subdirectory such as lib), one way of building the dynamic application using GCC 4.8.2 under Solaris 11.3 is:
$ cd ~/project/myapp/1
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 \
-L./lib -R./lib -lgreeting \
-o myapp-1 \
myapp-main.cpp
$ file myapp-1
...: ELF 64-bit LSB executable AMD64 ..., dynamically linked, ...
$ ldd myapp-1
libgreeting.so.1 => ./lib/libgreeting.so.1
libstdc++.so.6 => /usr/lib/64/libstdc++.so.6
libm.so.2 => /lib/64/libm.so.2
libgcc_s.so.1 => /usr/lib/64/libgcc_s.so.1
libc.so.1 => /lib/64/libc.so.1
NOTE
NOTENote that I avoid LD_LIBRARY_PATH by using the -R option. But the problem is that I'm using a relative path, which is not recommended when using the -R option. Nevertheless, I'll go on like this just for the sake of simplicity of this post and assume I'll always make the executable directory the current directory before running the application.
So far, running myapp-1 should produce:If the shared libraries remains on the same directory as the executable this shall work, of course, but it may cause some pitfalls related to the runtime-linker dynamic object search resolution if the libraries are moved to other place. I shall discuss other options for this requirements and considering the runtime-linker configuration on another post.
$ ./myapp-1
MyApp - Greeting version: 1.0.0
Hello (1.0.0).
NOTE
NOTEIf perhaps some different segmented building strategy is needed, specially if the executable source-code is split in many files (not the case of this particular example), the final step could be adjusted as follows:
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 \
-c myapp-main.cpp
$ g++ \
-m 64 \
-L./lib -R./lib -lgreeting \
-o myapp-1 \
myapp-main.o
Case the executable had multiple object-files it would be just a matter of listing them in sequence right after myapp-main.o.
Again if one decides to use the GNU linker (/usr/gnu/bni/ld) instead of the Solaris linker (/usr/bin/ld) then one will have considerably more work to do on the final steps as follows:
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 \
-c myapp-main.cpp
The following should yield the same as $MACHTYPE:
$ MACH=$(g++ -dumpmachine)
On what follows the order seems important:
$ /usr/gnu/bin/ld \
-m elf_x86_64_sol2 \
-o myapp-1 \
/usr/lib/amd64/crt1.o \
/usr/lib/amd64/crti.o \
/usr/lib/amd64/values-Xc.o \
/usr/lib/amd64/values-xpg6.o \
/usr/gcc/4.8/lib/gcc/$MACH/4.8.2/amd64/crtbegin.o \
myapp-main.o \
-L./lib -R./lib \
-lgreeting
-L/lib/amd64:/usr/lib/amd64 \
-lstdc++ -lm -lgcc_s -lc \
/usr/gcc/4.8/lib/gcc/$MACH/4.8.2/amd64/crtend.o \
/usr/lib/amd64/crtn.o
All this extra work of specifying libraries and startup/exit-codes would be automatically taken care of by the g++ front-end, except that it would invoke the Solaris linker because it was configured as so out-of-the-box.
Now suppose time has gone by and the major version 1 of the Greeting library will get some implementation change or fix. As the scope of change is strictly internal it will just get a new minor version and release. Had the change or fix altered the the interface (lib-greeting.h) then it should get a new major version. For the sake of this example I'll (arbitrarily) choose the new version as 1.2.3.
Line 22 of lib-greeting-main.cpp will become:
...
17
18 namespace Greeting // {{{1
19 {
20
21 // externals {{{2
22 Version version( 1, 2, 3 );
23 // }}}2
24
25 }
26 // namespace Greeting }}}1
27
...
Line 27 of lib-greeting-hello.cpp and will become:
...
21
22 std::string Main::hello() const // {{{2
23 {
24 std::ostringstream oss;
25
26 oss
27 << "Hello, world!"
28 << " ("
29 << version.major << "."
30 << version.minor << "."
31 << version.release
32 << ")."
33 << std::ends;
34
35 return oss.str();
36 }
37 // }}}2
38
...
I've chosen to rebuild the changes as follows:
$ cd ~/project/greeting/1.2.3
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 -fPIC -nodefaultlibs \
-shared -Wl,-soname=libgreeting.so.1 -o libgreeting.so.1.2.3 \
lib-greeting-main.cpp \
lib-greeting-hello.cpp
Next I copy the new library libgreeting.so.1.2.3 next to the previous version, say libgreeting.so.1.0.0 and just update the libgreeting.so.1 symbolic-link as follows:
$ cd ~/project/myapp/1/lib
$ cp ~/project/greeting/1.2.3/libgreeting.so.1.2.3 .
$ rm libgreeting.so.1
$ ln -s libgreeting.so.1.2.3 libgreeting.so.1
Hence, in ~/project/myapp/v1/lib, where I previously had:
$ l libgreeting.so*
lrwxrwxrwx 1 ... libgreeting.so -> libgreeting.so.1
lrwxrwxrwx 1 ... libgreeting.so.1 -> libgreeting.so.1.0.0
-rwxr-xr-x 1 ... libgreeting.so.1.0.0
Now I have:
$ l libgreeting.so*
lrwxrwxrwx 1 ... libgreeting.so -> libgreeting.so.1
lrwxrwxrwx 1 ... libgreeting.so.1 -> libgreeting.so.1.2.3
-rwxr-xr-x 1 ... libgreeting.so.1.0.0
-rwxr-xr-x 1 ... libgreeting.so.1.2.3
The application remains untouched, because it's dynamically pointing to major version 1 of the Greeting library which remains at 1 meaning that whatever new minor version or release the libgreeting.so.1 symbolic-link points it won't break the application (the library interface hasn't changed).
$ cd ~/project/myapp/1
$ ldd myapp-1
libgreeting.so.1 => ./lib/libgreeting.so.1
libstdc++.so.6 => /usr/lib/64/libstdc++.so.6
libm.so.2 => /lib/64/libm.so.2
libgcc_s.so.1 => /usr/lib/64/libgcc_s.so.1
libc.so.1 => /lib/64/libc.so.1
Running the unchanged application how results in:
$ ./myapp-1
MyApp - Greeting version: 1.2.3
Hello, world! (1.2.3).
So far this all seems great! I was able to build a new minor version and release of the shared library which was seemless used by the application which wasn't rebuild. This is extremelly powerful and simple. There's no "DLL hell".
Now, the last part of this post is the advent of major version 2 of the Greetings shared library. The new major version number is justified because the library publishes changes (enhancements and / or new features) through its public interfaces (lib-greeting.h).
As a good practice, although not required, the new public interface will remain backward compatible which means that the application doesn't need to be changed, not even recompiled, but just relinked in order to point to the new major version of the library. Case backward compatibility wasn't a requirement, then the application would probably have to be reengineered and hence recompiled as well.
WARNING
For the sake of exemplification, at my arbitrary discretion, the new version of the libgreeting.so.2 will be 2.1.5. I'll exemplify some library implementation changes but keeping backward compatibility and just requiring the relink of the application.Note that for the public interface to remain backward compatible it's absolutely necessary that all previous entry-point symbols remain the same. Not to mention other binary compatibility issues such as the same compiler and linker versions and so on.
This means for instance that previous non-inline member functions must not become inlined on the newer version. As a consequence be careful on the backward compatibility implementation in order to not introduce an additional (overhead) call to the legacy code.
The only safe C++ way of achieving this (interface preservation while keeping binary compatibility and not incurring on extra indirections) that comes to my mind is through (kind of reverse) C++ inheritance. What I mean by reverse is that in (major) version n of the library, the previous interface from (major) version n-1 must acutally inherit from the new interface of (major) version n. This is kind of the opposite of the usual derivation of version n from version n-1. I'll give an example of this in what follows.
If none of this is possible the backward compatibility will only become possible by recompiling the whole application, not just relinking to the new shared library version.
The lib-greeting.h will have some changes, but of course they will be mostly relevant to newer applications which could take advantage of new features.
NOTE
One thing perhaps I've not realized in time for major version 1 is that I could have emcopassed its struct Main by a namespace V1 and have inserted an using alias within its parent Greeting namespace in order to keep the application from knowing about V1.
For instance:
...
namespace Greeting
{
namespace V1
{
...
struct Main
{
...
};
...
}
// namespace V1
using V1::Main;
}
// namespace Greeting
...
1 #if !defined( ACME_LIB_GREETING_H )
2 #define ACME_LIB_GREETING_H
3
4 // headers {{{1
5
6 // standard headers {{{2
7 #include <string>
8 // }}}2
9
10 // custom headers {{{2
11 #include "lib-version.h"
12 #include "lib-greeting-v2.h"
13 // }}}2
14
15 // }}}1
16
17 namespace ACME
18 {
19
20 namespace Library
21 {
22
23 namespace Greeting // {{{1
24 {
25
26 extern Version version;
27
28 struct Main : public V2::Main // {{{2
29 {
30 std::string hello() const;
31 };
32 // }}}2
33
34 }
35 // namespace Greeting }}}1
36
37 }
38 // namespace Library
39
40 }
41 // namespace ACME
42
43 #endif // ACME_LIB_GREETING_H
44
Next I shall show the interface for the new features of major version 2. For the sake of organization I've decided to keep it on a separated header lib-greeting-v2.h:
1 #if !defined( ACME_LIB_GREETING_V2_H ) 2 #define ACME_LIB_GREETING_V2_H 3 4 // headers {{{1 5 6 // standard headers {{{2 7 #include <string> 8 // }}}2 9 10 // custom headers {{{2 11 // }}}2 12 13 // }}}1 14 15 namespace ACME 16 { 17 18 namespace Library 19 { 20 21 namespace Greeting // {{{1 22 { 23 24 namespace V2 // {{{2 25 { 26 27 struct Main // {{{3 28 { 29 std::string hi( const std::string & name ) const; 30 }; 31 // }}}3 32 33 } 34 // namespace V2 }}}2 35 36 } 37 // namespace Greeting }}}1 38 39 } 40 // namespace Library 41 42 } 43 // namespace ACME 44 45 #endif // ACME_LIB_GREETING_V2_H 46
Line 22 of lib-greeting-main.cpp will become:
...
17
18 namespace Greeting // {{{1
19 {
20
21 // externals {{{2
22 Version version( 2, 1, 5 );
23 // }}}2
24
25 }
26 // namespace Greeting }}}1
27
...
For the sake of simplification of this post I've decided to put all the implementation on the source-file lib-greeting-goodwill.cpp which has become as follows:
1 // headers {{{1
2
3 // standard headers {{{2
4 #include <string>
5 #include <sstream>
6 // }}}2
7
8 // custom headers {{{2
9 #include "lib-greeting.h"
10 // }}}2
11
12 // }}}1
13
14 // {{{1
15
16 namespace LIB = ACME::Library::Greeting;
17
18 std::string
19 LIB::Main::hello() const // {{{2
20 {
21 try
22 {
23 std::ostringstream oss;
24
25 oss
26 << "Hello, world!"
27 << " ("
28 << version.major << "."
29 << version.minor << "."
30 << version.release
31 << ")."
32 << std::ends;
33
34 return oss.str();
35 }
36 catch ( ... )
37 {
38 return "";
39 }
40 }
41 // }}}2
42
43 std::string
44 LIB::V2::Main::hi( const std::string & name ) const // {{{2
45 {
46 try
47 {
48 std::ostringstream oss;
49
50 oss
51 << "Hi ["
52 << ( name.empty() ? "stranger" : name.c_str() )
53 << "] ("
54 << version.major << "."
55 << version.minor << "."
56 << version.release
57 << ")."
58 << std::ends;
59
60 return oss.str();
61 }
62 catch ( ... )
63 {
64 return "";
65 }
66 }
67 // }}}2
68
69 // }}}1
70
I've chosen to rebuild the changes as follows:
$ cd ~/project/greeting/2.1.5
$ g++ \
-m64 -std=c++11 -march=core2 -mtune=core2 -fPIC -nodefaultlibs \
-shared -Wl,-soname=libgreeting.so.2 -o libgreeting.so.2.1.5 \
lib-greeting-main.cpp \
lib-greeting-goodwill.cpp
Next I copy the new library libgreeting.so.2.1.5 next to the previous versions, but as this is a new major version I'll have to create libgreeting.so.2 symbolic-link as follows:
$ cd ~/project/myapp/1/lib
$ cp ~/project/greeting/2.1.5/libgreeting.so.2.1.5 .
$ ln -s libgreeting.so.2.1.5 libgreeting.so.2
I also want that the new backward compatible shared library become the default linking artifact. New applications will benefit from new features and old applications which are relinked against it won't be broken!
$ rm libgreeting.so
$ ln -s libgreeting.so.2 libgreeting.so
For my old application, it suffices to relink it:
$ cd ~/project/myapp/1
$ g++ \
-m 64 \
-L./lib -R./lib -lgreeting \
-o myapp-1 \
myapp-main.o
$ ldd myapp-1
libgreeting.so.1 => ./lib/libgreeting.so.2
libstdc++.so.6 => /usr/lib/64/libstdc++.so.6
libm.so.2 => /lib/64/libm.so.2
libgcc_s.so.1 => /usr/lib/64/libgcc_s.so.1
libc.so.1 => /lib/64/libc.so.1
Running the re-linked application now results in:
$ ./myapp-1
MyApp - Greeting version: 2.1.5
Hello, world! (2.1.5).
And that's it!
A long post I've considered worthwhile!