# libxml++ - An XML Parser for C++

Murray Cumming

    murrayc@murrayc.com

12th September 2004

This is an introduction to libxml2's C++ binding, with simple examples.

## libxml++

libxml++ is a C++ API for the popular libxml2 XML parser, written in C. libxml2 is famous for its high performance and compliance to standard specifications, but its C API is quite difficult even for common tasks.

libxml++ presents a simple C++-like API that can achieve common tasks with less code. Unlike some other C++ parsers, it does not try to avoid the advantages of standard C++ features such as namespaces, STL containers or runtime type identification, and it does not try to conform to standard API specifications meant for Java. Therefore libxml++ requires a fairly modern C++ compiler such as g++ 8 or g++ 9. libxml++ 3.9.1 and later require a C++17-compliant compiler.

But libxml++ was created mainly to fill the need for an API-stable and ABI-stable C++ XML parser which could be used as a shared library dependency by C++ applications that are distributed widely in binary form. That means that installed applications will not break when new versions of libxml++ are installed on a user's computer. Gradual improvement of the libxml++ API is still possible via non-breaking API additions, and new independent versions of the ABI that can be installed in parallel with older versions. These are the general techniques and principles followed by the GNOME project, of which libxml++ is a part.

### Installation

libxml++ is packaged by major Linux and *BSD distributions and can be installed from source on Linux and Windows, using any modern compiler, such as g++ or MSVC++.

For instance, to install libxml++ and its documentation on Debian, use apt-get or synaptic like so:

        # apt-get install libxml++5.0-dev libxml++5.0-doc

To check that you have the libxml++ development packages installed, and that your environment is working properly, try `pkg-config libxml++-5.0 --modversion`.

Links for downloading and more documentation can be found at libxmlplusplus.github.io/libxmlplusplus. libxml++ is licensed under the LGPL, which allows its use via dynamic linking in both open source and closed-source software. The underlying libxml2 library uses the even more generous MIT licence.

### UTF-8 and xmlpp::ustring

The libxml++ API takes, and gives, strings in the UTF-8 Unicode encoding, which can support all known languages and locales. This choice was made because, of the encodings that have this capability, UTF-8 is the most commonly accepted choice. UTF-8 is a multi-byte encoding, meaning that some characters use more than 1 byte. But for compatibility, old-fashioned 7-bit ASCII strings are unchanged when encoded as UTF-8, and UTF-8 strings do not contain null bytes which would cause old code to misjudge the number of bytes. For these reasons, you can store a UTF-8 string in a std::string object. However, the std::string API will operate on that string in terms of bytes, instead of characters.

The libxml++ API indicates when a string should be provided as UTF-8, or will be provided as UTF-8, by using the xmlpp::ustring type alias. However, this is really just a std::string, whose operator[] and size() consider bytes, not characters.

### Compilation and Linking

To use libxml++ in your application, you must tell the compiler where to find the include headers and where to find the libxml++ library. libxml++ provides a pkg-config .pc file to make this easy. For instance, the following command will provide the necessary compiler options: `pkg-config libxml++-5.0 --cflags --libs`

When using autoconf and automake, this is even easier with the PKG_CHECK_MODULES macro in your configure.ac file. For instance:

        PKG_CHECK_MODULES(SOMEAPP, libxml++-5.0 >= 5.0.0)
        AC_SUBST(SOMEAPP_CFLAGS)
        AC_SUBST(SOMEAPP_LIBS)

## Parsers

Like the underlying libxml2 library, libxml++ allows the use of 3 parsers, depending on your needs - the DOM, SAX, and TextReader parsers. The relative advantages and behaviour of these parsers will be explained here.

All of the parsers may parse XML documents directly from disk, a string, or a C++ std::istream. Although the libxml++ API uses only xmlpp::ustring, indicating the UTF-8 encoding, libxml++ can parse documents in any encoding, converting to UTF-8 automatically. This conversion will not lose any information because UTF-8 can represent any locale.

Remember that white space is usually significant in XML documents, so the parsers might provide unexpected text nodes that contain only spaces and new lines. The parser does not know whether you care about these text nodes, but your application may choose to ignore them.

### DOM Parser

The DOM (Document Object Model) parser parses the whole document at once and stores the structure in memory, available via DomParser::get_document(). With methods such as Document::get_root_node() and Node::get_children(), you may then navigate into the hierarchy of XML nodes without restriction, jumping forwards or backwards in the document based on the information that you encounter. Therefore the DOM parser uses a relatively large amount of memory.

You should use C++ RTTI (via `dynamic_cast<>`) to identify the specific node type and to perform actions which are not possible with all node types. For instance, only Elements have attributes. Here is the inheritance hierarchy of node types:

- xmlpp::Node

  - xmlpp::Attribute

    - xmlpp::AttributeDeclaration

    - xmlpp::AttributeNode

  - xmlpp::ContentNode

    - xmlpp::CdataNode

    - xmlpp::CommentNode

    - xmlpp::EntityDeclaration

    - xmlpp::ProcessingInstructionNode

    - xmlpp::TextNode

  - xmlpp::Element

  - xmlpp::EntityReference

  - xmlpp::XIncludeEnd

  - xmlpp::XIncludeStart

All Nodes created by the DOM parser are leaves in the node type tree. For instance, the DOM parser can create TextNodes and Elements, but it does not create objects whose exact type is ContentNode or Node.

Although you may obtain pointers to the Nodes, these Nodes are always owned by their parent Node. In most cases that means that the Node will exist, and your pointer will be valid, as long as the Document instance exists.

There are also several methods which can create new child Nodes. By using these, and one of the Document::write_*() methods, you can use libxml++ to build a new XML document.

#### Example

This example looks in the document for expected elements and then examines them. All these examples are included in the libxml++ source distribution.

Source Code

File: `main.cc`

    #include <libxml++/libxml++.h>
    #include <iostream>
    #include <cstdlib>

    std::ostream& operator<<(std::ostream& o, const std::optional<xmlpp::ustring>& s)
    {
      o << s.value_or("{[(no value)]}");
      return o;
    }

    void print_node(const xmlpp::Node* node, unsigned int indentation = 0)
    {
      const std::string indent(indentation, ' ');
      std::cout << std::endl; //Separate nodes by an empty line.

      const auto nodeContent = dynamic_cast<const xmlpp::ContentNode*>(node);
      const auto nodeText = dynamic_cast<const xmlpp::TextNode*>(node);
      const auto nodeComment = dynamic_cast<const xmlpp::CommentNode*>(node);

      if(nodeText && nodeText->is_white_space()) //Let's ignore the indenting - you don't always want to do this.
        return;

      const auto nodename = node->get_name2();

      if(!nodeText && !nodeComment && nodename) //Let's not say "name: text".
      {
        const auto namespace_prefix = node->get_namespace_prefix2();

        std::cout << indent << "Node name = ";
        if(namespace_prefix)
          std::cout << namespace_prefix << ":";
        std::cout << nodename << std::endl;
      }
      else if(nodeText) //Let's say when it's text. - e.g. let's say what that white space is.
      {
        std::cout << indent << "Text Node" << std::endl;
      }

      //Treat the various node types differently:
      if(nodeText)
      {
        std::cout << indent << "text = \"" << nodeText->get_content2() << "\"" << std::endl;
      }
      else if(nodeComment)
      {
        std::cout << indent << "comment = " << nodeComment->get_content2() << std::endl;
      }
      else if(nodeContent)
      {
        std::cout << indent << "content = " << nodeContent->get_content2() << std::endl;
      }
      else if(auto nodeElement = dynamic_cast<const xmlpp::Element*>(node))
      {
        //A normal Element node:

        //line() works only for ElementNodes.
        std::cout << indent << "     line = " << node->get_line() << std::endl;

        //Print attributes:
        for (const auto& attribute : nodeElement->get_attributes())
        {
          const auto namespace_prefix = attribute->get_namespace_prefix2();

          std::cout << indent << "  Attribute ";
          if(namespace_prefix)
            std::cout << namespace_prefix << ":";
          std::cout << attribute->get_name2() << " = "
                    << attribute->get_value2() << std::endl;
        }

        const auto attribute = nodeElement->get_attribute("title");
        if(attribute)
        {
          std::cout << indent;
          if (dynamic_cast<const xmlpp::AttributeNode*>(attribute))
            std::cout << "AttributeNode ";
          else if (dynamic_cast<const xmlpp::AttributeDeclaration*>(attribute))
            std::cout << "AttributeDeclaration ";
          std::cout << "title = " << attribute->get_value2() << std::endl;
        }
      }

      if(!nodeContent)
      {
        //Recurse through child nodes:
        for(const auto& child : node->get_children())
        {
          print_node(child, indentation + 2); //recursive
        }
      }
    }

    int main(int argc, char* argv[])
    {
      bool validate = false;
      bool set_throw_messages = false;
      bool throw_messages = false;
      bool substitute_entities = true;
      bool include_default_attributes = false;

      int argi = 1;
      while (argc > argi && *argv[argi] == '-') // option
      {
        switch (*(argv[argi]+1))
        {
          case 'v':
            validate = true;
            break;
          case 't':
            set_throw_messages = true;
            throw_messages = true;
            break;
          case 'e':
            set_throw_messages = true;
            throw_messages = false;
            break;
          case 'E':
            substitute_entities = false;
            break;
          case 'a':
            include_default_attributes = true;
            break;
         default:
           std::cout << "Usage: " << argv[0] << " [-v] [-t] [-e] [filename]" << std::endl
                     << "       -v  Validate" << std::endl
                     << "       -t  Throw messages in an exception" << std::endl
                     << "       -e  Write messages to stderr" << std::endl
                     << "       -E  Do not substitute entities" << std::endl
                     << "       -a  Include default attributes in the node tree" << std::endl;
           return EXIT_FAILURE;
         }
         argi++;
      }
      std::string filepath;
      if(argc > argi)
        filepath = argv[argi]; //Allow the user to specify a different XML file to parse.
      else
        filepath = "example.xml";

      try
      {
        xmlpp::DomParser parser;
        if (validate)
          parser.set_validate();
        if (set_throw_messages)
          parser.set_throw_messages(throw_messages);
        //We can have the text resolved/unescaped automatically.
        parser.set_substitute_entities(substitute_entities);
        parser.set_include_default_attributes(include_default_attributes);
        parser.parse_file(filepath);
        if(parser)
        {
          //Walk the tree:
          const auto pNode = parser.get_document()->get_root_node(); //deleted by DomParser.
          print_node(pNode);
        }
      }
      catch(const std::exception& ex)
      {
        std::cerr << "Exception caught: " << ex.what() << std::endl;
        return EXIT_FAILURE;
      }

      return EXIT_SUCCESS;
    }

