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 |
The keyword list parser looks for occurances of exactly the substring |
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 ofEnumeration()
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.
readXML()
and writeXML()
functionsclass 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 |
The two members are written into file as follows:
<Sensor id="12">
<Quantity>Temperature</Quantity>
</Sensor>
The implementation looks as follows:
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.
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 {
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:
#define NANDRAD_READWRITE \
void readXML(const TiXmlElement * element); \
TiXmlElement * writeXML(TiXmlElement * parent) const;
The header is now very short:
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 |
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:
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:
bool operator==(const Sensor & other) const { return !operator!=(other); }
The declaration and the definition of the equality operator can be replaced by a define:
#define NANDRAD_COMP(X) \
bool operator!=(const X & other) const;
So the class declaration becomes:
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
:
#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 {
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
};
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:
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:
#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 {
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 {
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
#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; }
|
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//
andKeyword:
!) -
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:
This will generate the keyword category 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()
andwriteXML()
functions are always supported (see section Reading/writing custom complex types)
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:
-
parent XML-Element name is always the same as the class name, so in the example above the xml-tag is
SerializationTest
. -
child tag names are composed of the capitalized variable name without
m_
prefix, som_testParameter
becomesTestParameter
-
attribute names are composed of the variable name without
m_
prefix, som_flagFive
becomes attributeflagFive
-
for vector quantities (for example
std::vector<Interface> m_interfaces
, the variable name is used to generate the list-type XML tag, hereInterfaces
(again just by capitalizing the variable name string). Inside the list the actual members are written, hereby callingwriteXML()
in the child elements (Interface::writeXML()
in the example above) -
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). -
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 |
For types |
The code generator creates additional code to prevent writing of undefined data:
-
IBK::Parameter
,IBK::IntPara
andIBK::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:
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 |
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:
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.
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 Care has to be taken when an existing optional data member is deactivated by setting its id to |
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:
-
create your own
readXML()
andwriteXML()
functions (possibly by copy&pasting other generated functions fromncg_*
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 theNANDRAD_READWRITE_PRIVATE
macro (see example in section Custom read/write functionality). -
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 instd::vector<NamedDblVector>
whereNamedDblVector
contains astd::string
andstd::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:
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 |
You cannot combine |
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.
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.
class Bla {
public:
....
// ignore embedded class declaration when
// generating keyword list/serialization code
class Blubb { // NO KEYWORDS
};
}