Asp Scripting Platform

Other documents


Asp Application Developer's Guide

Introduction

It is often desirable to have a scripting capability within an embedded application. The Asp scripting platform was created for such a purpose. The purpose of this document is to guide embedded application developers through the process of incorporating Asp into their applications.

Why use Asp?

Embedded systems are usually resource constrained. However, many scripting environments utilize significant amounts of resources such as memory and uninterruptable processing time. To address these concerns, Asp was designed to restrict the amounts of resources that are required when running scripts. Of course, Asp can be used where these constraints are not as rigid, but its design is particularly targeted at platforms where resources are limited.

The Asp language resembles Python. However, unlike in the Python environment, Asp scripts must be compiled before being executed in the Asp script engine. The resulting executable is usually smaller in relation to its source code because it contains compact machine-like instructions, and all variable names have been converted to numbers to save space.

The engine is provided as a small library that is linked into the host application. Because many embedded systems support only C libraries, the Asp script engine is implemented entirely in C.

Asp attempts to behave in the best ways possible with respect to memory utilization. The engine makes no use of dynamic memory allocation (i.e., malloc, free, etc.). The amount of memory allocated for script execution is defined by the application during Asp initialization and remains constant. The engine's implementation makes no use of recursion, minimizing the demands on the run-time stack*.

Asp also leaves the application in control of the processor as much as possible. Instead of giving complete control of the processor to the script engine when running a script, Asp provides a routine to advance the script by a single instruction at a time. Each instruction takes a very small amount of time to execute, allowing the application to perform other critical tasks such as servicing watchdog timers, handling asynchronous events, etc. Also, if a script needs to be paused or stopped, the application may simply stop advancing the script to pause its execution or reset the engine to abort it.

Asp scripts may call appropriately designed application functions. Sometimes, the task that a function performs may take a long time to complete. In such cases, Asp provides a re-entry model that allows application functions to be non-blocking (and re-entrant) while making the function call from the script appear to block, simplifying script logic. This allows the application to remain in control of script execution and other tasks in a single location within the code.

For details on writing Asp scripts and preparing them for execution, see the Script Writer's Guide.

* Logic that is recursive in nature is implemented using iteration and uses Asp's data memory stack (which is bound by the amount of data memory that is made available to the engine) instead of using the embedded system's run-time stack.

Getting started

To incorporate the Asp script engine into an application, the application code must be linked with the Asp engine library. The library can be provided as static or shared varieties. For Linux environments, the library can be linked using -laspe on the gcc command line. If the predefined math functions are used by the application, -laspm must also be specified.

To use the Asp application programmer interface (API) in the code, include the <asp.h> header.

#include <asp.h>

Many of the API functions return enumerated error codes. Functions related to loading script executable code into the engine return a value of type AspAddCodeResult, while functions related to script execution return a value of type AspRunResult. Values for these enumerations are listed and described in the AspAddCodeResult and AspRunResult tables.

Including the <asp.h> header defines the ASP_VERSION macro. This macro is provided to guard against changes to the API in future versions of the library. The value is a four-byte unsigned integer composed of the four components of the version (major, minor, patch, and tweak), with the major component being the most-significant byte. The following example shows how to handle a difference with some of the collection-oriented functions introduced in version 0.7.0.0 of the library.

#if ASP_VERSION < 0x00700000
AspTupleAppend(engine, tuple, value);
AspUnref(value);
#else
AspTupleAppend(engine, tuple, value, true);
#endif

While every attempt is made to keep the API as stable as possible, sometimes changes are necessary. The above technique can come in handy if the application code must support multiple versions of Asp that have differing APIs.

Development flow

For scripts to be useful, they must be able to call C functions within the application to perform actions implemented in the application code. Asp uses an application specification file to describe those functions defined within the application that a script can call.

To accommodate the inclusion of these function specifications, developing applications that use Asp requires additional steps in the development flow. First, the script-callable functions are identified in an application specification source file (.asps). This file is then processed to produce three output files: a C header, a C source file, and a binary application specification file (.aspec) which is used by the Asp compiler when compiling scripts for the application. The next section describes the syntax of the application specification source file, and the figure below shows where it fits into the development flow.

In the diagram, the region enclosed in dotted outline shows the development flow. (Outside of that is the script preparation and execution flow.) As shown, the Asp generator (aspg) is used to generate the two C source files that must be included in the build of the application. The generated header should be included in any source files that define script-callable functions to ensure they are defined properly. This is especially true if the functions are implemented in C++ to ensure they are defined using the C linkage convention. The diagram also shows the Asp engine library being linked into the application

Application specification

As mentioned above, script-callable functions must be described in an application specification source file (.asps) and then used in the development flow. This source file describes (a) functions that scripts may call, and (b) variables with optionally assigned initial values. The syntax for describing a function resembles the Asp def statement without the code body. Instead of a colon (:) at the end of the statement, an equals sign (=) followed by the name of the C function is given. Here is a short example defining two functions.

def sleep(s) = asp_sleep
def print(*args) = asp_print

With these definitions in place, a script that calls the sleep or print functions will end up calling the application's asp_sleep and asp_print C functions, respectively*. Notice that positional and named (keyword) variable parameter lists are supported. For example, the print function definition includes the *args parameter, which represents a variable-length tuple of arguments.

Function parameters may be assigned default values, but the values are restricted to simple constants. These are None, ... (the ellipsis), the Boolean values False and True, integers, floating-point values, and strings. So, for example, an empty tuple (()) is not a valid default value. Here is an example of a function that converts its argument to a string. Its only parameter has a default value of the empty string so that it may be called with no arguments, resulting in the function presumably returning an empty string.

def str(x = '') = asp_str

In addition to functions, variables may be defined and assigned an initial constant value. The assigned value has the same restrictions as those for function parameter default values; they must be simple constants. Variables defined in this way provide a convenient way for the application to supply constants for use within scripts.

PI = 3.1415926535

It is also possible to define a name without assigning a value. No code is generated during script initialization for such definitions, but a symbol value is reserved for the name.

xpos
ypos
width
height

The generated header includes a macro definition for each name used in the specification. This includes names of functions, function parameters, and variables, with or without an assigned value. The form of this name is shown below. These definitions allow the application to use the symbols by name to, for example, test for the presence of an argument in the named variable parameter lists (e.g., **kwargs).

#define ASP_APP_app-name_SYM_var-name symbol-value

It should be noted that all the function and assigned variable definitions described in the application specification are initialized prior to each new script execution. So, a script that makes changes to the definitions during its execution will not affect the environment of the next script to run. For example, if a script deletes an application function definition or assigns a new value to an application variable, these changes remain in effect only until the end of the script's execution. The next script sees the original definitions when it starts.

Asp defines several built-in functions, but to use them, they must be defined in the application's specification file. To facilitate this, application specification source files for these built-in functions are provided and may be included in the main specification using the include statement. Currently, there are five such files.

include sys
include type
include collect
include iter
include math

The names of the associated C functions for these built-ins all start with AspLib_, so applications should avoid naming their own variables or functions with this prefix. The include files are available in a common include directory. On Linux systems, this is typically /usr/include/asps. The built-in functions are described in the Script Writer's Guide.

It is also possible to build your own libraries of script-callable functions. When writing the specification, the first line in the specification file should be the lib statement, which identifies the functions as being delivered in a library instead of being compiled directly in the application. The only time this truly matters is when using shared libraries, especially with Windows (i.e., DLLs). Specifying lib triggers aspg to decorate the function declarations with the ASP_LIB_API macro, which expands to the linkage specification required for the applicable usage (i.e., import vs. export in Windows). If specified, the lib statement must be the first statement in the .asps file.

lib

It is possible that when including another specification into your own, not all of its definitions will be needed or even desirable. In these cases, definitions may be removed using the del statement. In many cases, dropping definitions will cause the C compiler to drop the associated implementation from the final application executable.

include common
del clock, sleep

In the above example, if the clock and sleep functions were defined in the included common definitions, but were not needed or desired, they can be removed from the application's specification as indicated. Notice that multiple names can be given in a single del statement, separated by commas.

As with Asp's syntax, comments are permitted. Also, line continuations may be used when a definition spans multiple lines.

# Function to delete a range of files by time.
def delete_files_by_time \
    (start_time, end_time, \
     timeout = None) = asp_delete_files_by_time

