bottom prev next

Chained Linking

Before we investigate composition in depth let us take a look at Chained Linking which is not a C source code pattern. Rather, it is a pattern that can be used in make files.

The essence of Chained Linking is capturing the usage hierarchy of data types in commands that build them.

Chained Linking consequences:

  • minimization of the number of explicit dependencies in make files
  • elimination or reduction of code duplication in make files


Sample Problem

Let application app depend explicitly on typea.

Let typea depend explicitly on typeb which is of no concern to app.

Let typeb depend explicitly on typec which is of no concern to typea and to app.

Implement this hierarchy of dependencies via Chained Linking.


Sample Solution

Step 1

Implement typec as a shared library. For the purposes of this discussion it is irrelevant whether this implementation is patterned or not:

libtypec.h:

extern void typeC( void );

libtypec.c:

#include <stdio.h> #include "libtypec.h" extern void typeC( void ) { printf( "%s:%d: typeC()\n", __FILE__, __LINE__ ); }

Since this library does not depend on any other components its build instructions are the traditional ones:

gcc -g -c -fPIC -I . libtypec.c gcc -g -shared -o libtypec.so libtypec.o


Step 2

Implement typeb as a shared library reflecting the fact that it depends on typec:

libtypeb.h:

extern void typeB( void );

libtypeb.c:

#include <stdio.h> #include "libtypec.h" #include "libtypeb.h" extern void typeB( void ) { typeC(); printf( "%s:%d: typeB()\n", __FILE__, __LINE__ ); }

We observe here that typec should only be used by and visible to typeb's implementation. Consequently, libtypec.h is included in the source file libtypeb.c - not in the (presumably) public header libtypeb.h file.


Step 3

To build libtypeb.so via Chained Linking we will reflect or capture the hierarchy of usage of typec in the upcoming build lines.

Since in our example no other components rely on or use typec explicitly then libtypeb.so should be the only component that is burdened with the task of linking in the corresponding dependencies.

Consequently, instead of requiring the application(s) to link in libtypec.so we will instruct libtypeb.so to do so:

gcc -g -c -fPIC -I . libtypeb.c gcc -g -L . -shared -o libtypeb.so libtypeb.o -ltypec

Observe the -ltypec option at the end of the link line. Use your operating system's facility to investigate the result of the above operation.

On UNIX/Linux machines ldd produced the following relevant output:

ldd libtypeb.so libtypea.so => ./libtypea.so (0x00007f2a7c440000)


Step 4

Implement typea as a shared library reflecting the fact that it depends on typeb:

libtypea.h:

extern void typeA( void );

libtypea.c:

#include <stdio.h> #include "libtypeb.h" #include "libtypea.h" extern void typeA( void ) { typeB(); printf( "%s:%d: typeA()\n", __FILE__, __LINE__ ); }


Step 5

Since the only component that depends explicitly on typeb is libtypea.so then the latter library should link libtypeb.so in relieving other components from this duty:

gcc -g -c -fPIC -I . libtypea.c gcc -g -L . -shared -o libtypea.so libtypea.o -ltypeb

At this point we expect libtypea.so to depend on libtypeb.so which depends on libtypec.so:

ldd libtypea.so libtypeb.so => ./libtypeb.so (0x00007f411b073000) libtypec.so => ./libtypec.so (0x00007f411aa9f000)

We observe further that even though the only explicit dependency that we specified for libtypea.so was libtypeb.so the linker nonetheless have found and recorded the target's library implicit dependency on libtypec.so.


Step 6

Implement the application that uses typea directly:

app.c:

#include <stdio.h> #include "libtypea.h" extern int main( int argc, char* argv[] ) { typeA(); return 0; }


Step 7

Here the mechanics of Chained Linking are brought to light. Instead of linking all the involved libraries in we will link the only library on which app depends explicitly - libtypea.so:

gcc -g -c -I . app.c gcc -g -L . -o app app.o -ltypea

Examining app's dependencies we obtain:

ldd app libtypea.so => ./libtypea.so (0x00007f9fe5a44000) libtypeb.so => ./libtypeb.so (0x00007f9fe5470000) libtypec.so => ./libtypec.so (0x00007f9fe526e000)

For comparison purposes here is how app would be build the traditional way:

gcc -g -c -I . app.c gcc -g -L . -o app app.o -ltypea -ltypeb -ltypec


Step 8

Execute app:

./app libtypea.c:7: typeC() libtypea.c:10: typeB() libtypea.c:10: typeA()

The program works as expected and the above ldd's output explains why. While building the executable image for app the linker examines, finds and records all of its dependencies which relieves us of the necessity to perform these tasks manually.

By the time app's main() is invoked the entire hierarchy of all the dependencies is loaded, the relevant functions are relocated, their code is mapped into the program's address space and the executable is fully operational.


Cost Analysis

The sum total of all the commands and make files (not shown) that must be modified if a new dependency must be added to or an existing dependency must be removed from, say, libtypeb.so is equal to one. In comparison, \(N\) commands somehow distributed over \(M\) make files that build app-like programs the traditional way must be modified to achieve the same result.


Exercise

1) make typec depend on typed which should be used by and visible only to libtypec.so.

Estimate the cost of the above change.


Files

libtypec.h libtypec.c libtypeb.h libtypeb.c libtypea.h libtypea.c mklib.sh

app.c mkapp.sh

\(\blacksquare\)

top prev next