Launched Versus Called Modules

There are five ways to run a module. Each has a different behavior and a different effect on your application. Of these five, the most frequently used are called modules, launched modules and subroutines.

Called Modules

Started from steady-state code. For example, displaying a widget on a page or a sub-process that runs while the state is open. And example of the later would be a driver in its Connected state calling a ReceiveUpdates submodule.

The module is called into the current state and runs only while that state remains active. When control passes to another state, all called modules stop and resources that they were using are freed. It is important to be aware of the timing for when this happens. Given the following example:

If Action_trigger NextState;
[
 { script block }
]

All called modules in the current state stop as soon as action_trigger becomes TRUE. If code in the script block depended on a called module, it will fail because the variable holding that called module is now Invalid.

Parameter values are passed by reference. Any change to the parameter values in the calling or the called module will affect those in the other.

A set of parenthesis must always follow the module call, regardless of whether parameters are included within in them.

The term, "calling state", is used to refer to the state from which the module is called. Called modules may themselves contain module calls but take care not to call the first module from within the second. A circular or a recursive situation will result in the modules calling each other repeatedly until resources are depleted and the application crashes.

A return statement is optional.

Double-Sets with Called Modules:

Use care if you decide to create a called module to change the value of a variable. For example:

X = SomeCustomFunction(X);

When called in steady state, this is a double-set of X, therefore X will be Invalid. The reason is that X is passed by reference, and set within the called module as well as being explicitly set by the return value of the module. In steady state, the following would be better:

SomeCustomFunction(X);

Called Module Template

<
MyCalledModule
(
  { Parameters }
)
[
  { Variables }
]
Main [
   { Steady-state code... }
  Return( { Return value, if any } );
]
>

Example: A called graphics module

The called module in this example is written as a submodule. It will take a value from the calling module and increment it. Both the calling and the called module will display the value.

In the following example the parameter CounterVal is initialized to zero when it is declared in the submodule Counter. This does not prevent a value from being passed to the submodule. The initialization value is used only if no parameter is provided when calling Counter.

  1. Create a new script application and edit the AppRoot.SRC file as follows:
{================================= System ====================================}
{=============================================================================}
(
  System                      { Provides access to system library functions   };
  Layer                       { Provides access to the application layer      };
)
[
  Graphics               Module { Contains user graphics                      };
  Counter                Module { Another module                              };
  WinTitle = "User Application" { Window title                                };
  RunningOnVIC                  { TRUE if this is a VIC session               };
]

Init [
  If 1 Main;
  [
    RunningOnVIC = IsVICSession();
  ]
]

Main [
  Window(  0,   0           { Upper left corner   },
         800, 600           { View area           },
         800, 600           { Virtual area        },
         Graphics()         { Start user graphics },
          {65432109876543210}
         0b00010000000110011,
         Concat(WinTitle, RunningOnVIC ? " - %S" : ""),
         0, 1);
]

<
{============================= System\Graphics ===============================}
{ This module handles all of the graphics for the application                 }
{=============================================================================}
Graphics
[
  X = 10;
]


Main [
  ZText(100, 100, Concat("X = ", X), 2, 0);
  Counter(X);
]
{ End of System\Graphics }
>


<
{============================= System\Counter ================================}
{ A simple called module to increment and display a value                     }
{=============================================================================}
Counter
(
  CounterVal;
)
Main [
  ZText(100, 140, Concat("CounterVal = ", CounterVal), "<FF00FF00>", 0); 
  If TimeOut(1, 2);
  [
    CounterVal++;
  ]
]
>

Return statements in called modules:

  1. Continue with the same application...
  2. Add a second variable to the Graphics module, Y.
  3. Display Y below X.
    (After doing so, check the coordinates of all the ZText functions in all the modules to ensure that one does not display in the same space as another.)
  4. In Counter, add a return statement in steady state as follows:
Return(CounterVal * 2);
  1. In Graphics, assign the return value of Counter to Y:
Y = Counter(X);
  1. Run the application and observe the result.
  2. Move the Return statement to the script block in Counter.
  3. Run the application. You might need to start and stop it a few times to notice the difference in behavior. Hint: The script block doesn't run (and therefore nothing is assigned to Y) until the next TimeOut.

