Using a stylesheet to generate a thread-based message dispatching framework

An XSLT stylesheet, usually installed as /usr/share/libcxx/threadmsgdispatcher.xsl or /usr/local/share/libcxx/threadmsgdispatcher.xsl , produces generic code for a message dispatching-based object from a convenient stylesheet that produces most of the spaghetti code for a typical dispatcher object. An XML file lists the names of public methods and the parameters to the private class methods, and the stylesheet generates the supporting code.

<class name="mythreadObj">
  <method name="logerror">
    <comment> //! logerror() function</comment>
    <param>
      <decl>const char *file</decl>
      <default>""</default>
    </param>
    <param>
      <decl>int line</decl>
      <default>-1</default>
    </param>
    <param>
      <comment>//! Descriptive error message</comment>
      <decl>const std::string &amp;error_message</decl>
      <default>"unknown error"</default>
    </param>

    <attributes> noexcept</attributes>
  </method>

  <method name="testas">
    <virtual />
    <param>
      <decl>testas_class param</decl>
      <as>int</as>
    </param>
  </method>
</class>

Use an XSLT processor, such as xsltproc to run the stylesheet:

$ xsltproc /usr/share/libcxx/threadmsgdispatcher.xml errorthread.xml >errorthread.inc.H

This results in the following contents of errorthread.inc.H (with some reformatting for readability):

// AUTOGENERATED -- do not edit

#ifndef libx_autogen_suppress_

public:
 //! logerror() function
template <typename param_type_1,
          typename param_type_2,
          typename param_type_3>
    void logerror(
        param_type_1 && param_1,
        param_type_2 && param_2,
        param_type_3 && param_3)
{
    this->sendevent(&mythreadObj::dispatch_logerror, this,
                    std::forward<param_type_1>(param_1),
                    std::forward<param_type_2>(param_2),
                    std::forward<param_type_3>(param_3));
}

//! Provide default value
void logerror()
{
    logerror("");
}

//! Provide default value
template <typename param_type_1> void logerror(
        param_type_1 && param_1)
{
    logerror(
        std::forward<param_type_1>(param_1),
        -1);
}

//! Provide default value
template <
    typename param_type_1,
    typename param_type_2> void logerror(
        param_type_1 && param_1,
        param_type_2 && param_2)
{
    logerror(
        std::forward<param_type_1>(param_1),
        std::forward<param_type_2>(param_2),
        "unknown error");
}

template <
    typename param_type_1>
    void testas(
        param_type_1 && param_1)
{
    this->sendevent(&mythreadObj::dispatch_testas, this,
                    int(std::forward<param_type_1>(param_1)));
}

private:

//! Internal implementation of the logerror() message

void dispatch_logerror(
//! Message parameter
      const char *file,

//! Message parameter
      int line,

//! Descriptive error message
      const std::string &error_message) noexcept;

//! Internal implementation of the testas() message

virtual void dispatch_testas(
//! Message parameter
      testas_class param);

This generated code gets inserted directly into a class declaration:

#include <string>
#include <x/threadmsgdispatcher.H>

class errorthread : public x::threadmsgdispatcherObj {

#include "errorthread.inc.H"

public:
    void run(x::ptr<x::obj> &threadmsgdispatcher_mcguffin)
};

All that's left is the actual implementation of the two dispatch_name() methods. The final product is a class with two public API methods: logerror() that takes a filename, a line number, and an error message string; and test_as() that takes a custom class name as a parameter.

The end result: define the method names and their parameters in the XML definition file, the stylesheet generates the stub code for those methods, then implement the dispatch_name() private class method.

XML definitions

Using the same XML file as an example:

<class name="mythreadObj">
  <method name="logerror">
    <comment> //! logerror() function</comment>
    <param>
      <decl>const char *file</decl>
      <default>""</default>
    </param>
    <param>
      <decl>int line</decl>
      <default>-1</default>
    </param>
    <param>
      <comment>//! Descriptive error message</comment>
      <decl>const std::string &amp;error_message</decl>
      <default>"unknown error"</default>
    </param>

    <attributes> noexcept</attributes>
  </method>

  <method name="testas">
    <virtual />
    <param>
      <decl>testas_class param</decl>
      <as>int</as>
    </param>
  </method>
</class>

The <class> element has a required name attribute that gives the name of the class for the generated methods, and contains one or more <method> elements.

Each <method> has a required attribute, name that's essentially the name of the public API method, and contains an optional <comment> element, and a list of <param> elements, one for each parameter to the public API function. This list may be empty, if the public API function takes no parameters.

<method> has an optional attributes parameter that, if present gets inserted immediately after the class keyword, verbatim. This can be used to specify noexcept (as in the example), or set gcc extensions on the method declaration.

<param> has an optional <comment> element, and a required <decl> element that contains a literal declaration of a C++ variable.

