LabVIEW

cancel
Showing results for 
Search instead for 
Did you mean: 

Creating and/ or manipulating strings in DLL and pass to LabView by LStrHandle

Solved!
Go to solution

Hi,

 

I've been trying this out for a while now (different approaches, different "kind-of-working" solutions). For the start, I want to do a seemingly very simple thing:

 

- Call a DLL from Labview by a Call Library Function Node.

- In the DLL a string is generated. Let's for now say a constant string which should be output to Labview. I dont know the size of the string before the DLL call and if there's any way to realize it, I would like to keep it that way.

- Using the "String Type" and C-String option in the Call Library Parameters is - I think - not an option (I'm very happy to discuss the reasons... this is supposed to be part of a bigger issue). The String should be passed as LStrHandle (I guess :). At the moment it is Adapt-to-Type and then Handles-By-Value setting in the Call Library Node parameters.

 

Here's the very simple code in C++ for the DLL:

 

 

 

 

 

#include "pch.h"
#include <string>
#include <stdint.h>

typedef struct {
    int32_t	cnt;		/* number of bytes that follow */
    char	str[1];		/* cnt bytes */
} LStr, * LStrPtr, ** LStrHandle;

void copyStringToLStrHandle(string cpString, LStrHandle LVString) {
    (*LVString)->cnt = int32_t(cpString.size()); // I'm telling the Handle what string size to expect
    cpString.copy((*LVString)->str, int32_t(cpString.size())); // I'm copying the string into the handle
}

void __declspec(dllexport) testStringsPassing(LStrHandle LabViewString) {
    copyStringToLStrHandle("This is the new String", LabViewString);
}

 

 

 

 

 

And the very simple call from Labview:

itchy00_0-1603101357740.png

 

So here's the thing... this seems to kind-of work within Labview - although from time to time it freezes or crashes. When I build an executable from Labview the program crashes almost immediately.

 

So I assume the reason is (as most of the time) memory miss-management, wrong (*missing*) allocation for the char array within LStr or something in this direction ?! I figure the problem is that I am not allocating anything but just copy the newStrings char array to the LVStrings char array, which is not properly "prepared" for it... how would I achieve that?

 

 

A few notes:

- Note that I am not explicitly calling any malloc (is this where the problem is?) I would really like to use the functionalities of the C++ <string> class for generating and manipulating my strings before "sending" them back to Labview. If any chance I would like to avoid super basic char array coding.

- Am I going a completely wrong direction here? The reason I would like to avoid allocation of memory before the DLL call (from within Labview) is that I kind of want to obscure as much code as possible within the DLL... a lot of the "newString" information will be coming from a proprietary file format.

- I found somewhere a nice suggestion, to create a new LStrHandle in the DLL and simply point the Handle passed from Labview to that new Handle... but I never got that solution to work!?

- My final goal is to have a Labview Cluster (actually a nested Cluster within a cluster) with some strings and numbers, and being able to fill the cluster by a DLL call. Therefore I pass the cluster as Adapt to Type and cannot specify "String and C-String pointer" in the Library Call Node as if I was working only on a single string control and string indicator in LabView.

In the end, this would be an example of the (nested) output Cluster in which I want to be able to manipulate the strings and numbers via DLL:

nestedTypedefLabview.PNGnestedStructsCpp.PNG

Again passing that as Adapt-To-Type and Handles-By_Value as in the NI documentation examples for cluster passing.

 

 

I would really appreciate any help and hints on this issue. As I said: I'm very happy to discuss the details of my idea here. I'm totally open to another approaches than the one I've taken so far.... !! And I'm not an C++ expert... so please don't go too hard on these details in the first shot 😉

 

Thanks a lot!!!

0 Kudos
Message 1 of 7
(3,579 Views)
Solution
Accepted by topic author itchy00

There is one single answer that sums everything up: Memory Management

 

Just because you can use nodes in LabVIEW without ever having to worry about proper memory allocations (aside ffrom when you write highly inefficient code with huge data arrays that will bomb your system with out of memory errors), does not mean that you can continue to do that when you move across the Call Library Node boundary into C space.

 

Here you MUST care about every single byte you try to write into (and read from) to be properly allocated from the memory manager before you do that.

 

typedef struct {
    int32_t	cnt;		/* number of bytes that follow */
    char	str[1];		/* cnt bytes */
} LStr, * LStrPtr, ** LStrHandle;

void copyStringToLStrHandle(string cpString, LStrHandle LVString) {
    (*LVString)->cnt = int32_t(cpString.size()); // I'm telling the Handle what string size to expect
    // vvvvvvv Bad, bad!!
    cpString.copy((*LVString)->str, int32_t(cpString.size())); // I'm copying the string into the handle
}

void __declspec(dllexport) testStringsPassing(LStrHandle LabViewString) {
    copyStringToLStrHandle("This is the new String", LabViewString);
}

 

You pass in from the diagram an empty string. Since this string is passed by value, LabVIEW has to allocate something here, if it was passed in by reference, LabVIEW would actually pass in a NULL pointer as that is the canonical value for empty arrays and strings.

But this handle only contains 4 bytes of memory, just enough to store 0 into the cnt element. The characters after that simply have no allocated space since there is no space needed to store 0 characters.

 

Instead you need to do this:

 

#include "extcode.h"

#include "lv_prolog.h"
typedef struct {
    int32_t	cnt;		/* number of bytes that follow */
    char	str[1];		/* cnt bytes */
} LStr, * LStrPtr, ** LStrHandle;
#include "lv_ëpilog.h"

MgErr copyStringToLStrHandle(string cpString, LStrHandle LVString)
{
    int32 len = int32_t(cpString.size());
    MgErr err = NumericArrayResize(uB, 1, (UHandle*)&LVString, len);
    if (!err)
    {
        cpString.copy((*LVString)->str, len); // I'm copying the string into the handle
        (*LVString)->cnt = len); // I'm telling the Handle what string size to expect
    }
    return err;
}