The above definition uses a number sign (#) to introduce a comment, and a backslash (\) to continue the definition onto another line.

* The examples use C function names starting with asp_ as a matter of convention. However, note that there are no restrictions on the name of the C function (other than being a valid C identifier). It is advisable to avoid names starting with Asp_, however, since this prefix is used in the library implementation.)

Engine control

A script engine instance is defined by the AspEngine type (a typedef of a structure definition). Most functions in the engine's API take an AspEngine pointer as their first argument. Note that because everything about a script engine is defined in this structure, it is possible to have multiple script engine instances present simultaneously in a single application. It is left to the imagination of the developer as to how this might be applied.

The following sections describe the API functions that control the script engine or query it for information. In most cases, these functions are not designed to be called from within a script-callable application function; they should only be used outside the context of an executing script.

The basic outline of script execution logic is as follows:

  1. Declare/allocate an AspEngine structure instance.
  2. Optionally use the AspDataEntrySize function to determine the number of bytes to allocate for the engine's data area.
  3. Declare/allocate space for the code and data areas.
  4. Optionally initialize a script context area, if used.
  5. Call AspInitialize to initialize the engine instance and data area.
  6. Optionally call AspSetCodePaging to enable script code paging.
  7. Identify the code to run in the engine. This can be done in one of three ways.
    1. Call AspAddCode one or more times to load script executable code into the code area, and then, when all the code has been added, call AspSeal to seal off the code and perform final checks prior to execution.
    2. Make a single call to AspSealCode to identify the location and size of an external block of script executable code.
    3. Assuming that code paging has been enabled via AspSetCodePaging, make a single call to AspPageCode to identify the location of an external resource where pages of script executable code can be read.
  8. Optionally add script arguments via AspSetArguments or AspSetArgumentsString, depending on how the arguments are defined in the application.
  9. Run the script by calling AspStep repeatedly and checking its return value. Any value other than AspRunResult_OK indicates that the script has ended.
  10. To execute the same script again, reinitialize the script context area if applicable, call AspRestart, and then go back to step 8.
  11. To prepare to run a different script, reinitialize the script context area if applicable, call AspReset, and then go back to step 6.

Details of these and other engine control functions are given in the sections below.

Initialization

The following functions prepare the engine to execute a script: AspInitialize, AspSetCodePaging, AspAddCode, AspSeal, AspSealCode, AspPageCode, AspSetArguments (and the related AspSetArgumentsString), AspRestart, and AspReset.

AspInitialize

AspRunResult AspInitialize
    (AspEngine *,
     void *code, size_t codeSize, void *data, size_t dataSize,
     const AspAppSpec *, void *context);
AspRunResult AspInitializeEx
    (AspEngine *,
     void *code, size_t codeSize, void *data, size_t dataSize,
     const AspAppSpec *, void *context, AspFloatConverter);

To initialize the engine, use the AspInitialize function and pass it the address of an AspEngine instance and the other listed parameters. The application is responsible for allocating memory for the AspEngine instance (very small), the code and data blocks, and, if used, the context area (more on script contexts in a subsequent section). If the context is not being used, pass a null pointer. The required AspAppSpec pointer should point to the global instance defined in the generated application specification C source file and declared in the generated C header. Its name is AspAppSpec_app, where app is the base name of the application specification file name. The AspAppSpec instance is treated as read-only and can therefore be shared between multiple engine instances if desired.

