Alternative proposal (was Plugin project proposal)
Nicolas Williams
Nicolas.Williams at oracle.com
Wed Jul 21 13:44:40 EDT 2010
Here's my sketch for something close to a generic plugin framework that
can be instantiated for different plugin types, in C. This design
obtains strong type safety in both, the application and the plugin code.
First, what it looks like:
- For each plugin type you must define its interface:
- plugin type name
- function pointer typedefs
- entry point symbols and function declarations for them
corresponding to the function pointer typedefs
- plugin interface versions
These could be collected into one file that is used to generate
header files for the application and the plugin. That's how I'd do
it, and I'd use the C pre-processor to process that file. One could
instead make it so that all parts of the system just include this one
file -- it's just that the heavy use of macros would make it
difficult to find declarations with cscope. More on this below.
- Some build system logic would be desirable to ensure that plugin
interface versions are immutable. There'd be a make target that
generates a file that is version controlled along with the rest of
the project, and a make target that checks that the current
definition of a plugin type is not incompatible with what was
previously checked in.
- For every plugin type the application developer must provide
macros/functions to derive from arguments what plugin to call. If
the application needs to call all plugins in some controlled fashion
the way PAM does, then a generic control engine could be put
together. I believe nothing in Kerberos will require this, so I'm
not addressing such a feature, though it could be added to this
sketch fairly simply.
- The framework itself provides just a few utility functions and
macros, as well as the build tools to generate header files from
plugin type definitions:
- a function to load a given plugin of a given plugin type
- a function to load all plugins of a given plugin type
- a macro to obtain the function pointer for a given entry point in
a given plugin of a given plugin type
- a macro to call a given entry point in a given plugin of a given
plugin type
Note I've not even mentioned whether this is a dlsym-a-function-at-a-
time or a dlsym-one-function-that-returns-a-v-table design. That's
because this design can easily accommodate either approach.
The magic is all in the macros used to define plugin types and their
interfaces, as well as in the macros to access plugins mentioned above.
A plugin type definition would look like so:
#define PLUG_TYPE_UPCASE_NAME MY_PLUGIN_TYPE
#define PLUG_TYPE_DOWNCASE_NAME my_plugin_type
/*
* For the generated header for inclusion in the app this generates
* function pointer typedefs named entry_point1 ## _fct.
*
* For the generated header for inclusion in the plugins this
* generates forward function declarations. This allows the
* compiler to enforce that the plugin's entry points' prototypes
* match the prototypes defined in the plugin type definition.
*/
PF_DEF_ENTRY_POINT(entry_point1, (argument list for entry_point1))
PF_DEF_ENTRY_POINT(entry_point2, (argument list for entry_point2))
..
PF_DEF_ENTRY_POINT(entry_pointN, (argument list for entry_pointN))
/*
* This generates an enum. The PF_DEF_VERSION_BEGIN() macro outputs
* the beginning of the enum declaration and PF_DEF_VERSION_END()
* outputs the end of it (the closing brace, effectively, as well
* as, perhaps, a VERSION_MAX element). The PF_DEF_VERSION() macro
* outputs a single enum element.
*/
PF_DEF_VERSION_BEGIN()
PF_DEF_VERSION(VERSION_1)
PF_DEF_VERSION(VERSION_2)
..
PF_DEF_VERSION(VERSION_N)
PF_DEF_VERSION_END()
/*
* This particular design minimizes the need for the programmer to
* repeat themselves, but requires a build system with multiple
* pre-processing steps to generate, for example, multiple structs.
*
* This assumes that the prototype for any one entry point never
* changes. If you want to change an entry point's prototype then
* you must introduce a new entry point instead.
*
* In a dlsym-each-entry-point approach these macros are to be
* evaluated twice: once to output a single, internal v-table
* struct, and once to output a struct and initializer for it to
* populate required/optional information per-entry point.
*
* In a dlsym-a-single-function-that-returns-v-table approach these
* macros would generate either one v-table (all additions must be
* ad the end) or one v-table per-version.
*
* Removing required/optional information would significantly
* simplify the build system aspects of this design. I added them
* here just to illustrate the flexibility of a "plugin type
* compilation" approach.
*/
PF_DEF_INTERFACE_BEGIN()
PF_ADD_REQUIRED_ELEMENT(VERSION_X, entry_pointX)
PF_ADD_OPTIONAL_ELEMENT(VERSION_Y, entry_pointY)
...
PF_DEF_INTERFACE_END()
The plugin entry point lookup/call macros would be as I described
earlier, something like this:
/*
* Note the use of # and ## pre-processor operators. I am not sure
* if these are new in C99.
*
* Note that this involves a branch, with a slow path to load a
* plugin entry point by name. But in a dlsym-one-function-that-
* returns-a-v-table approach there'd be no need to do a lookup by
* name if we also generate a single [internal, per-plugin-type]
* v-table for all versions of the plugin type, to keep this macro
* simple. The same is true of a dlsym-each-entry-point design if
* the load function dlsyms all the entry points immediately. In
* such alternative designs there'd be no need to use the #
* operator, nor would there be this one branch.
*/
#define STRINGIFY(name) #name
#define PF_LOOKUP_ENTRY_POINT(context, plug_type, entry_point) \
((((context)->plugins[(plug_type)][entry_point ## _index]) \
? ((context)->plugins[(plug_type)][entry_point ## _index]) \
: (((context)->plugins[(plug_type)][entry_point ## _index]) \
= (entry_point ## _fct)pf_lookup_entry_point((context), \
(plug_type), STRINGIFY(entry_point)))))
Similarly for a PF_CALL_ENTRY_POINT() macro:
/*
* This uses C99 variadic macro support, but can be re-cast into a
* C89 non-variadic macro.
*/
#define PF_CALL_ENTRY_POINT(context, plug_type, entry_point, ...) \
(PF_LOOKUP_ENTRY_POINT((context), (plug_type), \
entry_point))(__VA_ARGS__)
Apps would call a pf_load_plugin() function with the following
signature:
krb5_error_code pf_load_plugin(krb5_context context,
int plug_type,
const char *plug_name);
or maybe even a PF_LOAD_PLUGIN() macro that evaluates to a conditional
to avoid calling pf_load_plugin() if the given plugin is already loaded.
The only "advanced" C features used above are: the C pre-processor token
concatenation operator (##) (C89, I believe), the C pre-processor
stringizing (#) operator (also C89, I believe), and variadic macros (new
in C99). This design can be altered to require none of these without
sacrificing strong type safety. For example, this alternative avoids
the use of variadic macros:
#define PF_CALL_ENTRY_POINT(context, plug_type, entry_point, args) \
(PF_LOOKUP_ENTRY_POINT((context), (plug_type), \
entry_point)) args
which would be used thusly:
ret = PF_CALL_ENTRY_POINT(ctx, PLUG_PREAUTH, foo, (arg1, arg2));
You can fill in details missing from the above sketch fairly easily, I
think.
Nico
--
More information about the krbdev
mailing list