NI TestStand

cancel
Showing results for 
Search instead for 
Did you mean: 

Reset global/static global variables in DLL loaded by TestStand

I have projects where I create multiple sequence files that call a DLL with C or extern C'd C++ code.  Each project has a unique DLL.  Once the sequence execution starts the DLL is loaded ( preload since that's the default ) and the DLL isn't unloaded until the sequence file is closed ( again default behavior ).

 

What I'd like to do is the following:

 

* Be able to reset all global/static global variables in the DLL between executions

* Be able to run the top level or any subsequence independently

* Be able to run any sequence multiple times

 

I've considered the following solutions:

 

* Change each sequence to unload after execution  --> potential downfall if someone forgets to change the setting on a new sequence

 

* Add a function call to the DLL with some generic name and a step in a/all the process model sequence(s) to call and the function would call functions in each module to reset the static variables --> potential downfall if you forget one variable

 

* Update some known callback to do an UnloadAllModules --> have to change many sequences, possible to miss one

 

* Add a RunState.Engine.UnloadAllModules call to the process model sequence --> so far I can't get this to work.  I added it before the PreUUTCallback inside the UUT Loop ( SequentialModel ).  That way, each execution would be unloaded.  This doesn't seem to work and I think it's because the UnloadAllModules wants to unload the station model as well and it can't be unloaded since it's to be used and/or because it's in use when that step is called.

 

Does anyone have another thought on how to do that helps mitigate the concerns?  I can write the code to guard against variables not being cleared, can force a clear for some variables, and hope if all goes well that this is a mute concern but in reality you want to know the state of values at program start.  This is a function of the DLL being loaded and multiple executions potentially being run before the DLL is unloaded.

 

Thanks.

-G-

 

 

 

-G-
0 Kudos
Message 1 of 10
(9,715 Views)

It might be best to avoid the use of global variables altogether. If you have some per-execution state, you can pass those variables in as a parameter to your functions (you could even use struct passing to pass in multiple values). The initial value for the parameter could be based on a local variable in your top-level sequence.

 

The main reason why I'd recommend you not use global variables is that it makes it harder to make the sequences threadsafe. If you want to be able to run multiple instances of your execution at the same time, it will be harder to do so correctly if you are storing the state in global variable. If you don't care about this, then you could keep the global variables and expose initialization functions to clear them at various points in which you want them cleared. I think forcing the modules to unload is going to be more error prone (because you have to remember to always change the setting) and might have other unintended consequences. I think making an explicitly call into your dlls to initialize the state would be more straightforward and easier to maintain since it wouldn't require any special kind of load options.

0 Kudos
Message 2 of 10
(9,674 Views)

Thanks for the reply.  After some testing of unloading ( which doesn't seem to happen ) I concur that not messing with that would be the better route.  The problems I run into are: 1. I now have to add a step to each sequence and my goal was to reduce the number of initialization steps and 2. I don't know of a good way to determine if the initialization should occur without having a parameter be passed around in TS.  For instance, since I need to be able to run a subsequence 5 levels down independently ( for troubleshooting ) and having others skip the steps getting to that point is problematic I'd just tell the person to run sequence X.  That means it has to have the setup and clean up steps the same as the actual top level sequence.  Thus, when running sequence X the call to the reinit function would have to know that it hasn't been reinit and do so.  The problem is that I can't use a static global to track that ( the whole chicken and egg thing we're trying to solve ) so I'd have to have that data come from TS and since it needs passed among sequences it'd need to be a parameter.  While 1 is better than many my goal was to remove parameters that are solely used to hold data between function/step calls.  I want to separate parameter/local usage to data operated on in TS ( can be passed down if necessary but the data originated in TS and is manipulated in TS ) and have code variables/information that has no need to go into TS from going to TS.  Currently, I pass each piece of equipment's init count and handle(s) to TS and pull them back down in every test step.  I also have to pass them among all sequences.  This is error prone ( the order of the parameters, forgetting to check pass by reference, etc. ) and a pain when using the batch model as all the parameters are copied to file globals, updated, copied back, etc.  Maybe I'm trying to have my cake and each it too.  I really hoping someone else had needed to the state of their code to be in a known state and typically runs multiple test executions in a row ( without closing the sequence or TS ).  Is there any way to load test code without being in a DLL and not using LabView?  ( everything is straight C with the potential of C++ compiled in CVI or Visual Studio )  If the code doesn't persist between executions this all goes away.  Thanks.

-G-
0 Kudos
Message 3 of 10
(9,670 Views)

