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

An XSLT stylesheet, usually installed as /usr/share/libcxx/msgdispatcher.xsl or /usr/local/share/libcxx/msgdispatcher.xsl, produces generic code for a message dispatching-based object from a stylesheet. An XML file defines the names of methods implemented by a class as a public interface, and the stylesheet generates the supporting code:

<class name="errorthread">
  <method name="logerror" attributes='__attribute__((visibility("hidden")))'>
    <param><decl>const char *<name>file</name></decl></param>
    <param><decl>int <name>line</name></decl></param>
    <param type="class"><decl>std::string <name>error_message</name></decl></param>
  </method>

  <method name="report_errors">
    <param><decl>void (*<name>callback_func</name>)(const char *file, int line, const std::string &amp;errmsg)</decl></param>
  </method>
</class>

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

$ xsltproc /usr/share/libx/msgdispatcher.xml errorthread.xml >errorthread.inc.H

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

$ xsltproc /usr/share/libx/msgdispatcher.xml errorthread.xml
// AUTOGENERATED -- do not edit

#ifndef libx_autogen_suppress_inner
private:

//! Internal message object sent by logerror()

class __attribute__((visibility("hidden"))) logerror_msg {
public:

//! Message parameter
  const char *file;

//! Message parameter
  int line;

//! Message parameter
  std::string error_message;

//! Message constructor

  logerror_msg(
    //! Parameter
    const char *file_arg,
    //! Parameter
    int line_arg, //! Parameter
    const std::string &error_message_arg) :
    file(file_arg),
    line(line_arg),
    error_message(error_message_arg)
  {
  }

//! Destructor

  ~logerror_msg() noexcept {}
};


//! Internal message object sent by report_errors()

class report_errors_msg {
public:

//! Message parameter
  void (*callback_func)(const char *file, int line, const std::string &errmsg);

//! Message constructor

  report_errors_msg(
    //! Parameter
    void (*callback_func_arg)(const char *file, int line, const std::string &errmsg)) :
    callback_func(callback_func_arg)
  {
  }

//! Destructor

  ~report_errors_msg() noexcept {}
};


public:

//! Send the logerrormessage to the class 

  void logerror(//! Parameter
                const char *file_arg,
                //! Parameter
                int line_arg, //! Parameter
                const std::string &error_message_arg)
  {
    x::msgdispatcherObj::sendevent<logerror_msg>(this, file_arg, line_arg, error_message_arg);
  }

//! Send the report_errorsmessage to the class 

  void report_errors(//! Parameter
                     void (*callback_func_arg)(const char *file, int line, const std::string &errmsg))
  {
    x::msgdispatcherObj::sendevent<report_errors_msg>(this, callback_func_arg);
  }


public:
  template<typename obj_type, typename msg_type>
      friend class x::dispatchablemsgObj;

private:


//! Process message sent by logerror()

  void dispatch(const logerror_msg &msg);

//! Process message sent by report_errors()

  void dispatch(const report_errors_msg &msg);

#endif

This generated code gets inserted directly into a class declaration:

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

class errorthread : public x::eventqueuemsgdispatcherObj {

#include "errorthread.inc.H"

public:
    void run();
};

run() would typically be the default message dispatching loop:

void errorthread::run()
{
  while (1)
  {
    msgqueue->pop()->dispatch();
  }
}

All that's left is the actual implementation of the two dispatch() 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 report_errors() that takes a callback function pointer.

An internal class gets declared for each method: method_msg. The method_msg class holds the arguments of its public API method. Auto-generated public methods use sendevent to dispatch internal classes to the therad.

The end result: define the method names and their parameters in the XML definition file, the stylesheet generates the stub code for those methods, that puts the arguments into a method_msg object, then implement a dispatch() method that takes a const reference to a method_msg.

Stylesheet parameters

The msgdispatcher.xsl stylesheet takes several parameters:

mode

This parameter defines the content to produce from the stylesheet. The allowed values are:

all

The default value, generates everything: message class declarations, public API function declaration and their content, which creates the message class instance and invokes event() to put it into the message queue.

messagedecl

