On some linux systems this works. Can I generally design plugin based apps such that there is no library, but only header files and the executable?
Afaik this always works if the interface classes are interfaces in the sense that they only contain pure virtual functions. But can I also define classes in the interface containing symbols that have to be bound by linking against an executable containing them?
Use case: an executable foo, the app, offers plugins an interface through a shared library libfoo. Plugins (shared libs) are loaded at runtime. Both, the app and plugins, link against libfoo to resolve symbols in the classes both of them use. Is this necessary or can put the classes in the executable target and let the plugins link the executable instead?
CodePudding user response:
I'm most familiar with Windows, where the answer to the strict question you asked is NO but you can still do what you want.
On Windows, EXE and DLL files both use the same file format, "Portable Executable". But they still have very important differences:
- relocations table is most often missing from EXE files
- entry points are different
As a result of these differences, attempting to load a native EXE file via LoadLibrary()
WILL FAIL (LoadLibraryEx(LOAD_LIBRARY_AS_DATAFILE)
is fine and can be used with Windows Resource API). (.NET assembly EXE files are an exception -- they don't contain any actual code, just intermediate language, and code addresses are always determined dynamically, so relocation fixups aren't needed)
However, your scheme "let the plugins link the executable" will still work, because EXE files do support an export table, and the loader can bind imports of DLLs to exports of the EXE.
Unfortunately, the C ABI is not standardized on Windows, so exporting C classes is very fragile and results in lockin to a particular compiler. To maintain loose coupling, you need to either export plain C functions, or COM interfaces.
Using interfaces allows you to avoid the entire issue -- you can define a class in the executable that implements an interface described in a header file, pass an interface pointer to the plugin, and the plugin can save that pointer use that for all calls back to the executable without ever having any import entries. For example, the predecessor to COM, "Object Linking and Embedding", defined IObjectWithSite
and IOleClientSite
interfaces.
CodePudding user response:
I'm most familiar with Linux (or other ELF based systems).
If you use a PIE executable and build with --export-symbols
, you can skip the use of (e.g.) libfoo.so
The executable will load the plugins and will provide any API needed symbols to the plugin(s)
Below is a complete test case for you ...
Note that you may be able to add/reduce some options. From the ###
in the Makefile
, I was doing some considerable hacking until I hit upon a combination that worked.
FILE: Makefile
# pieplugin/Makefile -- make file for pieplugin
#
# SO: can we use an executable file as shared library on all platformswindows
# SO: mac l
# SITE: stackoverflow.com
# SO: 70370572
XFILE = foo
XOBJ = foo.o
PLUGINS = libplug1.so
PLUGINS = libplug2.so
CFLAGS = -Wall -Werror -I.
###CFLAGS = -g
PIEFLAGS = -fpie
PIEFLAGS = -fPIC
###PIEFLAGS = -fpic
PICFLAGS = -fPIC
###PICFLAGS = -fpic
PICFLAGS = -nostdlib
PICFLAGS = -nodefaultlibs
PLUG_CFLAGS = $(CFLAGS)
PLUG_CFLAGS = $(PICFLAGS)
PLUG_LFLAGS = -shared
###PLUG_LFLAGS = $(PICFLAGS)
###PLUG_LFLAGS = -no-pie
CC = gcc
###CC = clang
LDSO = $(CC)
LDSO = ld
XFILE_LFLAGS = -Wl,--export-dynamic
all: $(PLUGINS) $(XFILE)
foo.o: foo.c
$(CC) $(CFLAGS) $(XFILE_CFLAGS) -c foo.c
$(XFILE): foo.o
$(CC) -o $(XFILE) $(XFILE_LFLAGS) foo.o -ldl
file $(XFILE)
plug1.o: plug1.c
$(CC) $(PLUG_CFLAGS) -c plug1.c
libplug1.so: plug1.o
$(LDSO) $(PLUG_LFLAGS) -o libplug1.so plug1.o
file libplug1.so
plug2.o: plug2.c
$(CC) $(PLUG_CFLAGS) -c plug2.c
libplug2.so: plug2.o
$(LDSO) $(PLUG_LFLAGS) -o libplug2.so plug2.o
file libplug2.so
test:
./$(XFILE) $(PLUGINS)
xtest: clean all test
clean:
rm -f $(XFILE) $(PLUGINS) *.o
FILE: foo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dlfcn.h>
#include <foopriv.h>
#define PLUGSYM(_fnc) \
plug->plug_##_fnc = plugsym(plug,"plugin_" #_fnc)
// plugsym -- load symbol from plugin file
void *
plugsym(plugin_t *plug,const char *sym)
{
void *fnc = dlsym(plug->plug_so,sym);
int sverr = errno;
printf("plugsym: loading %s from %s at %p\n",
sym,plug->plug_file,fnc);
if (fnc == NULL) {
printf("plugsym: failed -- %s\n",strerror(sverr));
exit(1);
}
return fnc;
}
// plugload -- load plugin file
void
plugload(const char *tail)
{
char file[1000];
plugin_t *plug = calloc(1,sizeof(*plug));
strcpy(plug->plug_file,tail);
sprintf(file,"./%s",tail);
printf("plugload: dlopen of %s ...\n",file);
//plug->plug_so = dlopen(file,RTLD_LOCAL);
//plug->plug_so = dlopen(file,RTLD_GLOBAL);
plug->plug_so = dlopen(file,RTLD_LAZY);
int sverr = errno;
printf("plugload: plug_so=%p\n",plug->plug_so);
#if 1
if (plug->plug_so == NULL) {
printf("plugload: failed -- %s\n",strerror(sverr));
exit(1);
}
#endif
PLUGSYM(fncint);
PLUGSYM(fncflt);
plug->plug_next = plugin_list;
plugin_list = plug;
}
int
main(int argc,char **argv)
{
--argc;
argv;
// NOTE: in production code, maybe we use opendir/readdir to find plugins
for (; argc > 0; --argc, argv)
plugload(*argv);
for (plugin_t *plug = plugin_list; plug != NULL; plug = plug->plug_next) {
printf("main: calling plugin %s fncint ...\n",plug->plug_file);
plug->plug_fncint(NULL);
}
for (plugin_t *plug = plugin_list; plug != NULL; plug = plug->plug_next) {
printf("main: calling plugin %s fncint ...\n",plug->plug_file);
plug->plug_fncflt(NULL);
}
return 0;
}
// functions provided by foo executable to plugins ...
void
foo_fncint(fooint_t *ptr,const char *who)
{
printf("foo_fncint: called from %s ...\n",who);
}
void
foo_fncflt(fooflt_t *ptr,const char *who)
{
printf("foo_fncflt: called from %s ...\n",who);
}
FILE: plug1.c
// plug1.c -- a plugin
#include <foopub.h>
void ctors
initme(void)
{
}
void
plugin_fncint(fooint_t *ptr)
{
foo_fncint(ptr,"plug1_fncint");
}
void
plugin_fncflt(fooflt_t *ptr)
{
foo_fncflt(ptr,"plug1_fncflt");
}
FILE: plug2.c
// plug2.c -- a plugin
#include <foopub.h>
void ctors
initme(void)
{
}
void
plugin_fncint(fooint_t *ptr)
{
foo_fncint(ptr,"plug2_fncint");
}
void
plugin_fncflt(fooflt_t *ptr)
{
foo_fncflt(ptr,"plug2_fncflt");
}
Here is the output of make xtest
:
rm -f foo libplug1.so libplug2.so *.o
gcc -Wall -Werror -I. -fPIC -nostdlib -nodefaultlibs -c plug1.c
ld -shared -o libplug1.so plug1.o
file libplug1.so
libplug1.so: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, not stripped
gcc -Wall -Werror -I. -fPIC -nostdlib -nodefaultlibs -c plug2.c
ld -shared -o libplug2.so plug2.o
file libplug2.so
libplug2.so: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, not stripped
gcc -Wall -Werror -I. -c foo.c
gcc -o foo -Wl,--export-dynamic foo.o -ldl
file foo
foo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=d136c53c818056fdbec75294ea472ab8c056ca52, not stripped
./foo libplug1.so libplug2.so
plugload: dlopen of ./libplug1.so ...
plugload: plug_so=0x7fd320
plugsym: loading plugin_fncint from libplug1.so at 0x7fad7f93e037
plugsym: loading plugin_fncflt from libplug1.so at 0x7fad7f93e059
plugload: dlopen of ./libplug2.so ...
plugload: plug_so=0x7fd940
plugsym: loading plugin_fncint from libplug2.so at 0x7fad7f939037
plugsym: loading plugin_fncflt from libplug2.so at 0x7fad7f939059
main: calling plugin libplug2.so fncint ...
foo_fncint: called from plug2_fncint ...
main: calling plugin libplug1.so fncint ...
foo_fncint: called from plug1_fncint ...
main: calling plugin libplug2.so fncint ...
foo_fncflt: called from plug2_fncflt ...
main: calling plugin libplug1.so fncint ...
foo_fncflt: called from plug1_fncflt ...