1) One idea for checking if you need to do the initialization (assuming you are still using global variables to store your state in this case) is to check to see if you have a non-process model caller (the assumption being, if you do have such a caller, then the global state has already been initialized). Something like the following should work:

 

Locals.DoInitializationAndCleanup = (!PropertyExists("RunState.Caller") || RunState.Caller.SequenceFile.SequenceFileType == SeqFileType_Model)

 

2) For the case of using a context variable and passing it around as a parameter, you don't necessarily need to pass the data into teststand in order for teststand to hold the variables for your code modules. Instead you can create an initialize function that returns a pointer to your data structure and a cleanup function which frees it. When calling your initialize function, use the "pointer/handle" type in the TestStand adapter and store the pointer in an Object Reference variable. Pass that pointer into your cleanup function to free it. Also pass the pointer into your other functions to access the state variables (the parameter can just be a pointer to the C/C++ data type of your struct/class). Storing state in a context variable (not the same as the sequence context) like this is a common software pattern. It allows you to have multiple separate sets of state variables active at the same time (allowing for multiple separate executions) and it's easy to add new variables to the context if you are using a struct or class as your context object because you can just add new member variables or fields to it. If you do things this way you can tell if you need to initialize your variable by using a precondition like:

Parameters.MyContext == Nothing

Then you can set a local varible in the post expression like:

Locals.DidInitialization = true

Then use the following for the precondition for your cleanup:

Locals.DidInitialization

 

Hope this helps,

-Doug

0 Kudos
Message 4 of 10
(9,649 Views)

Doug, these are great suggestions.

 

For (1) when I run it I get an Unknown variable or property name error for 'SequenceFile'.  RunState.Caller is present but SequenceFile isn't.  It seems there's a case where the first portion of the condition results in a false thus the second part is evaluated but the entire variable chain isn't present.  BTW, I'm using TestStand 2013.

 

For (2) I found the Object Reference variable but don't know what you mean by pointer/handle type in the TS adapter.  I assume you're referring to the adapter list ( LabView, LabWindows/CVI, C/C++ DLL, .NET, ActiveX/COM, HTBasic, and None is what I have ).  I'd normally use the LabWindows/CVI or C/C++ DLL adapter.  Oh, wait did you mean the pointer/handle type as a parameter ( think argument ) to an action or test step?

 

