Home AMX User Forum AMXForums Archive Threads Tips and Tricks

STACK_VARs in Waits - a solution

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().
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:
ERROR: I:\amx\Master Code\sample.axs(219): C10246: Array parameter [STR] must have all bounds specified since it is referenced within a WAIT
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?

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.

Comments

  • Spire_JeffSpire_Jeff Posts: 1,917
    You forgot 1 option. You can use a Local_Var instead of Stack_Var.

    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
  • mpullinmpullin Posts: 949
    Spire_Jeff wrote: »
    You forgot 1 option. You can use a Local_Var instead of Stack_Var.

    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
    Isn't it the case that a LOCAL_VAR won't hold its value if there is a later call to the same function that changes the value? You make a good point though. Also forgot the "nearly fanatical devotion to the pope" option :p
  • Spire_JeffSpire_Jeff Posts: 1,917
    Yes, I believe that would be the case, but depending on the implementation of a Global Variable, the same situation would exist. (at least in my mind).

    And I don't think the pope option is available yet as AMX doesn't fully support PnP (Plug n Pray) :)

    Jeff
  • jjamesjjames Posts: 2,908
    Typically, I'd just do something like this (if there aren't many options):
    STACK_VAR INTEGER i
    
    SWITCH(i)
    {
       CASE 1: WAIT 10 SEND_STRING dvDEVICE,"'SENDING 1'"
       CASE 2: WAIT 10 SEND_STRING dvDEVICE,"'SENDING 2'"
       CASE 3: WAIT 10 SEND_STRING dvDEVICE,"'SENDING 3'"
       CASE 4: WAIT 10 SEND_STRING dvDEVICE,"'SENDING 4'"
       CASE 5: WAIT 10 SEND_STRING dvDEVICE,"'SENDING 5'""
    }
    
    Something along those lines works for me. This is how I handle it unless there are gobs of options, otherwise I just use a LOCAL_VAL and make use of naming the wait and CANCEL_WAITs. ;)
  • cwpartridgecwpartridge Posts: 120
    mpullin wrote: »
    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. But the questions remain... by who? and for what?

    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.
    Using Waits - limitations
    - References to STACK_VAR variables are not allowed within waits. STACK_VARs are temporary variables that cease to exist when the block in which they are declared is exited.

    - Variable copies are made of functions and subroutine parameters. This can have speed/execute penalties.

    - Within functions and subroutines, a RETURN is not allowed within a WAIT.

    - A BREAK or CONTINUE cannot appear within a WAIT if it takes execution out of the scope of the WAIT.

    - The code within a WAIT cannot reference a function or subroutine array parameter whose bounds are unspecified.
    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
  • Spire_JeffSpire_Jeff Posts: 1,917
    /*prepare for a thread hijack*/

    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
  • cwpartridgecwpartridge Posts: 120
    Spire_Jeff wrote: »
    /*prepare for a thread hijack*/

    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?];)
  • jjamesjjames Posts: 2,908
    Spire_Jeff wrote: »
    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.

    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?
  • cwpartridgecwpartridge Posts: 120
    jjames wrote: »
    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?
  • jjamesjjames Posts: 2,908
    Right, okay. That's what I thought everyone was saying because I remembered this thread here:
    http://amxforums.com/showthread.php?t=2571
  • mpullinmpullin Posts: 949
    jjames wrote: »
    Right, okay. That's what I thought everyone was saying because I remembered this thread here:
    http://amxforums.com/showthread.php?t=2571
    Ah yes, I remember this thread now. In fact I posted in it, but my post was never answered:
    mpullin wrote: »
    If you wanted to force Netlinx to leave the original value alone, couldn't you do something like CHECK_FOR_SOMETHING(nVAR+1-1) or even CHECK_FOR_SOMETHING(nVAR+0) ?

    Isn't that the same as passing the result of an expression like CHECK_FOR_SOMETHING(1+2)?
    Maybe if I finish all my tasks today I can test this.

    ... 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.
  • cwpartridgecwpartridge Posts: 120
    mpullin wrote: »
    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.

    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... :D
  • mpullinmpullin Posts: 949
    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... :D
    lol, I meant all variables involved in the function call, should have been more careful there. I'd think the ability to write a self-replicating program would be one of NetLinx's better guarded secrets... :D
  • AuserAuser Posts: 506
    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.

    I think I'm missing something here. Consider the following program:
    PROGRAM_NAME='Rubbish'
    
    DEFINE_CONSTANT
    long  TL_TIMES[] = {100}
    
    DEFINE_VARIABLE
    volatile integer nSomeValue = 0
    
    DEFINE_FUNCTION Accumulate(integer _nDelta, _nResult)
    {
      _nResult = _nResult + _nDelta
    }
    
    DEFINE_START
    TIMELINE_CREATE(1, TL_TIMES, 1, TIMELINE_ABSOLUTE, TIMELINE_REPEAT)
    
    DEFINE_PROGRAM
    send_string 0:1:0,"'nSomeValue=',itoa(nSomeValue),$0d,$0a"
    
    DEFINE_EVENT
    timeline_event[1]
    {
      wait 10
      {
        Accumulate(10,nSomeValue)
      }
    }
    

    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?
  • mpullinmpullin Posts: 949
    Part II : Help plz

    Okay so I revised my function to use different WAITS so I can stack up multiple events. Here is the result:
    DEFINE_FUNCTION SEND_COMMAND_TTL(DEV device,CHAR str[200], INTEGER TTL,INTEGER SEED){
        SEND_STRING 0, "'SEND_COMMAND_TTL ',str,':',itoa(TTL),':',itoa(SEED)";
        if(TTL == 0 || TTL > 65000) SEND_COMMAND device, str;
        else{
    	SWITCH(SEED){
    	    case 0: wait 10 'scttl 0' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 1: wait 10 'scttl 1' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 2: wait 10 'scttl 2' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 3: wait 10 'scttl 3' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 4: wait 10 'scttl 4' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 5: wait 10 'scttl 5' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 6: wait 10 'scttl 6' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 7: wait 10 'scttl 7' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 8: wait 10 'scttl 8' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 9: wait 10 'scttl 9' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 10: wait 10 'scttl 10' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 11: wait 10 'scttl 11' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	    case 12: wait 10 'scttl 12' SEND_COMMAND_TTL(device,str,TTL-1,SEED+0);
    	}        
        }
    }
    
    Now I fire off this sequence of commands:
    x = atoi("REMOVE_STRING(action,'`',1)");
    	    SEND_COMMAND vdvTSTAT, "'T-1 HEAT-',itoa(x)";
    	    SEND_COMMAND_TTL(vdvTSTAT, "'T-2 HEAT-',itoa(x)", 1, 1);
    	    x = atoi("REMOVE_STRING(action,'`',1)");
    	    SEND_COMMAND_TTL(vdvTSTAT, "'T-1 COOL-',itoa(x)", 2, 2);
    	    SEND_COMMAND_TTL(vdvTSTAT, "'T-2 COOL-',itoa(x)", 3, 3);
    	    SEND_COMMAND_TTL(vdvTSTAT, "'T-1 FMODE-',itoa(atoi("action"))", 4, 4);
    	    SEND_COMMAND_TTL(vdvTSTAT, "'T-2 FMODE-',itoa(atoi("action"))", 5, 5);
    
    and I'd expect to see all those commands fire off, a second apart, instead I see this:
    Line     14 (09:03:57):: ACTION: globalTemp`68`76`1
    Line     15 (09:03:57):: SEND_COMMAND_TTL T-2 HEAT-68:1:1
    Line     16 (09:03:57):: SEND_COMMAND_TTL T-1 COOL-76:2:2
    Line     17 (09:03:57):: SEND_COMMAND_TTL T-2 COOL-76:3:3
    Line     18 (09:03:57):: SEND_COMMAND_TTL T-1 FMODE-1:4:4
    Line     19 (09:03:57):: SEND_COMMAND_TTL T-2 FMODE-1:5:5
    Line     20 (09:03:58):: <unrelated event>
    Line     21 (09:03:58):: SEND_COMMAND_TTL T-2 FMODE-1:4:5
    Line     22 (09:03:58):: SEND_COMMAND_TTL T-2 FMODE-1:3:5
    Line     23 (09:03:58):: SEND_COMMAND_TTL T-2 FMODE-1:2:5
    Line     24 (09:03:58):: SEND_COMMAND_TTL T-2 FMODE-1:1:5
    Line     25 (09:03:58):: SEND_COMMAND_TTL T-2 FMODE-1:0:5
    Line     26 (09:03:59):: SEND_COMMAND_TTL T-2 FMODE-1:65535:5
    
    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. :(
  • jjamesjjames Posts: 2,908
    EDIT: Nevermind . . .
  • mpullinmpullin Posts: 949
    Update to above situation

    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:
    DEFINE_FUNCTION SEND_COMMAND_TTL(DEV device,CHAR str[200], INTEGER TTL,INTEGER SEED){
        SEND_STRING 0, "'SEND_COMMAND_TTL ',str,':',itoa(TTL),':',itoa(SEED)";
        if(TTL == 0 || TTL > 65000) SEND_COMMAND device, str;
        else{
    	SWITCH(SEED){
    	    case 0: wait 10 'scttl 0' SEND_COMMAND_TTL(device,str,TTL-1,0);
    	    case 1: wait 10 'scttl 1' SEND_COMMAND_TTL(device,str,TTL-1,1);
    	    case 2: wait 10 'scttl 2' SEND_COMMAND_TTL(device,str,TTL-1,2);
    	    case 3: wait 10 'scttl 3' SEND_COMMAND_TTL(device,str,TTL-1,3);
    	    case 4: wait 10 'scttl 4' SEND_COMMAND_TTL(device,str,TTL-1,4);
    	    case 5: wait 10 'scttl 5' SEND_COMMAND_TTL(device,str,TTL-1,5);
    	    case 6: wait 10 'scttl 6' SEND_COMMAND_TTL(device,str,TTL-1,6);
    	    case 7: wait 10 'scttl 7' SEND_COMMAND_TTL(device,str,TTL-1,7);
    	    case 8: wait 10 'scttl 8' SEND_COMMAND_TTL(device,str,TTL-1,8);
    	    case 9: wait 10 'scttl 9' SEND_COMMAND_TTL(device,str,TTL-1,9);
    	    case 10: wait 10 'scttl 10' SEND_COMMAND_TTL(device,str,TTL-1,10);
    	    case 11: wait 10 'scttl 11' SEND_COMMAND_TTL(device,str,TTL-1,11);
    	    case 12: wait 10 'scttl 12' SEND_COMMAND_TTL(device,str,TTL-1,12);
    	}        
        }
    }
    
    New result:
    Line      2 (10:27:05):: ACTION: globalTemp`68`76`1
    Line      3 (10:27:05):: SEND_COMMAND_TTL T-2 HEAT-68:1:1
    Line      4 (10:27:05):: SEND_COMMAND_TTL T-1 COOL-76:2:2
    Line      5 (10:27:05):: SEND_COMMAND_TTL T-2 COOL-76:3:3
    Line      6 (10:27:05):: SEND_COMMAND_TTL T-1 FMODE-1:4:4
    Line      7 (10:27:05):: SEND_COMMAND_TTL T-2 FMODE-1:5:5
    Line      8 (10:27:05):: <junk from some other module>
    Line      9 (10:27:06):: SEND_COMMAND_TTL T-2 FMODE-1:4:1
    Line     10 (10:27:06):: SEND_COMMAND_TTL T-2 FMODE-1:3:2
    Line     11 (10:27:06):: SEND_COMMAND_TTL T-2 FMODE-1:2:3
    Line     12 (10:27:06):: SEND_COMMAND_TTL T-2 FMODE-1:1:4
    Line     13 (10:27:06):: SEND_COMMAND_TTL T-2 FMODE-1:0:5
    Line     14 (10:27:07):: SEND_COMMAND_TTL T-2 FMODE-1:65535:1
    Line     15 (10:27:07):: SEND_COMMAND_TTL T-2 FMODE-1:65534:2
    Line     16 (10:27:07):: SEND_COMMAND_TTL T-2 FMODE-1:65533:3
    Line     17 (10:27:07):: SEND_COMMAND_TTL T-2 FMODE-1:65532:4
    
    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.
  • jjamesjjames Posts: 2,908
    I had a feeling that's what the problem was, but retracted my statement once I read you were using SEED + 0, which still . . . whatever. :D
  • Spire_JeffSpire_Jeff Posts: 1,917
    I wonder if (0 + SEED) would be interpreted any different??? or even (1 + SEED) - 1 ....

    Maybe the compiler is optimizing out the + 0? At least, that is my current thought.

    Jeff
  • mpullinmpullin Posts: 949
    Spire_Jeff wrote: »
    I wonder if (0 + SEED) would be interpreted any different??? or even (1 + SEED) - 1 ....

    Maybe the compiler is optimizing out the + 0? At least, that is my current thought.

    Jeff
    If the compiler did that, it would make TWO of us too elegant for our own good. :D

    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.
  • jjamesjjames Posts: 2,908
    Just curious . . . what exactly are you trying to accomplish with your function?
  • mpullinmpullin Posts: 949
    Mission statement
    jjames wrote: »
    Just curious . . . what exactly are you trying to accomplish with your function?
    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.
  • jjamesjjames Posts: 2,908
    mpullin wrote: »
    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.
    Indeed an interesting topic, though certainly nothing I want to play around with.
Sign In or Register to comment.