LabVIEW

cancel
Showing results for 
Search instead for 
Did you mean: 

Load LabVIEW-created dll in .NET Framework or .net Core

Solved!
Go to solution

Does anybody have experience with creating a normal dll in LabVIEW (not an interop assembly) and loading this dll in .net Framework or .net Core?

 

I am trying to share LabVIEW code with .NET developers via a dll that I have created in LabVIEW but have run into problems when simply attempting to add the dll to the list of references in .net. Any comments and help on this topic would be appreciated. Thanks.

0 Kudos
Message 1 of 8
(4,938 Views)

You can't add a normal DLL to a .Net project and hope that your IDE simply does the right thing. Normal DLLs do not contain any information aside from the function name itself that .Net could use to create a correct Interop wrapper for.

You (or someone in the knows about .Net Interop) needs to provide a definition of the function calls that you want to use in the language that you are using (most likely C# but if your users use something else you have to create an according definition for every language you want to support.

 

For each function you want to let the programmer interface to you need to define an according interface prototype definition similar to this:

 

[DllImport("user32.dll", CharSet = CharSet.ANSI)]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

 

This tells the C# compiler where to load your function from and what datatypes the function parameters have. 

Rolf Kalbermatter  My Blog
DEMO, Electronic and Mechanical Support department, room 36.LB00.390
Message 2 of 8
(4,888 Views)

Thank you for the information rolfk.

 

Now I have been able to load a LabVIEW-built dll in .net Core using the method you have described using ints and doubles as inputs and outputs. I've come across another issue when attempting the same for LabVIEW strings.

 

The example you provided shows a prototype using String's but I am noticing that LabVIEW provides a string output with three possible ways to pass the string in the .h file that LabVIEW builds in addition to the dll. These three ways are as a (i) String Handle Pointer (LStrHandle *StringOut) , (ii) C String Pointer (char StringOut[], int32_t len), or a  (iii) Pascal String Pointer (PStr StringOut, int32_t len). None of these is directly mentioned as a string/String like the prototype that you have attached.

 

Do you know how these LabVIEW data types should be referred to when calling the functions in C#?

0 Kudos
Message 3 of 8
(4,875 Views)

Well, this has less to do with datatypes than memory management. The strings in the example are all input parameters to the function. That means that the caller has allocated the string already and simply passes it to the DLL function. The DLL function only reads the contents of the string and does NOT change anything in it. If you want to pass back a string from a function to the caller you have a fundamental problem.

The caller doesn't know how big the string is so it can't just allocate the right amount of data and hand it to the function to fill in the info. But letting the function allocate the string buffer and returning it to the caller has the problem that the function can't deallocate the buffer later on but instead the caller has to do that, but that only works if both the caller and callee agree which memory allocator to use for allocting and deallocating the buffer. In C there is no common memory allocator that is guaranteed to always work, in fact each C runtime library implements its own memory allocator (and Windows has itself several different ones that are used depending on the submodule the code is contained in).

There are several possible solutions for this:

1) Let the caller allocate the buffer anyways and tell the function in an extra parameter how big the buffer is that it can fill in. This again has several suboptions that the programmer of the function has to choose from in case the passed in buffer is to small:

1a) fail the function call with a failure, either a general failure that just says, to bad but I can't do this

1b) or a more specific failure that says, sorry one of the passed in buffers is to small please try again with a larger one

1c) or optionally return in an extra parameter the needed size to allocate a bigger buffer and pass this in a second call to the function again

1d) and last but not least just copy as many data into the buffer as it can hold and truncate the rest

 

2) let the function allocate the buffer (only possible by defining the parameter as a pointer to a buffer pointer) and specify in the documentation which memory allocator to use to deallocate the buffer afterwards (but that won't work for malloc()/free() as each C runtime has its own implementation so if your DLL was compiled with a different compiler (version) than the caller they will still not use the same memory allocator. You explicitedly have to use an OS provided allocator like GlobalAlloc() to not create nasty problems).

2a) or provide an extra function in the DLL to deallocate the parameter afterwards that will use the same memory allocator that was used when creating the buffer and that your caller needs to explicitly invoke.

 

3) Define a specific managed contract that defines in detail how memory buffers need to get allocated, resized and deallocated. .Net managed code is such a management contract, LabVIEW memory handles is another one, significantly different to the one .Net uses.

 

LabVIEW DLLs when you define a string or array handle to be passed out also include an extra exported function to allocate and deallocate the according memory handle. So if you choose that option in your DLL all you would need to do is pass in an IntPtr by reference initialized to NULL and the LabVIEW DLL will then allocate a LabVIEW string handle of the necessary size and return it and you simply need to use the extra provided function afterwards to deallocate that buffer again. But LabVIEW memory handles are a little bit difficult to handle from non C code. They are in fact a pointer to a pointer to a memory buffer where the first four bytes are an int32 formatted in the machine endianess that specify the number of 8 bit characters that will follow. Dereferencing that double pointer in C#, while not impossible, is not a trivial task.

