2. Building plugins

The shared objects technology is used, among other things, to provide the so-called “plug-in system”, that allows us to link in compiled code at runtime providing (eventually optional) features.

To implement plug-in systems, you usually need to call the dynamic linker at runtime to ask it to load the plug-in's shared object. This object might just be a standard shared object or might require further details to be taken into consideration.

The call into the dynamic linker also varies for what concerns interface and implementation. Since most Unix-like systems provide this interface through the dlopen() function, which is pretty much identical among them, lots of software relies on just this interface, and leaves to libtool the task of building the plugins.

Software that is interested in wider portability among different operating systems will be interested instead in using the wrapper library and interface called libltdl.

2.1. Using libltdl for plug-ins

2.1.1. Linking, bundling, installing libtldl

Because of the wide adoption of libltdl in many types of applications, its support in autotools is available with great flexibility. This is because its wrapping abilities can easily be used on systems where libtool proper is not usually installed, and thus it's often convenient to have a local copy of it.

But with bundling libraries, problems ensue, and it can especially be a problem to choose between bundling a local copy of the library or just using the system one. The macros provided by libtool, even the recent version 2, support three styles for bundling libltdl: sub-configured directory, non-recursive inline build, or finally recursive inline build.

As well as these three options, there is also the more “standard” option of simply requesting the presence of the library in the system, as is done for any other dependency and checking for it. This method is neither assisted nor well-documented by the libtool manual and is thus rarely used.

For all three bundling styles as provided by libtool, the reference macros in the configure.ac file are LT_CONFIG_LTDL_DIR and LTDL_INIT. When using the sub-configured option, these two are the only two calls that you need. When using the inline build, you need some extra calls.

Example 3.2. Buildsystem changes for bundled libltdl

In configure.ac, for the various cases, commented

# automake needed when not using sub-configured libltdl
# subdir-objects only needed when using non-recursive inline build
AM_INIT_AUTOMAKE([subdir-objects])

# the inline build *requires* the configure header, although the name
# is not really important
AC_CONFIG_HEADERS([config.h])

# the inline build *requires* libtool with dlopen support
LT_INIT([dlopen])

# find the libltdl sources in the libltdl sub-directory
LT_CONFIG_LTDL_DIR([libltdl])

# only for the recursive case
AC_CONFIG_FILES([libltdl/Makefile])

# choose one
LTDL_INIT([subproject])
LTDL_INIT([recursive])
LTDL_INIT([nonrecursive])

The changes for Makefile.am (or equivalent) are trivial for the sub-configured and recursive options (just add the new directory to SUBDIRS), but are a bit more complicated for the non-recursive case. The following is a snippet from the libtool manual to support non-recursive libltdl inline builds.

AM_CPPFLAGS =
AM_LDFLAGS =

BUILT_SOURCES =
EXTRA_DIST =
CLEANFILES =
MOSTLYCLEANFILES =

include_HEADERS =
noinst_LTLIBRARIES =
lib_LTLIBRARIES =
EXTRA_LTLIBRARIES =

include libltdl/Makefile.inc

Whatever option you choose to follow at this point, you must actually bundle the sources in your tree. You probably don't want to add them to your source control system, but you want to add the libtoolize --ltdl command to your autogen.sh script or similar.