As discussed in the next section, it is possible that the code area will not be needed (e.g., when all scripts that will be run already reside in the application's memory). In these special cases, the code and codeSize parameters may be set to a null pointer and zero, respectively.

In the rare case that the target platform does not support the IEEE 754 binary64 (i.e., double-precision) floating-point format, a second version of the initialization function, AspInitializeEx is provided which accepts an additional parameter of type AspFloatConverter, a function pointer type as defined here.

typedef double (*AspFloatConverter)(uint8_t ieee754_binary64[8]);

Conversion is required because floating-point values in script binaries and application specification files are encoded using the standard format. If used, the supplied function must convert the 8-byte IEEE 754 binary64 value into the native floating-point format. The input value is supplied as an 8-byte array and will already be in the native byte-order. The return value is a double in the native format, regardless of whether its size is 8 bytes or not.

AspInitialize needs to be called only once per engine instance. Note also that there is no matching close function; when the script engine is no longer needed, the space occupied by the AspEngine instance may be reclaimed, along with the code, data, and context areas.

The size of the code and data areas is specified in bytes. It should be noted that the data area consists of a set of fixed-sized slots known as data entries. The size of a single entry can be ascertained by using the AspDataEntrySize function. Knowing this size can help ensure there are no wasted data bytes by using a multiple of the entry size for the data area byte size.

AspAddCode, AspSeal, and AspSealCode

AspAddCodeResult AspAddCode
    (AspEngine *, const void *code, size_t codeSize);
AspAddCodeResult AspSeal(AspEngine *);
AspAddCodeResult AspSealCode
    (AspEngine *, const void *code, size_t codeSize);

Once the engine is initialized, script binary code can be loaded. The code can be loaded in blocks, if more convenient for the application, by making repeated calls to AspAddCode. Each call appends the specified block to the end of any code already in the engine. Once all the code is loaded, the AspSeal function must be called to complete the loading process.

If all the code of a script executable is already in a contiguous area of memory, instead of copying it into the engine's code area, it may be used in place by identifying its location with AspSealCode. In this special case, the identified code (i.e., the code pointed to by the code parameter and with a size given by codeSize) includes the entire script executable's content, including the header fields. This is useful in cases where predefined scripts are included in the code of the application. If an application runs only predefined scripts, the engine's code area may be omitted altogether by specifying a null pointer for it in the call to AspInitialize and giving it a size of zero.

These functions return an AspAddCodeResult enumerated value. Several things can go wrong during the loading process; details of the error codes are given below. If all the calls to AspAddCode and AspSeal (or the single call to AspSealCode) return AspAddCodeResult_OK, then the script is ready to run. See the Execution Control section for functions that control script execution. Additional functions applicable prior to execution are discussed below.

AspAddCodeResult error code suffix Description
OK No error has occurred.
InvalidFormat Script binary executable files must start with the four ASCII characters AspE. If this is not the case, this error code is returned.
InvalidVersion The major and minor components of the script compiler are written to the script binary executable and are compared to the corresponding version components of the engine running the code. This error code is returned if the version components do not match exactly, indicating an incompatibility issue.
InvalidCheckValue The application specification defines a check value (a four-byte cyclic redundancy check value, or CRC) based on the names of the application functions, the names and any default values of function parameters, and the names and assigned values of application variables. The compiler writes this value into the script binary executable. It is compared to the check value in the AspAppSpec structure provided during engine initialization and if different, this error is returned.
OutOfCodeMemory The amount of code space is insufficient to load the amount of code contained in the script binary file. This condition cannot happen when using AspSealCode, as the space where the script's executable code resides is outside the engine's code area.
InvalidState AspAddCode, AspSeal, or AspSealCode has been called when the engine is in a state where calls to these functions are not expected, for example, when a script is running.

AspSetCodePaging and AspPageCode

AspRunResult AspSetCodePaging
    (AspEngine *, uint8_t pageCount, size_t pageSize, AspCodePageReader);
AspAddCodeResult AspPageCode(AspEngine *, void *id);

In addition to the two methods of preparing for script execution described above, a third method can be used that defers the loading of code until it is needed during execution. This method, termed code paging, is useful when the size of a script could exceed the size of the defined code area. Instead of storing the entire script in the code area, the code area is split into one or more fixed-sized pages for caching blocks of code during execution.

To enable code paging, call the AspSetCodePaging function, specifying the number of pages, the size of each page, and a routine that will be called during execution to read in pages of code. The page reading routine must be supplied by the application and must be compatible with the the AspCodePageReader function pointer declaration as given below. The AspSetCodePaging function may also be used to disable code paging by specifying zero for either the pageCount or pageSize, or both. Once set, the paging state (enabled or disabled) remains in effect; there is no need to call AspSetCodePaging before running each script. Also note that the pageSize must be at least 12 bytes (for implementation reasons), although it is expected it will normally be much larger than this.

typedef AspRunResult (*AspCodePageReader)
    (void *id, uint32_t offset, size_t *size, void *codePage);

Assuming code paging is enabled, instead of loading the script (e.g., with AspAddCode, etc.), the location of the resource containing the code (i.e., the executable .aspe file) is identified by calling AspPageCode. It is up to the application to determine how script code resources are identified. For example, if the target system supports files, the identifier could be a C FILE pointer or a POSIX file handle. Whatever is supplied in the call to AspPageCode is simply passed to calls of the supplied read function, so the read function will typically cast the void pointer id argument to the applicable type.

The read routine supplied by the application will be called by the engine when a block of code is needed during execution. The read function is called with the id argument identifying the resource to read from (e.g., file), the byte offset into the resource, the requested page size, passed by reference, and the destination codePage where the content should be stored. If the read returns fewer bytes than requested, the size should be updated to reflect the actual number of bytes read. (The engine uses this update to detect the end of the code.) This is also applicable if the requested offset is past the end of the content, in which case the size should be updated to 0. In all these cases (full page read, partial page read, and no data), the function should return AspRunResult_OK. Only if there is a problem with the code resource identifier or the resource itself (e.g., invalid file descriptor, file access error, etc.) should the function return something else (e.g., AspRunResult_Application). Returning something other than AspRunResult_OK will result in the script execution ending in error.

The application developer is encouraged to choose page count and size values that are appropriate for the system at hand. During execution, after all the cache pages have been used, the engine will replace the least recently used cache page when reading a new page in. Depending on the speed at which pages can be read, using a sufficient number of pages can improve performance, especially in scripts that use a lot of nested function calls, or in other cases that may cause the program counter to jump around a lot.

A small implementation detail should be noted. To manage the cached code pages, a small amount of space is stolen from the end of the data area. The amount of space taken is equal to the specified page count times the size of each cached page record, which is currently 2 bytes. This reallocation of data space should not have any material effect on script execution if sufficient margin is included in the size of the data area. For example, if the maximum number of 255 pages is specified, the data area will be reduced by a total of 510 bytes, which is equivalent to 32 data entries.

AspSetArguments and AspSetArgumentsString

AspRunResult AspSetArguments(AspEngine *, const char * const *args);
AspRunResult AspSetArgumentsString(AspEngine *, const char *s);

Asp supports the notion of passing external arguments into a script. This must be done after the code has been loaded and prior to execution (i.e., after either AspSeal or AspRestart has been called and before calling AspStep for the first time).

External arguments may be specified in one of two ways: as an array of character strings, or as a single string. The first method resembles the way external arguments are passed to the C/C++ main routine (i.e., argc and argv parameters) and is invoked using AspSetArguments. The final item in the args array must be a null pointer. The second method, using AspSetArgumentString, utilizes an argument parser to extract individual arguments from the single string. The application may use either method, depending on which is more convenient.

If the second method is used, the parsing rules are as follows. Normally, arguments are separated by one or more whitespace characters (space, tab, etc.). Single or double quotes marks (' or ") may be used to surround an argument that contains whitespace. A single quote may appear within a double-quoted string, and vice versa. The backslash character (\) is used outside quoted sequences or within double-quoted strings to escape the meaning of these special characters, including the backslash character itself.

See the Script Writer's Guide for how to access external arguments from within a script.

AspSetCycleDetectionLimit and AspGetCycleDetectionLimit

AspRunResult AspSetCycleDetectionLimit(AspEngine *, uint32_t limit);
uint32_t AspGetCycleDetectionLimit(AspEngine *);

Theoretically, it would be possible to create data structures that would cause AspStep to execute endlessly. An example is a list that contains a reference to itself. In this theoretical situation, the engine could get hung up cycling through the same parts of the structure. In some cases, because of the way Asp handles recursion, the engine can run out of memory, in which case the script's execution ends with an error. However, without intervention, it would possible in other circumstances for the engine never to return from AspStep.

To deal with this situation, the engine enforces a limit on the number of items it will visit within a data structure. Initially, this limit is set (in AspInitialize) to half the number of elements in the data area, which approximates the size of the largest possible data structure. If this value is too large or too small for the application at hand, it may be adjusted using AspSetCycleDetectionLimit. The current value may be queried with AspGetCycleDetectionLimit.

AspRestart and AspReset

AspRunResult AspRestart(AspEngine *);
AspRunResult AspReset(AspEngine *);

At any point after the script is ready to run, has started running, or is finished execution, it may be restarted from the beginning by calling AspRestart. This reinitializes the data area, including the clearing of any previously defined external arguments, but leaves the script code intact. This can be useful if a script is to be run multiple times, as it circumvents the need to reload the code.

To run a different script, the AspReset function must be called prior to loading the code of the new script. This clears not only the data area, but also the code area, restoring the engine back to the state it was in immediately following the call to AspInitialize.

In both cases, any functions and variables defined by the application are restored to their original state, ensuring there is no cross contamination from the actions of one script to another.

Execution control

Scripts are executed using the AspStep function. Other functions are available to query various aspects of the engine related to execution: AspIsReady, AspIsRunning, AspIsRunnable, AspProgramCounter, and AspLowFreeCount.

AspStep

AspRunResult AspStep(AspEngine *);

Scripts are executed by repeated calls to the AspStep function. Each call executes a single instruction of the script. Most lines of code from the script's source file are translated by the compiler into several machine-like instructions, each of which is executed by a call to AspStep, so it may take several calls to AspStep to execute a single source line of script code.

The AspStep function returns an AspRunResult enumerated value, the values of which are documented below. Two values are important enough to mention here. A value of AspRunResult_OK indicates that the next instruction executed successfully and that the script is still considered to be in the running state. A value of AspRunResult_Complete indicates that the script has completed execution successfully. All other values indicate that something has gone wrong during execution. After getting any return value other than AspRunResult_OK, the script's execution is considered to be ended.

AspRunResult error code suffix Description
OK No error has occurred.
Complete The script has successfully finished executing. Note that while most error codes can be returned by script-callable application functions, this one cannot be. An attempt to do so will cause AspStep to return AspRunResult_InvalidAppFunction instead.
InitializationError A non-specific error occurred during the call to AspInitialize. This can be caused by passing invalid parameters to this function.
InvalidState An API function has been called when the engine is in a state where it is not expected. Calling engine control functions from within a script-callable application function call will cause this error.
InvalidInstruction An invalid instruction has been encountered in the script executable code. This would normally indicate some sort of memory corruption in the code area, or a bug in the Asp compiler.
InvalidEnd The script has indicated that it is finished, but the engine's internal stack (maintained in the data area) is not empty. This may indicate a bug in the Asp compiler.
BeyondEndOfCode In reading the next instruction or its operand(s), the program counter would advance beyond the end of the code. This may indicate memory corruption in the code area, or a bug in the Asp compiler.
StackUnderflow The engine is attempting to pop a value off its internal stack (maintained in the data area), but the stack is empty. This may indicate a bug in the Asp compiler.
CycleDetected In one of several situations where the engine handles recursion by using iteration, the number of iterations has exceeded the set limit, indicating that it is likely operating on a self-referencing data structure (e.g., a list containing a references to itself).
InvalidContext The engine is attempting to execute an instruction associated with an Asp global or local statement outside the context of a function. This may indicate a bug in the Asp compiler.
Redundant A redundant global or local statement is being made when the variable in question already has the indicated override.
UnexpectedType An operand is not of an expected type. Application functions can also return this code to end execution when a function argument is of an unexpected type. For example, the index value of an indexing expression is expected to be an integer.
SequenceMismatch A mismatch between the sequences on the left and right sides of an assignment has been encountered.
NameNotFound An attempt to load the value of a variable or otherwise use its value (e.g., calling a function by name) finds that the variable is not defined (i.e., has not been assigned a value).
KeyNotFound An attempt to look up a key in a set or dictionary has been made and the key cannot be found.
ValueOutOfRange A value is not in the expected range. For example, the index value used in an indexing expression is out of range for the given sequence. Another example is that the right operand of a shift operation is less than zero.
IteratorAtEnd An attempt to use the value referred to by an iterator finds that the iterator has advanced to the end of the iterable. This should not happen when using the iterators provided by the for statement.
MalformedFunctionCall One of several constraints was violated while processing a function call. Typically, this has to do with how arguments are specified, for example, placing keyword arguments before positional ones in the argument list, or using an unknown parameter name.
InvalidAppFunction A script-callable application function returned AspRunResult_Complete, presumably in an attempt to end script execution successfully. This is not permitted. However, a script may call the exit system function for such a purpose.
UndefinedAppFunction An attempt has been made to call an application function that is not defined by the application. This may indicate a bug in the Asp compiler.
DivideByZero The script has attempted to use zero as the divisor of a division or modulus operation.
ArithmeticOverflow The script has attempted to perform an operation that would result in arithmetic overflow (or underflow) such as adding two integer values whose sum cannot be represented as a 32-bit signed integer. Unlike Python, Asp does not support arbitrary precision integers.
OutOfDataMemory The script has attempted to allocate a data entry when no more are available in the data area.
Again This code may be returned by script-callable application functions to request re-entry in order to continue the operation. It is never returned by AspStep or any other API function.
Abort A script has executed an assert statement with a condition that evaluates to False.
Call The special value returned by the AspCall function to cause subsequent invocation of the target function. Script-callable functions should not use this return value explicitly, but instead use the value returned by AspCall.
InternalError An unexpected condition has occurred in the engine, likely caused by memory corruption or a bug in the engine's implementation.
NotImplemented The sought-after functionality has not yet been implemented in the engine. Check subsequent versions.
Application Script-callable application functions may return codes unique to the application by using this code as a base for codes equal to or greater than this code.

Normally, a script ends when either it reaches its end or an error occurs. However, a script can also be ended prematurely by calling the exit function, which takes an optional exit code. The code is translated into an appropriate AspRunResult value according to the following rules. If the code is an integer, zero is translated to AspRunResult_OK, positive values are translated to AspRunResult_Application + code (limiting the value to INT32_MAX if necessary), and negative values are translated to INT32_MAX. A code of None (the default) is translated to AspRunResult_OK. All other types of values (even the Boolean and floating-point types) are simply translated to AspRunResult_Application.

Execution information

bool AspIsReady(const AspEngine *);
bool AspIsRunning(const AspEngine *);
bool AspIsRunnable(const AspEngine *);
size_t AspProgramCounter(const AspEngine *);

After a script is loaded, the engine enters a state where it is ready to run the script. The state is changed to running after the first call to AspStep. The AspIsReady, AspIsRunning, and AspIsRunnable return Boolean values indicating whether the engine is ready to run a script, currently running a script, or either one, respectively. The script's program counter may be queried using the AspProgramCounter function. The value returned is a byte offset into the code.

AspLowFreeCount

size_t AspLowFreeCount(const AspEngine *);

The AspLowFreeCount function returns the lowest number of available data entries during or after script execution. This is a useful metric to determine whether, for any planned script, the engine has sufficient data storage to run the script. Care must be taken to add a margin of safety, as data memory utilization can vary from one run to another depending on the various paths through the script's logic that may take place. Note that to get meaningful data, the function must be called prior to restarting/resetting the engine for the next run.

If needed, the value returned by this routine can be converted to bytes by multiplying it by the value returned by the AspDataEntrySize function.

AspCodePageReadCount

size_t AspCodePageReadCount(AspEngine *, bool reset);

When code paging is enabled, the AspCodePageReadCount function returns the number of times the read function (specified in the call to AspSetCodePaging) has been called since the start of execution, or since the previous call to AspCodePageReadCount with reset set to true. In the unlikely event that the read count reaches its maximum value before being reset, it is no longer incremented until a reset occurs.

The read count can be used to assess the performance of various choices of page size and count when code paging is enabled. If code paging is disabled, AspCodePageReadCount will always return zero.

Miscellaneous functions

Several additional functions are provided in the engine control API and are documented below.

AspDataEntrySize

size_t AspDataEntrySize(void);

As mentioned above, AspDataEntrySize returns the number of bytes of a single data entry within the engine's data area. The actual value should be 16, but the function has been provided to guard against any change in future versions of the library.

AspEngineVersion and AspCodeVersion

void AspEngineVersion(uint8_t version[4]);
void AspCodeVersion(const AspEngine *, uint8_t version[4]);

Asp uses four-component version numbers in several places. The components are referred to as major, minor, patch, and tweak (after CMake conventions). Asp keeps tight control over compatibility by employing version checking. For example, the major and minor components of the version of the code generated by the compiler must match those of the engine running the code. A mismatch is one of the reasons that AspAddCode (or AspSealCode) can fail.

Version information can be ascertained about both the engine and the code loaded into the engine by using the AspEngineVersion and AspCodeVersion functions, respectively. In both cases, the version information is stored at the address provided by the version argument, which is assumed to be a four-element byte array. The first element in the array is the major component and the others follow the expected order. Note that if code has not been completely loaded into the engine, AspCodeVersion will return unpredictable values.

AspMaxCodeSize and AspMaxDataSize

size_t AspMaxCodeSize(const AspEngine *);
size_t AspMaxDataSize(const AspEngine *);

It is up to the application to decide on the amount of memory to allocate for the code and data areas. However, if the application needs to know these values in an area of code that does not have access to the values used during initialization, they may be obtained using AspMaxCodeSize and AspMaxDataSize. Both are maximum sizes (as specified to AspInitialize) and are reported in bytes.

AspTraceFile and AspDump

void AspTraceFile(AspEngine *, FILE *);
void AspDump(const AspEngine *, FILE *);

If the Asp library has been built with the ASP_DEBUG macro defined (typically enabled via the ENABLE_ENGINE_DEBUG CMake option), the AspTraceFile and AspDump functions are available. The AspTraceFile function may be used to direct instruction trace output to the specified C file handle, the default being stdout. The AspDump function dumps out the contents of the data memory in a human-readable form to the given C file handle. Normally, these functions are not defined. Application developers should only use a debug version of the library in cases where debugging their script-callable functions is required.

Function interface

As mentioned previously, script-callable application functions have a specific type of definition which is defined by the Asp generator, aspg. Prototypes of the application's script-callable functions can be found in the generated C header, and look something like this:

AspRunResult function_name
    (AspEngine *,
     AspDataEntry *argument1,
     AspDataEntry *argument2,
     …
     AspDataEntry **returnValue);

It is the responsibility of the application developer to provide the implementation for these functions. This involves extracting values from the arguments, interacting with other parts of the application, and returning an appropriate return value. In the case of mutable arguments like lists and dictionaries, the function may also manipulate the contents of such collections if desirable.

The following sections discuss the API that script-callable functions use to perform these tasks. All Asp objects are represented as AspDataEntry instances, all of which reside in the engine's data area. All API functions that work with script objects use pointers to AspDataEntry. The API includes functions for: type checking, value extraction, conversion to string, collection information, object creation, collection manipulation, and a few sundry other tasks.

To return a value to the script, the returnValue argument is used. If left untouched, the script will receive a None object as the return value. To return anything else, the implementation should assign a data entry pointer to *returnValue.

Script-callable application functions must return a valid AspRunResult value, normally AspRunResult_OK to indicate success. Any other value (with the exception of AspRunResult_Again; see below) indicates an error which will cause the script to end. Application-specific error codes can be returned and are formed by adding a non-negative value to AspRunResult_Application. See the AspRunResult table for a full list of available error codes.

Type checking

bool AspIsType(const AspDataEntry *);
bool AspIsIntegral(const AspDataEntry *);
bool AspIsNumber(const AspDataEntry *);
bool AspIsNumeric(const AspDataEntry *);
bool AspIsSequence(const AspDataEntry *);
bool AspIsIterator(const AspDataEntry *);
bool AspIsAppObject(const AspDataEntry *);

In the syntax citation above, Type stands for any one of the types supported by Asp. The actual function names are: AspIsNone, AspIsEllipsis, AspIsBoolean, AspIsInteger, AspIsFloat, AspIsSymbol, AspIsRange, AspIsString, AspIsTuple, AspIsList, AspIsSet, AspIsDictionary, AspIsForwardIterator, AspIsReverseIterator, AspIsFunction, AspIsModule, AspIsAppIntegerOject, AspIsAppPointerOject, and AspIsType, all corresponding to the applicable type in the name. Each one returns true if the argument is of the applicable type.

In addition to these functions are a few that check for meaningful groups of types. AspIsIntegral returns true if the argument is either Boolean or integer. AspIsNumber returns true for integer and floating-point values. AspIsNumeric returns true for Booleans, integers, and floating-point values. AspIsSequence returns true for tuples and lists, but not for strings, even though in some respects, strings can be treated like sequences. The AspIsIterator function returns true if the argument is either a forward or reverse iterator. And finally, AspIsAppObject returns true if the object is either of the two types of application object: integer or pointer.

Value extraction

bool AspIsTrue(AspEngine *, const AspDataEntry *)
bool AspIntegerValue(const AspDataEntry *, int32_t *);
bool AspFloatValue(const AspDataEntry *, double *);
bool AspSymbolValue(const AspDataEntry *, int32_t *);
bool AspRangeValues
    (AspEngine *, const AspDataEntry *,
     int32_t *start, int32_t *end, int32_t *step, bool *bounded);
bool AspStringValue
    (AspEngine *, const AspDataEntry *,
     size_t *size, char *buffer, size_t index, size_t bufferSize);
bool AspAppObjectTypeValue
    (AspEngine *, const AspDataEntry *, int16_t *);
bool AspAppIntegerObjectValues
    (AspEngine *, const AspDataEntry *, int16_t *appType, int32_t *value);
bool AspAppPointerObjectValues
    (AspEngine *, const AspDataEntry *, int16_t *appType, void **value);

All Asp objects can be converted to a Boolean value, which is useful for use in conditional statements. The AspIsTrue function returns true if the given Asp object would evaluate to True in a script conditional expression (e.g., an if or while expression).

To extract a value from an Asp object, the AspIntegerValue, AspFloatValue, AspSymbolValue, AspRangeValues, and AspStringValue functions are provided. All these functions return true to indicate success. In the case of the first three, the sought value is deposited at the given address. Since ranges consist of multiple values, three integer addresses, start, end, and step, are required to receive the components of the range, and an additional Boolean address, bounded is required to indicate whether the end value is used. (Note that the bounded parameter was added in Asp version 1.1.)

Note that it is possible to extract an integer or floating-point value from objects of Boolean, integer, or floating-point type. For example, using AspFloatValue on a Boolean object will result in a value of either 0.0 or 1.0, depending on whether the object was False or True, respectively. Calling AspIntegerValue on a floating-point value involves rounding truncation.

The AspStringValue function is a bit more complex. The size parameter can be a null pointer, but if an address is given, the full size of the Asp string is written to the given address. Since Asp strings are not null terminated like C strings are, the value at size refers only to the characters of the Asp string. The buffer parameter may also be a null pointer, in which case no data is extracted. If buffer is specified, bufferSize must indicate the number of bytes in the buffer. The index parameter indicates what byte within the Asp string to start copying from.

So, a typical use of AspStringValue could be as follows. First, the function is called to ascertain the size of the Asp string by passing an address for size and a null pointer for buffer. The application can then use whatever means necessary to allocate enough space for the string data, plus an additional byte for a null terminator. It can then call AspStringValue a second time, this time passing a null pointer for size (since it is already known), a pointer to the allocated memory for buffer, 0 for index (since we are copying the entire content), and the size of the buffer (including the null terminator) for bufferSize. Since the buffer contains sufficient room, AspStringValue will also write a terminating null character at the end of the buffer. Note that in cases where only a portion of the string is being extracted (i.e., bufferSize is less than or equal to *size - index), a null terminator is not written, as that would write past the end of the buffer. In this case, the buffer cannot be treated as a C string, so care must be taken.

Another approach, if applicable, is to deal with the string content in a piecemeal fashion. For example, if the application is searching for a substring within the string or performing some other operation that does not require the entire string to be in memory all at once, it may be possible to extract parts of the string and process them in a loop. To treat the pieces as C strings, the application should use a buffer one byte larger than the size given in bufferSize and ensure that the final byte of this buffer is set to the null character, as AspStringValue will not write to it.

The type and value of application-specific objects may be obtained by using AspAppIntegerObjectValues or AspAppPointerObjectValues. If only the type is needed, AspAppObjectTypeValue may be used. More information on application objects may be found in the section on object creation).

Conversion to string

AspDataEntry *AspToString(AspEngine *, AspDataEntry *value);
AspDataEntry *AspToRepr(AspEngine *, const AspDataEntry *value);

Sometimes it is convenient to convert an Asp object to a string. An example would be to print its value to a log. For this purpose, the AspToString function creates a new Asp string object with a textual representation of the given Asp object. A pointer to the new object is returned. A null pointer return value indicates that insufficient data memory exists to fulfill the request, so in this case, the calling function should return AspRunResult_OutOfDataMemory. In the normal case of a non-null return value, it should be noted that the application is responsible for ensuring the created object is either destroyed (via AspUnref) or its ownership is transferred to another Asp object that will persist after the function returns. See the section on reference counting for more information.

The AspToRepr function is also provided. It replicates the functionality of Python's repr function. This can come in handy, for example, when displaying strings containing special characters, which will be converted to printable escape sequences.

The astute reader will notice that the AspToString's value argument is not declared as const. This is because the object being passed could in fact already be a string. In this case, instead of creating a new string object, the reference count of the existing string object is incremented, thus modifying the given object entry. (AspToString internally calls AspRef in this case.) The application need not treat this any differently than converting other types of objects. It is still responsible for decrementing the use count or otherwise dealing with the ownership of the object whose address is returned.

Collection information

AspRunResult AspCount(AspEngine *, const AspDataEntry *, int32_t *count);
AspDataEntry *AspElement(AspEngine *, AspDataEntry *sequence, int32_t index);
int32_t AspRangeElement(AspEngine *, const AspDataEntry *range, int32_t index);
char AspStringElement(AspEngine *, const AspDataEntry *str, int32_t index);
AspDataEntry *AspFind
    (AspEngine *, AspDataEntry *tree, const AspDataEntry *key);
AspDataEntry *AspNext(AspEngine *, AspDataEntry *iterator);

Asp supports a few types of collections: tuples, lists, sets, and dictionaries. Strings can also be treated as collections (of characters) in some cases. Ranges represent sequences of integer values, and can therefore sometimes be treated like collections. The application function API includes a few functions for getting information from these types of objects.

AspCount yields the number of items inside the given object. For a collection, this is the number of items in the collection. Strings are treated like collections in this regard, returning the number of characters in the string. For ranges, the number of elements that would be generated if the range was expanded into a list is computed. For all other object types, the function returns 1.

It should be noted that prior to version 1.1, the AspCount function's signature was different, as indicated below. If application code must support multiple versions of Asp, the ASP_VERSION macro can be used to conditionally compile code for both versions.

unsigned AspCount(const AspDataEntry *); /* old signature */

The reason for the change was to accommodate the fact that attempting to return the number of elements in an unbounded range cannot be accomplished. In this case, an error is indicated in the return value.

To access the element of a sequence (i.e., a tuple or list, but not a string or range), the AspElement function is used, which accepts an index value and returns a pointer to the data entry at the indicated position within the sequence. It does not increment the use count of the returned entry. Normally, indices are zero-based, ranging from 0 to one less than the number of elements. However, as in the Asp language, negative indices are allowed in the range -1 to the number of elements expressed as a negative integer. An index value of -1 refers to the last element. If the given index value is out of range, a null pointer is returned.

Unlike tuples and lists, strings and ranges do not contain objects. Strings consist of characters and ranges represent sequences of integers. Because of this, the AspElement function does not handle either of them. Instead, the AspStringElement function is provided for accessing the characters of a string, and the AspRangeElement function for accessing the integer members of a range. Instead of returning a data entry pointer, they return the character or integer at the indicated position, respectively. The index argument works in the same way as with AspElement, permitting the use of negative index values. An out-of-range index value causes AspStringElement to return the null character ('\0'), and AspRangeElement to return zero.

To locate an item in a set or dictionary, use the AspFind function. Like the AspElement function, it does not increment the use count of the returned data entry. As expected, if a matching item is not found in the collection, a null pointer is returned. Otherwise, for sets, the matching key entry is returned; for dictionaries, the entry referring to the value associated with the matching key is returned.

The AspNext function works with an iterator to yield the next value in an iterable (i.e., a collection-like object). Successive calls to AspNext return subsequent values in the iterable. Unlike the functions described above, the caller is responsible for unreferencing the returned object or otherwise dealing with its ownership. This is because in some cases (like with iterating over a range or a string), a new object is created. In the cases where an existing object is returned, AspNext increments the use count so that treatment of the returned object is consistent. If the iterator is advanced to the end of the iterable, AspNext returns a null pointer.

Object creation

AspDataEntry *AspNewType(AspEngine *);
AspDataEntry *AspNewBoolean(AspEngine *, bool value);
AspDataEntry *AspNewInteger(AspEngine *, int32_t value);
AspDataEntry *AspNewFloat(AspEngine *, double value);
AspDataEntry *AspNewSymbol(AspEngine *, int32_t value);
AspDataEntry *AspNewRange
    (AspEngine *, int32_t start, int32_t end, int32_t step);
AspDataEntry *AspNewUnboundedRange
    (AspEngine *, int32_t start, int32_t step);
AspDataEntry *AspNewString(AspEngine *, const char *buffer, size_t size);
AspDataEntry *AspNewIterator
    (AspEngine *, AspDataEntry *iterable, bool reserved);
AspDataEntry *AspNewAppIntegerObject
    (AspEngine *, int16_t appType, int32_t value,
     void (*destructor)(AspEngine *, int16_t appType, int32_t value));
AspDataEntry *AspNewAppPointerObject
    (AspEngine *, int16_t appType, void *value,
     void (*destructor)(AspEngine *, int16_t appType, void *value));
AspDataEntry *AspNewType(AspEngine *, const AspDataEntry *object);

In the syntax citation above, Type stands for one of the types supported by Asp that does not require any more information to create the object. The actual function names are: AspNewNone, AspNewEllipsis, AspNewTuple, AspNewList, AspNewSet, and AspNewDictionary. Each one returns a pointer to the newly created data entry. In the case of the collection-oriented functions, empty collections are created. Additional functions are provided that accept additional parameter(s) to provide a specific value for the created object.

Two functions support the creation of range objects: AspNewRange and AspNewUnboundedRange. The AspNewRange accepts start, end, and step parameters. The AspNewUnbounededRange function creates a range object with no specified end, so only start and step parameters are required.

For AspNewString, a character buffer and size must be provided to initialize the value of the new string object. To create an empty string, specify a size of 0. In this case, buffer is not used, so a null pointer can be passed.

For AspNewIterator, the iterable object is specified, along with a Boolean indicating whether the iterator should be a normal (forward) iterator or a reverse iterator. The iterable object may be any of the collection types, a string, or a range. Use AspNext with the new iterator to iterate through each value. Iterators can also be tested to determine whether they are at their end by converting them to a Boolean value using AspIsTrue. A value of false indicates the iterator has reached its end.

In addition to specifying an iterable object, an existing iterator may be passed as the AspNewIterator function's iterable parameter. In this case, a copy of the existing iterator is returned which retains its current position. When the reversed parameter is set to true, the type of iterator returned depends on the type of iterator passed, as the direction is simply changed to be the opposite of the original iterator.

Asp supports two application-specific object types. One uses an integer to identify an object within the application, and the other uses a pointer. Integer-type application objects are created using AspNewAppIntegerObject, while pointer-type ones are created with AspNewAppPointerObject. An application may use one or the other, or both, depending on the nature of the objects being referenced. In addition to the integer or pointer value, a 16‑bit type value is used to identify the object's type in the application. The contents of such objects are inaccessible to script code; scripts can only pass them around, group them into collections, etc., but the main purpose of these objects is to carry information sufficient for the application to locate and manipulate data structures that only the application knows about.

A couple of examples should suffice to explain how application objects are used, and how to decide which type is preferable in a given situation. First, we will consider an open file descriptor, which is an integer value in POSIX systems (e.g., the return value of the POSIX open function). Asp scripts have no notion of files, but an application can provide an interface for interacting with files by creating, returning, and accepting as parameters, application objects representing open files. In this example, an integer-type application object would be appropriate to hold the file descriptor.

Pointer-type application objects can be used anywhere an application uses a pointer to refer to an object or structure instance. For example, instead of using POSIX calls for a file interface, an application could use the C FILE * pointer value returned by fopen to represent an open file.

For structure (or C++ class) instances that the application manages, the type of application object can depend on how the application manages the objects. An integer can be used, for example, to index an array of objects. A pointer can be used to reference a dynamically allocated structure. In any case, the integer or pointer must uniquely identify an object in the application.

For both integer-type and pointer-type application objects, a destructor function must be specified when they are created. This destructor handles objects whose use count drops to zero during script execution. But note that other objects may remain when a script finishes execution, and must be handled by the application. Further discussion of automatic application object destruction and the responsibilities of the application can be found in the section on destruction of application objects.

In the case of the AspNewType function, the type of the passed object is used to determine the value of the new type object.

It is the responsibility of the application to manage the ownership of the newly created object. Initially, its use count is set to 1. If nothing else is done to the object, calling AspUnref on it will cause it to be destroyed. See the section on reference counting for more information.

Sequence manipulation

bool AspTupleAppend
    (AspEngine *, AspDataEntry *tuple, AspDataEntry *value, bool take);
bool AspListAppend
    (AspEngine *, AspDataEntry *list, AspDataEntry *value, bool take);
bool AspListInsert
    (AspEngine *, AspDataEntry *list,
     int32_t index, AspDataEntry *value, bool take);
bool AspListErase
    (AspEngine *, AspDataEntry *list, int32_t index);

Items can be inserted into sequences or erased from sequences using one of the above functions, each of which returns true to indicate success.

AspTupleAppend is used to build a tuple. Note that in Asp, tuples are immutable, but in C, a mechanism is required to define the elements of a newly created tuple. An application function must not abuse this routine to modify the contents of a pre-existing tuple. Another function, AspListAppend, is used to add an item to the end of a list. Items can also be inserted at a position other than the end using AspListInsert. This last function accepts an index value which may be negative and works the same way as in element access (see the section on collection information). For negative index values, the item is inserted after the indicated position so that the newly inserted item is at the indicated position when the insertion is completed.

An item can be removed from a list using AspListErase. The index parameter follows the same rules as for element access.

All the append/insert functions accept a Boolean argument named take. This indicates whether the sequence should take ownership of the passed value entry. If a take value of true is given, the function will call AspUnref to decrement the reference count of the object, obviating the need for the application to perform this operation. This is the normal case and prevents the inserted object from causing a memory leak into the engine's data area. Only if the inserted object is used elsewhere (e.g., inserted into another collection, or returned in *returnValue) should the value of take be set to false.

Building strings

bool AspStringAppend
    (AspEngine *, AspDataEntry *str,
     const char *buffer, size_t bufferSize);

Like tuples, strings are immutable in Asp, but the application needs a way to build them. So, the AspStringAppend function is provided for that purpose. As with tuples, an application must not abuse the function by modifying a pre-existing string object. Using AspStringAppend, an application can build an Asp string from several smaller parts if desired. The data to be appended is specified in buffer and its length in bufferSize.

Set and dictionary manipulation

bool AspSetInsert
    (AspEngine *, AspDataEntry *set, AspDataEntry *key, bool take);
bool AspSetErase
    (AspEngine *, AspDataEntry *set, AspDataEntry *key);
bool AspDictionaryInsert
    (AspEngine *, AspDataEntry *dictionary,
     AspDataEntry *key, AspDataEntry *value, bool take);
bool AspDictionaryErase
    (AspEngine *, AspDataEntry *dictionary, AspDataEntry *key);

Inserting objects into sets and dictionaries is accomplished using the AspSetInsert and AspDictionaryInsert functions, respectively. In the case of AspSetInsert, only a key is required, whereas AspDictionaryInsert requires both a key and an associated value. Items can be removed from sets and dictionaries by using AspSetErase and AspDictionaryErase, respecitively. The return value is true if the specified key was found and erased from the collection.

As with the sequence manipulation functions, the insert functions also take a take argument to indicate whether the collection will assume exclusive ownership of the object(s) being inserted. For AspDictionaryInsert, this applies to both the key and the value, so if the application needs to treat these two objects differently (e.g., the value is used elsewhere but the key is not), then it can take one of two courses of action. It can still specify true for the take argument, and then call AspRef on the object that will be shared. Or the take argument can be set to false, and then AspUnref can be called on the object not being used elsewhere. Either method achieves the same goal of managing use counts to avoid leaks into the engine's data area.

Reference counting

void AspRef(AspEngine *, AspDataEntry *object);
void AspUnref(AspEngine *, AspDataEntry *object);

Several sections above have mentioned reference counting. This section explains the topic in a bit more detail.

When an object is created, whether by the script's code or through the use of one of the AspNew… functions, it is represented in the engine's data area as a data entry with an initial use count of 1. The AspRef and AspUnref functions directly manipulate this use count, increasing or decreasing it by 1, respectively. When AspUnref reduces the use count of an object's entry to zero, the entry is immediately freed (i.e., marked as a free entry and added to the free list where it can subsequently be allocated to house another object).

Normally, the engine's logic manages the reference counts to ensure no objects become orphaned and thus tie up data area entries (a condition called a leak). When an application's C code manipulates objects, it sometimes becomes necessary for it to manually control the use counts.

The use count of application function arguments is automatically handled by the engine. Likewise, the returnValue parameter is managed by the engine. This requires a bit of a closer look. Upon entry, the value at *returnValue is actually a C null pointer (i.e., it does not point to a None object as one might think). This means that when an application function assigns the pointer of a newly created object to *returnValue, there is no need to first unreference it. In fact, doing so would be an error since there is no object there. When the function returns, the engine checks the value at *returnValue, and if it is a C null pointer, the engine creates a None object and supplies it as the default return value to the calling script*.

It should be noted that if an application causes a data area leak, the condition is not permanent. When the script ends and another one is started, the entire data area is reset, and any entries that contained leaked objects are freed for use when the new script runs. Still, care should be taken not to introduce data entry leaks, as it could lead to an out of data memory condition, causing the script to terminate unnecessarily.

See also the section on the destruction of application objects, which require special consideration.

* Strictly speaking, this is not an accurate description because the engine's data area contains only one shared None object. So, when a None object is created, the use count of the singleton None is incremented. Unreferencing this shared None object should never cause it to be destroyed, as its use count should never reach zero.

Destruction of application objects

When the use count of an application object is reduced to zero, a destructor function, specified when the object was created, is called. (See the object creation section for details.) This function can be used to perform any resource management tasks required to clean up the object. For example, if the object represents an open file, the destructor function would be used to close the file.

Although the destructor function is called for any application object whose use count drops to zero, it should be noted this will not happen for all application objects when a scripts ends, either successfully or in error. For this reason, applications must organize such objects in a way that, upon script completion, any objects that remain can be cleaned up. Failing to deal with application objects remaining after a script finishes execution can result in unintentional memory and/or resource leaks. Of course, an application can decide to allow objects to persist from one script invocation to the next, but ulimately, the application is responsible for resource management in this regard.

Like other application functions, the destructor function should perform its task quickly and return control back to the engine in a timely fashion. Unlike regular application functions, there is no way to return AspRunResult_Again to cause a re-entrant call, so if the destruction of an application object could take a considerable amount of time to complete, then instead of causing a delay in the engine, the destructor function should note the condition in the context area, and then perform the waiting outside of script execution, i.e., between calls to AspStep.

Script argument access

AspDataEntry *AspArguments(AspEngine *);

As mentioned in the section on setting arguments, an application may provide arguments to a script. These are accessible to the script by accessing the args variable in the sys namespace. If it is necessary for the application to access the arguments, it can obtain access to the argument tuple by calling AspArguments.

If no external arguments have been supplied, and even if the application does not provide support for external arguments (i.e., it does not call the AspSetArguments or AspSetArgumentsString functions), the object returned by AspArguments will still exist; it will be a tuple containing a single empty string. (The first element always refers to a concatenated string of all the arguments, and the first argument, if present, appears as the second element, index 1.)

If arguments have been supplied, all the elements in the tuple will be strings. It is the job of the application (or the script) to parse the arguments into meaningful values.

AspContext and AspAgain

void *AspContext(const AspEngine *);
bool AspAgain(const AspEngine *);

When an application calls AspInitialize, one of the arguments that is passed is a pointer to a context area. The purpose of this is to provide an area associated with the script engine instance that contains application-specific data that script-callable application functions can use. If specified, the application should define and allocate a suitable structure to store whatever information will be required. While application functions always have access to global variables, the context area is associated with a specific script engine instance. If multiple script engine instances with the same application specification are being used, per-instance data can be stored in the context area, keeping it separate from that of other instances.

To gain access to the area from within a script-callable function, use the AspContext function and cast the returned void pointer to the application-defined type for the area. The context area can be used to solve many design issues. It should be noted that the context area is not cleared between script runs, as the engine has no idea of how the area is structured. So, the application must ensure the area is sufficiently reset prior to running a new script. Depending on the application, some data may need to persist between script runs, while other data may need to be reset between runs.

The next few paragraphs describe a particular issue that is solved by using both the context area and another feature the Asp library supports.

As mentioned in the sections covering engine control and the AspStep function, the Asp engine executes script instructions one at a time, each instruction taking very little time to execute, thus leaving the application in control of the processor at a high frequency. One of those instructions is the CALL instruction which calls either a script-defined function or an application function. Because script-defined functions are made up of individual instructions, they too behave in such a way as to leave the application in control of the processor on a regular basis. However, in the case of script-callable application functions, the C code of the function could potentially hold on to control of the processor, making it possible that a call to AspStep that executes a CALL instruction could take an indeterminant amount time to complete. For example, an application function that runs a motor through a motion profile and waits for its completion could take an unacceptably long period of time to complete, starving the main control loop of the application of processing cycles.

To solve this problem, the Asp engine provides two design elements that work together to allow application functions to relinquish processor control (i.e., return) while appearing to block in the script, waiting for completion of their task. The first design element is the context area (discussed above), and the second is a mechanism by which an application function can arrange for re-entry and then determine whether it is being called for the first time or is being re-entered in the context of a previous call.

The context area is used to store state information to support re-entrant application functions. To make an application function re-entrant, and yet appear as blocking to the calling script, two things must happen. First, the function must return a special value, AspRunResult_Again, on all but the final invocation. This indicates to the engine that the CALL instruction is not yet finished. On the next call to AspStep, the same CALL instruction is executed again, causing the application function to be re-entered. The second thing that must be done is for the application function to check whether it is being called for the first time or as a subsequent re-entry. This is accomplished by calling the AspAgain function, which returns true for a re-entrant call.

On the initial call into the function (when AspAgain returns false), state information in the context area should be initialized. On re-entrant calls, the state can be queried and/or updated to progress through to the end goal of the function. When the function determines that its task is complete, it should return AspRunResult_OK (or an error value if appropriate) to mark the execution of the CALL instruction complete and allow the engine to move on to the next instruction.

Note that there is nothing forcing the application to make calls to AspStep if it can otherwise determine that a script-callable function is in an unfinished state and no progress can be made, making it a waste of time to call it. In this situation, the application would have to determine (from the main loop) when a condition sufficient to advance the logic of the script-callable function has occurred, and then resume calling AspStep to allow that logic to advance, updating its state, and possibly finishing its task.

There are other ways an application can retain control of the processor without using the forgoing technique (e.g., multitasking). However, in designs that depend on a main loop running at a fixed frequency, this mechanism makes running Asp scripts possible without compromising the integrity of the system.

Calling script functions from application functions

bool AspAddPositionalArgument
    (AspEngine *, AspDataEntry *value, bool take);
bool AspAddNamedArgument
    (AspEngine *, int32_t symbol, AspDataEntry *value, bool take);
bool AspAddIterableGroupArgument
    (AspEngine *, AspDataEntry *value, bool take);
bool AspAddDictionaryGroupArgument
    (AspEngine *, AspDataEntry *value, bool take);
void AspClearFunctionArguments(AspEngine *);
AspRunResult AspCall(AspEngine *, AspDataEntry *function);
AspRunResult AspReturnValue(AspEngine *, AspDataEntry **returnValue);
int32_t AspNextSymbol(AspEngine *);
AspDataEntry *AspLoadLocal(AspEngine *, int32_t symbol);
AspRunResult AspStoreLocal
    (AspEngine *, int32_t symbol, AspDataEntry *value, bool take);

Asp provides the ability for script-callable application functions to call other functions, including those that are defined in the currently running script. Typically, the target function is passed as an argument from the script into the application function. An example would be an Asp-callable sort function implemented in the application that calls a passed script function to perform element comparisons.

Application functions that use this facility must be written in a special way that avoids overusing the application's stack in order to guard against the possibility of recursion, whether intentional or not. The basic steps to call a function are as follows:

  1. Specify any calling arguments by using any of the AspAddTypeArgument functions. This step is not required if there are no arguments being passed to the target function. The AspClearFunctionArguments function may be used if there is a need to clear the argument list and start over. As with other functions that take a take argument, be careful to manage object use counts by indicating whether or not the argument list should take ownership of the argument object. Normally, this will be true for newly created argument objects, and false otherwise. The AspAddTypeArgument functions increment the object's use count when take is false so that the object is not destoyed when the argument list is consumed (and destroyed) during the call.
  2. Use AspCall to prepare for the function to be invoked. Note that AspCall does not directly call the target function. Instead, it returns a special return value that the application function must return. When the engine regains control, the special return value causes the engine to invoke the target function.
  3. When the target function exits, the engine calls back into the application function, as if it had originally returned AspRunResult_Again (see the section on the again feature*). At this point, the application function must extract and take ownership of the called function's return value. The return value object is extracted by calling AspReturnValue. The application is responsible for the return value object. For example, if it is no longer needed after its underlying value has been extracted, it should be freed using AspUnref.

The above steps ignore saving and restoring state between reentries into the application function. In the section on the again feature, the context area was mentioned as a location where state information could be saved. But because of the potential for recursion when calling functions from within application functions, using the context area is not recommended here. The context area can be thought of as a global space for the engine. What is needed to handle recursion is a local space to save and restore state. This is where the AspNextSymbol, AspLoadLocal, and AspStoreLocal functions come in.

Therefore, the general approach to designing an application function that calls other (i.e., script) functions is as follows:

  1. Upon every entry into the function, assign symbols for each required state variable using AspNextSymbol. These symbols are guaranteed to be unique with respect to the symbols used for the application function's own parameters.
  2. For each of the state variables, declare an appropriately typed local C variable to hold the state value. The type should be compatible with an Asp data type (e.g., integer, Boolean, etc.).
  3. Call AspAgain to determine whether this is the first or a subsequent invocation. On the first invocation (i.e., when AspAgain returns false): On subsequence invocations:
  4. Based on the current state, perform the main logic of the function, updating the C state variables. If the state indicates a return from calling another function, process the return value as described above. When calling another function with AspCall, perform the next step before exiting the function with the value that AspCall returns.
  5. Create new Asp objects of the appropriate types for each of the C state variables (e.g., AspNewInteger for an integer, etc.). Make sure to check the return values to guard against an out-of-memory condition. Then use AspStoreLocal to save the values into the function's local scope using the applicable symbol for each variable, specifying true for the take parameter.
  6. If AspCall was used, return its return value. Otherwise, return AspRunResult_OK or AspRunResult_Again as appropriate. When returning AspRunResult_OK, the local scope is automatically cleaned up, so there is no need to explicitly destroy the objects that were stored for saving and restoring state between invocations.

It should be noted that there are no functions for saving/restoring variables in the global scope. As mentioned above, the context area can be used as a storage location for state that is global with respect to the engine. Beyond that, simple C global variables can be used to store state that would apply to all engines running in the application.

* The AspCall function actually returns AspRunResult_Call, but the application should not use this value explicitly, but simply use the value returned by AspCall as the return value of the application function.

AspAssert

AspRunResult AspAssert(AspEngine *, bool);

The implementation of the Asp engine uses the AspAssert function for ensuring certain conditions hold true, causing an error to occur if not. This function is also available for application functions to use. It is like the C assert macro, except that it does not cause the entire application to abort, and it always performs the test, even in non-debug builds. Instead, if the passed argument is false (indicating a failed assertion), it prevents the engine from executing any more instructions, and returns AspRunResult_InternalError. Even if the function does not return the error code (which it should), execution of the script is still eventually halted, and subsequent calls to AspStep return the same error value. If, on the other hand, the argument is true, AspAssert returns AspRunResult_OK and all is well.

Note that the Asp language includes an assert statement, but its behaviour is slightly different than that of the AspAssert function. On a failed assertion, the assert statement causes an ABORT instruction to execute, which causes AspStep to return AspRunResult_Abort (instead of AspRunResult_InternalError). An application function can replicate this behaviour by testing the condition of interest in an if statement and returning AspRunResult_Abort.

Debug library

In addition to the Asp engine library, which is linked into an embedded application, the Asp platform also provides a debug library for use on an external machine that can receive run-time information from the embedded application. For Linux environments, the debug library can be linked using -laspd on the gcc command line. Applicable code should include the <asp-info.h> header.

#include <asp-info.h>

The routines in this library make use of the debug information file (.aspd) generated by the compiler. Their primary purposes are (a) translating error codes into human-readable strings, and (b) translating a program counter into the associated script source location, both useful for determining the nature of a script run error. The following sections describe the available routines.

Note also that the functionality of this library is provided in the aspinfo utility. See the Script Writer's Guide for a description.

Error code translation

const char *AspAddCodeResultToString(int result);
const char *AspRunResultToString(int result);

Functions in the Asp engine library return AspAddCodeResult and AspRunResult error codes. The debug library routines AspAddCodeResultToString and AspRunResultToString translate these codes into human-readable strings so that an operator running a script can get a description of errors that occur when a script fails to complete successfully.

Note that these routines accept integers rather than the specific error type used in the engine library. The reason for this is that the computer that is deciphering debug information is not normally the same machine running the embedded application, and therefore may not have access to the <asp.h> header where these types are defined. As mentioned above, it is necessary for the embedded application to somehow communicate the error codes to the external system prior to translating the codes. How this is done is specific to each application.

Source information

AspSourceInfo *AspLoadSourceInfoFromFile(const char *fileName);
AspSourceInfo *AspLoadSourceInfo(const char *data, size_t size);
void AspUnloadSourceInfo(AspSourceInfo *);
AspSourceLocation AspGetSourceLocation(const AspSourceInfo *, size_t pc);
const char *AspGetSourceFileName(const AspSourceInfo *, unsigned index);
const char *AspGetSymbolName(const AspSourceInfo *, uint32_t symbol);

When a script ends abnormally, the engine library provides a means of obtaining the program counter which identifies the location of the instruction that caused the error. Once the value of the program counter is communicated to the external system, and assuming the debug information file that matches the script is available, the debug library can be used to translate the program counter value into a source location consisting of file name, line number, and column.

The AspLoadSourceInfoFromFile routine is used to load the debug information file (.aspd). It creates an AspSourceInfo structure instance which is used in subsequent calls to other functions. It accepts a single argument specifying the name of the debug information file. If the contents of the file are already in memory, the AspLoadSourceInfo function may be used to load the content instead. When the source information structure is no longer needed, it should be deallocated using AspUnloadSourceInfo, which takes a pointer to the structure as its only argument. It should be noted that, unlike the engine library, these debug library routines make use of dynamic memory allocation (i.e., malloc and free) to allocate enough memory for the content. Also, when using AspLoadSourceInfo, the source data is shared and must remain in memory until after the call to AspUnloadSourceInfo.

Once an AspSourceInfo structure instance as been created as described above, a program counter value may be translated using the AspGetSourceLocation function. This function returns an AspSourceLocation structure instance, which is defined as follows.

typedef struct AspSourceLocation
{
    const char *fileName;
    unsigned line, column;
} AspSourceLocation;

In the event an invalid program counter is passed, the returned structure will have its fileName member set to the null pointer. Otherwise, the name of the source file and the applicable line and column of script source code will be populated.

The AspGetSourceLocation function may also be used during the execution of a script, provided the embedded application can transmit the current program counter frequently enough. This technique may be used to trace the execution of a script, providing real-time source location information to an operator. In this case, it may be useful to obtain a list of source file names prior to execution so that they may be preloaded (e.g., if they are going to be displayed, with the currently executing line highlighted). The names may be obtained by calling the AppGetSourceFileName function with a zero-based index. Repeatedly calling this routine with increasing values of the index parameter until it returns a null pointer will return the names of all the source files comprising the script.

Symbol numbers may also be translated into their corresponding name via the AspGetSymbolName function. Since symbols are printed as numeric values, this can help identify a variable or other named object of interest. Note that older versions of the compiler did not write symbol name information into the debug information file. In this case, AspGetSymbolName returns a null pointer. If the symbol information is present, but the symbol number is not found, an empty string is returned. In this way, the two error conditions can be distinguished.

More information

For the curious, there is the Internals document, which describes how Asp works under the hood. It describes the engine's instruction set, the layout of data area entries, and how the compiler generates code, among other topics.

Copyright (c) 2024 Canadensys Aerospace Corporation