C++ programming on Cloud 9

A weblog dedicated to Visual C++, interoperability and other stuff.

Fun with template classes: Manipulating LabVIEW arrays in C++, part 1

One of the best things about LabVIEW is its ability to call external code. This code can be used in the form of a dll, a CIN (custom compiled C code) an ActiveX object or a .NET class.

I have often created dlls (on windows) or shared object libraries (linux) to create an interface between LabVIEW and external native code.

This blog entry is about working with LabVIEW arrays in external code. This is simple as long as you create an interface that takes a pointer to the array data.

For data input this poses no problem. For data output this is also simple as long as you follow the C style: supply a valid data pointer, a buffer size and a pointer to a variable that will receive the number of elements written.

Of course, if you don’t know the number of elements in advance, your code becomes more complex, and will likely result in more external function calls and over-allocation.

To overcome this problem we need to dynamically allocate LabVIEW arrays in our external code that can be passed to LabVIEW. This is much more complex than working with a simple data pointer.

What are LabVIEW arrays?

A LabVIEW array is in fact a structure that contains a length specifier and the actual data elements. The C declaration is like this:

typedef struct

  {

    int dimSize;

    int data[1];

  } t_LvArray, *p_LvArray, **h_LvArray;

The array contains a dimension size specifier that indicates the size of the data part, and the data part itself.

The data part declaration is specified as an array with size 1. the reason for this is that the size specifier and the data are part of the same block of data, so data cannot be a pointer.

In order to be able to dereference data, it is declared as an array. The actual size is not 1, but whatever is mentioned in dimSize. It is only declared as an array of 1 byte because 0 sized arrays cannot exist in the C language.

Whenever LabVIEW has to pass a LabVIEW style array to external code, it does this by supplying a relocate able handle.

In case of multidimensional arrays the declaration is somewhat different:

typedef struct

  {

    int dimSize[NUM_DIMENSIONS];

    int data[1];

  } t_LvArray, *p_LvArray, **h_LvArray;

Each dimension has an element in the dimSize array to indicate the size of that dimension. For a 3 dimensional array, this array has 3 elements.

The data block is still declared as a 1D array.

It is also worth to note that LabVIEW structures and arrays are packed with 1-byte alignment. I.e. there is no padding between structure fields to align variables on address boundaries. This means that declarations should be done like this to insure proper packing:

#pragma pack(push)

#pragma pack(1)

  typedef struct

  {

    int dimSize[Q];

    T data[1];

  } t_LvArray, *p_LvArray, **h_LvArray;

#pragma pack(pop)

The syntax will be different for different compilers. This syntax is used with Microsoft Visual C++.

What is the problem with LabVIEW arrays?

On first glance the handles seem easy to use, but there are 2 distinct problems when you want to use them.

The first is simple you need a unique declaration for each data type and each number of dimensions for which you want to create an array.

typedef struct

  {

    int dimSize

    int data[1];

  } t_LvArrayInt1D, *p_LvArrayInt1D, **h_LvArrayInt1D;

typedef struct

  {

    int dimSize;

    double data[1];

  } t_LvArrayDbl1D, *p_LvArrayDbl1D, **h_LvArrayDbl1D;

typedef struct

  {

    int dimSize[2];

    double data[1];

  } t_LvArrayDbl2D, *p_LvArrayDbl2D, **h_LvArrayDbl2D;

This becomes tedious after a while, and it is cumbersome to maintain a whole header file of declarations for all possible types.

The second problem is that you have to manually do all the arithmetic for calculating the position in the data block. You have to have different calculations for each unique combination of data type and number of dimension size. This is very prone to errors, and even more tedious to maintain than the array declarations themselves. The standard indexing notation for multi dimensional array cannot be used because the array size can change at runtime.

The third problem is that it is impossible to represent a zero length array. Zero length C style arrays do not exist. Of course the LabVIEW memory manager programmers could have implemented a hack for this, but they didn’t.

This means that if you want to resize arrays for output, you need to create an interface that takes a pointer to a handle. This way you can allocate a new array if the handle is NULL, or you can resize it if it isn’t. As a result, you are now working with a pointer to a pointer to a pointer to a data block. That is a lot of dereferencing to take care of.

The fourth problem is that the code for resizing and allocating, like the code for indexing in the arrays is unique per data type and per dimension size.

C++ template classes to the rescue

Luckily for us, there is a way around all these issues: C++ template classes.

C++ template classes are the most complex but also most powerful feature of the C++ language. It allows you to define templates for classes and functions that are only specialized for specific types at compile time.

LabVIEW arrays of any type and number of dimensions can be implemented using the following class:

template <class T, int Q=1> class CArrayHandlePtr

{

public:

  /*type definition for LabVIEW array handle*/

  #pragma pack(push)

  #pragma pack(1)

  typedef struct

  {

    int dimSize[Q];

    T data[1];

  } t_LvArray, *p_LvArray, **h_LvArray;

  #pragma pack(pop)

  h_LvArray* m_HandlePtr;

  /*resize the array*/

  void Resize(int DimSize[Q])

  {

    /*implementation omitted*/

  }

  T& operator[](int Index[Q])

  {

    /*implementation omitted*/

  }

};

T and Q have to be supplied at compile time. T is the data type of the elements in the array, and Q is the number of dimensions. The compiler will create a correct implementation of t_LvArray that represents the LabVIEW style array.

The class has 2 other functions: a resize functions that allows you to resize the array (or to allocate it if the array handle was NULL), and a [] operator that returns a reference to the element specified in the Index array.

The constructor trick

The template class does not have a constructor. The reason for this is that there is no point in using true LabVIEW arrays except as function arguments to external code.