MgErr __declspec(dllexport) testStringsPassing(LStrHandle LabViewString)
{
    return copyStringToLStrHandle("This is the new String", LabViewString);
}

 

Then you also need to add the cintools directory in LabVIEW to your header directories and library directories and link the resulting object file with labviewv.lib from that directory which provides you the NumericArrayResize() function linking.

 

It is VERY important to always use LabVIEW manager functions in your C code if you need to allocate, resize and deallocate any LabVIEW handles that you receive from the diagram or return to the diagram!!!

Rolf Kalbermatter
My Blog
Message 2 of 7
(3,513 Views)

Thank you so much Rolf for that answer. I knew it was all about memory management but my (super naive) hope was that I could achieve all this myself.... but then I read into and played around for some hours with the Labview Manager Functions. I'm totally convinced now that this is the ONLY way to go!! Thank you.

Late night yesterday, I finally came to a somewhat different solution than yours using some other Labview Manager Functions. It is working so far without any crashes or freezes, so I figure it's not too bad and just an alternative way of implementation - but please don't hesitate to let me know if it is 😂

 

I see in your code you are somehow doing the resizing of allocated space "manually" using NumericArrayResize. Here's my approach using DSNewHandle which kind of does those details for me, I guess:

 

#include "pch.h"
#include <string>
#include <stdint.h>
#include "extcode.h"

string LStrHandle2String(LStrHandle LVString) { // This function creates a C++ string from a Labview LStrHandle
    string cppString((char*)LStrBuf(*LVString), 0, LStrLen(*LVString));
    return cppString;
}

void string2LStrHandle(string cppString, LStrHandle* LVString) { // This functions writes a C++ string into a Labview LStrHandle
        //Dispose of the "old" handle since we need a new one anyway
    DSDisposeHandle(LVString);
    // Allocate enough memory for the new LStrHandle
    (*LVString) = (LStrHandle)DSNewHandle(sizeof(int32) + cppString.size() * sizeof(uChar));
    // Fill the LStr buffer with new string
    cppString.copy((char*)LStrBuf(*(*LVString)), cppString.size()); // could also use memcpy() here
    // Inform LStrHandle about new string size
    LStrLen(*(*LVString)) = cppString.size();
}

void __declspec(dllexport) fillLStrHandle(LStrHandle* LVString) { // Call StringHandle by Pointer

    string newString = "This is a string out of nowhere!"; // This string will be written to LabView's LStrHandle
    string2LStrHandle(newString, LVString);

}

void __declspec(dllexport) copyLStrHandle(LStrHandle* LVString, LStrHandle LVStringToCopy) { // Call first StringHandle by Pointer, second one by value

    string copyString = LStrHandle2String(LVStringToCopy);
    string2LStrHandle(copyString, LVString);

}

I'm not sure if the DSDisposeHandle() is really necessary (or advised). It seems work without it, but I found it somewhat intuitive before calling DSNewHandle().

 

  • First function fillLStrHandle() does exactly that: Fill a string indicator in Labview with a string generated in the DLL.
  • Second function copyLStrHandle() reads from a string control and copies the LStrHandles string buffer to a string indicator.

 

copyLStrHandle2.PNGcopyLStrHandle.PNG

 

 