As the title of this section suggests, you can technically even install the libltdl that you just built. This is not enabled by default, and rightly so (you'd be installing unrequired software outside of the scope of the build process). The reason why this is at all possible is that the macros used by the libtool package are exactly the same as is provided to third-party developers.

Finally there is no provided macro to check for the library in the system to rely on; since it also does not provide a pkg-config datafile. The best practices choice is simply to discover the library through AC_CHECK_LIB.

To do that you can use the following snippet of code, for instance:

Example 3.3. Checking for libltdl

AC_CHECK_HEADER([ltdl.h],
    [AC_CHECK_LIB([ltdl], [lt_dladvise_init],
        [LIBLTDL=-lltdl], [LIBLTDL=])],
    [LIBLTDL=])

It's important to check for a function that is present in the currently supported version of libltdl. This snippet checks for the lt_dladvise_init function that is a new interface present in libtool 2.2 and later.


2.2. Building plug-ins for dlopen()

When building plug-ins that are to be used directly with the dlopen() interface (or equivalent) and not through the libltdl interface, you usually just need the shared object files, without versioning or other frills. In particular, given the plug-ins cannot be wrapped statically, you don't need to build the static version at all.

For this reason when building these very 'casual' types of plug-ins, we just rely on three flags for the libtool script:

-module

Ignore the restriction about the lib- prefix for the plug-in file name, allowing free-form names.

-avoid-version

Allow the target to not provide any version information, removing the need to provide it. Almost all the plug-in systems don't use the library version to decide whether to load the objects, and rely instead on the path they find.

-shared

Disable entirely the build of the static version of the object, this reduces the number of installed files, as well as avoiding the double-build that would be needed for all the systems where static libraries and shared objects have different build requirements.

Note

This option will make the package incompatible with the --disable-shared option during ./configure call, as well as stopping the build when shared objects are not supported at all.

-export-dynamic

The current object's exposed symbols have to be accessible through dlsym() or equivalent interfaces.

See Section 3.1, “-export-dynamic.

Example 3.4. Building plug-ins for dlopen() usage

pkglib_LTLIBRARIES = foo_example_plugin.la

foo_example_plugin_la_SOURCES = example.c
foo_example_plugin_la_LDFLAGS = -avoid-version -module -shared -export-dynamic

2.3. Exposing fixed and variable plugins' interfaces

When designing plugin interfaces you have two main choices available: either you use a fixed interface or a variable one. In the former case, all plugins export a set of symbols with a pre-selected name, independent of the names of the plugins themselves. With the latter option, the symbols exported by each plugin are instead named after the plugin. Both alternatives have up- and downsides, but these are another topic altogether.

For instance, a fixed interface can consist of three functions plugin_init(), plugin_open() and the plugin_close(), that need to be implemented by each plugin. On the other hand, in the case of a variable interface, the foo plugin would export foo_init(), foo_open() and the foo_close().

Depending on which of the two alternative solutions is chosen, you have alternative approaches to tell the link editor to only show the interface symbols and nothing else, as delineated in Section 3.2, “-export-symbols and -export-symbols-regex, and exemplified below.

Example 3.5. Exposing symbols for plugins with fixed interface

Since each plugin with a fixed interface exports the same set of symbols, and such interface is rarely extended or reduced, the easiest solution here is to use the static -export-symbols option with a fixed list of symbols:

AM_LDFLAGS = -avoid-version -module -shared -export-dynamic \
             -export-symbols $(srcdir)/plugins.syms

pkglib_LTLIBRARIES = foo.la bar.la baz.la

foo_SOURCES = foo1.c foo2.c foo3.c

bar_SOURCES = bar1.c bar2.c bar3.c

baz_SOURCES = baz1.c baz2.c baz3.c

Example 3.6. Exposing symbols for plugins with variable interface

When the interface is variable, it is common to have either a prefix or a suffix with the name of the plugin itself; you could then use a generic list of symbols and produce its specific symbol list, or you can make use of -export-symbols-regex with a wider match on the interface.

One of the downsides of using this method is that you then have to carefully name the functions within the plugin's translation units, but the build system should not be used to add workarounds for badly written code.

AM_LDFLAGS = -avoid-version -module -shared -export-dynamic \
             -export-symbols-regex '^([a-zA-Z0-9]+)_(init|open|close)$$'

pkglib_LTLIBRARIES = foo.la bar.la baz.la

foo_SOURCES = foo1.c foo2.c foo3.c

bar_SOURCES = bar1.c bar2.c bar3.c

baz_SOURCES = baz1.c baz2.c baz3.c