State changes and called modules

  1. Continue with the same application...
  2. Working in the Graphics module, add a new state and a trigger to switch to that state as follows: Note the new colors in ZText, to help indicate the new state.
...
  If X >= 15 SecondState;
]
SecondState [
  ZText(100, 80, "Now in the second state", "<FFFF0000>", 0);
  ZText(100, 100, Concat("X = ", X), "<FFFF0000>", 0);
  ZText(100, 120, Concat("Y = ", Y), "<FFFF0000>", 0);
]
  1. Try to answer the following questions:
    What happened to the display of CounterVal?
    Why isn't Y double the value of X1?

Launched Modules

Started from within a script block. For example, launching several Analog Status tags when an application starts. The launched modules have a life that is independent of the state containing the script block and will continue to run and use system resources until they are slain explicitly, or until the caller (not state) stops.

Uses for launched modules include:

  • A code snippet that you want to run once - much like a typical function in other languages.

  • Anything where you need a number of independent instances, such as tags.

  • A sub-process that keeps running even if the calling module changes the active state. Calls to retrieve tag history for a report are a good example, where one state builds and launches the query, then another state waits for the query to finish and processes the result.

  • A sub-process that does an assigned task, then slays itself when finished. The HTTPSend() function is an example.

Parameter values are passed by value, with values that are current at the time the module is launched. It is very common to use pointers to parameters when you need to pass information out of a launched module.

If, in addition to launching the module, you want to create a variable that holds a reference to the object value of that module, use the form, X = MyModule( ); The return value of this function is always the object value of the launched instance.

Launched Module Template

<
MyLaunchedModule
(
  { Parameters }
)
[
  { Variables }
]
Main [
  { Code. Often involves a sequence of 
    asynchronous actions over multiple states, 
    ending in a Slay(). }
]
>

Launching a module

  1. Continue to work in the same application as for called modules.
  2. Remove all statements that contain the variable Y.
  3. Add an initialization state to the Graphics module as follows:
Init [
  If 1 Main;
  [
    Counter(X);
  ]
]
  1. Remove the Return statement from Counter.
  2. Run the application.
  3. Change the trigger to transition to SecondState in Graphics as follows:
If Timeout(1, 10) SecondState;
  1. Run the application again.

The expected behavior is for X to keep counting up, regardless of the state that the parent module is in.

Subroutine Modules

These look exactly the same as a launched module except that the module in question contains a Return() statement. Execution of the thread that launches a subroutine will stop, waiting for a value to be returned. Subroutines that take time to return can have a severe effect on your applications.

The Return() function will have varying effects on your code depending on how it is used. You should take the time to read the notes for this function carefully before writing subroutines.

A Return() statement in a called module does not have the same effect that it does in a subroutine.

A subroutine may launch another module but note that a subroutine is never considered to be the caller or owner of any module that it launches. Rather, the nearest parent object that is not a subroutine is considered to be the parent of both the subroutine and any subsequent child modules launched. This means that a subroutine may end but any modules that it launches will continue to run until the caller ends or until those modules are slain. Use with care.

An object is slain when its caller is slain, not its parent. If the parent is slain, parentage passes up to the grandparent, if any.

Subroutine Template

<
MySubroutine
(
  { Parameters }
)
[
  { Variables }
]


Main [ 
  If 1;
  [
    { Script code... }
    Return( { Return value if any } );
  ]
]
>

Callable Subroutines

These are a special case of subroutine in that they work equally well in both script and steady-state

One problem with creating subroutines is that you may not have control over where and when the module may be called. For example, you might write the following, intending that this module be called only from within a script block:

<
ConvertXY
(
  X          { Value to be converted                            };
)
[
  Result     { Calculated result                                };
]


Main [
  If 1;
  [
     { Result = math for the calculation goes here. }
    Return(Result);
  ]
]
>

If this module is called from steady state, you will have an If 1 condition. To avoid this, a watch function can be used to ensure that the code only executes once:

  If Watch(1);
  [
     { Result = math for the calculation goes here. }
    Return(Result);
  ]