The other two variants require the caller to allocate a buffer and pass in the allocated length in the extra len parameter. The LabVIEW DLL will then first execute the VI that creates the string handle and after that copy from that handle as many bytes as you specified in the len parameter into the buffer. It will also terminate the string with a NULL character for C string pointers as that is how C strings are terminated, so in fact it will copy up to len - 1 elements into the buffer (less if the created string is smaller) and then append a NULL character after that. The memory management for the Pascal string pointer works the same but the DLL will copy len - 1 characters into the buffer starting at byte offset 1 and then update the first byte (byte offset 0) in the buffer with the length of the string. No NULL character is appended here as the length is specified in the first byte. Also important to realize is that since the length of a Pascal String is defined by the first byte in the string buffer, such a string can at most contain 255 characters.

 

So for your LabVIEW DLL you most likely want to go for the C String Pointer option and add according Interop attribute keywords to the parameter definition that tell .Net Interop that it needs to preallocate a certain amount of bytes for that buffer and also pass that number to the len parameter. And live with the limitation that the returned string may be truncated if you choose a to small buffer size for .Net Interop to allocate.

 

 

[DllImport("myDLL.dll", CharSet = CharSet.Ansi)]
public static extern int MyFunction(String input,
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] String output, int len);

 

 

Something like this should work although I did not test this or even tried to compile it, so there might be some syntax issues with it.

Alternatively you might want to investigate Custom Marshallers to specifically marshal the data in custom C# code that you also have to provide in your interface file.

Rolf Kalbermatter  My Blog
DEMO, Electronic and Mechanical Support department, room 36.LB00.390
Message 4 of 8
(4,853 Views)

Thanks for the insight and information rolfk.

I've added functions in a LabVIEW-created dll that (i) simply outputs a constant string, and (ii) has an input of a string and simply outputs the string with no manipulation to the string. I've tried to call these in .net Core with multiple attempts and different prototypes. I've reduced my code to only performing case (i), included below.

 

 

namespace ConsoleApp1
{
    class Program
    {
        [DllImport("LVarithmetic.dll", CharSet = CharSet.Ansi)]
        public static extern void PrintString([MarshalAs(UnmanagedType.EXAMPLE)] out string Strout );

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            string tempstring;
            PrintString(out tempstring);
            Console.WriteLine(tempstring);
            Console.ReadKey();
        }
    }
}

 

where .EXAMPLE has been (LPstr, LPWstr, LPTStr, Bstr, and HString). Each of LPstr, LPWstr, LPTStr, Bstr cause a crash of the exe during runtime. HString doesn't crash the exe, but prints out a bunch of jibberish: ?  ?`? ?? AA??? ???????? ? ? ??k?????????????  ???????? ????????????? ??? ??? ?? ??? ? @ ? ??? ?? @ ??? ??? ??? ?? ??? ? @ ? ??? ?? @ ??? ??? ??? ?? ??? ? @ ? ??? ?? @ 

 

I also tried it with your recommended:

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)]

option but an error pops up under the UnmanagedType.ByValTStr string, telling me that ByValTStr is valid only for fields. I noticed that if I define a struct and struct element with the ByValTStr and SizeConst option the compiler will not provide an error.

 

At this point I am simply trying different options and seeing if anything returns something that works.

 

I also read somewhere about creating a struct in C# that represents the data type that the dll is expecting. In this case it will be an public int and public char array (likely with the MarshalAs option that you provided ByValTStr). Here's what I'm thinking to try first:

struct LStrHandle {
    public int cnt;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)]
    public char []str;
}

This is also me flying blind for the most part and seeing if I can run into something that works.

 

I'd appreciate any further comments and insight into this topic that you might have.

0 Kudos
Message 5 of 8
(4,823 Views)
Solution
Accepted by topic author Zeagler

I have found an example that NI posted on how to perform this task with a string. It uses a Stringbuilder datatype in C# that was not even on my radar.

 

Anyway, here's the link to the example: https://forums.ni.com/t5/Example-Programs/Using-NET-to-Call-LabVIEW-DLLs-That-Pass-String-Data-Types...

This link comes with a zip file with all the necessary files, though there may be a mismatch between your runtime engine and the included created-dll. I opened the LabVIEW-aspect of the project and re-built the dll for my version of LabVIEW and used this.

 

To be fair, I only found this link after finally finding someone that previously ran into this problem on stackoverflow: https://stackoverflow.com/questions/19912212/function-parameters-for-labview-dll

 

I haven't implemented this in my code but yet, but I'm happy to share this path forward. Cheers.

0 Kudos
Message 6 of 8
(4,805 Views)

Don't forget to mark your answer as the solution.  This is sure to help someone in the future.  🙂

Bill
CLD
(Mid-Level minion.)
My support system ensures that I don't look totally incompetent.
Proud to say that I've progressed beyond knowing just enough to be dangerous. I now know enough to know that I have no clue about anything at all.
Humble author of the CLAD Nugget.
Message 7 of 8
(4,796 Views)

Ah, yes. Thank you billko.

 

As a follow-up, I was able to implement the described method within the dll that I was creating IN 64-bit LabVIEW '17. I was also able to call this dll in .NET Core, which was the goal. I did have to go through some debugging during the process and one thing that I hadn't originally noticed was that when creating the dll in LabVIEW the calling convention is set to standard. I went ahead and implemented this and like I said it works - so I'm not sure on its significance but it might be something to look at if there's an issue during implementation.

 

 

0 Kudos
Message 8 of 8
(4,788 Views)