Currently, I don't have any completed projects that are multithreaded but we're looking at one now.  Since the test equipment would need to be shared the thought is to continue running the SequentialModel and AFTER setup have subsequences "Use New Thread" and then have a Wait step at the bottom of Main to collect results and synchronize before clean up.  Since subsequences will call setup and clean up as well I can use your (1) precondition on the init step.  For other equipment set up steps we already have the code increment an init count rather than reinitialize if that call into the function is not the first one.  We'd need to protect the init count, actual device setup/clean up and any shared resources, between threads/tests, which means we'd need to protect the global variables if we go that route.  Is there any other concern off the top of your head that we'd need to address?  The init count and device handles would be globals as previously stated.  In a previous reply and then with (2) solution you're eluding to removing the globals.  I understand they're not desired but since we don't want multiple copies, due to threads, of the same equipment variables ( which is what would happen with an init function returning a pointer into the Object Reference that's stored as a TS Parameter, correct? ) isn't protected the globals inside the code and not having the top level setup steps be threaded the better route? ( from the perspective of 1 instance of the variable, not passing it around sequences via parameters, etc. )  Any insight I'd appreciate.

 

Thanks!

-G-
0 Kudos
Message 5 of 10
(9,639 Views)

For (1), is that an analyzer warning or a runtime error? I used that expression in TestStand 2014 (I don't think 2014 has any difference in behavior than what 2013 had) and it worked in the cases which I tried. You can always do PropertyExists("RunState.Caller.SequenceFile") if you need to.

 

For (2), yes, I mean as the parameter or return value data type in the DLL adapter or CVI adapter. Basically, you can store a raw C/C++ pointer in an Object Reference variable using that data type.

 

I'm not sure I completely understand your description of your proposed architecture. But yes, if your globals represent shared instruments then leaving them as globals might make the most sense, you will need to acquire a lock or a critical section in each thread before accessing the gobal or instrument though. I have no idea what the global variables are/were for that you are using in your dll, so it's hard for me to know what kind of state is in there that could be thread specific, but if there is any thread specific state you might want to not use global variables for such state, or you could perhaps use thread local variables (kind of like per-thread globals) rather than passing a context variable around. Passing context variables around is a bit more flexible than thread local storage though in that it's not tied to a specific thread necessarily, but rather tied to the ownership and usage of the context variables.

 

-Doug

0 Kudos
Message 6 of 10
(9,603 Views)

Hello All,

 

  Jumping in with a question related to re initializing station globals.  You guys came very close to answering my question, so I think this is the right place to start.

 

I have a test sequence that uses Station Globals to define hardware resources (device names, physical names, handles, etc.).  The structure is pretty standard, with startup steps to initialize channels, and cleanup steps to close them.  When the sequence completes correctly, everything is cleaned up, and I can restart the test.  However, when there are errors during the test run that interfere with the cleanup steps (for example, TestStand may try to close a reference that doesn't exist) I can't re-run the sequence without closing and reopening TestStand (even if I run cleanup). Something about the rebooting process is correcting the problem.   I suspect this has something to do with the Station Global Object References, and that they are not "clean" when I attempt a re-run after errors.

 

  So my question is this: Is there a way to force the references to reset or release, outside of the usual "close" functions?  Please advise.  Thanks.

 

GSinMN

0 Kudos
Message 7 of 10
(9,534 Views)

Why not add protection to the clean up steps so they only run if needed ( precondition that setup executed and didn't error ) and then in the code module called by the clean up step have protection on the individual device handles/references/etc. so if they're not initialized nothing happens to them?  That answer seems simplistic so I hope I didn't misunderstand your question.

-G-
0 Kudos
Message 8 of 10
(9,529 Views)

Hello Grasshopper,

 

  Thanks for the quick response.  I should have added the note "or if there is a better way to do it please let me know".  Sounds like you understood the issue just fine. 

 

  So any suggeestions how I can check to see if a device is still initialized before I try to close the reference?  Thanks much.

 

GSinMN 

0 Kudos
Message 9 of 10
(9,527 Views)

Sorry for the delay; I don't recall seeing an email that you replied.

 

To detect if the device is initialized you could do a couple of things.

1. Depending on the device and your type of code if the reference to the device is a pointer value then you could use !0 for initialized.

2. You could add an initialization count to your device code.  I do that and is part of my original question.  Example:  we have code for controlling a serial port.  An integer init count and the HANDLE of the port are passed back and forth to TestStand.  So, in a setup step the setup code is called and it reads a TestStand parameter that holds the init count.  If the count is 0 we actually set up the port.  Then we increment the count.  If the count is >0 we just increment the count.  The count is stored back to TS.  Every test step in the Main ( non-setup, non-cleanup ) area of TS can access the serial port by calling a function that gets the init count, confirms it's >= 1, and gets the HANDLE value from TS.  In a clean up step ( which is preconditioned to only run if the setup step ran and didn't error [assumes if an error occurs during setup post actual serial port setup that the cleanup is done there in setup]) the init count is checked again.  If it's >1 it's decremented.  If it's 1, the actual serial port is closed and then the count is decremented again.  The init count and HANDLE parameters are passed amongst sequences and the setup and clean up steps are copied to each sequence. 

 

Do either of those help?

 

We may be hunting for the same solution though, ultimately.  Thus, I want to eliminate the init count and handle parameters in the sequences and have a global for each device.  I.e. a setup step for a power supply would still check the init count or pointer value and do work like I just mentioned above but instead of storing the device handle and init count in TS they'd be global variables in a DLL.  Each sequence would still have the same setup and clean up steps but they wouldn't be reading and storing to/from TS Parameters/Locals/Station Globals, etc. except once for the device details for actual setup.  We also have a step right after the setup step that sets the device to defaults.  Over time it gained a precondition to occur only when the init count was 1.  ( don't want a power supply resetting, etc. )  Thus, that step's code I'm moving into the setup function.  It's one time init and that code is guarded with it's own flag.  All is well if clean up steps are ran properly.  What I want to guarantee is that those variables are reset the next time you run the sequence.  I.e. you run the sequence and then say rerun without closing TS or unloading the module.  Using Dug's advice I have a setup tests step that resets all the globals in one function.  That setup step is preconditioned to only run if sequence running is the top level sequence.  I.e. it works if you run a subsequence as the top level or if you run the top level sequence.  The kicker here is that you still have to remember to add your reset code to the setup tests function.  If you forget a variable the state could be off badly.  Thus, I'm searching for a better way to guard against that.  My biggest dilemma is that the lifetime of those variables is the lifetime of the DLL and the DLL's lifetime isn't the same as a test execution's lifetime.

-G-
0 Kudos
Message 10 of 10
(9,115 Views)