Generate only message class declarations, only class method_msg;.

messagedef

Generate the complete definitions of all the message classes, their constructors and destructors.

apidecl

Generate declarations of public methods, only void method( arguments ).

apidef

Generate complete definition of the public methods, including their contents, that instantiates the message class and invokes event().

dispatch

Declare dispatch() methods that receive messages queued up by the public API methods.

decl

This equivalent to messagedecl, apidecl, and dispatch.

def

This equivalent to messagedef and apidef.

scope

This parameter has two values, inner the default value, and outer which adds the name of the class in the generated code. Using the previous example, and running the stylesheet with the mode parameter set to messagedef produces the following code:

// AUTOGENERATED -- do not edit

#ifndef libx_autogen_suppress_inner

//! Internal message object sent by logerror()

class __attribute__((visibility("hidden"))) logerror_msg {
public:

  //! Message parameter
  const char *file;

  //! Message parameter
  int line;

  //! Message parameter
  std::string error_message;

  //! Message constructor

  logerror_msg(//! Parameter
               const char *file_arg, //! Parameter
               int line_arg, //! Parameter
               const std::string &error_message_arg) :
    file(file_arg),
    line(line_arg),
    error_message(error_message_arg) {}

  //! Destructor

  ~logerror_msg() noexcept {}
};

//! Internal message object sent by report_errors()

class report_errors_msg {
public:

  //! Message parameter
  void (*callback_func)(const char *file, int line, const std::string &errmsg);

  //! Message constructor

  report_errors_msg(//! Parameter
                    void (*callback_func_arg)(const char *file,
                                              int line,
                                              const std::string &errmsg)) :
    callback_func(callback_func_arg) {}

  //! Destructor

  ~report_errors_msg() noexcept {}
};
#endif

Setting scope to outer in addition with mode set to messagedef produces the following:

// AUTOGENERATED -- do not edit

#ifndef libx_autogen_suppress_outer

//! Internal message object sent by logerror()

class __attribute__((visibility("hidden"))) errorthread::logerror_msg {
public:

//! Message parameter
  const char *file;

//! Message parameter
  int line;

//! Message parameter
  std::string error_message;

//! Message constructor

  logerror_msg(//! Parameter
const char *file_arg, //! Parameter
int line_arg, //! Parameter
const std::string &error_message_arg) :
    file(file_arg),
    line(line_arg),
    error_message(error_message_arg) {}

//! Destructor

  ~logerror_msg() noexcept {}
};


//! Internal message object sent by report_errors()

class errorthread::report_errors_msg {
public:

//! Message parameter
  void (*callback_func)(const char *file, int line, const std::string &errmsg);

//! Message constructor

  report_errors_msg(//! Parameter
void (*callback_func_arg)(const char *file, int line, const std::string &errmsg)) :
    callback_func(callback_func_arg) {}

//! Destructor

  ~report_errors_msg() noexcept {}
};
#endif

Note

With mode set to def, the scope parameter is ignored, and presumed to be set to outer.

messages and dispatch

These parameters are set to either public, protected, or private (the default for both). These parameters are used when mode is all or decl. These parameters specify whether the generated code declares the message classes and the dispatch() methods as public: or private:.

If dispatch is not public the stylesheet produces a friend declaration for x::dispatchablemsgObj<typename obj_type, typename msg_type> when mode is all or decl, so that it is able to invoke the dispatch() methods.

Check your XSLT processor's documentation for instructions on setting stylesheet parameters. With xsltproc, use the --stringparam option:

xsltproc --stringparam mode messagedef --stringparam scope outer /usr/share/libx/msgdispatcher.xml errorthread.xml

XML definitions

Using the same XML file as an example:

<class name="errorthread">
  <method name="logerror">
    <param><decl>const char *<name>file</name></decl></param>
    <param><decl>int <name>line</name></decl></param>
    <param type="class"><decl>std::string <name>error_message</name></decl></param>
  </method>

  <method name="report_errors">
    <param><decl>void (*<name>callback_func</name>)(const char *file, int line, const std::string &amp;errmsg)</decl></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 is typically used to set gcc extensions on a class declaration.

