1. Overview

What is the NANDRAD-Code-Generator? Basically, it is a tool to parse NANDRAD header files, pick up lines with special comments and then generate code for:

  • translating enumeration values into strings, descriptions, colors and units (very handy for physical parameter lists)

  • reading and writing xml data structures (elements and attributes) for designated member variables

  • comparing data structures with default instances (so that only modified members are written to file), and other utility functions

Actually, the NANDRAD code generator is also used for the VICUS library. And the keyword list functionality is further used in the NandradSolver itself.

1.1. How does it work?

The code generator is called as part of the CMake build chain (see sections command line options and CMake Automation). It detects changes in header files and then updates the respective generated files, including the file NANDRAD_KeywordList.cpp. It places the class-specific generated files into a subdirectory ncg (for NANDRAD Code Generator files) where they are compiled just as any other cpp file of the project.

Since the generated code is plain C++ code, it can be parsed and debugged with any IDE just as regular code. Just remember:

Any changes made to generated code will be overwritten!

2. Functionality Example

2.1. Keyword support

You may have a class header, that looks as follows:

class Interval {
public:
	/*! Parameters. */
	enum para_t {
		IP_START,		// Keyword: Start		[d] 'Start time point'
		IP_END,			// Keyword: End			[d] 'End time point'
		IP_STEPSIZE,	// Keyword: StepSize	[h] 'StepSize'
		NUM_IP
	};

	...

	/*! The parameters defining the interval. */
	IBK::Parameter   m_para[NUM_IP];
};

Here, you have a list of enumeration values. These are used to index one of the parameters in the member variable array m_para. In the code, you will use these enumerations like:

double endtime = m_para[Interval::IP_END].value;

Sometimes, however, you need the keyword as a string, or need to get the unit written in the comment line. So, you write code like:

// construct a parameter
m_para[Interval::IP_END].set(
        NANDRAD::KeywordList::Keyword("Interval::para_t", Interval::IP_END), // the keyword is used as name
        24, // value
        NANDRAD::KeywordList::Unit("Interval::para_t", Interval::IP_END) // the unit
    );

Normally, the unit is only meaningful for user interface representation, or when the value is printed to the user as default output/disply unit.

Also useful is the description, mostly in informative/error messages. Recommended would be:

if (condition_failed) {
    IBK::IBK_Message(IBK::FormatString("Parameter %1 (%2) is required.")
        .arg(KeywordList::Keyword("Interval::para_t", Interval::IP_END))
        .arg(KeywordList::Description("Interval::para_t", Interval::IP_END)) );
}

Also, when parsing user data, for example from format:

<IBK:Parameter name="Start" unit="d">36</IBK:Parameter>
<IBK:Parameter name="End" unit="d">72</IBK:Parameter>

You may need code like:

nameAttrib = getAttributeText(xmlTag, "name"); // might be 'End'

// resolve enumeration value
Interval::para_t p = (Interval::para_t)KeywordList::Enumeration("Interval::para_t", nameAttrib);

2.2. Keyword-definition syntax

The syntax for a keyword definition line is:

    SOME_ENUM_VALUE,	// Keyword: KEYWORD1 KEYWORD2  [unit] <#FFEECC> {defaultValue} 'description'

KEYWORD1 is the mandatory keyword, that should normally match the Enum-value, for example:

    ID_Category,        // Keyword: Category

KEYWORD2 is an optional (potential outdated) keyword. Unit, color (htmlcode), default value and description are all optional attributes.

Mind the separation chars. These must not appear inside other separation chars, so []<>{} must not be used inside the description!

The keyword list parser looks for occurances of exactly the substring // Keyword:. So, if you merely comment out a line with a keyword, also falsify this substring, for example to // ---Keyword:. Otherwise the code-generator will still count the line and generate false enum value/keyword string mappings (and also potentially cause access violations/seg faults, because of invalid enum values being returned).

2.3. Static member functions of class KeywordList

The code generator will now create the implementation of the functions:

  • KeywordList::Enumeration

  • KeywordList::Description

  • KeywordList::Keyword

  • KeywordList::Unit

  • KeywordList::Color - meaningful for coloring types in user interfaces

  • KeywordList::DefaultValue - can be used as default value in user interfaces

  • KeywordList::Count - returns number of enumeration values in this category

  • KeywordList::KeywordExists - convenience function, same as comparing result of Enumeration() with -1

For optional parameters that are not provided, these functions return default values, i.e. empty strings, IBK::Unit() and 0 as default value.

2.4. Usage

The header file NANDRAD_KeywordList.h is always the same and can be included directly. The corresponding implementation file NANDRAD_KeywordList.cpp is generated in the same directory as the NANDRAD header files.

2.5. CMake automation

The automatic update of the keyword list is triggered by a custom rule in the NANDRAD CMake project file:

# collect a list of all header files of the Nandrad library
file( GLOB Nandrad_HDRS ${PROJECT_SOURCE_DIR}/../../src/*.h )

# run the NandradCodeGenerator tool whenever the header files have changed
# to update the NANDRAD_KeywordList.h and NANDRAD_KeywordList.cpp file
add_custom_command (
	OUTPUT   ${PROJECT_SOURCE_DIR}/../../src/NANDRAD_KeywordList.cpp
	DEPENDS  ${Nandrad_HDRS} NandradCodeGenerator
	COMMAND  NandradCodeGenerator
	ARGS     NANDRAD ${PROJECT_SOURCE_DIR}/../../src
)

where NandradCodeGenerator is built as part of the tool chain as well. The rule has all header files as dependencies so that any change in any header file will result in a call to the code generator. The code generator will then create the file NANDRAD_KeywordList.cpp.

3. Read/write to XML support, and utility macros

A second task for the code generator is to create functions for serialization of data structures to XML files. Hereby, the TinyXML-library is used.

3.1. Supported data types

See section XML Serialization for a list and examples of supported data types.

3.2. Read/write code

Since reading/writing XML code is pretty straight forward, much of this code writing can be generalized. Let’s take a look at a simple example.

Class Sensor, with declarations of readXML() and writeXML() functions
class Sensor {
public:
	// *** PUBLIC MEMBER FUNCTIONS ***

	void readXML(const TiXmlElement * element);
	TiXmlElement * writeXML(TiXmlElement * parent) const;

	// *** PUBLIC MEMBER VARIABLES ***

	/*! Unique ID-number of the sensor.*/
	unsigned int						m_id = NANDRAD::INVALID_ID;		// XML:A:required
	/*! Name of the measured quantity */
	std::string							m_quantity;						// XML:E
};

Since we use C++11 code, member variable initialization with the = assignment in header is ok and saves creating default constructors. Do this!

The two members are written into file as follows:

<Sensor id="12">
    <Quantity>Temperature</Quantity>
</Sensor>

The implementation looks as follows:

Implementation of Sensor::readXML()
void Sensor::readXML(const TiXmlElement * element) {
	FUNCID(Sensor::readXML);

	try {
		// search for mandatory attributes
		if (!TiXmlAttribute::attributeByName(element, "id"))
			throw IBK::Exception( IBK::FormatString(XML_READ_ERROR).arg(element->Row()).arg(
				IBK::FormatString("Missing required 'id' attribute.") ), FUNC_ID);

		// reading attributes
		const TiXmlAttribute * attrib = element->FirstAttribute();
		while (attrib) {
			const std::string & attribName = attrib->NameStr();
			if (attribName == "id")
				m_id = readPODAttributeValue<unsigned int>(element, attrib);
			else {
				IBK::IBK_Message(IBK::FormatString(XML_READ_UNKNOWN_ATTRIBUTE).arg(attribName)
				    .arg(element->Row()), IBK::MSG_WARNING, FUNC_ID, IBK::VL_STANDARD);
			}
			attrib = attrib->Next();
		}
		// search for mandatory elements
		// reading elements
		const TiXmlElement * c = element->FirstChildElement();
		while (c) {
			const std::string & cName = c->ValueStr();
			if (cName == "Quantity")
				m_quantity = c->GetText();
			else {
				IBK::IBK_Message(IBK::FormatString(XML_READ_UNKNOWN_ELEMENT).arg(cName)
				    .arg(element->Row()), IBK::MSG_WARNING, FUNC_ID, IBK::VL_STANDARD);
			}
			c = c->NextSiblingElement();
		}
	}
	catch (IBK::Exception & ex) {
		throw IBK::Exception( ex,
		    IBK::FormatString("Error reading 'Sensor' element."), FUNC_ID);
	}
	catch (std::exception & ex2) {
		throw IBK::Exception( IBK::FormatString("%1\nError reading 'Sensor' element.")
		    .arg(ex2.what()), FUNC_ID);
	}
}

In this function there is a lot of code that is repeated nearly identical in all files of the data model. For example, reading of attributes, converting them to number values (including error checking), testing for known child elements (and error handling) and the outer exception catch clauses. Similarly, this looks for the writeXML() function.

Implementation of Sensor::writeXML()
TiXmlElement * Sensor::writeXML(TiXmlElement * parent) const {
	TiXmlElement * e = new TiXmlElement("Sensor");
	parent->LinkEndChild(e);

	e->SetAttribute("id", IBK::val2string<unsigned int>(m_id));
	if (!m_quantity.empty())
		TiXmlElement::appendSingleAttributeElement(e,
		    "Quantity", nullptr, std::string(), m_quantity);
	return e;
}

In order for the code generator to create these two functions, we need to add some annotations to original class declaration:

Class Sensor, with annotations for read/write code generation
class Sensor {
public:
	// *** PUBLIC MEMBER FUNCTIONS ***

	void readXML(const TiXmlElement * element);
	TiXmlElement * writeXML(TiXmlElement * parent) const;

	// *** PUBLIC MEMBER VARIABLES ***

	/*! Unique ID-number of the sensor.*/
	unsigned int						m_id = NANDRAD::INVALID_ID;		// XML:A:required
	/*! Name of the measured quantity */
	std::string							m_quantity;						// XML:E
};

