LabVIEW Object-Oriented Programming


Functional Globals

A functional global is a LabVIEW programming construct (or pattern) that is very commonly used. It is a simple and elegant way to do object-oriented programming in LabVIEW. A functional global (which acts as our object) is a VI, which contains a While Loop + case structure combination, as shown in Figure D.1

Figure D.1. The essential parts of a functional global


The While Loop is configured to iterate only once. (Note that the return selector is configured to Stop if True and a value of TRUE is wired to it.) The While Loop contains a shift register that is used exclusively for storing the object's data (a shift register is much faster than other storage mechanisms, such as globals and locals, because there is only one copy, ever, of the data in memory). Each frame of the case structure is a method that may be called by setting the enum value that is passed into the functional global. The first case of the case structure is the "Init" frame that defines the object's data type.

It is important that functional global VIs are not reentrant, so that each location where it is called as a subVI refers to the same instance of the VI. This allows all subVI instances to refer to the same object (they all share the same shift register "object" data). If, however, the functional global were reentrant, each instance would have a unique and independent data space (having its own independent copy of the shift register data). Refer to Chapter 15, "Advanced LabVIEW Features," for more information about the reentrant VI setting, its features, and considerations.


Make sure that the "method" enum is a type definition (as discussed in Chapter 13, "Advanced LabVIEW Structures and Functions") because you will be using this enum in many locations. You will want all instances of this enum to be updated when you modify (add, remove, rename, etc.) the methods of your object.


A functional global can be thought of as a singleton, an object-oriented pattern where only one instance (or object) of a class will exist. The functional global is both a class and an object, rolled into one.

Example: Functional Queue.vi

Now let's look at an example of how we will use the functional global, in practice. We will create a queue object that, in addition to the standard "Init" method, has an "enqueue" method for adding elements to the queue and a "dequeue" method for removing elements from the queue.

Functional Global "Core"

The functional global "core" is a single VI that contains the object data and methods. When programming applications that use a functional global object, you will not call the functional global "core" as a subVI directly; rather, you should create public method VIs, one for each method in your functional global "core." Keep this in mind while we discuss the functional global "core," as it will make more sense when the public methods are introduced in the next section.

Figure D.2 shows the front panel of a functional global core named Functional Queue.vi. The method enum has one element for each method, and each method (except for "Init") has an input and output cluster on the front panel. The method enum and the input/output clusters have all been made type definitions, as described in Chapter 13.

Figure D.2. Functional Queue.vi front panel


Figure D.3 shows the connector panel of Functional Queue.vi. Note again that there is one input cluster and one output cluster for each method declared in the method enum.

Figure D.3. Functional Queue.vi connector pane


Now let's take a look at the block diagram of Functional Queue.vi. Figure D.4 through Figure D.6 show the block diagram of Functional Queue.vi with the three method case frames visible: "Init," "enqueue," and "dequeue." Note that each frame unbundles only its own input cluster and bundles only its own output cluster. Each method accesses only its own input arguments and passes out only its output arguments.

Figure D.4. Functional Queue.vi's "Init" method case


Figure D.5. Functional Queue.vi's "enqueue" method case


Figure D.6. Functional Queue.vi's "dequeue" method case


Functional Global "Public Method" VIs

The functional global "core" VI that we have just created, Functional Queue.vi, contains all of the object's methods and data. However, it is quite cumbersome and confusing to use as a subVI. For example, the inputs and outputs that are used depend on the value passed to the method enum. Additionally, it would be very difficult to debug applications that call Functional Queue.vi directly because we cannot easily find all instances that are called with a specific method value. So, in order to make using Function Queue.vi practical, we create a "wrapper" VI for each method. These will be our object's "public method VIs," the VIs that users of our functional global object will call as subVIs. Figure D.7 through Figure D.9 show the three public method VIs of our object: Init Queue.vi, Enqueue.vi, and Dequeue.vi. As you can see, each VI has individual inputs and outputs and does not use the input and output clusters.

