LabVIEW Encapsulation - Part I
It has been a while since my last post. LabVIEW 8.0 deadlines sure have a way of shuffling priorities. Now that things are released, I am happy to announce my return.
Perhaps the biggest complaint I hear from newbie LabVIEW developers is that LabVIEW code is hard to look at. They say something along the lines of "I don't like LabVIEW because the code looks like spaghetti." I am a little amazed how people can make statements like this. It's true that I don't think LabVIEW is the best solution for every problem out there, but it is hardly fair to discredit a programming language because of bad programming heuristics.
First and foremost, I have a confession to make. Yes, I have written spaghetti block diagrams in the past and yes, I do believe that some of my early G code (LabVIEW code) should be destroyed and sent back to pits of hell from where it came. But then again, looking back at my first C++ program I wrote back in high school, I can understand why. Unless you are a gifted person, your first ANYTHING is going to be sub-par. How many times have you seen undocumented functions or methods? How many times have you seen a method that is over 500 lines long? How many times have you seen ugly code that just won't die? LabVIEW is a programming language. It's the developer's responsibility to make sure they can code correctly in that language. It's not right to blame LabVIEW for the horrible coding standards that others programmers follow.
So what is the correct way of programming in LabVIEW?
Advanced text-based programming languages yield three important features: encapsulation, inheritance, and polymorphism. Arguably, the most important feature is encapsulation. Why? Without encapsulation, your application will have difficulties materializing due to scalability issues. Code quickly becomes unmaintainable because bugs cannot be barricaded. Don't get me wrong, inheritance and polymorphism are important. But polymorphism and inheritance without encapsulation would yield a still-born programming language.
Have you ever noticed how programming in LabVIEW is a lot like programming in C? A LabVIEW strict typedef is a lot like a C-struct. They both wrap (or encapsulate) data. Nice! A LabVIEW VI is a lot like a C function. The input terminals of the VI are like function parameters and the output terminals are like return results. If the first terminal of the VI (or the first parameter of the C function) is the LabVIEW typedef (the C struct), then we would have the equivalent of a C++ class (without the inheritance, polymorphism or access protection of course).
As an example, let's create a C class called Rect. Now obviously classes are not universally supported in C, so we need a way to fake a class in C. Luckily, this is not hard because a class is simply a definition of data members and the methods to manipulate those members. Ignoring inheritance, polymorphism, access
modifiers, operators, constructors, destructors (and so on) for a moment, you can (very roughly) simulate a C++ class in C using a struct and writing functions where each function's first parameter is the this parameter. Neat! Leaving the implementation out (to avoid clutter), the Rect "class" can look like this:
typedef struct rect { int left; int top; int right; int bottom; } Rect; /******************************************************************/ /* Initializes the specified Rect instance with the specified left, * top, right, bottom parameters */ void Rect_Init(Rect* this, int left, int top, int right, int bottom) /* Returns the width of the specified Rect instance */ int Rect_GetWidth(Rect* this) /* Returns the heigth of the specified Rect instance */ int Rect_GetHeight(Rect* this) /* Moves the specified Rect instance 'x' units vertically and 'y' * units horizontally. */ void Rect_Move(Rect* this, int x, int y) /* Returns the union of the specified Rect instance with 'withRect'. * Modifies 'this' instance.*/ void Rect_Union(Rect* this, Rect* withRect) /******************************************************************/ void main() { Rect screenRect; Rect windowRect; /* Initialize a rectangles. This is the equivalent to * a constructor */ Rect_Init(&screenRect, 0, 0, 800, 600); Rect_Init(&windowRect, 0, 0, 100, 100); /* Let's pretend the user has requested to move our * window rect to position 200, 200 on the screen */ Rect_Move(&windowRect, 200, 200); assert(Rect_GetWidth(&screenRect) == 800); assert(Rect_GetHeigth(&screenRect) == 600); assert(Rect_GetWidth(&windowRect) == 100); assert(Rect_GetHeigth(&windowRect) == 100); }
To translate the above C "class" into LabVIEW, we would define a typedef control to replace the C struct, and one VI for each function for that struct (five VIs altogether because there are five C functions). The parameters for the C functions would correspond to the input and output terminals for the VI. Because the first parameter in the function list is always going to be the this pointer, this parameter would also be the first input and output terminal for the corresponding LabVIEW VI.
The naming convention I used is simple. I named my VIs xxxx_yyyy.vi, where xxxx is the class name (in this case Rect) and yyyy is the method name (in this case Move). I used this convention to alleviate collision between VI names, which is a big no-no up until LabVIEW 8.0. Each class has a this pointer so in LabVIEW, we must write a typedef named xxxx.ctl, where xxxx is the class name (in this case Rect.ctl). Separating my logic into VIs is much like separating a C/C++ function into sub-functions. It makes code easier to read, use, and understand.
Rect.ctl is a LabVIEW typedef which will replace the C struct. Since this VI contains all data members, it will be passed into each VI like a this function parameter.
|
|
Rect_Init.vi acts like a constructor. All classes need a way to initialize their data members. Callers would use this VI to get a new rect object.
|
|
Rect_GetHeight.vi returns the height of the specified rect instance. Even though it is a simple calculation, it redocudes celler code pollution.
|
|
Rect_GetWidth.vi returns the width of the specified rect instance. Even though it is a simple calculation, leaving this calculation to the caller reduces code pollution.
|
|
Rect_Move.vi moves the rect instance to the specified x-y coordinates.
|
|
Rect_Union.vi modifies the rect instance by taking the union of itself and the passed-in rect.
|
|
| Simple example demonstrating cleaner code. |
|
So, is the above form of encapsulation for everyone? Unfortunately, no. The techniques described above have one serious downfall. As data structures increase in size (typedef that contain other typedefs, which contain other typedefs), performance takes a serious hit. Why? Don't forget that the LabVIEW typedef acts like the this parameter so it must be added as a terminal to each VI. Each time we call into a method VI (like Rect_Move.vi), LabVIEW needs to copy all the information inside Rect.ctl. For significantly large LabVIEW typedefs, performance starts to slide. Another major downfall is that each method VI (Rect_Move.vi, Rect_Union.vi, etc.) needs to be recompiled if there are any changes made to LabVIEW typedef (Rect.ctl). Is this a silver bullet solution? No. Is this a step in the right direction? Absolutely! Is there a way to pass a reference to this or is there a way to simulate a pointer in LabVIEW? Thankfully, yes! I will cover how to do this in
my next article.
Why should you care about writing well-encapsulated LabVIEW structures? Well, throwing out scalability, maintainability, and good programming practices for a moment, National Instruments is experimenting with object oriented programming (OOP) concepts in LabVIEW and once released, these concepts could have a big impact on LabVIEW development in the future. But you do not have to wait until tomorrow before you can write good LabVIEW code today. The code described above can be easily converted to OOP and writing well encapsulated objects in LabVIEW is a good start in writing good maintainable code that will be ready for tomorrow's features. Breaking your LabVIEW application into modular components will help you write reusable, robust code that will be much easier to maintain.
Finally, I would like to add that I do not recommend creating Rect.ctl since LabVIEW already has a build in rect typedef. I created this file simply to illustrate a point.
I would like to thank Marc Page and Lynn Huang for their help and contributions to this article.
Charles Kalapati is a software engineer in the LabVIEW group at National Instruments.







1 Comments:
You made me feel a lot better. I'm just started with LabView and some of my early attempts make me want to become a beachcomber. I'll perservere and aim still higher as my Latin teacher once said when I made 11% in an exam.
Post a Comment
<< Home