The // XML:A says: make this an attribute. The // XML:E says: make this a child-element. The additional required keyword means: this attribute (or element) must be provided, otherwise readXML() will throw an exception.

The annotations can be used for quite a few data types. Rules for these are given in section XML Serialization.

3.2.1. Special features

There are a few special syntax forms supported by the code generator, see XML Serialization for details.

3.2.2. Naming convention for XML attributes and tag names

Note that attributes (XML:A) will always start with lowercase letter: "m_idComponent" -→ "idComponent"

while tag names (XML:E) will start with capital letter: "m_idSurfaceHeating" -→ "IdSurfaceHeating"

3.3. Utility Macros

Since the declaration for the readXML() and writeXML() functions are always the same, we can avoid typing errors by using a define:

Global code generator helpers
#define NANDRAD_READWRITE \
	void readXML(const TiXmlElement * element); \
	TiXmlElement * writeXML(TiXmlElement * parent) const;

The header is now very short:

Class Sensor, using code generator
class Sensor {
public:
	// *** PUBLIC MEMBER FUNCTIONS ***

	NANDRAD_READWRITE

	// *** PUBLIC MEMBER VARIABLES ***

	/*! Unique ID-number of the sensor.*/
	unsigned int						m_id = NANDRAD::INVALID_ID;		// XML:A:required
	/*! Name of the measured quantity */
	std::string							m_quantity;						// XML:E
};

The implementation file NANDRAD_Sensor.cpp is no longer needed and can be removed.

The code generator will create a file: ncg_NANDRAD_Sensor.cpp with the functions Sensor::readXML() and Sensor::writeXML().

To avoid regenerating (and recompiling) all ncg_* files whenever one header file is modified, the code generator inspects the file creation times of the ncg_XXX.cpp file with the latest modification/creation data of the respective ncg_XXX.h file. The code is only generated, if the header file is newer than the generated file.

3.3.1. Comparison operator macro

When checking if the content of an object is effectively the same as that of another (possibly freshly constructed) object, we need a comparison operator. Actually, we usually need both operator== and operator!= (depending on the alorithm used, either of the two is needed). The code for the class Sensor normally looks like that:

Comparison operator (inequality)
bool Sensor::operator!=(const Sensor & other) const {
	if (m_id != other.m_id)				return true;
	if (m_quantity != other.m_quantity)	return true;
	return false;
}

The other comparison operator is normally just implemented using the other:

Comparison operator (equality)
bool operator==(const Sensor & other) const { return !operator!=(other); }

The declaration and the definition of the equality operator can be replaced by a define:

Global code generator helpers
#define NANDRAD_COMP(X) \
	bool operator!=(const X & other) const;

So the class declaration becomes:

Class Sensor, with comparison function declarations
class Sensor {
public:
	// *** PUBLIC MEMBER FUNCTIONS ***

	NANDRAD_READWRITE
	NANDRAD_COMP(Sensor)

	// *** PUBLIC MEMBER VARIABLES ***

	/*! Unique ID-number of the sensor.*/
	unsigned int						m_id = NANDRAD::INVALID_ID;		// XML:A:required
	/*! Name of the measured quantity */
	std::string							m_quantity;						// XML:E
};

3.3.2. Custom read/write functionality

Sometimes, the default read/write code is not enough, because something special needs to be written/read as well. Here, you can simply use an alternative define NANDRAD_READWRITE_PRIVATE:

Global code generator helpers
#define NANDRAD_READWRITE_PRIVATE \
	void readXMLPrivate(const TiXmlElement * element); \
	TiXmlElement * writeXMLPrivate(TiXmlElement * parent) const;

which tells the code generator to generate the read/write code inside the XXXPrivate-functions.

You can now implement readXML() and writeXML() manually, hereby re-using the auto-generated functionality. Below is an example:

Class Sensor, using code generator with private read/write functions
class Sensor {
	NANDRAD_READWRITE_PRIVATE
public:
	// *** PUBLIC MEMBER FUNCTIONS ***

	NANDRAD_READWRITE
	NANDRAD_COMP(Sensor)

	// *** PUBLIC MEMBER VARIABLES ***

	/*! Unique ID-number of the sensor.*/
	unsigned int						m_id = NANDRAD::INVALID_ID;		// XML:A:required
	/*! Name of the measured quantity */
	std::string							m_quantity;						// XML:E
};
Implementation file NANDRAD_Sensor.cpp
void Sensor::readXML(const TiXmlElement * element) {
	// simply reuse generated code
	readXMLPrivate(element);

	// ... read other data from element
}


TiXmlElement * Sensor::writeXML(TiXmlElement * parent) const {
	TiXmlElement * e = writeXMLPrivate(parent);

	// .... append other data to e
	return e;
}

3.3.3. Only writing data if not still default content

To avoid writing empty tags or default values, you can write code like:

Implementation of writeXML with default check
TiXmlElement * Sensor::writeXML(TiXmlElement * parent) const {
    // check if we still have default data
    if (*this == Sensor())
        return; // still default, do not write anything

	TiXmlElement * e = new TiXmlElement("Sensor");
	parent->LinkEndChild(e);

	e->SetAttribute("id", IBK::val2string<unsigned int>(m_id));
	if (!m_quantity.empty())
		TiXmlElement::appendSingleAttributeElement(e,
		    "Quantity", nullptr, std::string(), m_quantity);
	return e;
}

However, the code generator cannot write this automatically, because sometimes it is desired to write even default content. Also, a comparison-operator is not always available.

You can, however, use the macro NANDRAD_READWRITE_IFNOTEMPTY(X) instead of the regular NANDRAD_READWRITE macro for this:

Macro with check for default values
#define NANDRAD_READWRITE_IFNOTEMPTY(X) \
	void readXML(const TiXmlElement * element) { readXMLPrivate(element); } \
	TiXmlElement * writeXML(TiXmlElement * parent) const { if (*this != X()) return writeXMLPrivate(parent); else return nullptr; }

Since this macro uses the functions readXMLPrivate() and writeXMLPrivate() you also need to tell the code generator to use the private function versions, as in the following example:

Class Sensor, using code generator with private read/write functions and check to not write default data
class Sensor {
	NANDRAD_READWRITE_PRIVATE
public:
	// *** PUBLIC MEMBER FUNCTIONS ***

	NANDRAD_READWRITE_IFNOTEMPTY(Sensor)
	NANDRAD_COMP(Sensor)

	// *** PUBLIC MEMBER VARIABLES ***

	/*! Unique ID-number of the sensor.*/
	unsigned int						m_id = NANDRAD::INVALID_ID;		// XML:A:required
	/*! Name of the measured quantity */
	std::string							m_quantity;						// XML:E
};

For classes such as Sensor, that define a member variable m_id which is initialized with NANDRAD::INVALID_ID it is also possible (and better) to use the macro NANDRAD_READWRITE_IFNOT_INVALID_ID, which does not require implementation of a comparison operator.

Class Sensor, using code generator with private read/write functions and check to not write unused data objects
class Sensor {
	NANDRAD_READWRITE_PRIVATE
public:
	// *** PUBLIC MEMBER FUNCTIONS ***

	NANDRAD_READWRITE_IFNOT_INVALID_ID

	// *** PUBLIC MEMBER VARIABLES ***

	/*! Unique ID-number of the sensor.*/
	unsigned int						m_id = NANDRAD::INVALID_ID;		// XML:A:required
	/*! Name of the measured quantity */
	std::string							m_quantity;						// XML:E
};

3.3.4. Utility Macro Overview

All utility macros
#define NANDRAD_READWRITE \
	void readXML(const TiXmlElement * element); \
	TiXmlElement * writeXML(TiXmlElement * parent) const;

#define NANDRAD_READWRITE_IFNOTEMPTY(X) \
	void readXML(const TiXmlElement * element) { readXMLPrivate(element); } \
	TiXmlElement * writeXML(TiXmlElement * parent) const { if (*this != X()) return writeXMLPrivate(parent); else return nullptr; }

#define NANDRAD_READWRITE_IFNOT_INVALID_ID \
	void readXML(const TiXmlElement * element) { readXMLPrivate(element); } \
	TiXmlElement * writeXML(TiXmlElement * parent) const { if (m_id != INVALID_ID) return writeXMLPrivate(parent); else return nullptr; }

#define NANDRAD_READWRITE_PRIVATE \
	void readXMLPrivate(const TiXmlElement * element); \
	TiXmlElement * writeXMLPrivate(TiXmlElement * parent) const;

#define NANDRAD_COMP(X) \
	bool operator!=(const X & other) const; \
	bool operator==(const X & other) const { return !operator!=(other); }

#define NANDRAD_COMPARE_WITH_ID \
	bool operator==(unsigned int x) const { return m_id == x; }

#define NANDRAD_COMPARE_WITH_NAME \
	bool operator==(const std::string & name) const { return m_name == name; }

NANDRAD_READWRITE_IFNOTEMPTY and NANDRAD_READWRITE_IFNOTEMPTY must be used in conjunction with NANDRAD_READWRITE_PRIVATE.

4. Specifications

4.1. Command line arguments for the code generator

The code generator is called with the following syntax:

SYNTAX:  NandradCodeGenerator <namespace> <path/to/src> <generateQtSrc> <prefix> <ncg-dir>
         <namespace> is usually NANDRAD (used also to compose file names).
         <path/to/<lib>/src> is + separated list of input directories to read the header files
         from.
         Keywordlist-source files are written into the first (or only) source directory.
         <prefix> is the file prefix <prefix>_KeywordList.cpp.
         <generateQtSrc> is 1 when Qt source should be generated, 0 otherwise.
         <ncg-dir> is the path to the directory where ncg_xxx.cpp files are written to.

Running the code generator with argument --help prints this help page.

Example:

> NandradCodeGenerator NANDRAD ~/git/SIM-VICUS/externals/Nandrad/src 0 NANDRAD ncg

or

> NandradCodeGenerator NANDRAD_MODEL ~/git/SIM-VICUS/NandradSolver/src 0 NM ncg