Figure D.7. Init Queue.vi public method VI


Figure D.8. Enqueue.vi public method VI


Figure D.9. Dequeue.vi public method VI


Now let's take a look at the block diagrams of our public method VIs. Figure D.10 through Figure D.12 show the block diagrams of Init Queue.vi, Enqueue.vi, and Dequeue.vi. Note that each of these VIs sets the method enum and handles the bundling and unbundling of the input and output clusters (respectively).

Figure D.10. Init Queue.vi block diagram


Figure D.11. Enqueue.vi block diagram


Figure D.12. Dequeue.vi block diagram


Programming Applications Using the Public Methods

Now let's look at an example of how we might use our object. Figure D.13 shows the block diagram of a VI named Reverse Array.vi that uses the queue object to reverse the order of an array. First, each of the array elements is enqueued onto the front of the queue in a For Loop. Then, each of the array elements is dequeued from the front of the queue in a While Loop, until the queue is empty. In this example, we are treating the queue as a FIFO, which will cause the output array to be the reverse of the input array.

Figure D.13. Reverse Array.vi block diagram


Looking at the front panel of Reverse Array.vi, shown in Figure D.14, populated with an input array and showing the output array after the VI is run, you can see that, in fact, the output array is the reversed input array.

Figure D.14. Reverse Array.vi front panel


Functional Globals: Final Thoughts

In summary, it is important to remember the following points about functional globals:

  • The functional global core contains the object data and methods.

    • The method enum contains one element for each method.

    • Each method has an input and output cluster.

    • Each method unbundles its own input cluster and bundles its own output cluster.

    • The method enum, input clusters, and output clusters should all be type definitions.

    • Make sure to initialize your object data inside the "Init" case.

    • The functional global core should not be reentrant. Otherwise, the public method VIs would each call a unique instance of the core.

  • The functional global public methods are used to program your application. They are the programmer's "interface" to the object.

    • The functional global public methods are the only VIs that call the functional global core as a subVI.

    • There is usually one public method for each method defined in the method enum and implemented inside the core's case structure.

    • The public methods set the method enum when calling the functional global core.

    • The public methods bundle the appropriate method's input cluster, from control elements on its front panel, and pass it into the core.

    • The public methods unbundle the appropriate method's output cluster and pass elements to output indicators on its front panel.

Note that functional globals implement the Class, Object, and Encapsulation concepts mentioned at the beginning of this appendix. Functional globals can be safely called an object-based programming technique, because they do not implement all the object-oriented concepts. The encapsulation provides modular application components that hide the details of how each method does its job and how the object stores its data. This allows you to modify and improve the functional global without affecting code that uses it (unless, of course, you change inputs, outputs, or behavior of the public methods).

GOOP

GOOP is almost an anachronympeople use it so casually, they often forget that it is an acronym for Graphical Object-Oriented Programming. GOOP generally refers to a pattern of object-oriented programming that uses a "semaphored" object data store that allows multiple object instances "by reference." There are several implementations of GOOP, all of which use a wizard to generate and help you edit classes from a template. Each of these implementations of GOOP provide support for the Class, Object, and Encapsulation object-oriented concepts mentioned at the beginning of this appendix.

So what do "semaphored object data store" and "multiple object instances by reference" mean? The next sections will attempt to shed some light on these concepts.

Semaphored Data Store

In Chapter 13, you learned that semaphores are used to lock and unlock access to a shared resource. An object data store is certainly a shared resourceit is shared by all the class's methods, because each method needs to access the object's data. In the case of a functional global, which we learned about in the last section, each method is a separate case of a case structure within the functional global core. And, because only one case of the Case Structure can execute at a given time, there is no chance that two methods will attempt to access the object data at the same time. Recall, the functional global core is not reentrant, so if two different public methods are running in parallel, only one of them can call the functional global corethe other public method VI will have to wait until the call in the other public method VI has completed. However, if we had a situation where two methods that operate on the object data might be able to run at the same time, we would need a mechanism to lock, or semaphore, the object data.