This means that the array handle pointer will be initialized by LabVIEW. The class has only 1 member variable: the handle pointer itself. If that is supplied by LabVIEW, we change its meaning from a simple address value to a templacized class instance.

This might seem a bit dodgy, but there is no constructor code that needs to be run. If the address member is initialized, the object is considered to be properly constructed. This is entirely legal C++ code.

The only downside is that we have to be sure that the definition of the data type in LabVIEW has to match what we think it to be. There is no way for the external code to know if the array is 2D or 3D for example. It is the responsibility of the programmer to insure this.

Of course, if you decide you want to use true LabVIEW arrays within your C++ code for internal purposes, you can add a constructor that will allocate memory through the LabVIEW memory manager. Just be sure to dispose of it afterwards, and to provide proper copy constructors to prevent heap errors.

Resizing the array

The implementation for the resize function is this:

  int Resize(int DimSize[Q])

  {

    int totalSize = 1;

    int errorCode = 0;

    /*calculate number of elements*/

    for(int i=0; i<Q; i++)

      totalSize *= DimSize[ i];

    /*convert to number of bytes*/

    totalSize *= sizeof(T);

    /*add size of the dimension array*/

    totalSize += Q * sizeof(int32);

    if(0 >= totalSize)

      return mgArgErr;

    if(NULL != *m_HandlePtr)

    {

      errorCode = DSSetHSzClr(*m_HandlePtr, totalSize);

      if(noErr != errorCode)

        return errorCode;

    }

    else

    {

      *m_HandlePtr = (h_LvArray)DSNewHClr(totalSize);

      if(NULL == *m_HandlePtr)

        return mZoneErr;

    }

    for(int i=0; i<Q; i++)

      (**m_HandlePtr)->dimSize[ i] = DimSize[ i];

    return noErr;

  }

TotalSize is the size in bytes of the complete memory block, including the dimension size specifiers. Some extra checking is done to insure that the total size is a positive number.

Then the handle is resized if it was a valid handle, or a new handle is allocated if it was a NULL handle. Finally, the dimension sizes have to be filled in manually because the memory manager does not know this information.

Indexing in the array

The final functionality that has to be implemented is the [] operator that will allow the programmer to read and write values to and from a position in the array.

T& operator[](int Index[Q])

  {

    int index = 0;//sizeof(t_LvArray);

    for(int i=0; i<Q-1; i++)

    {

      int subSize = 1;

      for(int j=1; j<Q; j++)

        subSize *= Index[ i] * (**m_HandlePtr)->dimSize[ j];

      index += subSize;

    }

    index += Index[Q-1];

    return (**m_HandlePtr)->data[index];

  }

};

Because the real data block is just a flat 1D array, we have to calculate the position in the array that matches with the specified indices. This calculation is not so hard to understand:

With each dimension that is accounted for, the index in that dimension is multiplied with the total number of elements in a sub-dimension. This value (the subSize value) is calculated for each dimension except for the last dimension (the least significant dimension). That index is just added to the intermediate total because it has no number of elements per sub dimension.

The resulting index is the position in the 1D data block that represents the element at the specified multidimensional indices.

Using the array

Using the array is very simple, as can be seen in this example:

int fun2Ddouble(

                              int requestCountRows,

                              int requestCountColumns,

                              CArrayHandlePtr<double, 2> Array)

{

  int index[2] = {0,0};

  int size[2] = {requestCountRows, requestCountColumns};

  Array.Resize(size);

 

  double i=0;

  for(int row=0; row<requestCountRows; row++)

    for(int column=0; column<requestCountColumns; column++)

    {

      index[0] = row;

      index[1] = column;

      Array[index] = i++;

    }

  return 0;

}

Limitations of this design

Using the magic of C++ templates, we are able to implement 1 class that can represent all LabVIEW arrays of any given size or data type.

That does not mean that we are totally satisfied however. The follow

It is regrettable that we have to use arrays as index to the [] operator. It would be nicer to be able to use true multidimensional array syntax. i.e:

  for(int row=0; row<requestCountRows; row++)

    for(int column=0; column<requestCountColumns; column++)

      Array[row][column] = i++;

See the next chapter for some more info on that.

Another ‘problem’ if you wish, is that the [] operator does not protect against buffer overflow. This is trivial to implement, but I have not done so for performance reasons. And after all, true C style arrays do not protect against it either. It would perhaps be a good idea to implement this using conditional compilation, enabling the programmer to turn this feature on or off at compile time. This is just proof of concept code however, so I am not going to do that for now.

A performance problem is also that the sub dimension sizes are recalculated for all dimensions, each time the [] operator is invoked. It would make more sense to store these values somewhere, but we cannot do that. The requirement that an instance of the template class is 100 % identical with the LabVIEW definition means that we are not allowed to store additional data in the class body.

Afterthoughts

This article explains how you can use C++ template classes to create 1 class that you can use to access LabVIEW arrays of any data type and number of dimension. Bear in mind that the code is just proof of concept code.

The example LabVIEW and C++ code is available for download under the MIT license.

I have already found a solution of the multidimensional syntax problem. With some additional programming it is possible to use the [row][column] notation for 2D arrays, and [page][row][column] notation for 3D arrays.

The solution is not limited by the number of dimensions (you could use 100D arrays for all I care) but it is a bit too complex to include it as part of this article.

I will dedicate my next blog article to the solution for that problem.

Comments

Cluebat-man to the rescue said:

This article is a follow-up on a previous article: &#39;Manipulating LabVIEW arrays in C++&#39; in which

# October 25, 2006 8:04 AM