<param> has an optional <comment> element, and a required <decl> element that contains a literal declaration of a C++ variable, with the variable's name marked with a <name> element (and without a trailing semicolon).

As shown in the example with the generated code, each parameter to a public API method get stored into a message class, with the definition of class members taken literally from the <param> elements.

Setting the optional <param> attribute type to class indicates that the parameter definition is a class definition, and that the constructor in the generated code for the public API method, and the message class's constructor, receives the parameter as a constant reference.

<method name="logerror">
  <param><decl>const char *<name>file</name></decl></param>
  <param><decl>int <name>line</name></decl></param>
  <param><decl>std::string <name>error_message</name></decl></param>
</method>

This generates the followeing public API declaration.

void logerror(const char *file_arg, int line_arg,
              std::string error_message_arg)

Setting error_message's <param>'s type attribute to class changes this to:

void logerror(const char *file_arg, int line_arg,
              const std::string &error_message_arg)

Setting <param> to type to weakptr passes x::ptrs to reference-counted objects as weak references:

<method name="report_errors">
  <param type="weakptr"><decl>mcguffinptr <name>mcguffin</name></decl></param>
</method>

This generates the following message class (slightly reformatted for readability):

// AUTOGENERATED -- do not edit

#ifndef libx_autogen_suppress_inner
private:


//! Internal message object sent by report_errors()

class report_errors_msg {
public:

  //! Message parameter
  x::weakptr<mcguffinptr> mcguffin;

  //! Message constructor

  report_errors_msg(//! Parameter
                    const mcguffinptr &mcguffin_arg) :
                    mcguffin(mcguffin_arg)
  {
  }

  //! Destructor

  ~report_errors_msg() noexcept {}
};


public:

  //! Send the report_errorsmsg to the class 

  void report_errors(//! Parameter
                     const mcguffinptr &mcguffin_arg)
  {
    x::msgdispatcherObj::sendevent<report_errors_msg>(this, mcguffin_arg);
  }


public:
  template<typename obj_type, typename msg_type>
      friend class x::dispatchablemsgObj;

private:


//! Process message sent by report_errors()

  void dispatch(const report_errors_msg &msg);

#endif

The stylesheet generates code that declares the parameter as a reference to the specified x::ptr, but the message stores a weak reference. The parameter declared with a weakptr should always be a x::ptr. Here, for example, the declaration typedef x::ptr<x::obj> mcguffinptr; should be somewhere in scope.

This is useful when the actual parameter to the API call is a reference to a reference-counted object, and the executing thread terminates without processing the message, but something else holds a reference on the thread object preventing it from being destroyed. Since the message on the dead thread's message queue contains only a weak reference this won't prevent the parameter to the API call from being destroyed if no other references to it exist.

The dispatch() method is expected to recover a strong reference, when it picks up the message, and have a well-defined error path if the weakly-referenced object has already been destroyed.

Note

Specifying that a parameter is a weak reference implies that the caller retains the reference to the parameter object, after the API call completes. Otherwise, with the API call complete, the caller's reference to the object goes out of scope may go out of scope, and the referenced object gets destroyed, before the executing thread has a chance to recover a strong reference to the parameter object.

<method> has an optional virtual attribute:

<method name="report_errors" virtual="1">
  <param type="weakptr"><decl>mcguffinptr <name>mcguffin</name></decl></param>
</method>

The virtual attribute results in the dispatch() method getting declared as a virtual method:

//! Process message sent by report_errors()

  virtual void dispatch(const report_errors_msg &msg);

Optional message class constructor and serialization function

The <method> element has an optional default attribute that may be set to 1:

<method name="dump" default="1" serialize="1">
  <param type="class"><decl>std::string <name>filename</name></decl></param>
  <param default="0"><decl>int <name>count</name></decl></param>
  <param default="1"><decl>int <name>repeat</name></decl></param>
  <param type="class" dontserialize="1"><decl>x::ptr&lt;x::obj&gt; <name>mcguffin</name></decl></param>
</method>