Recall from the "Semaphores: Locking and Unlocking Shared Resources" section of Chapter 13, that we stated something to the effect of, "Semaphores having data is the fundamental concept of GOOP." Queues and notifiers have data, but semaphores do not. However, imagine that semaphores have data, and voilàyou have GOOP.

Let's now look at an example of why semaphores are important when accessing shared data. Notice in Figure D.15 that although we have set the value of Numeric and String, only Numeric actually remains in the Obj. Data once the code completes execution. What has happened is a classic race condition, where two parallel operations read data, operate on it, and then write back the modified data. In our example, the operation on top a has overwritten the result of the operation on the bottom. In order for us to remedy this problem, we must ensure that the upper and lower operations occur sequentially and not in parallel.

Figure D.15. Unsemaphored GOOP Global.vi block diagram and front panel (after running)


Of course, we could remedy this problem by putting the two data modification steps into separate frames of the sequence structure, but what if these operations are happening in completely different VIs in completely different parts of your application? It is easy to see that this could get tricky in a hurry. But, by using semaphores to force sequential access to our shared data, we can ensure that only one data modification operation occurs at a given time. As you can see in Figure D.16, by using a semaphore to enforce sequential access to our shared data structure, the upper and lower data modification operations are no longer executed in parallel. Both data modification operations are successful, as we can see by the presence of the updated Number and String values in the final value of Obj. Data.

Figure D.16. Semaphored GOOP Global.vi block diagram and front panel (after running)


In the example we just looked at, we used a global variable to store our object data. And, the upper and lower data modification operations were our methods. The semaphore was used to ensure that only one method could operate on the data at a given time. Now, let's take this concept another step forward and more closely couple the semaphore to the object data (remember, semapore + data = GOOP). Figure D.17 shows how the internal components of a GOOP class (created using the NI GOOP Wizard) are used to control access to the shared object data. Note that the data flows directly out of the Get Data to Modify VI and directly into the Set Modified Data VI. These two VIs are analogous (and similar in behavior) to the Acquire Semaphore and Release Semaphore VIs, but with the addition of object data (remember, semapore + data = GOOP).

Figure D.17. GOOP Class Example.vi block diagram


The previous example (shown in Figure D.17) contains internal support VIs used by GOOP classes. These internal VIs are not intended to be called by developers programming applications that use the class; rather, they are intended to be called as subVIs inside of class method VIs.

Figure D.18 shows an example of a class method VI named MyObj SetNumeric.vi. This VI is a member of the "MyObj" class and is the SetNumeric method. Its purpose is to set the value of the Numeric element of the object data. It is inside of the method VIs where we perform work based on method arguments and object data. While inside of a method that modifies the data, we have exclusive access to the object datano other instance of Get Data to Modify will be able to access the data until the data is written back to the data store using Set Modified Data (which, again, is analogous to the behavior of Acquire Semaphore and Release Semaphore).

Figure D.18. MyObj SetNumeric.vi block diagram


Now, using class method VIs, as shown in Figure D.19, our example becomes much simpler. Note that MyObj SetString.vi works in a similar fashion as MyObj SetNumeric.vi. And, MyObj Create.vi and MyObj Destroy.vi are simple VIs that call the Create Object and Delete Object VIs shown in Figure D.17these are referred to as the object constructor method and object destructor method (respectively), in object-oriented terminology.

Figure D.19. GOOP Class Method Example.vi block diagram