<method> has an optional virtual element that prepends virtual to the private dispatch_name method.

The <method> element has an optional default element. Normally, the stylesheet generates a public method that forwards its arguments to sendevent(). For example, given following stylesheet:

<class name="mythreadObj">
  <method name="logerror">
    <comment> //! logerror() function</comment>
    <param>
      <decl>const char *file</decl>
    </param>
    <param>
      <decl>int line</decl>
    </param>
    <param>
      <comment>//! Descriptive error message</comment>
      <decl>const std::string &amp;error_message</decl>
    </param>

    <attributes> noexcept</attributes>
  </method>
</classname>

This results in the following code getting generated for this public method:

template <
    typename param_type_1,
    typename param_type_2,
    typename param_type_3>
    void logerror(
        param_type_1 && param_1,
        param_type_2 && param_2,
        param_type_3 && param_3)
{
    this->sendevent(&mythreadObj::dispatch_logerror, this,
                    std::forward<param_type_1>(param_1),
                    std::forward<param_type_2>(param_2),
                    std::forward<param_type_3>(param_3));
}

The following stylesheet specifies default values for all parameters:

<class name="mythreadObj">
  <method name="logerror">
    <comment> //! logerror() function</comment>
    <param>
      <decl>const char *file</decl>
      <default>""</default>
    </param>
    <param>
      <decl>int line</decl>
      <default>-1</default>
    </param>
    <param>
      <comment>//! Descriptive error message</comment>
      <decl>const std::string &amp;error_message</decl>
      <default>"unknown error"</default>
    </param>

    <attributes> noexcept</attributes>
  </method>
</class>

This generates additional overloaded public methods that supply the default values for the parameters:

//! logerror() function
template <
    typename param_type_1,
    typename param_type_2,
    typename param_type_3>
    void logerror(
        param_type_1 && param_1,
        param_type_2 && param_2,
        param_type_3 && param_3)
{
    this->sendevent(&mythreadObj::dispatch_logerror, this,
                    std::forward<param_type_1>(param_1),
                    std::forward<param_type_2>(param_2),
                    std::forward<param_type_3>(param_3));
}

//! Provide default value
void logerror()
{
    logerror("");
}

//! Provide default value
template <
    typename param_type_1> void logerror(
        param_type_1 && param_1)
{
    logerror(
        std::forward<param_type_1>(param_1),
        -1);
}

//! Provide default value
template <
    typename param_type_1,
    typename param_type_2> void logerror(
        param_type_1 && param_1,
        param_type_2 && param_2)
{
    logerror(
        std::forward<param_type_1>(param_1),
        std::forward<param_type_2>(param_2),
        "unknown error");
}

Generating doxygen documentation

<comment> elements in <method> and <param> provide a mechanism for inserting Doxygen tags:

  <method name="write">
    <comment>/*!
    Write an object
    */</comment>

    <param type="class">
      <comment>/*!
      Object to be written
      */</comment>
    <decl>objref object</decl></param>
  </method>

The resulting output from the stylesheet looks like this (reformatted for readability).

// AUTOGENERATED -- do not edit

#ifndef libcxx_autogen_suppress_inner

/* ... */

/*!
    Write an object
*/

  void write(/*!
             Object to be written
             */

             const objref &object_arg)
  {

/* ... */

#endif

GNU make macros

A GNU make rule is installed by default as /usr/share/libcxx-version/libcxx.mk or /usr/local/share/libcxx-version/libcxx.mk, and contains a couple of useful gmake macros. This rule requires GNU make and xsltproc:

include /usr/share/libcxx-version/libcxx.mk

$(call THREADMSGDISPATCHER_GEN,errorlog.H,errorlog.xml)

This example creates a rule for the target errorlog.H with a dependency errorlog.xml. The target gets created by running the threadmsgdispatcher.xsl stylesheet.

Using GNU make macros with automake

In configure.ac:

LIBCXX_INIT

In Makefile.am:

@LIBCXX_AM@

# Followed by macro calls:

$(call THREADMSGDISPATCHER_GEN,errorlog.H,errorlog.xml)

# ...

The LIBCXX_INIT autoconf macro sets LIBCXX_AM to the include statement that pulls in libcxx.mk.

libcxx.mk does a rudimentary check if AUTOMAKE is set. If so, it automatically generates a make distclean rule to remove all targets generated by the macros. The LIBCXX_CLEANFILES variable accumulates the list of targets produced by all macro invocations, and may be utilized by non-automake makefiles for similar purposes. Additionally, all .xml dependencies get added to EXTRA_DIST. Any assignments to EXTRA_DIST in Makefile.am must occur before inclusion of libcxx.mk. It is permitted to use += to add to EXTRA_DIST after the inclusion of libcxx.mk.