While this will prevent the "If 1" condition and work either as a called module or as a subroutine, there is no allowance for its parameters to change when used as a called module. By including these in the watch we now have a universal function that can be called from a script as a subroutine or from a module's active state code where the returned value will change as the calling parameters change

  If Watch(1, X);
  [
     { Result = math for the calculation goes here. }
    Return(Result, FALSE);
  ]

Note the addition of the second parameter in the Return() statement. This should be included unless you intend to create a resettable function such as Watch() or TimeOut(). It's likely that you do not.

Callable Subroutine Template

<
MyCallableSubroutine
(
  { Parameters }
)
[
  { Variables }
]
Main [ 
  If Watch(1, {, Parameters });
  [
     { Script code... }
    Return( { Return value } );
  ]
]
>

Call a script-only function in steady-state

Building on the information just presented, you can create a subroutine to run a script-only function as needed and use that subroutine in your steady-state expressions within tags and graphics. A common example is to call the CurrentTime() function.

  1. Create the following module as a file in your application:
RightNow.SRC
[
  RightNow;
]


Main [ 
  If Watch(1);
  [
    RightNow = CurrentTime();
    Return(RightNow);
  ]
]
  1. Declare the module in the Plugins section of your application's AppRoot.SRC:
  [ (PLUGINS)       {===== Modules added to other base system modules =====}
    RightNow    Module "RightNow.SRC";
  ]
  1. Import file changes.
  2. In your application, create an I/O and Calculations tag that uses the Numeric Calculation mode.
  3. In the Calculation tab of your tag, open the Expression Editor and enter the following:
Code.RightNow()
  1. Note that, much like using an AbsTime() function in an expression, this is evaluated once and then does not update.

 

Threaded Modules

A threaded module is like a launched module, except each will run in its own thread. Use only when there is a clear need. Each new thread imposes additional overhead on the CPU and RAM. Launching many threaded modules will slow your overall application.

To launch a threaded module, use the Thread() function.

Queued Modules

Like called modules except that only one instance may run at a time, regardless of how many calls are issued. Often used in device driver modules where I/O reads and writes must occur sequentially.

The queued module should be designed to return a value to signal that it has finished. The calling module should be designed to transition to another state upon receiving that signal, thus stopping the first instance and allowing the next in the queue to begin.

Add the keyword "queued" in the module declaration to ensure that it runs as a queued module

{========================== SampleDriver ==========================}
{ This shows the first few variables in a device driver            }
{==================================================================}
[    { Driver modules }
  ErrorMessage  Module { Returns an error message from a code      };
  QUEUED RSem   Module { Read semaphore                            };
  QUEUED WSem   Module { Write semaphore                           };

Queued modules are useful in driver development.

Quick Reference

Type Start from Ends when Parameter values Effect on application Called modules
Called Steady state Calling state ends Passed by reference. Changes in value are passed into and out of the running module. No effect Belong to this module
Queued Steady state Calling state ends Passed by reference No effect Belong to this module
Launched Script Calling module ends or Slay is called Passed by value. Changes in value are not passed in or out unless pointers are used as the parameters No effect Belong to this module.
Subroutine Script The Return function executes Passed by value. Changes in value are not passed in or out unless pointers are used as the parameters Suspends execution of entire thread until subroutine returns. Belong to the first non-subroutine parent of this module.
Threaded Script Calling module ends or Slay is called Passed by value. Changes in value are not passed in or out unless pointers are used as the parameters No effect Belong to this module

Module Do's and Don'ts

Called Modules

Never call Slay() in a called module

Don't change parameter values unless they are output parameters. Doing so sets values passed into the module because they are passed by reference.

Launched Modules

Never call Return(). The presence of Return makes a launched module a subroutine.

Always consider the module's lifetime. It will live as long as its calling module unless it is slain. If it is performing a sub-process that should come to an end, use the Slay() function when the process is complete.

Never stop the caller of a launched module that is performing a sub-process until that process is complete.

Subroutines

Always call Return().

Never have an If statement wait for a condition. Always use If 1 or If Watch(1). In fact, never wait for anything within a subroutine. If you need to wait for something, use a launched module instead of a subroutine.

Callable Subroutine

Never use an If 1. When called in steady state this will consume CPU constantly.