4.2. Keyword List Support

The parse requires fairly consistent code to be recognized, with the following rules. Look at the following example:

class MyClass {
public:


    enum parameterSet {
        PS_PARA1,      // Keyword: PARA1     'some lengthy description'
        PS_PARA2,      // Keyword: PARA2     [K] <#4512FF> {273.15} 'A temperature parameter'
        NUM_PS
    }

    enum otherPara_t {
        OP_P1,         // Keyword: P1
        OP_P2,         // Keyword: P2
        OP_P3,         // Keyword: P3
        NUM_OP
    }
...
}

Here are the rules/conventions (how the parser operates):

  • a class scope is recognized by a string class xxxx (same line)

  • an enum scope is recognized by a string enum yyyy (same line)

  • a keyword specification is recognized by the string // Keyword: (with space between // and Keyword:!)

  • either all enumeration values (except the line with NUM_XXX) must have a keyword specification, or none (the keyword spec is used to increment the enum counter)

  • you must not assign a value to the enumeration like MY_ENUM = 15, - the parser does not support this format. With proper scoping, you won’t need such assignments for parameter lists.

The parser isn’t a c++ parser and does not know about comments. If the strings mentioned above are found inside a comment, the parser will not know the difference. As a consequence, the following code will confuse the parser and generate wrong keyword categories:

class MyClass {
public:

    /* Inside this
       class my stuff will work
       perfectly!
    */

    enum para_t {
    ...
    }
...
}

This will generate the keyword category my::para_t because class my is recognized as class scope. So, do not do this! Same applies to enum documentation.

Thankfully, documentation is to be placed above the class/enum declaration lines and should not interfere with the parsing.

When using class forward declarations, always put only the class declaration on a single line without comments afterwards:

// forward declarations
class OtherClass;
class OtherParentClass;
class YetAnotherClass;

The parser will detect forward declarations when the line is ended with a ; character. Again, this should normally not be an issue, unless someone uses a forward declaration of a class inside a class scope.

4.3. Keyword Parameters

A keyword specification line has the following format:

KW_ENUM_VALUE,  // Keyword:   Keyword-Name  [unit]  <color>  {default value} 'description'

The Keyword-Name can be actually a list of white-space separated keywords that are used to convert to the enumeration value: for example:

SP_HEATCONDCOEFF, // Keyword: HEATCONDCOEFF ALPHA [W/m2K] 'Heat conduction coefficient'

Allows to convert strings HEATCONDCOEFF and ALPHA to enum value SP_HEATCONDCOEFF, but conversion from SP_HEATCONDCOEFF to string always yields the first keyword HEATCONDCOEFF in the list.

The remaining parameters unit, color, default value and description are optional. But if present, they must appear in the order shown above. This is just to avoid nesting problems and is strictly only required from the description, since this may potentially contain the characters <>[]{}.

The default value must be a floating point number in C locale format. Similarly as color and unit, this parameter is meaningful for user interfaces with somewhat generic parameter input handling.

4.4. XML Serialization

In order for the CodeGenerator to work correct, we need a few lots of conventions:

  • only one class per file

  • only member variables with // XML:A or // XML:E annotations are written/read (code generated for them)

  • all member variables must be prefixed m_

  • only the types used in the following test class are currently supported. Complex types with own readXML() and writeXML() functions are always supported (see section Reading/writing custom complex types)

Example class with different types currently supported by code generator
class SerializationTest {
public:

	NANDRAD_READWRITE

	enum test_t {
		t_x1,												// Keyword: X1
		t_x2,												// Keyword: X2
		NUM_test
	};

	enum intPara_t {
		IP_i1,												// Keyword: I1
		IP_i2,												// Keyword: I2
		NUM_IP
	};

	enum splinePara_t {
		SP_ParameterSet1,									// Keyword: ParameterSet1
		SP_ParameterSet2,									// Keyword: ParameterSet2
		NUM_SP
	};


	enum ReferencedIDTypes {
		SomeStove,											// Keyword: SomeStove
		SomeOven,											// Keyword: SomeOven
		SomeHeater,											// Keyword: SomeHeater
		SomeFurnace,										// Keyword: SomeFurnace
		NUM_RefID
	};

	// -> id1="5"
	int					m_id1		= 5;					// XML:A:required
	// -> id2="10"
	unsigned int		m_id2		= 10;					// XML:A
	// -> flag1="0"
	bool				m_flag1		= false;				// XML:A
	// -> val1="42.42"
	double				m_val1		= 42.42;				// XML:A
	// -> testBla="X1"
	test_t				m_testBla	= t_x1;					// XML:A
	// -> str1="Blubb"
	std::string			m_str1		= "Blubb";				// XML:A
	// -> path1="/tmp"
	IBK::Path			m_path1		= IBK::Path("/tmp");	// XML:A
	// -> u1="K"
	IBK::Unit			m_u1		= IBK::Unit("K");		// XML:A

