Developing and debugging Nasal code
The FlightGear forum has a subforum related to: Nasal Scripting |
Nasal scripting |
---|
Nasal internals |
---|
Memory Management (GC) |
Developing and debugging in Nasal
Developing Nasal code
Because code in the Nasal directory is parsed only at FlightGear startup, testing and debugging Nasal code can be slow and difficult.
FlightGear provides a couple of ways to work around this issue:
Nasal Console
The Nasal Console is available in FlightGear's menu (Debug/Nasal Console). Selecting this menu opens a Nasal Console dialog.
This dialog has several tabs, of which each can hold separate Nasal code snippets, all of which are saved on exit and reloaded next time. This is useful for little tests, or for executing code for which writing a key binding is just too much work, such as "props.dump(props.globals)".
If you want to add more tabs (radio buttons in the Nasal Console dialog) to hold more code samples, just add more <code> nodes to autosave.xml.
Loading/reloading Nasal code without re-starting FlightGear
A common problem in testing and debugging Nasal programs is that each testing step requires stopping and re-starting FlightGear, a slow process.
Below is described a technique for loading and executing a Nasal file while FlightGear is running. FlightGear will parse the file, display any errors in the FlightGear console window, and then execute the code as usual.
Using this technique, you can start FlightGear, load the Nasal code you want to test observe any errors or test functionality as you wish, make changes to the Nasal file, reload it to observe parse errors or change in functionality, and so on to repeatedly and quickly run through the change/load/parse/test cycle without needing to re-start FlightGear each time.
The key to this technique is the function io.load_nasal(), which loads a nasal file into a nasal namespace.
Step-by-step instructions showing how to use this technique to load, parse, and test a Nasal file while FlightGear is running:
Create the Nasal file to test
Create a text file named $FG_ROOT/foo/test.nas with this text:
print("hi!"); var msg="My message."; var hello = func { print("I'm the test.hello() function") }
Notes: You can create the file in any directory you wish, as long as Nasal can read the directory--but the file IOrules in the Nasal directory restricts which directories Nasal may read and write from.
You can give the file any name and extension you wish, though it is generally most convenient to use the .nas extension with Nasal files.
Load the file and test
Start FlightGear. You can import the file above into FlightGear by typing the following into the Nasal Console dialog and executing the code:
io.load_nasal(getprop("/sim/fg-root") ~ "/foo/test.nas", "example");
getprop("/sim/fg-root") gets the root directory of the FlightGear installation, ~ "/foo/test.nas" appends the directory and filename you created. The final variable "example" tells the namespace to load for the Nasal file.
You'll see the message "hi!" on the terminal, and have function "example.hello()" immediately available. You can, for instance, type "example.hello();" into one of the Nasal console windows and press "Execute" to see the results; similarly you could execute "print (example.msg);".
If you find errors or want to make changes, simply make them in your text editor, save the file, and execute the io.load_nasal() command again in the Nasal Console to re-load the file with changes.
It's worth noting that Nasal code embedded in XML GUI dialog files can be reloaded by using the "debug" menu ("reload GUI").
You may also want to check out the remarks on Memory management.
Managing timers and listeners
Note: If your Nasal program sets listeners, timer loops, and so on, they will remain set even when the code is reloaded, and reloading the code will set additional listeners and timer loops.
This can lead to extremely slow framerates and unexpected behavior. For timers you can avoid this problem by using the loopid method (described above); for listeners you can create a function to destroy all listeners your Nasal program creates, and call that function before reloading the program. (And cleaning up timer loops and listeners is a best practice for creating Nasal programs in FlightGear regardless.)
The same problem may occur while resetting or re-initializing parts of FlightGear if your code isn't prepared for this. And obviously this applies in particular also to any worker threads you may have started, too!
For complex Nasal scripts with many timers and listeners, it is therefore generally a very good idea to implement special callbacks so that your scripts can respond to the most important simulator "signals", this can be achieved by registering script-specific listeners to signals like "reinit" or "freeze" (pause): the corresponding callbacks can then suspend or re-initialize the Nasal code by suspending listeners and timers. Following this practice helps ensure that your code will behave properly even during simulator resets.
In other words, it makes sense to provide a separate high-level controller routine to look for important simulator events and then pause or re-initialize your main Nasal code as required.
If you are using System-wide Nasal modules, you should register listeners to properly re-initialize and clean up your Nasal code.
In its simplest form, this could look like this:
var cleanup = func {}
setlistener("/sim/signals/reinit", cleanup);
This will invoke your "cleanup" function, whenever the "reinit" signal is set by the FlighGear core.
Obviously, you now need to populate your cleanup function with some code, too.
One of the easiest ways to do this, is removing all listeners/timers manually here, i.e. by adding calls to removelistener():
var cleanup = func {
removelistener(id1);
removelistener(id2);
removelistener(id3);
}
This would ensure that the corresponding listeners would be removed once the signal is triggered.
Now, keeping track of all listeners manually is tedious.
On the other hand, you could just as well use a vector of listener IDs here, and then use a Nasal foreach loop:
var cleanup = func(id_list) {
foreach(var id; id_list)
removelistener(id);
}
Obviously, this would require that you maintain a list of active listeners, too - so that you can actually pass a list of IDs to the cleanup function.
This is one of those things that can be easily done in Nasal, too - just by introducing a little helper wrapper:
var id_list=[];
var store_listener = func(id) append(id_list,id);
The only thing required here, would be replacing/wrapping the conventional "setlistener" call with calls to your helper:
store_listener( setlistener("/sim/foo") );
store_listener( setlistener("/foo/bar") );
If you were to do this consistently across all your Nasal code, you'd end up with a high level way to manage all your registered listeners centrally.
You could further generalize everything like this:
var id_list=[];
var store_listener = func(property) append(id_list,setlistener(property) );
store_listener("/sim/foo/bar");
This will ensure that any "store_listener" call stores the listener's ID in the id_list vector, which makes it possible to easily remove all listeners, too:
var cleanup_listeners = func {
foreach(var l; id_list)
remove_listener(l);
}
Similarly, you could just as well "overload" functions like settimer() setlistener() in your script, this can be easily achieved by saving a handle to the original functions and then overriding them in your script:
var original_settimer = settimer;
var original_setlistener = setlistener;
Now, you have handles stored to the original functions, to make sure that you are referring to the correct version, you can also refer to the "globals" namespace.
Obviously, it makes sense to only store and clean up those listeners that are not themselves required to handle initialization/resets, otherwise you'd remove the listeners for your init routines, too.
Next, you can easily override settimer/setlistener in your script:
var original_settimer = settimer;
var original_setlistener = setlistener;
var settimer = func(function, time, realtime=0) {
print("Using your own settimer function now");
}
var setlistener = func(property, function, startup=0, runtime=1) {
print("Using your own setlistener function now!");
}
In order to call the old implementation, just use the two handles that you have stored:
- original_settimer
- original_setlistener
var original_settimer = settimer;
var original_setlistener = setlistener;
var settimer = func(function, time, realtime=0) {
print("Using your own settimer function now");
original_settimer(function, time, realtime);
}
var setlistener = func(property, function, startup=0, runtime=1) {
print("Using your own setlistener function now!");
original_setlistener(property, function, startup, runtime);
}
So this is a very simple and elegant way to wrap and override global behavior. So that you can easily implement script-specific semantics and behavior, i.e. to automatically store handles to registered listeners:
var original_settimer = settimer;
var original_setlistener = setlistener;
var cleanup_listeners = [];
var settimer = func(function, time, realtime=0) {
print("Using your own settimer function now");
original_settimer(function, time, realtime);
}
var setlistener = func(property, function, startup=0, runtime=1) {
print("Using your own setlistener function now!");
var handle = original_setlistener(property, function, startup, runtime);
append(cleanup_listeners, handle);
}
Thus, you can now have a simple "cleanup" function which processes all listeners stored in "cleanup_listeners" using a foreach loop and removes them:
var original_settimer = settimer;
var original_setlistener = setlistener;
var cleanup_listeners = [];
var remove_listeners = func {
foreach(var l; cleanup_listeners) {
removelistener(l);
}
}
var settimer = func(function, time, realtime=0) {
print("Using your own settimer function now");
original_settimer(function, time, realtime);
}
var setlistener = func(property, function, startup=0, runtime=1) {
print("Using your own setlistener function now!");
var handle = original_setlistener(property, function, startup, runtime);
append(cleanup_listeners, handle);
}
The only thing that's needed now to handle simulator resets, is registering an "anonymous" listener which triggers your "remove_listeners" callback upon simulator reset:
_setlistener( "/sim/signals/reinit", remove_listeners );
Note how we're using the low level _setlistener() call directly here, to avoid adding the listener id to the "cleanup" vector, which would mean that we're also removing this listener - i.e. the cleanup would then only work once.
Now, you'll probably have noticed that it would make sense to consider wrapping all these helpers and variables inside an enclosing helper class, this can be accomplished in Nasal using a hash.
This would enable you to to implement everything neatly organized in an object and use RAII-like patterns to manage Nasal resources like timers, listeners and even threads.
Debugging
The file debug.nas, included in the Nasal directory of the FlightGear distribution, has several functions useful for debugging Nasal code. These functions are available to any Nasal program or code executed by FlightGear.
Aside from those listed below, several other useful debugging functions are found in debug.nas; see the debug.nas file for the list of functions and explanation.
Note that the debug module makes extensive use of ANSI terminal color codes. These create colored output on Linux/Unix systems but on other systems they may add numerous visible control codes. To turn off the color codes, go to the internal property tree and set
/sim/startup/terminal-ansi-colors=0
Or within a Nasal program:
setprop ("/sim/startup/terminal-ansi-colors",0);
debug.dump
debug.dump([<variable>]) ... dumps full contents of variable or of local variables if none given
The function debug.dump() dumps the contents of the given variable to the console. On Unix/Linux this is done with some syntax coloring. For example, these lines
var as = props.globals.getNode("/velocities/airspeed-kt", 1); debug.dump(as);
would output
</velocities/airspeed-kt=1.021376474393101 (DOUBLE; T)>
The "T" means that it's a "tied" property. The same letters are used here as in the property-browser. The angle brackets seem superfluous, but are useful because debug.dump() also outputs compound data types, such as vectors and hashes. For example:
var as = props.globals.getNode("/velocities/airspeed-kt", 1); var ac = props.globals.getNode("/sim/aircraft", 1); var nodes = [as, ac]; var hash = { airspeed_node: as, aircraft_name: ac, all_nodes: nodes }; debug.dump(hash);
yields:
{ all_nodes : [ </velocities/airspeed-kt=1.021376474393101 (DOUBLE; T)>, </sim/aircraft="bo105" (STRING)> ], airspeed_node : </velocities/airspe ed-kt=1.021376474393101 (DOUBLE; T)>, aircraft_name : </sim/aircraft="bo 105" (STRING)> }
debug.backtrace
debug.backtrace([<comment:string>]} ... writes backtrace with local variables debug.bt ... abbreviation for debug.backtrace
The function debug.backtrace() outputs all local variables of the current function and all parent functions.
debug.breakpoint
If you need a backtrace controlable via the property browser at runtime, this is for you. see Nasal library/debug
debug.benchmark
debug.benchmark(<label:string>, <func> [, <repeat:int>])
... runs function <repeat> times (default: 1) and prints execution time in seconds,prefixed with <label>.
This is extremely useful for benchmarking pieces of code to determin
debug.exit
debug.exit() ... exits fgfs