Jump to content

Object serialization (XML) and LabVIEW


Recommended Posts

For reference:

This thread is in part an off-shoot of post #18 in this thread: http://lavag.org/top...oop-data-tools/.

There is however, a thread (and an idea!) here (http://forums.ni.com...m/idi-p/1776294) and the discussion thread there includes links to quite a few related LAVA threads.

Top-level goal:

I will describe what we want to be able to do at the top-level using the attached diagram:

We have an object that we wish to serialize (flatten) [start in the diagram] and send to another system or system component. The other system component needs to process that information, and may itself send us some other serialized object [Enable in the diagram] that we need to deserialize (convert back to an object) for use in our component.

Why do this? This is a common use case in many software engineering applications. Our particular use case is to implement the Command Pattern as a way of sending instructions. For instance, Enable is one object of type Command. The system executes Enable::execute() when Enable is the object on the wire. Similarly, it executes MoveToPosition::execute if the object type is MoveToPosition(). Note that the objects may contain attributes suitable for the command type (e.g., MoveToPosition has an attribute for the demand position). [serialization captures the object state information attribute values, not the all information about the object. In particular, the methods form part of the class definition.]

Basic Implementation:

We serialize and deserialize objects to send between between applications within a component (for instance, between the View application and the Controller application) and between components throughout our system quite effectively. The Flatten To XML and Unflatten From XML methods in the diagram work quite well for this purpose, as do the Flatten To String and Unflatten To String functions on the Data Manipulation palette, with one catch. They only work within LabVIEW, however.

Limitations (the problem):

We need to handle the general case where we may implement one or more of the components [Another Component in the diagram] in some other development environment. The two components must be able to speak the same language across the interface, of course. XML is, I think, the best suited approach for this, but the native LabVIEW Flatten To XML and Unflatten From XML methods will not work for this purpose. The critical problem is that if an object contains default data the flattened XML indicates that the data is default, which is only useful if the reader has access to the original class (in this case in LabVIEW). Another serious problem is that that the flattened XML format is not in a format that it is appropriate for this sort of data interchange. A much better approach is that of Simple XML (which is the approach JKI’s EasyXML takes).

Workaround implemented:

We have actually implemented a working interface between LabVIEW and Java components by writing specific methods on each class to break down the data into pieces JKI’s EasyXML can digest. This works but it is quite cumbersome to provide a functionality that I think is super advantageous for effective implementation of interoperable systems and I suggest, not coincidentally, for the future success of LabVIEW. [Aside: the other critical piece that is missing, I think, is the other ML—UML integration.]

Other workarounds?

We have spent a great deal of time attempting to implement other approaches to serialization (starting in 2009). These have centered on interpreting or transforming the flattened object information (XML or string). We have inevitably run into two road blocks.

  1. The “default” issue mentioned above. There is no reasonable way, to my knowledge, to deal with this issue.
  2. Access to type information (especially to build an object from XML data). In the thread mentioned above, I sought the default value of the class. This is actually not sufficient (at least not in the form we have it). What we need is access to the class data cluster (i.e., be able to assemble a cluster of the proper type and values)--and then be able to write the cluster value to the object. I don’t think this is currently possible.

Possible solutions:

All possible solutions involve some action on NI’s part, as far as I can see.

  1. Allow for a third party developer to implement an XML framework. This requires resolution of both road blocks I just mentioned. (Note that resolution of road block #2 can be problematic from a security point of view, so NI may not want to do this.)
  2. Develop a serialization format suitable for this purpose. This would involve creating a variation of the existing XML framework that already exists (and would be relatively easy to do, I think).

post-6989-0-56043000-1328315070_thumb.pn

Link to comment

Oh man. Paul, the timing of your post is ridiculously relevant for me. A few months back I had discovered a critical flaw in the serialization strategy I've been using for the past few years and have had to rethink things.

We need to handle the general case where we may implement one or more of the components ... in some other development environment. ... The critical problem is that if an object contains default data the flattened XML indicates that the data is default, which is only useful if the reader has access to the original class (in this case in LabVIEW).

In my opinion, this is one of the two fundamental problems with the native serialization mechanism. The other being the lack of extensibility.

With regards to your second roadblock:

Access to type information (especially to build an object from XML data). In the thread mentioned above, I sought the default value of the class. This is actually not sufficient (at least not in the form we have it). What we need is access to the class data cluster (i.e., be able to assemble a cluster of the proper type and values)--and then be able to write the cluster value to the object. I don’t think this is currently possible.

What you're referring to usually comes along for the ride in a language which has some means of type-reflection. I've mentioned before how I'd love to have it, but I don't think it's necessary for serialization.

I started writing a really long reply to this, but it's late here on the east coast and I've run out of gas. Hopefully I can get the time this weekend to compose what I've pieced together over the last few months if you're interested in my opinion. I haven't gotten around to implementing any of it, so it might be useful to sound off on it.

Link to comment

We have inevitably run into two road blocks.

  1. The “default” issue mentioned above. There is no reasonable way, to my knowledge, to deal with this issue.
  2. Access to type information (especially to build an object from XML data). In the thread mentioned above, I sought the default value of the class. This is actually not sufficient (at least not in the form we have it). What we need is access to the class data cluster (i.e., be able to assemble a cluster of the proper type and values)--and then be able to write the cluster value to the object. I don’t think this is currently possible.

What about using the LVClass references (like here) to determine the default object’s data clusters? AQ says that LVClass references are very slow, but, one could keep a look-up table of the needed information inside the Flattening VI (i.e. each object type needs to have its default info determined only once). I was looking into this stuff a few weeks ago, and it seemed to me that one could get around the “default” issue this way. Then one could make a VI that flattens an object, and then fills in the missing default pieces. One could also make VIs that alter the cluster values this way (I was actually thinking of making a VI that could take objects of different types, such as parent/child, and copy from one the the other all the common levels of cluster data).

I never did any actual tests, so there may be a roadblock, but I think LVClass references can give one the information one needs.

— James

Link to comment

Hopefully I can get the time this weekend to compose what I've pieced together over the last few months if you're interested in my opinion. I haven't gotten around to implementing any of it, so it might be useful to sound off on it.

Yes, of course, I would be most interested in it.

I never did any actual tests, so there may be a roadblock, but I think LVClass references can give one the information one needs.

Yes, I spent a lot of time trying to use the references to work with XML even back in 2009, but they don't give you what you need, at least not in the run-time environment.

It would be very interesting to know what methodology the native Flatten To XML and Unflatten From XML methods use and if it is possible for someone outside NI to do the same.

Link to comment

Yes, I spent a lot of time trying to use the references to work with XML even back in 2009, but they don't give you what you need, at least not in the run-time environment.

I was under the impression (from a post somewhere by AQ) that lvclass references work in run-time as of 2010.

Link to comment

From my understanding, LVClass refnums could not possibly work in run-time environment. A LVClass refnum is a reference to a ProjectItem. The project doesn't exist in run-time so how could this possibly work. Help clearly states for LVClass.Open invoke method 'Available in the LabVIEW Run-Time Engine: No'.

Therefore, the basic information needed to grow our own XML Serialization routine is unavailable in run-time. I think sufficient information would be class version and parent class name. These are available through the flatten-to-string (or xml) methods ONLY if the object is not of default value. I've attempted what drjdpowell suggests above: 'one could make a VI that flattens an object, and then fills in the missing default pieces', but I can't find a way to assemble missing pieces without these vital bits of information.

Edited by mike_nrao
Link to comment

As I see it there are a few bits of information that need to be shared for successful serialization of a data structure.

The Shared Schema

This is the most fundamental part, it is the contract that defines how to interpret what is being transmitted or stored. I'm not even being XML specific here, I mean schema in the most abstract sense-- some shared grammar that must be understood by both the code doing the serialization and that doing the deserialization. Maybe a schema is a literal XML schema, maybe it's a database schema, or maybe it's some obscure binary format.

At the very least, you probably want a schema to be version aware. If your data structure changes from a SGL to a DBL, between version 1.0 and 1.1, that's important to know. The schema might wish to carry along version history as well, but I wouldn't say this is a requirement.

In native code, you can think of each .lvclass as the schema for that class, though it doesn't really serve that purpose in the general sense since it doesn't express that information very well to anything other than LabVIEW.

The Class Identifier and Version

The object identifier is the fundamental unit that identifies what is being serialized. It could be anything so long as any given identifier resolves to a single type. Similarly most objects are going to want to be version aware to allow a given type to evolve over time. The native mechanism I believe uses the qualified class name and a four word (4 x U16) version number.

The Object Data

This is the actual value of what is being transmitted. As has been pointed out, simply saying "default" usually doesn't cut it. Strictly speaking though, if your schema maintains a whole version history and defines the default for each version, it is sufficient, but I find it far easier to always serialize all data simply because maintaining a version history in the schema is hard. LabVIEW can handle the "default" problem because the lvclass file holds the version history, all be it in an obfuscated binary format.

Designing a Serialization API

Let's start with deserialization. What are the fundamental steps in deserialization?

At it's heart, we're going to have some stream of data, and our job is to figure out what that data represents, create an instance of some representative object in our application, then properly transfer all or parts of the serialized data to that instance.

So let's define a base class, serializable.lvclass. In our schema, all objects which are serialized inherit from this class. What do we need to know to be able to create a proper instance of this type when we deserialize it?

Whatever our implementation is, at some level we're going to have to examine this data stream and figure out what to actually instantiate. If you're familiar with design patterns, this is should be a big flashing sign for a factory. The factory's job is to take an identifier in, create a concrete instance of whatever type we have to represent that identifier's schema, and return it as a serializable object.

Once the factory has returned us an instance of a serializable object, we can then call that object's dynamic deserialize.vi method, passing to it the remaining data in the stream. This method's job will be to consume as much of the stream as required by our schema. Internally, you can imagine the next piece of information deserialize will require is a version number, followed by any version specific data. It can then pass the data stream to its parent implementation, which will in turn consume what's required from the stream until the end of the line is reached and the base serializable.lvclass:deserialize.vi implementation.

Meanwhile a similar serializable.lvclass:serialize.vi dynamic method exists. It will write its version number, all data for the class, then pass the stream up the inheritance chain so each level in the hierarchy can get a crack at it. This method shouldn't serialize the class identifier, because that is handled at a scope outside of the serialize/deserialize methods. Recall deserialize.vi is not called until after our logic has consumed the identifier from the stream, so similarly we can expect the class identifier has already been committed to the stream before serialize.vi has been called.

There are some subtleties here I won't get into that really are best exposed in an example, which I definitely can't produce tonight. Quickly though:

  • A dynamic method serializable.lvclass:identifier.vi exists which must be overridden. The responsibility of this method is to return the unique class identifier for a concrete implementation.
  • A static serializeObject.vi method exists which takes a stream, and a serializable object as input. This method will first commit the value returned from identifier.vi to the stream, then pass the stream onto the object's serialize.vi method.
  • Similarly a static deserializeObject.vi method exists which consumes a class identifier from the stream, calls a factory method to get an instance of the appropriate serializable object, then passes the remaining stream onto the object's deserialize.vi method.
  • Since the serialize/deserialize methods are only intended to be called from within the scope of a class hierarchy, or by a static wrapper, I propose the static wrappers be member of serializable.lvclass and the dynamic methods be protected.

This all implies that class identifiers for ancestor classes are never serialized. That is the inheritance of a class is defined in the schema, and becomes hard-coded into a class implementation.

Also implied is versioning is delegated the class implementations as well. Versions can be absent entirely, or partially implemented. For example most of my applications can read most any version for which a schema was defined, but can only write the most recent schema for which the application is aware.

This is a lot of text, but fundamentally quite simple. There's only a single class and a handful of methods. The reason I haven't implemented a clear example so far is there are two "feats" that I think need to be overcome to change this from what's really a design pattern to something with a real utility as a re-use class.

  1. The factory. There are many ways to go about doing this, I haven't settled on how I wish to make it extensible. I have ideas, but I'm tired at this point and this post is already way too long.
  2. The stream. I really want to avoid writing yet another serialization scheme which is hard-coded for a file stream, a TCP/IP stream, a string, or a byte array. I really want to develop an abstraction layer that is fast, such that what the stream is becomes completely transparent to each class being serialized.

  • Like 1
Link to comment

From my understanding, LVClass refnums could not possibly work in run-time environment. A LVClass refnum is a reference to a ProjectItem. The project doesn't exist in run-time so how could this possibly work. Help clearly states for LVClass.Open invoke method 'Available in the LabVIEW Run-Time Engine: No'.

Therefore, the basic information needed to grow our own XML Serialization routine is unavailable in run-time. I think sufficient information would be class version and parent class name. These are available through the flatten-to-string (or xml) methods ONLY if the object is not of default value. I've attempted what drjdpowell suggests above: 'one could make a VI that flattens an object, and then fills in the missing default pieces', but I can't find a way to assemble missing pieces without these vital bits of information.

That’s what I thought, but AQ stated:

You must be using an old version of LabVIEW. They started working in RunTime in LV 2010. I would NOT recommend using them -- they are VERY slow, because they aren't meant to be used in production code (they instantiate entire projects behind the scenes because they assume you're getting one of these references in order to do manipulation of VIs into or out of libraries or to query about project layout stuff). They should be used for scripting and tools work, not for the actual work of your application. The only reason that they're in the runtime engine is someone wanted to be able to do reflection of projects from TestStand without needing the full development environment.

Link to comment
As I see it there are a few bits of information that need to be shared for successful serialization of a data structure. ...

I'm not going to pretend I have comprehended the entire post, but everything I digested seems reasonable to me.

I do have one concern, which is that I don't want to have create override methods for each class (especially since the only way I can use "Serializable" in LabVIEW now would be through inheritance and not via a Java-style interface). In particular, right now I can wire any LabVIEW object to a native Flatten To XML method, and the existing framework performs the serialization. It's just the output is not sufficient for sharing with the outside world, unfortunately. I like the API, though! :-)

Link to comment
That’s what I thought, but AQ stated:

Wow! I'd have to see it to believe it. If anyone knows how to get a LvClass refnum to a LabVIEW Class that's loaded in the run-time environment, please post solution here! Properties and invoke methods using LvClass refnum are indeed slow, but I'd be willing to take the hit on first-call then store the serialized default value for use on subsequent calls.

Link to comment
  • 4 weeks later...
  • 3 years later...

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.