STACK_VARs in Waits - a solution
mpullin
Posts: 949
Hi. I have come up with a solution for one of the most hated problems in NetLinx: How do you send a string after a period of time that includes a value from a STACK_VAR? The problem is, by the time the WAIT executes, the STACK_VAR no longer has a value and you get an error. Until now, your choices have been
1) Avoid this situation.
2) Put the contents of the STACK_VAR into a global var that you promise not to change in the next few seconds.
3) Make a switch/case that includes a case for every value the STACK_VAR could contain.
Well, now we have a 4th option. It's a tiny function I wrote that I like to call SEND_STRING_TTL().
What does it do?
SEND_STRING_TTL(dvDevice,"'I have ',itoa(nApples),' apples.'",4) will tell your device how many apples you have right now, 4 seconds from now. This function can be called anywhere. nApples can be a STACK_VAR, and dvDevice can be DATA.DEVICE!
How do we know it works?
You should try it for yourselves. Put this function in your program and call it with your diagnostics open. You'll see something like:
What's going on here?
The function is passed a device, string, and TTL (time to live).
If TTL is 0, we want to send the string to the device now and that's all.
If TTL is not 0, we want to recursively call this function again, after 1 second, passing a TTL that is one less. Like all recursive functions there must be a base case, in this case, TTL=0, that we are guaranteed to approach. Since INTEGERs can't be negative, we will definitely approach TTL=0 with each call.
How does it work?
I'm not sure exactly. My theory is that at the moment NetLinx sees a function call, it makes a copy of all the parameters of that function and includes them in some kind of a stack. Notice that if you take the 200 out and leave the str parameter unbounded, you get this error:
What can I do with this?
Any time you want to take an action later using the value of a STACK_VAR, or take an action longer using the target of an event like DATA.DEVICE, you can do it by using this recursive function. Recursive function calls can be a haven for those temporary variables that dry up like zombies whenever the light from outside their own scope shines on them.
1) Avoid this situation.
2) Put the contents of the STACK_VAR into a global var that you promise not to change in the next few seconds.
3) Make a switch/case that includes a case for every value the STACK_VAR could contain.
Well, now we have a 4th option. It's a tiny function I wrote that I like to call SEND_STRING_TTL().
DEFINE_FUNCTION SEND_STRING_TTL(DEV device,CHAR str[200],INTEGER TTL){ SEND_STRING 0, "'SEND_STRING_TTL(',itoa(device.NUMBER),':',itoa(device.PORT),':',itoa(device.SYSTEM),' ',str,' ',itoa(TTL),')'" // this is for demonstration purposes if(TTL = 0) SEND_STRING device, str; else{ wait 10 SEND_STRING_TTL(device,str,TTL-1); } }
What does it do?
SEND_STRING_TTL(dvDevice,"'I have ',itoa(nApples),' apples.'",4) will tell your device how many apples you have right now, 4 seconds from now. This function can be called anywhere. nApples can be a STACK_VAR, and dvDevice can be DATA.DEVICE!
How do we know it works?
You should try it for yourselves. Put this function in your program and call it with your diagnostics open. You'll see something like:
Line 59 (14:40:31):: SEND_STRING_TTL(0:8:1 I have 18 apples. 4)
Line 60 (14:40:32):: SEND_STRING_TTL(0:8:1 I have 18 apples. 3)
Line 61 (14:40:33):: SEND_STRING_TTL(0:8:1 I have 18 apples. 2)
Line 62 (14:40:34):: SEND_STRING_TTL(0:8:1 I have 18 apples. 1)
Line 63 (14:40:35):: SEND_STRING_TTL(0:8:1 I have 18 apples. 0)
What's going on here?
The function is passed a device, string, and TTL (time to live).
If TTL is 0, we want to send the string to the device now and that's all.
If TTL is not 0, we want to recursively call this function again, after 1 second, passing a TTL that is one less. Like all recursive functions there must be a base case, in this case, TTL=0, that we are guaranteed to approach. Since INTEGERs can't be negative, we will definitely approach TTL=0 with each call.
How does it work?
I'm not sure exactly. My theory is that at the moment NetLinx sees a function call, it makes a copy of all the parameters of that function and includes them in some kind of a stack. Notice that if you take the 200 out and leave the str parameter unbounded, you get this error:
Given the very specific language of this error, it's hard to believe the developers of NetLinx didn't have this usage in mind. But the questions remain... by who? and for what?ERROR: I:\amx\Master Code\sample.axs(219): C10246: Array parameter [STR] must have all bounds specified since it is referenced within a WAIT
What can I do with this?
Any time you want to take an action later using the value of a STACK_VAR, or take an action longer using the target of an event like DATA.DEVICE, you can do it by using this recursive function. Recursive function calls can be a haven for those temporary variables that dry up like zombies whenever the light from outside their own scope shines on them.
0
Comments
I do like your solution tho as I think it will allow multiple events to be queued simultaneously, but I would be inclined to test it just to make sure.
Jeff
And I don't think the pope option is available yet as AMX doesn't fully support PnP (Plug n Pray)
Jeff
If you take a look at the NetLinx KeyWords Help section under Waits: Using Waits - Limitations you'll see this behavior was designed for a specific reason. To understand why this was done in this manner, one needs to remember how parameters are passed to a function. They are passed by reference, which means no copy is made of the parameters that are passed to a call or function.
Hopefully I explain the thought process clearly...
If a function is called in a wait, and if we had purely passed by reference the parameters to that function in the wait, then if the value of the parameter(s) changed between when the wait was started and when it expired, the result of the function call in the wait would most likely differ from what the the programmer expected because the paramaters values had changed.
To try and overcome this, it was decided when NetLinx was designed that if a function was seen inside a wait, the compiler would automatically make a copy of the parameter reference and that copy would be referenced in the function call made by the wait code. Think of it as a snapshot of the function parameters when the wait was started.
The reason that passing by reference was done in the first place was to eliminate copying of parameters of a call or function, therefore making the execution faster since the time to copy is not needed. The reason the limitation states this can have speed/execution penalties is because we do need to make a copy of the parameters which takes longer than if we purely passed by reference.
HTH,
Chuck
Chuck,
Thanks for the explanation and I think I understand what you are talking about on all accounts. You talked about variables being passed as references to functions, which is something I am actually taking advantage of on some new code (I think), but this caused me a slight problem... Things are passed by reference, but I have not found a way to declare a pointer or reference variable. Is there any way to create a variable that is just a reference variable? I ask this only because there was a point where having a reference variable would have made my life much simpler. If it helps, I will try to remember the exact situation and post details.
Thanks,
Jeff
Sorry. No there is no way to declare a reference type in NetLinx. What I was describing is what takes place entirely under the hood but is not exposed.
[/HIJACK?];)
Maybe I'm not following, but are you saying that when I pass a global variable into a function, that it only references it? And if so, when you say reference, do you mean it creates a copy of it?
All under the hood here. There is not NetLinx syntax to enable the following explanation.
A reference is an internal address of the variable.
When passing by reference, we pass an internal address of the variable, not a copy of the variable. Since under the hood references are used constantly it is an easy way to determine which variable was used as the param.
If we made a copy, we'd have to create a copy of the right variable type, copy the contents, pass the copy, and destroy the copy when we are done. That's why we try and not pass by copy if it can be helped
Is that a little less murky?
http://amxforums.com/showthread.php?t=2571
... main topic now...
So what you're saying, cwpart, is that my SEND_STRING_TTL() does accomplish the desired goal but has speed/execute implications due to a copy of all variables being made every second. So one would have to keep that in mind before using it as a convenience tool in memory-tight projects. Thanks for your explanation btw. Very clear.
Just to clarify, it is not making a copy of ALL variables, just the variables that are parameters to any function in the body of the wait.
In your example, the compiler would make a copy of DEV device,CHAR str[200],and the result of the expression TTL-1 each time. By the time your example was finished, the params had been copied 5 times each (though only one set of copy params written to it 5 times).
Didn't want to make anyone cringe with the thought of all variables in a program being copied by this behavior...
I think I'm missing something here. Consider the following program:
If the above were strictly true, nSomeValue would never be altered because each call to function Accumulate() would be altering a copy of the variable, not the variable itself. And yet, when the program runs, nSomeValue is incremented each time the wait expires.
Does this therefore mean that when exiting the wait, the copied variables are written back to the original variables?
Okay so I revised my function to use different WAITS so I can stack up multiple events. Here is the result: Now I fire off this sequence of commands: and I'd expect to see all those commands fire off, a second apart, instead I see this: Only the T-1 HEAT (the pure SEND_COMMAND) and the T-2 FMODE (the final queued command) commands actually get sent to the device. The middle commands get clobbered even though I'm passing in a different SEED for each one, so they are using different WAITS... and the TTL and SEED are passed in as expressions (that's why I pass SEED+0 and not SEED), so NetLinx should be making a copy of those, right?
Why do all the events fire at the same time, 1 second after the initial function calls were made? How the heck does TTL get down past 0? (observe the 65535, highest possible int value)
I thought I understood how NetLinx deals with function calls and waits, but apparently I don't. Please help me, seasoned vets. My puppy is sick.
I couldn't decide why I was using the variable SEED inside a Switch that was based on the value of SEED. After all, I know what the value of SEED is, why not type it?
I didn't expect to get a different result... but I did. New function: New result: Very interesting. It's like the value of SEED whenever the function is called changes that value within other recursive instances of the same function. More tests are required.
Maybe the compiler is optimizing out the + 0? At least, that is my current thought.
Jeff
I am going about writing my program another way for now, due to a deadline, but I'm still very interested in this problem. BTW a problem still exists, hard coding the SEED is a step in the right direction but it appears the other 3 parameters are overwriting those in other instances which would explain how instances with "negative" values are surviving.
I want to create a function that can queue up a SEND_STRING or SEND_COMMAND to fire after a few seconds, with the possibility of including an event variable e.g. DATA.DEVICE or stack variable - neither of which can be passed into a wait - without creating a queue in memory i.e. a data structure that must be a set length and thusly waste memory.
At this point I have created another solution to my problem, which involves a queue data structure, but I still like this problem because it explores the relationship between recursive function calls and waits in NetLinx, as far as I can tell a previously untapped topic.