### SAX Parser

The SAX (Simple API for XML) parser presents each node of the XML document in sequence. So when you process one node, you must have already stored information about any relevant previous nodes, and you have no information at that time about subsequent nodes. The SAX parser uses less memory than the DOM parser and it is a suitable abstraction for documents that can be processed sequentially rather than as a whole.

By using the `parse_chunk()` method instead of for instance `parse_file()`, you can even parse parts of the XML document before you have received the whole document.

As shown in the example, you should derive your own class from SaxParser and override some of the virtual methods. These "handler" methods will be called while the document is parsed.

#### Example

This example shows how the handler methods are called during parsing.

Source Code

File: `myparser.h`

    #ifndef __LIBXMLPP_EXAMPLES_MYPARSER_H
    #define __LIBXMLPP_EXAMPLES_MYPARSER_H

    #include <libxml++/libxml++.h>

    class MySaxParser : public xmlpp::SaxParser
    {
    public:
      MySaxParser();
      ~MySaxParser() override;

    protected:
      //overrides:
      void on_start_document() override;
      void on_end_document() override;
      void on_start_element(const xmlpp::ustring& name,
                                    const AttributeList& properties) override;
      void on_end_element(const xmlpp::ustring& name) override;
      void on_characters(const xmlpp::ustring& characters) override;
      void on_comment(const xmlpp::ustring& text) override;
      void on_warning(const xmlpp::ustring& text) override;
      void on_error(const xmlpp::ustring& text) override;
      void on_fatal_error(const xmlpp::ustring& text) override;
    };

    #endif //__LIBXMLPP_EXAMPLES_MYPARSER_H

File: `main.cc`

    #ifdef HAVE_CONFIG_H
    #include <config.h>
    #endif

    #include <cstdlib>
    #include <cstring> // std::memset()
    #include <fstream>
    #include <iostream>

    #include "myparser.h"

    int
    main(int argc, char* argv[])
    {
      std::string filepath;
      if(argc > 1 )
        filepath = argv[1]; //Allow the user to specify a different XML file to parse.
      else
        filepath = "example.xml";

      // Parse the entire document in one go:
      auto return_code = EXIT_SUCCESS;
      try
      {
        MySaxParser parser;
        parser.set_substitute_entities(true);
        parser.parse_file(filepath);
      }
      catch(const xmlpp::exception& ex)
      {
        std::cerr << "libxml++ exception: " << ex.what() << std::endl;
        return_code = EXIT_FAILURE;
      }

      // Incremental parsing, sometimes useful for network connections:
      try
      {
        std::cout << std::endl << "Incremental SAX Parser:" << std::endl;

        std::ifstream is(filepath.c_str());
        if (!is)
          throw xmlpp::exception("Could not open file " + filepath);

        char buffer[64];
        const size_t buffer_size = sizeof(buffer) / sizeof(char);

        //Parse the file:
        MySaxParser parser;
        parser.set_substitute_entities(true);
        do
        {
          std::memset(buffer, 0, buffer_size);
          is.read(buffer, buffer_size-1);
          if(is.gcount())
          {
            xmlpp::ustring input(buffer, buffer+is.gcount());
            parser.parse_chunk(input);
          }
        }
        while(is);

        parser.finish_chunk_parsing();
      }
      catch(const xmlpp::exception& ex)
      {
        std::cerr << "Incremental parsing, libxml++ exception: " << ex.what() << std::endl;
        return_code = EXIT_FAILURE;
      }

      return return_code;
    }