It should also be noted that it is possible to create methods that do not modify the data. If this is the case, they call the utility VI named Get Data, which (unlike Get Data to Modify) does not acquire the object semaphore. This should only be used inside of methods that do not modify the data. The benefit of using Get Data is that (unlike Get Data to Modify) it does not wait for the object data semaphore to be releasedit returns immediately with the latest object data. Methods that call Get Data (and therefore do not modify the object data) are referred to as reading or read-only methods. Methods that call Get Data to Modify (and do modify the object data) are referred to as modifying or read-write methods. Note that only one modifying/read-write method will be able to execute at a time, due to the locking/unlocking provided by the object semaphore.

Multiple Object Instances by Reference

Up to now, we have seen only single instances of classesone object per class. However, using GOOP, we can create multiple objects (instances of our classes) simply by calling the class's Create (constructor) VI multiple times. Each time we call the Create VI, a new, unique object reference will be generated. This is similar to how each time one calls the Create Semaphore VI, a new, unique semaphore reference is generated.

The block diagram shown in Figure D.20 shows a simple example of how you can create multiple object instances, by simply calling the constructor method multiple times. Yes, that's all there is to it. Each reference refers to a unique object with its own separate data.

Figure D.20. GOOP Multiple Instances.vi block diagram


Obtaining the GOOP Wizard

By this point, you're probably excited to try GOOP, and we bet you're wondering how you can make your own GOOP classes. Well, it's pretty easysimply download the GOOP Wizard from ni.com. A simple search will help you find it. Additionally, there are many other third-party GOOP wizards (for example, OpenGOOP from OpenG.org, as discussed in Appendix C, "Open Source Tools for LabVIEW: OpenG")each of them uses a slightly different approach to how data is stored in memory. For GOOP beginners, we recommend using the NI GOOP Wizard, because it's easy to use and supported by NI.

GOOP Final Thoughts

GOOP is one evolutionary step forward from functional globals, adding support for multiple object instances by reference and also eliminating the need for a single "core" VI that contains every method. However, this comes with a price. First, GOOP is less efficient (it's hard to beat the read/write performance you gain by storing data in a shift register, as is done in functional globals). Second, GOOP classes have many "internal" support VIs that are used for managing access to the object data. It is not uncommon for each class to require 0.5 MB to 2 MB of support VIs, depending on which version of GOOP you are using. And, all of these support VIs can create a bit of a housekeeping problem considering each VI is a part of your project. Another issue is renaming classes. The NI GOOP Wizard, for example, does not support renaming classes. However, there are several tools that people have written to support these development tasks. Another excellent example is the GOOP Wizard from Endevo (www.endevo.se)the people at Endevo are the inventors of GOOP, by the way. They sell some very powerful GOOP tools with many great features. And, they have a more recent version of GOOP that supports inheritance, which we will learn about next.

GOOP Inheritance

We mentioned in the last section that GOOP provides support for the Class, Object, and Encapsulation object-oriented concepts. However, there are also implementations of GOOP frameworks that also provide support for Inheritance, Abstraction, and Polymorphism. (Note that object-oriented polymorphism is not at all the same thing as LabVIEW's polymorphic VIs, whose use of the term is a bit of a misnomer.) With these additions, GOOP Inheritance becomes a very complete object-oriented framework. The only drawbacks of these frameworks are the following:

  • Each class has even more internal support VIs than "regular" GOOP classes.

  • Data access is generally slower, because there are a few more hoops to jump through in order to access inherited data.

  • Editing operations due to increased inherited dependencies can be more complex and really do require additional editing tools in order to edit GOOP Inheritance classes.

An in-depth discussion of GOOP Inheritance is beyond the scope of this appendix. However, you are encouraged to learn more about it. Endevo (www.endevo.se) has a very good downloadable white paper describing the fundamentals of its GOOP Inheritance product. Additionally SciWare (www.sciware.com.au) sells a GOOP Developer product that supports Inheritance and also has good documentation on its use.




LabVIEW for Everyone. Graphical Programming Made Easy and Fun
LabVIEW for Everyone: Graphical Programming Made Easy and Fun (3rd Edition)
ISBN: 0131856723
EAN: 2147483647
Year: 2006
Pages: 294

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net