	// -> <Id3>10</Id3>
	int					m_id3		= 10;					// XML:E:required
	// -> <Id4>12</Id4>
	unsigned int		m_id4		= 12;					// XML:E
	// -> <Flag2>1</Flag2>
	bool				m_flag2		= true;					// XML:E
	// -> <Val2>41.41</Val2>
	double				m_val2		= 41.41;				// XML:E
	// -> <TestBlo>X2</TestBlo>
	test_t				m_testBlo	= t_x2;					// XML:E
	// -> <Str2>blabb</Str2>
	std::string			m_str2		= "blabb";				// XML:E

	// -> <Path2>/var</Path2>
	IBK::Path			m_path2		= IBK::Path("/var");	// XML:E
	// -> undefined/empty - not written
	IBK::Path			m_path22;							// XML:E

	// -> <U2>C</U2>
	IBK::Unit			m_u2		= IBK::Unit("C");		// XML:E
	// -> <X5>43.43</X5>
	double				m_x5		= 43.43;				// XML:E

	// -> <IBK:Flag name="F">true</IBK:Flag>  -> value of m_f.name is ignored
	IBK::Flag			m_f;								// XML:E
	// -> undefined/empty - not written
	IBK::Flag			m_f2;								// XML:E

	// -> <Time1>01.01.07 12:47:12</Time1>
	IBK::Time			m_time1;							// XML:E
	// -> undefined/empty - not written
	IBK::Time			m_time2;							// XML:E

	// -> <Table>Col1:1,5,3;Col2:7,2,2;</Table>
	DataTable			m_table;							// XML:E
	// -> undefined/empty - not written
	DataTable			m_table2;							// XML:E

	// -> 		<DblVec>0,12,24</DblVec>
	std::vector<double>		m_dblVec;						// XML:E

	// -> <Interfaces>...</Interfaces>
	std::vector<Interface>	m_interfaces;					// XML:E

	// -> <InterfaceA>....</InterfaceA>  instead of <Interface>..</Interface>
	Interface				m_interfaceA;					// XML:E:tag=InterfaceA

	// -> <IBK:Parameter name="SinglePara" unit="C">20</IBK:Parameter>
	IBK::Parameter		m_singlePara;						// XML:E

	// -> <IBK:IntPara name="SingleIntegerPara">12</IBK:IntPara>
	IBK::IntPara		m_singleIntegerPara = IBK::IntPara("blubb",12);	// XML:E

	// -> <IBK:Parameter name="X1" unit="C">12</IBK:Parameter>
	IBK::Parameter		m_para[NUM_test];					// XML:E

	// -> <IBK:IntPara name="I1">13</IBK:IntPara>
	IBK::IntPara		m_intPara[NUM_IP];					// XML:E

	// -> <IBK:Flag name="X2">true</IBK:Flag>
	IBK::Flag			m_flags[NUM_test];					// XML:E

	IDType				m_someStuffIDAsAttrib;				// XML:A
	IDType				m_someStuffIDAsElement;				// XML:E

	// -> <SomeStove>231</SomeStove> : Keywords must be unique!
	IDType				m_idReferences[NUM_RefID];	        // XML:E

	// -> <IBK:LinearSpline name="LinSpl">...</IBK:LinearSpline>
	IBK::LinearSpline	m_linSpl;							// XML:E

	// -> <LinearSplineParameter name="SplineParameter">...</LinearSplineParameter>
	NANDRAD::LinearSplineParameter	m_splineParameter;		// XML:E
	// -> <LinearSplineParameter name="AnotherSplineParameter">...</LinearSplineParameter>
	LinearSplineParameter			m_anotherSplineParameter;	// XML:E

	// -> <LinearSplineParameter name="ParameterSet1">...</LinearSplineParameter>
	NANDRAD::LinearSplineParameter m_splinePara[NUM_SP];	// XML:E

	// generic class with own readXML() and writeXML() function
	// -> <Schedule...>...</Schedule>
	Schedule			m_sched;							// XML:E

	// generic class with custom tag name
	// -> <OtherSchedule...>...</OtherSchedule>
	Schedule			m_sched2;							// XML:E:tag=OtherSchedule
};

The following conventions are used when composing the XML content:

  1. parent XML-Element name is always the same as the class name, so in the example above the xml-tag is SerializationTest.

  2. child tag names are composed of the capitalized variable name without m_ prefix, so m_testParameter becomes TestParameter

  3. attribute names are composed of the variable name without m_ prefix, so m_flagFive becomes attribute flagFive

  4. for vector quantities (for example std::vector<Interface> m_interfaces, the variable name is used to generate the list-type XML tag, here Interfaces (again just by capitalizing the variable name string). Inside the list the actual members are written, hereby calling writeXML() in the child elements (Interface::writeXML() in the example above)

  5. static arrays are supported, but only with enumeration index where the enum is parametrized with keyword list and NUM_xxx enumeration value as last enum value. The xml-tags are named as the keywords for the corresponding enum type).

  6. empty/undefined values are typically not written, for example when objects contain an empty m_name member variable

The following XML-output is generated from the class declaration above (with some test data):