So far so good!!! Again, although I somehow found that direction myself, thank you for making clear there should be NO WAY around using LabView Manager Functions in the C code when handling Handles 😉

 

 

No here's the next problem, of course 🙄:

While this seems to work very nicely now, I pointed out, that my actual goal is to write to a string indicator in a Cluster. For some reason, following along that same path I can't get it to work and I'm freaking out because I don't see why (hopefully not toooo obvious).

 

I have a cluster with a number and a string. In C++ that goes into a struct with:

typedef struct {
    double	clusterNum;		// a number
    LStrHandle* LVString;	// Labview String
} structWithString;

 

As written in this documentation I pass the pointer to the struct as Handles-By-Value in the Call Library Function Node.

» Calling External Code » Calling C/C++ DLLs » Passing and Receiving Pointers 

 

I can easily change the number via de-referencing the struct pointer:

void __declspec(dllexport) changeNumberInCluster(structWithString* LVclusterWithString) {
    (*LVclusterWithString).clusterNum = 42;
}

 

But when I try to work on the string handle the program crashes:

 

void __declspec(dllexport) fillLStrHandleInCluster(structWithString* LVclusterWithString) {
    string newString = "This string goes in the cluster!";

    DSDisposeHandle(*(LVclusterWithString->LVString));
    (*(LVclusterWithString->LVString)) = (LStrHandle)DSNewHandle(sizeof(int32) + newString.size() * sizeof(uChar));
    newString.copy((char*)LStrBuf(*(*(LVclusterWithString->LVString))), newString.size());
    LStrLen(*(*(LVclusterWithString->LVString))) = newString.size();
}

 

I really don't get the logic why it fails. I'm starting to read into this about LStrHandles should not be in structs but instead use pointers:

https://forums.ni.com/t5/LabVIEW/Passing-Cluster-Containing-Strings-in-C-DLL-as-Struct/td-p/2499478?... 

But so far I haven't really understood it, specifically why it is a problem just using the handles as before.... but I'm working on it!

 

If anyone can help I'd be glad!!!

 

 

PS Note:

I managed to get it working - at least the behaviour of this very simple "fill-String-Indicator" functionality - by changing LStrHandle* LVString in the typedef to a simple LStrHandle LVString and removing one "*" in every line that follows. But I would really like to understand the reason why working with a pointer to a handle fails at this point but worked perfectly for the string indicator outside a cluster.

One problem is that my string2LStrHandle() function expects a pointer to a handle and I would have to have another overload for copying to LStrHandles in clusters.... if so I would like to understand why 😉

0 Kudos
Message 3 of 7
(3,493 Views)

A LabVIEW struct as you intend to use should be declared as follows:

 

#include "lv_prolog.h"
typedef struct {
    double	clusterNum; // a number
    LStrHandle LVString; // Labview String
} structWithString;
#include "lv_epilog.h"

 

Notice the lack of a pointer token in front of LVString. This is because a. Handle is already a referenced pointer, so doing another referencing on top of that would be pretty insane. This is simply how LabVIEW organizes data in a cluster and you can’t change that, no matter how much you would like. It’s totally not the same as passing a handle directly as parameter. There you can configure if you want LabVIEW pass the C pointer (which for strings also makes LabVIEW resize the string to one more byte and at the end append a NULL.byte since that is how C strings are interpreted), or a handle and there if the handle itself or the pointer to the handle should be passed. This configuration has no effect on handles embedded in clusters! Here LabVIEW simply passes a pointer to its native cluster data structure, nothing more and nothing less!

 

You can get the right declaration easily by creating a Call Library Node and wiring it up as desired, then right click on the Call Library Node and select “Create C File”. (or something similar, don’t remember the exact wording atm).

 

As to allocating handles directly through DSNewHandle() there is first the extra sizeof(int32) you need to take into account and potential alignment issues if the array element size is more than 32 bit such as a double or pointer. NumericArrayResize() does that for you. In addition it checks the incoming array to be NULL and if so allocates a new handle, otherwise it resizes the existing handle which potentially is less costly than allocating a new one and deallocating the old one. 

You absolutely MUST deallocate every handle that you receive from the diagram, and is not NULL and not returned back to the diagram! Since you now pass the string as referenced handle to the function, LabVIEW will pass a NULL handle to the function if you wire an empty string to the input parameter, but DSDisposeHandle() is protected internally against that and simply does nothing. But if you leave away the deallocation and don’t reuse the handle through the use of NumericArrayResize() things will go into memory leak land as soon as a noob goes into your VI and changes the empty string constant into a non empty one. Sure it’s not his business to do that but that someone might be you too in a year from now!!

Rolf Kalbermatter
My Blog
Message 4 of 7
(3,470 Views)