This adds a default constructor to the definition of the generated message class. Each <param>'s optional default atribute gives the value for the message class's member that's initialized by the default constructor.

Finally, setting <method>'s serialize attribute to 1 generates a serialize() method in the message class definition, serializing all parameters except ones that have the dontserialize attribute set to 1. The msgdispatcher.xsl stylesheet produces the following code from the above XML definition:

// AUTOGENERATED -- do not edit

#ifndef libx_autogen_suppress_inner
private:


//! Internal message object sent by dump()

class dump_msg {
public:

//! Message parameter
  std::string filename;

//! Message parameter
  int count;

//! Message parameter
  int repeat;

//! Message parameter
  x::ptr<x::obj> mcguffin;

//! Message constructor

  dump_msg(//! Parameter
           const std::string &filename_arg,
           //! Parameter
           int count_arg,
           //! Parameter
           int repeat_arg,
           //! Parameter
           const x::ptr<x::obj> &mcguffin_arg) :
    filename(filename_arg),
    count(count_arg),
    repeat(repeat_arg),
    mcguffin(mcguffin_arg)
  {
  }

  //! Default constructor

  dump_msg() :
      count(0),
      repeat(1)
  {
  }

  //! Serialization function

  template<typename iter_type> void serialize(iter_type &iter)
  {
    iter(filename);
    iter(count);
    iter(repeat);
  }

  //! Destructor

  ~dump_msg() noexcept {}
};


public:

//! Send the dumpmessage to the class 

  void dump(//! Parameter
            const std::string &filename_arg,
            //! Parameter
            int count_arg,
            //! Parameter
            int repeat_arg,
            //! Parameter
            const x::ptr<x::obj> &mcguffin_arg)
  {
    x::msgdispatcherObj::sendevent<dump_msg>(this, filename_arg, count_arg, repeat_arg, mcguffin_arg);
  }


public:
  template<typename obj_type, typename msg_type>
      friend class x::dispatchablemsgObj;

private:


//! Process message sent by dump()

  void dispatch(const dump_msg &msg);

#endif

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 <name>object</name></decl></param>
  </method>

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

// AUTOGENERATED -- do not edit

#ifndef libx_autogen_suppress_inner

/* ... */

/*!
    Write an object
*/

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

             const objref &object_arg)
  {

/* ... */

#endif

The partial code fragment containing this output is typically saved in a separate .H file and the .H that declares the class #included from inside the class. Doxygen typically parses the main class file, but also attempts to read the standalone .H file with the auto-generated code, resulting in mild chaos.

For this reason, the autogenerated code is wrapped inside the ifndef/endif construct, as shown above. In the Doxyfile specify PREDEFINED = libx_autogen_suppress_inner accordingly, so Doxygen doesn't see it, but also add #undef libx_autogen_suppress_inner in the .H file with the main class definition, so that the #include from the class .H gets parsed.

Depending on whether autogenerated code uses outer scope, and how the autogenerated code gets included, predefining libx_autogen_suppress_outer may also be necessary.

GNU make macros

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

include /usr/share/libx/libcxx.mk

$(call MSGDISPATCHER_GEN,errorlog,all)

This example creates a rule for the target errorlog.all.H with a dependency errorlog.xml. The target gets created by running the msgdispatcher.xsl stylesheet with the mode parameter set to all. An optional third parameter to MSGDISPATCHER_GEN is a list of other parameter=value settings that are passed to xsltproc using the --stringparam option:

$(call MSGDISPATCHER_GEN,errorlog,all,dispatch=public)

Another useful invocation of MSGDISPATCHER_GEN generates declarations and definitions into separate files:

$(call MSGDISPATCHER_GEN,errorlog,decl def)

This macro creates two make targets, errorlog.decl.H and errorlog.def.H, with the same dependency, errorlog.xml, that get created by running the msgdispatcher.xsl stylesheet with the mode=decl and mode=def parameters.

Using GNU make macros with automake

In configure.ac:

LIBCXX_INIT

In Makefile.am:

@LIBCXX_AM@

# Followed by macro calls:

$(call MSGDISPATCHER_GEN,errorlog,all)

# ...

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.