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