Moving on from XML? A teaser for a possible alternative
The prominent role of XML throughout Genode has been a recurrent point of critique. Technical pros and cons notwithstanding, syntax is a matter of taste, and XML tastes not favorably to many. Over the past two years, I've secretly pondered over a tasteful alternative, which I'd like to share with you today.
Original requirements
When Christian and me started developing our new operating system in 2006, we were facing a huge mountain of uncertainties, ranging from kernel mechanisms, resource management, inter-component protocol design, over tool-chain and build-system questions, down to nitty-gritty programming details. Regarding system integration and configuration, we eschewed the baroque castles we knew from /etc/ on Linux, and instead longed for consistently using one single format. It should be able to express arbitrary structured information. It had to be simple to implement. It shouldn't raise too many eyebrows. Assuming that nobody was ever fired for using XML, we closed the case and lived happily ever since. Almost.
XML experiences
Code complexity: Our initial parser for the subset of XML we needed had the form of a single header file of only 300 lines of code. Living and breathing the principle of minimal complexity! Over the past 15 years, with the added support for attributes and quoting, the parser doubled in size. But it is tiny still. Bare-bones C++ with no dynamic memory allocation, without using the C++ standard library, and not even depending on a C runtime.
Practical use: In Genode, we use XML largely without ceremony. No namespaces, no XML declaration, no includes. Super powers like xslt play no role at all. XML is mostly edited by hand without special tools, just by using a regular text editor. XML is rarely written from scratch but mostly copied and adjusted. XML schema validation is an omnipresent part of our tooling to catch silly slip-ups early on. That said, XML is not solely written by humans. At Genode's runtime, XML is used as exchange format for propagating state from one component to another. During development and debugging, such XML-formatted states are routinely printed to the log and parsed by the human creature observing the log.
Inconveniences: For the past 18 years, XML served our purpose very well. Yet, even among best friends, not everything is always rosy.
-
The rather noisy syntax hides information behind many symbols (<, >, =, "). Even after years of daily exposure, looking through the syntax has not become a second nature.
-
Even though XML is whitespace-agnostic, in practice, we find proper indentation inevitable for human consumption. Hence, throughout Genode, one finds that all XML, whether generated or written by hand, is accurately indented.
-
When citing configuration snippets in textual documentation, like the Genode books or blog postings, the syntax stands out as tainting even simple concepts as rather technical and complicated. It not attractive for teaching material.
-
Quoting rules can get in the way, in particular when embedding XML as good-case test patterns in automated tests. Talking about XML using XML needs a healthy degree of goodwill.
-
Tools like xpath are powerful yet complicated. When not using the power, what remains is that it's complicated.
-
When using the Genode-based Sculpt OS, one can change most aspects of the system live by editing XML. For interactive use, the commenting-out of sub nodes requires many text-editing steps that diminish the interactive experience. The nesting of comments is unfortunately not possible either.
Invaluables: To followings aspects of XML are most appreciated.
-
The completeness of XML-formatted data is protected by the end tag. Unexpected truncation of data is always detected.
-
Speaking of tags, end tags provide a good sense of scope and orientation. Unlike Python's syntax or YAML, there are not tree branches dangling in the air.
-
XML schema definitions and xmllint catch errors early.
-
Nested nodes and their attributes are all we ever needed. Comments are a given. We never encountered any limitation where XML blocked our way.
-
With less than 1000 lines of code, the C++ implementation of a parser and a generator are relatively simple.
Verdict: Seasoned users certainly appreciate XML for what it is from a purely utilitarian perspective. But it does not evoke any form of fondness. Compared to Unix that is rightfully admired for the powerful and beautifully concise concept of pipes, a system that confronts the user with XML at every step may be tolerated, maybe even appreciated but unlikely loved. In fact, we are regularly confronted with the distaste for XML when introducing Sculpt OS to new users. Even Wikipedia lists XML as the bitter pill to swallow when considering Genode.
When we started out, we went for XML to make no mistake. A mistake we didn't make. Back then, we did not yet know the usage patterns and requirements that would emerge over the years. But now, with the power of hindsight attained, the perspective is no longer the same. We know exactly what we need.
Needs and goals
-
The less syntax, the better.
-
The expressed information should be identical to XML: A hierarchy of nodes where each node has a type and can have any number of attributes. Each attribute has a distinct tag name. Conversion from and to (our used subset of) XML retains all information.
-
Human-friendliness: Regardless of whether information is hierarchical or tabular, it should look and read natural.
-
Domain-specific-language-friendliness: Quoting rules should not stand in the way to extending the text format by embedding different languages.
-
Simple to parse and generate.
-
Support for "literate configuration" where documentation and data can appear side by side.
-
The benefits of XML should be preserved. Detection of truncated content, strong sense of scope, offline validation.
-
A syntax that can be used as a simple query language when used on a single line.
-
Pleasant interactive modification like disabling/enabling whole sub structures.
Introducing Human Readable Data (HRD)
For a quick first glance, let's have a peek at the Sculpt OS mixer configuration as XML on the left and the proposed new syntax on the right.
Still with me? Thanks for reading on. In the following, I will refer to the new syntax as "Human-Readable Data" (HRD). To go into more detail, let's take the configuration of the virtual network router of Sculpt OS as starting point:
<config verbose_domain_state="yes"> <report interval_sec="5" bytes="yes" config="yes" config_triggers="yes"/> <default-policy domain="default"/> <policy label_prefix="vbox" domain="vm"/> <policy label_prefix="wifi -> " domain="uplink"/> <domain name="uplink"> <nat domain="vm" tcp-ports="1000" udp-ports="1000" icmp-ids="1000"/> <udp-forward port="69" domain="vm" to="10.0.1.2"/> <tcp-forward port="2209" domain="vm" to="10.0.1.2"/> </domain> <domain name="vm" interface="10.0.1.1/24"> <dhcp-server ip_first="10.0.1.2" ip_last="10.0.1.200" dns_config_from="uplink"/> <tcp dst="0.0.0.0/0"> <permit-any domain="uplink"/> </tcp> <udp dst="0.0.0.0/0"> <permit-any domain="uplink"/> </udp> <icmp dst="0.0.0.0/0" domain="uplink"/> </domain> </config>
In HRD syntax, the same information is conveyed as follows:
config verbose_domain_state: yes + report interval_sec: 5 bytes: yes config: yes config_triggers: yes + default-policy domain: default + policy label_prefix: vbox | domain: vm + policy label_prefix: wifi -> | domain: uplink + domain uplink + nat domain: vm | tcp-ports: 1000 | udp-ports: 1000 | icmp-ids: 1000 + udp-forward port: 69 | domain: vm | to: 10.0.1.2 + tcp-forward port: 2209 | domain: vm | to: 10.0.1.2 + domain vm interface: 10.0.1.1/24 + dhcp-server ip_first: 10.0.1.2 | ip_last: 10.0.1.200 | dns_config_from: uplink + tcp dst: 0.0.0.0/0 | + permit-any | domain: uplink + udp dst: 0.0.0.0/0 | + permit-any | domain: uplink + icmp dst: 0.0.0.0/0 | | domain: uplink -
Let's dissect a little what we are seeing here.
-
HRD data starts with the type name of the top-level node
-
End of data marked by - at a separate line
-
The start of a line defines the role of the line.
-
The structure is defined by indentation. Each node-indentation level is two spaces.
A + at the start of a line defines a new sub node. The + is followed by the node type and an optional name attribute, separated by space:
config + domain uplink -
Nodes can be nested. A sub node is indented by two spaces.
config + domain uplink + nat -
Nodes can have attributes, separated by pipe characters. Multiple attributes can appear at the same line or at subsequent lines. Each attribute has the form tag: value, like on a form:
config + domain uplink + nat domain: vm tcp-ports: 1000 | udp-ports: 1000 | icmp-ids: 1000 -
Two spaces of indentation can optionally be replaced by a pipe character followed by a space to convey a sense of scope, if desired:
config + domain uplink + nat domain: vm | tcp-ports: 1000 | udp-ports: 1000 | icmp-ids: 1000 + udp-forward port: 69 | domain: vm | to: 10.0.1.2 + tcp-forward port: 2209 | domain: vm | to: 10.0.1.2 -
Lines starting with a dot are comments. By indenting those lines, the dots visually continue the sense of scope. This way, inline documentation nicely follows the flow.
config + domain uplink + nat domain: vm . . The following values limit the amount . of memory used for keeping connection . states. . | tcp-ports: 1000 | udp-ports: 1000 | icmp-ids: 1000 + udp-forward port: 69 | domain: vm | to: 10.0.1.2 + tcp-forward port: 2209 | domain: vm | to: 10.0.1.2 -
For embedding raw content, arbitrary printable characters can follow a : line prefix until the end of the line. No quoting is needed. This enables the embedding of arbitrary domain-specific languages. In the ROM-filter example below, the inline node contains raw XML syntax stated plain and clearly:
config + input name: leitzentrale_enabled | rom: leitzentrale | node: leitzentrale + attribute name: enabled + output node: config + inline | : <parent-provides> | : <service name="Gui"/> | : <service name="Timer"/> | : <service name="File_system"/> | : </parent-provides> | : | : <default-route> <any-service> <parent/> </any-service> </default-route> | : | : <default caps="100"/> + if + has_value input: leitzentrale_enabled | value: yes + then | + inline | : <start name="fader"> | : <config initial_fade_in_steps="100" fade_in_steps="20" alpha="210"/> | : </start> + else + inline : <start name="fader"> : <config fade_out_steps="30" alpha="0"/> : </start> -
A pipe character appends the information that follows to the current node. This is useful for presenting routing rules in a tabular way. You can think of a | as the start of a new line but with the current indentation retained. So a + can follow a | to attach a sub node to the current node. Or a : can follow a | to supply the remainder of the line as raw data. In the following example, the child nodes are sub nodes of their corresponding service nodes.
start + route + service File_system | + child vfs + service ROM | label_suffix: .lib.so | + parent + service ROM | label_last: /bin/bash | + child vfs_rom + service ROM | label_prefix: /bin | + child vfs_rom + any-service + parent + any-child -
The ability to put a deep tail of nested nodes on a single line happens to also clear the way for using HRD as a simple querying language (think of xpath). E.g., using a command line tool hrd, the following command would print the names of all start nodes found in Sculpt's current deploy configuration.
$ hrd get 'config | + start | : name' /config/deploy
A sub tree can be commented out at once by replacing its + character by an x, crossing it out. For example, in the following backdrop configuration, the sticks_blue.png image has swiftly been disabled by changing a single character.
config + libc + vfs | + rom genode_logo.png | + rom grid.png | + rom sticks_blue.png + fill color: #223344 x image png: sticks_blue.png | scale: zoom | anchor: bottom_left | alpha: 200 + image png: genode_logo.png anchor: bottom_right alpha: 150 -
That's basically it. Simple. Not because its trivial. But because of two years of digesting inspiration, iteration, and experimentation for making it so. All the while, Christian had been a delightful brain-storming sparring partner. Now, unable to make it any simpler, I'm settled on it. No, being honest, I pretty much love it.
Technicalities
With the proposed line-based approach, a parser can scan each line independently, inferring the meaning of each line by its indentation level and prefix character. The notation relies on no other keywords than the symbols |, +, x, :, ., and -. Since all of these symbols except | play merely a special role when occurring as a line prefix, they require no quoting rules when they appear as attribute values.
A sketch of a grammar may looks like this:
syntax + end | : regexp -$ + type | : regexp [a-z][a-z0-9_-]* + tag | : regexp [a-z][a-z0-9_-]*: + space | : regexp [ ]+ + align | : regexp [ ]* + delim | : regexp [|] + value | : regexp [ ]+[^|]* + anchor | : regexp [+][ ]|x[ ] + remain | : regexp .*$ + indent | : regexp ([|][ ]|[ ][ ])* + comment | : regexp [.][ ].*$ + raw | : regexp [:][ ].*$ + line | : <end> | <indent> <property> | <indent> <node> + name | : [<value>] + node | : <type> <space> <name> <details> | <type> <space> <attr> <details> + subnode | : <anchor> <node> + details | : [<space> <delim> <property>] + property | : <attr> <details> | <subnode> | <comment> | <raw> + attr | : <tag> <value> -
While experimenting with the conversion of real-world XML data taken from a running Sculpt OS back and forth between XML and HRD, I noticed that the HRD representation is usually about 10% to 20% smaller than the XML version. This is arguably not important for our use cases but not bad anyway.
What's next?
Switching out Genode's omni-present XML syntax by another is of course not a light-hearted decision. XML served us well after all. It's a change we may contemplate once, now with the comfort of a much more informed position than when we started out. But this can only be a once-in-a-lifetime change. A new take will be there to stay. Is it a good idea to even contemplate changing a time-tested concept? Isn't that to much friction and risk?
Let's face it, XML is a compromise. We would either need to live with the compromise forever, keeping Genode back from unfolding its true beauty, or we would need to tackle the sweeping change sometime in the future. But then, with a potentially grown adoption of Genode, the costs would be even higher than today. So once convinced of the profound superiority of a new discovered solution, waiting it out would be a waste.
To come to terms, I'd like to pursue the following questions in the near future:
-
How does a Sculpt OS variant using HRD compare to the current state? Would HRD be a perceptible win to end users and developers?
-
How does a HRD parser and generator stack up against XML in terms in implementation complexity?
-
Will our community share my sense of beauty and clarity of the new syntax?
-
What would be a sensible migration path and a comfortable timeline for everyone involved?
Further notes and examples
The short introduction above is of course not an exhaustive description. Below, I've collected additional thoughts and examples in a somewhat random order.
Sculpt's config/event_filter
config + output + chargen + remap | + key KEY_CAPSLOCK | to: KEY_ESC | + key KEY_F12 | to: KEY_DASHBOARD | + key KEY_LEFTMETA | to: KEY_SCREEN | + include rom: numlock.remap | + merge | + accelerate max: 50 | | | sensitivity_percent: 1200 | | | curve: 100 | | + button-scroll | | + input ps2 | | + vertical button: BTN_MIDDLE | speed_percent: -10 | | + horizontal button: BTN_MIDDLE | speed_percent: -10 | + touch-click | | + input touch | + input usb | + input touch | + input sdl + mod1 | + key KEY_LEFTSHIFT | + key KEY_RIGHTSHIFT + mod2 | + key KEY_LEFTCTRL | + key KEY_RIGHTCTRL + mod3 | + key KEY_RIGHTALT + mod4 | + rom capslock + repeat delay_ms: 230 | rate_ms: 40 + include rom: keyboard/en_us + include rom: keyboard/special + policy label: ps2 | input: ps2 + policy label: usb | input: usb + policy label: touch | input: touch + policy label: sdl | input: sdl -
The report-ROM configuration of the static part of Sculpt OS
config | verbose: no + policy label: leitzentrale_config -> leitzentrale | report: global_keys_handler -> leitzentrale + policy label: leitzentrale -> manager -> leitzentrale | report: global_keys_handler -> leitzentrale + policy label: pointer -> hover | report: nitpicker -> hover + policy label: pointer -> xray | report: global_keys_handler -> leitzentrale + policy label: pointer -> shape | report: shape + policy label: clipboard -> focus | report: nitpicker -> focus + policy label: runtime -> capslock | report: global_keys_handler -> capslock + policy label: runtime -> numlock | report: global_keys_handler -> numlock + policy label: numlock_remap_rom -> numlock | report: global_keys_handler -> numlock + policy label: event_filter -> capslock | report: global_keys_handler -> capslock + policy label: runtime -> clicked | report: nitpicker -> clicked + policy label: leitzentrale -> manager -> nitpicker_focus | report: nitpicker -> focus + policy label: leitzentrale -> manager -> nitpicker_hover | report: nitpicker -> hover + policy label: nit_focus -> leitzentrale | report: global_keys_handler -> leitzentrale + policy label: nit_focus -> slides | report: global_keys_handler -> slides + policy label: nit_focus -> hover | report: nitpicker -> hover + policy label: slides_gui_fb_config -> slides | report: global_keys_handler -> slides -
The connectors report given by the display driver
connectors | max_width: 3840 | | max_height: 2160 + merge DP-5 + connector eDP-1 | connected: true | | | width_mm: 280 | | | height_mm: 190 | | | brightness: 68 | + mode 2256x1504 | width: 2256 | | | height: 1504 | | | hz: 60 | | | id: 1 | | | width_mm: 285 | | | height_mm: 190 | | | preferred: true | | | used: true | + mode 2256x1504 | width: 2256 | | height: 1504 | | hz: 48 | | id: 2 | | width_mm: 285 | | height_mm: 190 + connector DP-1 | connected: false + connector DP-2 | connected: false + connector DP-3 | connected: false + connector DP-4 | connected: false + connector DP-5 | connected: true | | | width_mm: 520 | | | height_mm: 320 | + mode 1920x1200 | width: 1920 | | | height: 1200 | | | hz: 60 | | | id: 1 | | | width_mm: 518 | | | height_mm: 324 | | | preferred: true | | | used: true | + mode 1920x1080 | width: 1920 | | | height: 1080 | | | hz: 60 | | | id: 2 | + mode 1600x1200 | width: 1600 | | | height: 1200 | | | hz: 60 | | | id: 3 | + mode 1680x1050 | width: 1680 | | | height: 1050 | | | hz: 60 | | | id: 4 | + mode 1280x1024 | width: 1280 | | | height: 1024 | | | hz: 60 | | | id: 5 | + mode 1280x960 | width: 1280 | | | height: 960 | | | hz: 60 | | | id: 6 | + mode 1024x768 | width: 1024 | | | height: 768 | | | hz: 60 | | | id: 7 | + mode 800x600 | width: 800 | | | height: 600 | | | hz: 60 | | | id: 8 | + mode 640x480 | width: 640 | | | height: 480 | | | hz: 60 | | | id: 9 | + mode 720x400 | width: 720 | | height: 400 | | hz: 70 | | id: 10 + connector DP-6 | connected: false + connector DP-7 | connected: false -
Example of PCI devices reported by the platform driver
devices + device 00:02.0 | type: pci | used: true + io_mem pci_bar: 0 | phys_addr: 0xf2000000 | size: 0x400000 + io_mem pci_bar: 2 | phys_addr: 0xd0000000 | size: 0x10000000 + irq number: 1 + io_port_range pci_bar: 4 | phys_addr: 0x1800 | size: 0x8 + pci-config vendor_id: 0x8086 device_id: 0x46 class: 0x30000 revision: 0x2 sub_vendor_id: 0x17aa sub_device_id: 0x215a intel_gmch_control: 0x0 + device 00:16.0 | type: pci | used: false + io_mem pci_bar: 0 | phys_addr: 0xf2727800 | size: 0x80 + irq number: 2 + pci-config vendor_id: 0x8086 device_id: 0x3b64 class: 0x78000 revision: 0x6 sub_vendor_id: 0x17aa sub_device_id: 0x215f -
VFS configuration used by Sculpt's system shell subsystem
config + vfs + tar bash-minimal.tar + tar coreutils-minimal.tar + tar vim-minimal.tar + dir dev | + zero | + null | + terminal | + inline rtc | : 2018-01-01 00:01 + dir pipe | + pipe + dir rw | + fs label: target + dir report | + fs label: report + dir config | + fs label: config + dir tmp | + ram + dir share | + dir vim | + rom vimrc | binary: no + policy label_prefix: vfs_rom | root: / + default-policy writeable: yes | root: / -
The start node for bash in the system shell
Note the quoted value of the PS1 environment variable, which is used to capture the trailing space.
start /bin/bash | caps: 450 + resource RAM | quantum: 28M + exit propagate: yes + config + libc stdin: /dev/terminal | stdout: /dev/terminal | stderr: /dev/terminal | rtc: /dev/rtc | pipe: /pipe + vfs | + fs + arg value: bash + env key: TERM | value: screen + env key: PATH | value: /bin + env key: PS1 | value: "system:$PWD> " + route + service File_system | + child vfs + service ROM | label_suffix: .lib.so | + parent + service ROM | label_last: /bin/bash | + child vfs_rom + service ROM | label_prefix: /bin | + child vfs_rom + any-service + parent + any-child -
Interactive use of a hrd command-line tool
Control the presence of the falkon web browser. The -w argument instructs the tool to write back the change instead of printing the new version to stdout:
$ hrd -w enable 'config | + start falkon' /config/deploy $ hrd -w disable 'config | + start falkon' /config/deploy $ hrd -w toggle 'config | + start falkon' /config/deploy
Restart the falkon web browser:
$ hrd -w increment 'config | + start falkon | : version' /config/deploy
Add a new subsystem (invocations like this could be found in shell scripts):
$ hrd -w insert 'config' \ ': start fonts_fs | priority: -2' \ ': | pkg: ...' \ ': + route' \ ': + service ROM' \ ': + parent label: config -> managed/fonts' \ /config/deploy
Add a start node based on a launcher file:
$ hrd insert-file 'config | : launcher/vm_fs.hrd | as-node: start | name: vm_fs'
Check the compliance of a HRD against a schema:
$ hrd check --schema-path /config/schema <hrd-file>
Sketch of a schema definition
schema + simple_type boolean + restriction base: string + enum value: true + enum value: yes + enum value: on + enum value: false + enum value: no + enum value: off + simple_type session_label + restriction base: string + min_length value: 0 + max_length value: 160 + complex_type session_policy + attribute label_prefix | type: session_label + attribute label_suffix | type: session_label + attribute label | type: session_label . . -
Considerations
Throughout Genode, name attributes are pervasively used as unique IDs within the scope of one node. Think of connector name, directory name, button name, domain name, start name. Node definitions would normally look like:
+ connector name: HDMI-1 + mode name: 1920x1080 | width: 1920 | height: 1080 | hz: 60
Since the use of name attributes is such a common pattern, HRD handles it as special case where the name can appear directly after the node type.
+ connector HDMI-1 + mode 1920x1080 | width: 1920 | height: 1080 | hz: 60
As the attributes following the name belong to the named node, the formatting at mulitple lines can benefit from the use of leading | characters separating the name from the attributes.
+ connector HDMI-1 + mode 1920x1080 | width: 1920 | height: 1080 | hz: 60