Thanks a lot again!!! The thing with the extra pointer to a handle was also my problem...doesn't make sense.

 

A lot of the things you point out I kind of already had in the back of my mind but it would have taken me ages to find all that out the hard way! Since I'm used to (extensive) MATLAB and some Python and Julia scripting, this means entering a somewhat different world of programming...

 

Great help and I'm beginning (I think) to understand the logic behind the LabVIEW <-> C++ communication 👍 There is loads of NI documentation and board topics, but it's sometimes hard to find out where to start with a specific problem!!

 

I tried to post my (naive) questions as clear and lengthy as possible so that others might find it useful some day when they run into the same issues!

 

 

Just one last thing though:

 


@rolfk wrote:

 

There you can configure if you want LabVIEW pass the C pointer (which for strings also makes LabVIEW resize the string to one more byte and at the end append a NULL.byte since that is how C strings are interpreted), or a handle and there if the handle itself or the pointer to the handle should be passed.


Your last sentence (when talking about simple string control/indicator handles outside clusters), that I can configure if I want to pass it as handle itself or a pointer to the handle... is there any reason why one is better than the other? Because if I understand correctly - and I think I have tried both ways at some point - both configurations allow passing of a string to the DLL, manipulating it there and passing it back to Labview. The only (obvious) difference is one more layer of de-referencing (*) in the code, but both ways work.

I see the difference for - say - a number where passing by value is totally different from passing a pointer. But as you pointed out (haha 🙄) a handle already is a pointer, so where would that extra layer of pointing to the handle make sense in passing to a DLL? Am I missing something obvious here?

 

Anyway.. thanks a lot again. I consider my problem solved (for now) 😉

0 Kudos
Message 5 of 7
(3,441 Views)

Since a handle is already a reference to a pointer it is indeed not necessary to pass a valid handle to a function by reference in order for the function to be able to modify it. The LabVIEW memory manager perfectly allows to resize a valid handle.

 

But, this falls apart of you want to allow passing in a NULL handle. If the function for whatever reason does not need that handle it simply leaves it alone, but the caller needs to allocate a valid handle that is large enough to hold the cnt value. Memory manager calls are expensive as they almost always call into the the system kernel to do the actual allocation, which will involve a kernel context switch and back. So doing this work to allocate a valid, empty handle only to throw it away afterwards with another memory manager call if the function does not use it is fairly expensive. Even if the function wants to use it it will have to resize it which costs another memory manager call for a memory reallocation.

 

By passing a reference to the handle the caller can simply pass in a NULL value (no memory manager call needed), the function can leave it alone if it doesn't need it and the caller then doesn't have to deallocate the never used handle. If the function does need a valid handle it can allocate it right away with the right size and fill it in whatever way is needed and then pass back the newly allocated handle through the reference parameter.

 

Yes the function has to be prepared that such handles can be NULL on entry (but they don't have to) so if you do DSNewHandle() and friends yourself you would correctly have to do something similar to this:

 

MgErr MyFunction(LStrHandle *pHandle)
{
    MgErr err = noErr;
    int32 len = 123,
    /* This is just for demonstration if your array would contain for instance doubles or pointers (string handles). For our string handle it is not relevant since the array element size is a char and therefore 1 byte */
          possiblePadding = sizeof(arraydataType) > sizeof(int32) ? sizeof(arraydataType) - sizeof(int32) : 0;
    if (*pHandle)
    {
        err = DSSetHandleSize(*pHandle, sizeof(int32) + possiblePadding + len);
    }
    else
    {
        *pHandle = DSNewHandle(sizeof(int32) + possiblePadding + len);
        if (!*pHandle)
            err = mFullErr;
    }
    if (!err)
    {  
         MoveBlock(str, LStrBuf(**pHandle), len);
         LStrLen(**pHandle) = len;
    }
    return err;
}

 

Or you can do this:

 

MgErr MyFunction(LStrHandle *pHandle)
{
    int32 len = 123;
    MgErr err = NumericArrayResize(uB, 1, (UHandle*)pHandle, len);
    if (!err)
    {  
         MoveBlock(str, LStrBuf(**pHandle), len);
         LStrLen(**pHandle) = len;
    }
    return err;
}

 

 You tell me which one looks simpler and more straightforward. 😀

Rolf Kalbermatter
My Blog
0 Kudos
Message 6 of 7
(3,426 Views)

You can find example code on how to do that (or similar things) at:

https://forums.ni.com/t5/LabVIEW/Returning-an-array-of-strings-from-a-DLL/m-p/4094785

 

0 Kudos
Message 7 of 7
(3,357 Views)