04-28-2017 05:54 AM - edited 04-28-2017 05:55 AM
As someone who has used LV professionally for many years, I think I understand the concept of dataflow pretty well and although I don't use them very often, I thought I understood most of the intricacies of how to use call library function nodes to call external code.
However...I came across some code yesterday that I could not believe was possible since it completely breaks LV dataflow principles.
An array wire being returned by a CLFN gets magically/asynchronously updated, changing the data on the wire magically in the background.
Unfortunately, this is part of a hardware driver and I don't have access to the hardware so I can only post screenshots, but it's part of the driver for PicoScope USB devices and their streaming mode of operation. If you really want to see the LV, download the PicoScope LabVIEW driver.
Here is the example from PicoScope (with some minor modifications, I promise there is no funny business going on:
As you can see 'Start Stream' VI on the left returns an array and then inside the loop it has a 'Get Stream' VI which returns an index/length of that array. Now according to LabVIEW Dataflow, the values on array tunnel cannot change. So surely this does not work? Even if the index/length values change, the array is essentially a constant at that point so you would never get new data after the 'stream' VI. Well, it does work! I sense black magic!
Don't believe me? Here is a screenshot showing the contents of the array for a few loop iterations:
As you can see, the 'Channel A' array in the cluster is the full array returned by the 'Get Streaming' VI. You can see that for the first two iterations, the values are the same, but the start index/length gets updated. For the 3rd iteration, there array values have been updated and the start index has wrapped back around to 0.
Ok, so lets delve into the two VIs to see what is happening...
'Start Stream'
Ok, so this looks pretty standard for a CLFN - you create an array of the appropriate buffer size and pass it into the function and typically, functions like this would fill the array with your data. You have to allocate an array of the correct size or risk memory exceptions. The array parameter 'Channel A' is configured as a 'Array Data Pointer' for both CLFNs.
'Get Streaming Values'
04-28-2017 06:12 AM
If the code is using LV Memory Manager functions to maintain control of the memory which is used internally for buffering, i find this is absolutely possible.
Remember the famous sentence" The Wire is the Variable". Hence the tunnel is a representation of the memory behind it. If the DLL holds the memory, it could update values anytime with a parallel running thread.
The most critical thing is that this, in essence, exposes a race condition. So how is it made sure that values are updated orderly?
05-03-2017 04:40 AM
Hi Norbert, thanks for your input, but I don't think it really helps me that much to understand how external code (which could potentially be of unknown/dubious quality) can update the values on a wire asynchronously.
My understanding was that external code cannot access LV memory (you have to use the CLFN...). The wrapper they provide doesn't seem to be LabVIEW specific (it's for any language that doesn't support pointers/references) so I highly doubt it is calling any LV specific memory manager functions.
You are right though, there is potential for a race condition, but as this is manufacturer supplied code for device interfacing, there isn't much I can do about it (and it has seemingly worked for the client for a long time without issue).
The reason for my long post was to try and get an understanding of how this can be possible.
05-08-2017 09:16 AM
Hi Sam,
I don't think this should work. It is a horrible race condition waiting to happen, does the data on the wire get updated at the tunnel or the indicator? or somewhere in between? I cannot find any documentation from NI suggesting that it is possible to access memory internal to LabVIEW. I wonder if the author of the DLL made the driver in this way with little LabVIEW knowledge and assumed it would work, and it did. Or, did they know exactly what they were doing, in which case why would they do this? I imagine that if the wire was branched, LabVIEW would make a copy of the data and a new handle would be created. It might be that simply adding a branch outside the loop would break the code. It would be great to get some feedback from NI on this one, Is it a bug/known issue or a little known feature? I hope you get to the bottom of it!
Michael.
05-08-2017 09:27 AM
I saw something like that many moons ago and it blew my mind when I saw the data flow backwards through a wire.
A strategic "always copy" node may eliminate that situation.
Only people like Rolf can speak with assurance but I believe the create array is allocating a buffer that is used by the dll. Since the rest of your code does not modify the data LV works "in-place" in the buffer used by the dll. Show buffer allocations may confirm some of the guesses.
Ben
05-08-2017 02:04 PM
Sam_Sharp wrote:
My understanding was that external code cannot access LV memory (you have to use the CLFN...).
Well, as you've shown here, this isn't quite correct. When you pass any data to a DLL by reference (as a pointer or handle), you may be passing a pointer directly to the LabVIEW data. LabVIEW doesn't make any guarantees about when it will do that versus when it will pass a copy - it could change between versions, platforms, even depending on what type of data is passed - so you should always assume it's a copy, but clearly the authors of this driver didn't treat it that way. There's no reason for LabVIEW to make a copy when it doesn't need to do so, but at the same time it's definitely a bad idea for the DLL to hang onto the pointer past the end of the call, because LabVIEW could move and resize the array or release that memory at any time. That said, in the example code you show, there's no reason for LabVIEW to reallocate the array - it comes into the tunnel and never gets modified within the LabVIEW code, so LabVIEW doesn't need to touch it.
It's not good programming practice, but I'm not surprised it works. Another discussion of the same thing: http://forums.ni.com/t5/LabVIEW/How-do-I-get-the-memory-address-of-an-array/td-p/1851275
05-08-2017 02:11 PM
@Michael_78 wrote:
I imagine that if the wire was branched, LabVIEW would make a copy of the data and a new handle would be created.
An important concept here: LabVIEW never copies data at a wire branch. Data only gets copied at a node, when that node needs to modify the data on the wire and LabVIEW determines that the original data is still needed unchanged. The compiler will try to arrange operations to avoid such copies - for example, if you needed to index out several elements of an array and operate on them individually, in parallel with another operation that modified the array, LabVIEW will generally first index out the elements, then modify the array so that it doesn't need to make a copy of the entire array.
05-08-2017 08:00 PM
It's a pretty horrible abuse of a few optimization tricks LabVIEW does. And the fact that the DS (dataspace) handles LabVIEW nowadays uses do not get dynamically relocated unless you have somewhere an explicit resize or the array buffer ceases to be needed in the diagram. In the past LabVIEW also used AZ (application zone) handles which could be dynamically relocated at any time, but not for any wire data. Diagram wires for arrays and strings were always DS handles. AZ handles were dropped somewhere around LabVIEW 6 and now only DS handles are used everywhere.
So this works as intended by the programmer but only as long as nobody starts to make modifications to those VIs. An Always Copy in the wire or an explicit resize or even a wire branch that would cause a data copy could throw this completely off.
And the most interesting aspect, some future optimization improvement in a future LabVIEW version could render this code potentially completely useless.
Definitely not the way you want a library to be that gets delivered to customers.
05-09-2017 03:44 AM
05-09-2017 03:59 AM - edited 05-09-2017 04:01 AM
A DVR doesn't destroy the data it contains, but rather is a container datatype that protects it. You can think of a DVR a little bit like a reference to the data (or more simplistic a pointer, though that is in many ways misleading from a computer science point of view as a pointer is usually a bare bone low level element without any kind of higher level protection).
You can only access the DVR content through the Inplace structure and as long as the DVR content is referenced inside the Inplace structure, nobody else can access it. So the Inplace structure is implicitly doing an Acquire Semaphore and Release Semaphore on the semaphore each DVR contains too, besides the actual data reference.
However accessing a handle that is inside a DVR from external C code is an exercise that requires very specific C code in the DLL that is only semi-documented through the document and samples for creating your own Nvidia GPU algorithmes for the LabVIEW GPU Analysis Toolkit.
The functions that need to be called for this are not all part of the official External Code Reference Manual for LabVIEW.