<?xml version="1.0" encoding="UTF-8" ?>
<NandradProject>
	<SerializationTest id1="5" id2="10" flag1="0" val1="42.42" testBla="X1" str1="Blubb" path1="/tmp" u1="K">
		<Id3>10</Id3>
		<Id4>12</Id4>
		<Flag2>1</Flag2>
		<Val2>41.41</Val2>
		<TestBlo>X2</TestBlo>
		<Str2>blabb</Str2>
		<Path2>/var</Path2>
		<U2>C</U2>
		<X5>43.43</X5>
		<IBK:Flag name="F">true</IBK:Flag>
		<Time1>01.01.07 12:47:12</Time1>
		<Table>Col1:1,5,3;Col2:7,2,2;</Table>
		<DblVec>0,12,24</DblVec>
		<Interfaces>
			<Interface id="1" zoneId="0">

			</Interface>
		</Interfaces>
		<IBK:Parameter name="SinglePara" unit="C">20</IBK:Parameter>
		<IBK:IntPara name="SingleIntegerPara">12</IBK:IntPara>
		<IBK:Parameter name="X1" unit="C">12</IBK:Parameter>
		<IBK:IntPara name="I1">13</IBK:IntPara>
		<IBK:IntPara name="I2">15</IBK:IntPara>
		<IBK:Flag name="X2">true</IBK:Flag>
		<IBK:LinearSpline name="LinSpl">
			<X unit="-">0 1 1.4 2 </X>
			<Y unit="-">1 2 3.4 5 </Y>
		</IBK:LinearSpline>
		<LinearSplineParameter name="SplineParameter">
			<X unit="m">0 5 10 </X>
			<Y unit="C">5 4 3 </Y>
		</LinearSplineParameter>
		<LinearSplineParameter name="AnotherSplineParameter">
			<X unit="m">0 5 10 </X>
			<Y unit="C">5 4 3 </Y>
		</LinearSplineParameter>
		<LinearSplineParameter name="ParameterSet1">
			<X unit="m">0 5 10 </X>
			<Y unit="C">5 4 3 </Y>
		</LinearSplineParameter>
		<Schedule type="Friday">
			<StartDayOfTheYear>0</StartDayOfTheYear>
			<EndDayOfTheYear>0</EndDayOfTheYear>
			<DailyCycles>
				<DailyCycle />
			</DailyCycles>
		</Schedule>
		<OtherSchedule type="Friday">
			<StartDayOfTheYear>0</StartDayOfTheYear>
			<EndDayOfTheYear>0</EndDayOfTheYear>
			<DailyCycles>
				<DailyCycle />
			</DailyCycles>
		</OtherSchedule>
	</SerializationTest>
</NandradProject>

When writing custom types like Schedule in the example above, you must only have one object declared as member variable, since the xml-tag is generated based on the variable type name. This is due to the fact, that the code generator currently just calls writeXML() inside such complex types and these classes (currently) set the child xml tag name to the class name. In the example above, the class name is Schedule and hence the xml-tag is named Schedule and not Sched as it would be according to the standard naming rules.

For types IBK::Parameter, IBK::IntPara, IBK::LinearSpline and IBK::Flag the name must be set exactly to the name of the generated xml-tag name. So, a parameter with member variable m_transferCoefficient must be given the name TransferCoefficient. In case of static arrays, where the enumeration value determines keyword and thus xml-tag, the name is ignored.

The code generator creates additional code to prevent writing of undefined data:

  • IBK::Parameter, IBK::IntPara and IBK::Flag with empty name are not written

  • enumeration values where the value matches the corresponding NUM_xxx value are not written

  • IBK::Time with invalid time/date are not written

  • empty strings/paths are not written

  • undefined units (id=0) are not written

4.4.1. IDType variables

The variable type IDType is actually a typedef for unsigned int and should be used only for ID references. The typdef is declared in NANDRAD_CodeGenMacros.h. When the IDType is used for scalar variables, it is serialized exactly as unsigned int. However, it can also be used in a C-array with enumeration values, like in the following example:

IDType array variables
class SerializationTest {
public:

	NANDRAD_READWRITE

	enum ReferencedIDTypes {
		SomeStove,											// Keyword: SomeStove
		SomeOven,											// Keyword: SomeOven
		SomeHeater,											// Keyword: SomeHeater
		SomeFurnace,										// Keyword: SomeFurnace
		NUM_RefID
	};

	// -> <SomeStove>231</SomeStove> : Keywords must be unique!
	IDType m_idReferences[NUM_RefID];	                    // XML:E
};

The array size must be defined by an enumeration counter value that matches the size of the enumeration list. Also, the corresponding enumeration must have keywords for the keyword list.

Whether IDType variables are scalar variables or within a c-array, they are only written to XML if their value is not equal to NANDRAD::INVALID_ID.

4.4.2. Reading/writing custom complex types

Any data type not listed in the example above and with // XML:E annotation is treated by the code generator as a complex type with own functions readXML() and writeXML() according to the NANDRAD_READWRITE macro. The code generator with create code to simply call these functions when writing such code.

