Copyrighrt (c) 2014-2020 UPVI, LLC

Xnodes vs. Polymorphic VIs vs. Malleable VIs vs. Variants

Last Updated: Sunday, 03 January 2021 Published: Saturday, 02 January 2021 Written by Jason D. Sommerville

A question I am frequently asked is "Why do you use Xnodes in the Live HDF5 toolkit?" It's usually followed up with "Couldn't you just use Polymorphic VIs or Mutable VIs?" In short, the answer is "No." In this article, I'm going to explain why.

If you're wondering, "What is an Xnode?" the answer is that it is a mechanism introduced in LabVIEW 8 that allows the creation of a special, scriptable node. One can use LabVIEW scripting to generate LabVIEW code when a node--an xnode--is dropped on the diagram. They allow for the creation of LabVIEW code that, among other things, responds to the wired input and output terminals of the node. For more info, I will point the reader to the LabVIEW Wiki. Xnodes are not and never have been a supported feature of LabVIEW, and so using them is, perhaps, not the best choice for a toolkit that is intended to be stable. But, they are also the only way to achieve one of the main goals in the Live HDF5 toolkit: the toolkit should be able to store any LabVIEW datatype and read it back. This is not completely achievable, (though close, as I will show). It is therefore desirable that the toolkit should inform the programmer at compile-time rather than at run-time if it is unable to handle the datatype. To summarize, we have two objectives:

  1. Provide a node that can properly handle (almost) any LabVIEW datatype
  2. Provide a node that informs the programmer if it is unable to handle the datatype at compile-time

Polymorphic VIs

When presented with this problem, the first tool people try to reach for is Polymorphic VIs. But, these are a non-starter from the beginning. Polys allow one to create a set of VIs that work for a specific list of enumerated types. A good example is the DAQmx Read VI, which has a list of enumerated options which control what datatype is returned. As you can see from the picture below, there are quite a lot of options. But the list can be completely enumerated at the time the DAQ Read VI was designed.

DAQmx Read is a Polymorphic VI

In the case of HDF5, the list of acceptable datatypes cannot be enumerated. If it was only the primitive types we have to deal with--I32, DBL, Complex-128, Strings, etc.--we might have a chance. However, we also have to deal with clusters (which in HDF5 are called "compounds") and arrays. The programmer can write VIs that wish to save out clusters of primitives. (In fact, I do this all of the time to create named fields in HDF5 for data logging purposes. See, for example, the HDF5 Write Cluster Array Data.vi example that is included with Live HDF5.) For instance, consider the dataset with the cluster type as shown below.

An example of a cluster-type (compound) dataset

We cannot possibly enumerate every cluster type that a programmer may create. For this reason at least, polymorphic VIs fail at Objective 1.

Malleable VIs

Starting in LabVIEW 2017, malleable VIs were implemented in LabVIEW. An example of this type of VI is the Remove Duplicates From 1D Array node found on the Array palette. The malleable VI is capable of accepting any input and adjusting its internal code accordingly. Using Type Specialization Structure, different code can be implemented for different datatypes or groups of datatypes. Aha! This seems perfect? Why didn't I use this?

Remove Duplicates From 1D Array is a Malleable VI

This simple answer is that it was not available in 2011 when I started working on Version 1.0 of the Live HDF5 toolkit. It turns out there is another problem preventing their use. Malleable VIs must be inlined. From a compiler perspective, this choice of NI's makes a lot of sense. However, inlined VIs cannot be used recursively. One couldn't, for example, make a "Remove Duplicates From N-D Array" by calling Remove Duplicates From 1D Array recursively. However, Live HDF5 requires recursion in order to generate many datatypes. Why is that? Consider the the same cluster datatype for logging we used in the Polymorphic VI discussion above. In order to generate the HDF5 type, the toolkit must first recognize that it is creating a compound type for the LabVIEW cluster, and then recursively create element types for each of the members of the cluster. In fact, it can be much worse than this. The "pathological" dataset shown below has three levels of datatype recursion, with the lowest level being "I32 VLen array member of a cluster VLen array." (The outer array is not part of the datatype as that array is the dataspace for the dataset.) While I can't think of a good reason to have such a horrible dataset in real life, the toolkit needs to be able to handle any such recursive datatypes. Therefore, Malleable VIs also fail at Objective 1.

A very complicated, very recursive datatype

VIs with Variant Inputs and Outputs

A final option potentially available is the use of the variant datatype. Variants are sort of an envelope for any datatype. Any datatype can be cast to a variant and VIs may be written to support variant datatype inputs and outputs. A good example of this is the OpenG Variant Configuration library. For instance, Read Section Cluster accepts any variant-encapsulated cluster as input and returns the same variant-cluster, populated with data, as an output. In fact, this is exactly the same paradigm that is needed by Live HDF5.

OpenG's Read Section Cluster uses variant inputs and outputs

However, there is a problem with this approach. The programmer would like to know at compile-time whether or not the datatypes wired to the node are acceptable. And there is really no reason why the toolkit provider can't answer that question at compile time. In the example above, the Read Section Cluster VI only accepts as inputs variants which contain clusters, and specific clusters at that. If one were to wire, say, a variant containing an integer, it wouldn't work. Now, by "wouldn't work," what I mean is that it would generate an error at run-time. But the programmer knew, or should have known, at compile time that it wasn't going to work. For the Read Section Cluster VI, it's pretty straightforward for the programmer to tell if a wired datatype will work at runtime. "Is it a flat cluster with only primative elements? Good. Otherwise, try again." However, the decision is much more complicated with Live HDF5. For instance, consider the following two arrays wired to H5Dwrite.

One dataset that works, one that doesn't. What's the difference?

It is probably not immediately obvious to most people what why the first dataset works and the second doesn't. It could be very frustrating to discover this problem at run time, especially since the toolkit developer (me!) knew at compile-time that this would not work.

An added annoyance of using variants for outputs is the need to cast the variant back to strictly-typed data after a call that returns a variant, as shown below. This is both annoying because it's an extra node, and suffers the same problem in that we cannot figure out at compile time if a datatype won't work.

VIs which return variants typically need to have the data cast back to a strict type

Because of these reasons, variant inputs and outputs do not meet Objective 2--informing the developer at compile time of type errors.

Xnodes

And that brings us at last winding back to Xnodes. The Xnode provides a mechanism where the datatype of the wire can be checked at the time it is wired and the code can adapt itself to the datatype of the wire connected. In this sense, it is identical to the behavior of the Malleable VI. However, it has the added advantage that it actually works for the complex, recursive Live HDF5 datatypes. Xnodes also allow a few nifty features that Malleables do not, such as the ability to modify the icon of the node based on the inputs (see the Read and Write Attribute nodes, for instance).

In the final analysis, I don't recommend that anyone head down the Xnode development path unless you have no other option. They're not supported by NI, and for good reason. They are difficult to write correctly, subject to a number of limitations, which are not documented outside of NI, and generally all sorts of annoying. On the other hand, they were the only means available to achieve the objectives I had for Live HDF5, so I'm thankful to all the folks over at the LabVIEW Advanced Virtual Architects Group that made this work possible.

Footnote: Why doesn't that dataset type work?

The reason the first dataset works and the second does not is because the second dataset contains a 2-D array in the datatype. This is not supported by Live HDF5. Arrays inside of datatypes are represented as HDF5 VLens of the element type. VLens must be one-dimensional. The reason the outer array can be 2-D is because this is not part of the datatype, but is interpreted as the dataspace of the dataset. This can be (essentially) any rank one likes.