File: `myparser.cc`

    #include "myparser.h"

    #include <iostream>

    MySaxParser::MySaxParser()
      : xmlpp::SaxParser()
    {
    }

    MySaxParser::~MySaxParser()
    {
    }

    void MySaxParser::on_start_document()
    {
      std::cout << "on_start_document()" << std::endl;
    }

    void MySaxParser::on_end_document()
    {
      std::cout << "on_end_document()" << std::endl;
    }

    void MySaxParser::on_start_element(const xmlpp::ustring& name,
                                       const AttributeList& attributes)
    {
      std::cout << "node name=" << name << std::endl;

      // Print attributes:
      for(const auto& attr_pair : attributes)
      {
        std::cout << "  Attribute name=" <<  attr_pair.name << std::endl;
        std::cout << "    , value= " <<  attr_pair.value << std::endl;
      }
    }

    void MySaxParser::on_end_element(const xmlpp::ustring& /* name */)
    {
      std::cout << "on_end_element()" << std::endl;
    }

    void MySaxParser::on_characters(const xmlpp::ustring& text)
    {
      std::cout << "on_characters(): " << text << std::endl;
    }

    void MySaxParser::on_comment(const xmlpp::ustring& text)
    {
      std::cout << "on_comment(): " << text << std::endl;
    }

    void MySaxParser::on_warning(const xmlpp::ustring& text)
    {
      std::cout << "on_warning(): " << text << std::endl;
    }

    void MySaxParser::on_error(const xmlpp::ustring& text)
    {
      std::cout << "on_error(): " << text << std::endl;
    }

    void MySaxParser::on_fatal_error(const xmlpp::ustring& text)
    {
      std::cout << "on_fatal_error(): " << text << std::endl;
    }

### TextReader Parser

Like the SAX parser, the TextReader parser is suitable for sequential parsing, but instead of implementing handlers for specific parts of the document, it allows you to detect the current node type, process the node accordingly, and skip forward in the document as much as necessary. Unlike the DOM parser, you may not move backwards in the XML document. And unlike the SAX parser, you must not waste time processing nodes that do not interest you.

All methods are on the single parser instance, but their result depends on the current context. For instance, use `read()` to move to the next node, and `move_to_element()` to navigate to child nodes. These methods will return false when no more nodes are available. Then use methods such as `get_name()` and `get_value()` to examine the elements and their attributes.

#### Example

This example examines each node in turn, then moves to the next node.

Source Code

File: `main.cc`

    #include <libxml++/libxml++.h>
    #include <cstdlib>
    #include <iostream>

    struct indent {
      int depth_;
      explicit indent(int depth): depth_(depth) {};
    };

    std::ostream & operator<<(std::ostream & o, indent const & in)
    {
      for(int i = 0; i != in.depth_; ++i)
      {
        o << "  ";
      }
      return o;
    }

    std::ostream& operator<<(std::ostream& o, const std::optional<xmlpp::ustring>& s)
    {
      o << s.value_or("{[(no value)]}");
      return o;
    }

    int main(int /* argc */, char** /* argv */)
    {
      try
      {
        xmlpp::TextReader reader("example.xml");

        while(reader.read())
        {
          int depth = reader.get_depth();
          std::cout << indent(depth) << "--- node ---" << std::endl;
          std::cout << indent(depth) << "name: " << reader.get_name2() << std::endl;
          std::cout << indent(depth) << "depth: " << reader.get_depth() << std::endl;

          if(reader.has_attributes())
          {
            std::cout << indent(depth) << "attributes: " << std::endl;
            std::cout << indent(depth) << "attribute 0: " << reader.get_attribute2(0) << std::endl;
            std::cout << indent(depth) << "attribute 9: " << reader.get_attribute2(9) << std::endl;
            reader.move_to_first_attribute();
            do
            {
              std::cout << indent(depth) << "  " << reader.get_name2() << ": " << reader.get_value2() << std::endl;
            } while(reader.move_to_next_attribute());
            reader.move_to_element();
          }
          else
          {
            std::cout << indent(depth) << "no attributes" << std::endl;
          }

          std::cout << indent(depth) << "value: '" << reader.get_value2() << "'" << std::endl;
        }
      }
      catch(const std::exception& e)
      {
        std::cerr << "Exception caught: " << e.what() << std::endl;
        return EXIT_FAILURE;
      }
      return EXIT_SUCCESS;
    }