When reading an XML-file, the tag is compared with the typename of the member variable (Schedule in the example above for member variable m_sched) and if matched, an object of said type is created and the readXML() function is called for this child tag. Then, the variable is assigned to the member variable. Hence, the complex type also requires an assignment operator. This is usually automatically generated, but for classes with pointers or special resource management, you may need to provide this assignment operator in addition to the readXML() and `writeXML() functions.

4.4.3. Reading/writing custom complex types with user-defined tag names

Normally, the tag names for complex types are generated based on the complex type’s class name. For example:

Example for complex data member with automatic tag name generation
Interface m_iface;  // XML:E

will generate an XML-file with:

<Interface id="1" zoneId="0">
	<!--Interface to outside-->
</Interface>

If you would like to use a different tag name, for example to distinguish between different variables of the same complex type, you can use the XML:E:tag=<custom tag name> syntax.

Example for complex data member with custom tag name
Interface m_iface;  // XML:E:tag=MyFancyInterface

will generate an XML-file with:

<MyFancyInterface id="1" zoneId="0">
	<!--Interface to outside-->
</MyFancyInterface>

This feature works only with element tags and custom complex data types.

4.4.4. Handling of uninitialized IDs (= NANDRAD::INVALID_ID)

The code generator automatically inserts code that compares unsigned int parameters with the constant NANDRAD::INVALID_ID. If the variable holds this default value, the variable will not be written.

This avoids writing invalid IDs for optional references.

Unsigned int variables with value NANDRAD::INVALID_ID are expected to identify that an optional parameter is omitted or not provided. Hence, the respective variable should always be initialized with NANDRAD::INVALID_ID in the class header/constructor. When accessing the variable before or after reading the project file, it is possible to check by comparing with the constant if the variable is given or not.

Care has to be taken when an existing optional data member is deactivated by setting its id to NANDRAD::INVALID_ID. All other members should equally be cleared, so that a data member (with potentially mandatory ID) is not being written to file, whereby some regular data members appear in the XML tag, but not the ID. This will work during writing of the project, but fail, when the project is being read in again.

4.4.5. Unsupported data member types

For any kind of special data types, like std::map<std::string, std::vector<double> > you cannot use the code generator to create read/write code for. When you add a read/write annotation to such variables, the code generator will complain about unsupported types and may generate not compiling code.

In such cases you have two options:

  1. create your own readXML() and writeXML() functions (possibly by copy&pasting other generated functions from ncg_* files and adjusting the code to your needs). For other member variables whose types are supported by the code generator, you may still use the code generator, but you must use the NANDRAD_READWRITE_PRIVATE macro (see example in section Custom read/write functionality).

  2. change the type to something different, possibly creating another class with standardized behavior. So, for example, you could store std::map<std::string, std::vector<double> > data in std::vector<NamedDblVector> where NamedDblVector contains a std::string and std::vector<double> members, both of which are fully supported by the code generator. You may need to code the check for duplicate names yourself.

4.4.6. XML Comments

Sometimes, it is nice to add comments about certain data members in the file. These are not xml tags, but merely xml comments and as such have no meaning for the project (only for humans reading the file in the text editor).

Since comments may add quite a bit of text to project files and enlarge these without adding actual data, care should be taken to only add comments when necessary/helpful for manually checking the content of files.

Currently, only one string comment is allowed per class, and it will be written right after the opening tag of the class XML tag. To add a comment, you must create a std::string variable with a XML:C annotation.

For example:

Using a comment annotation on a string variable
class Interface {
public:

    ...

	/*! Comment, indicating the zone this interface links to. */
	std::string									m_comment;				// XML:C

	...
}

Suppose the string m_comment contains the text Interface to 'TF05.1, then the generated XML content will look like:

<Interface id="12" zoneId="1">
	<!--Interface to 'TF05.1'-->
	<InterfaceHeatConduction modelType="Constant">
    ...
</Interface>

You can have multi-line comments, by adding \n in the string, but the indentation in the XML file will be missing in subsequent lines. Generally, don’t do this.

You cannot combine XML:C with any other xml element option.

4.4.7. Serialization of inherited data members

Sometimes a base class holds data members that should be serialized in derived classes. Rather than manually implementing readXML() and writeXML() functions, you can simply copy the variable declaration into the derived class, add the XML:... serialization suffix and then prefix the entire line with //:inherited like in the following example.

Serialization of inherited data members
class Parent {
public:

    int m_value;
};


class Child : public Parent {

    // the following line tells the code generator to generate the serialization code
    // for variable m_value. The C++ compiler ignores this line.

    //:inherited int m_value; // XML:E
};

Basically, the code generator looks for the token //:inherited, removes it from the line and then handles the line just as a regular variable declaration with serialization tag.

4.5. Ignoring additional class declarations in same header file

Use a code comment NO KEYWORDS behind the class to ignore during keyword/serialization parsing to prevent the code generator from analysing this data structure.

Ignoring embedded class declaration
class Bla {
public:

    ....

    // ignore embedded class declaration when
    // generating keyword list/serialization code
    class Blubb { // NO KEYWORDS
    };

}