Custom Engines & The EngineAPI

An engine is a Perl script that uses the EngineAPI package to access the spacecraft memory resident model.

Hello World

You can create an engine anywhere, as an engine is just a Perl script:

$ touch hello.pl

And then execute it with spacecraft:

$ spacecraft hello.pl
** NOTE : Firing engine hello.pl.
** NOTE : Engine hello.pl finished in 0.001364661 seconds.
** NOTE : Finished in 0.004851019 seconds with 0 warnings, 0 errors.

To turn the script into an engine, you must include the EngineAPI:

use EngineAPI;

&sc_note(0,"Hello World!");

Now when you execute, you’re hooked into spacecraft:

$ spacecraft hello.pl
** NOTE : Firing engine hello.pl.
** NOTE : Hello World!
** NOTE : Engine hello.pl finished in 0.009796026 seconds.
** NOTE : Finished in 0.013642397 seconds with 0 warnings, 0 errors.

The first step is generally to get a handle to the memory resident model using the sc_get_space API:

use EngineAPI;

# Get a handle to the model:
my $space = &sc_get_space();

# And use the object oriented methods:
&sc_note(0,"Hello %s!", $space->sc_get_type);

Now when you execute, you’ve dereferenced the model. But since you’ve started from a blank canvas, the space doesn’t even have a type!

$ spacecraft hello.pl
** NOTE : Firing engine hello.pl.
** NOTE : Hello !
** NOTE : Engine hello.pl finished in 0.009796026 seconds.
** NOTE : Finished in 0.013642397 seconds with 0 warnings, 0 errors.

To see the power of the memory resident model, let’s create another script:

$ touch world.pl

With the following in it:

use EngineAPI;

my $space = &sc_get_space();

$space->sc_set_type("World");

Now when you execute, execute both engines! First the world.pl engine to construct the model, then the orignal hello.pl engine to output the model:

$ spacecraft world.pl hello.pl
** NOTE : Firing engine world.pl.
** NOTE : Engine world.pl finished in 0.009658130 seconds.
** NOTE : Firing engine hello.pl.
** NOTE : Hello World!
** NOTE : Engine hello.pl finished in 0.006746967 seconds.
** NOTE : Finished in 0.020468362 seconds with 0 warnings, 0 errors.

Notice how the hello.pl now reports the type set in world.pl – the model is passed between the scripts and you’ve separated model construction from model output!

You can always construct the model with the EngineAPI – and you’d do this when you need to convert an existing format – but it’s easier to describe the model in Rocket Fuel and load it that way.

So let’s try adding some Rocket Fuel and executing again:

$ touch World.rf
$ spacecraft World.rf hello.pl
** NOTE : Parsed World.rf in 0.000246411 seconds. Found 0 regions and 0 fields.
** NOTE : Firing engine hello.pl.
** NOTE : Hello World!
** NOTE : Engine hello.pl finished in 0.010435138 seconds.
** NOTE : Finished in 0.014355259 seconds with 0 warnings, 0 errors.

And voila, you’ve loaded the space defined in the World.rf Rocket Fuel and have a handle to it in your engine.

Now it’s a matter of using the EngineAPI to do what ever cool stuff you need to do with your definitions – including converting them to Rocket Fuel!

If at some point your engines become useful for more than just you, you can promote them to your local launchpad. If you do go that far, consider following the instructions for contributing, because you may at some point decide your engine is good for the rest of us.

Working with Bits

Because the Rocket Fuel fixed point numbering notation for working with spacecraft bits is not part of the Perl languagea, the EngineAPI interface uses Perl strings to exchange the sc_bit type. That is, when you see the sc_bit type in the EngineAPI, think of it as string holding a spacecraft fixed point bit number, not as a number. For example:

$offset = "64";
$size   = 32;

Here the $offset is a valid sc_bit because it is a string, while the $size is not because it is an integer. To keep it straight, just keep in mind that a bare sc_bit in perl is a syntax error:

$size = 32KB;  # Not valid perl

(a) yet, at least for the version embedded in spacecraft.

Working with Spaces, Regions and Fields

The spacecraft model – which is a hierarchy of regions within a space – is accessible inside an engine as a hierarchy of Perl objects.

At the top of the hierarchy is a singleton instance of a space object that is shared between engines. To get a handle to the space inside the engine, you use the sc_get_space function:

my $space = &sc_get_space();

A space is a region and a region is a node.

A field is also a node.

This leads to the following inheritance hierarchya in the EngineAPI:

 ┌─────────┐
 │  node   │
 └────┬────┘
      ├────────────┐
 ┌────┴────┐  ┌────┴────┐
 │ region  │  │  field  │
 └────┬────┘  └─────────┘
      │
 ┌────┴────┐
 │  space  │
 └─────────┘
FIGURE 1: Inheritance hierarchy

(a) not to be confused with the instance hierarchy actually mapped by the space.

Get Methods

All nodes have the following sc_get_* methods to access the data members:

print $node->sc_get_address();
print $node->sc_get_identifer();
print $node->sc_get_offset();
print $node->sc_get_name();
print $node->sc_get_size();
print $node->sc_get_span();
print $node->sc_get_type();
print $node->sc_get_description();
print $node->sc_get_property($key);
print $node->sc_get_filename();
print $node->sc_get_lineno();

But only regions have globs:

print $region->sc_get_glob();

And only fields have values:

print $field->sc_get_value();

Note:

Set Methods

All nodes have the following sc_set_* methods to set the data members:

$node->sc_set_offset($offset)           or &sc_error("set failed");
$node->sc_set_size($size)               or &sc_error("set failed");
$node->sc_set_name($name)               or &sc_error("set failed");
$node->sc_set_type($type)               or &sc_error("set failed");
$node->sc_set_description($description) or &sc_error("set failed");
$node->sc_set_property($key,$value)     or &sc_error("set failed");

And again only regions have globs:

$region->sc_set_glob($glob) or &sc_error("set failed");

and only fields has values:

$field->sc_set_value($value) or &sc_error("set failed");

The sc_set_* methods return 1 if the set is successful and 0 otherwise. A set can fail for a number of reasons, depending on what is being set. For example, changing an offset may cause the node to overlap with a sibling or extend past the end of it’s parent. If a set fails, an error is issued and the node is unchanged.

Note that you can’t directly set the address or identifier. That’s because they are calculated values that depend on the offset and name of a node and the node’s place in the hierarchy of regions.

Also note that you can’t set the filename & lineno because they are read-only.

Test Methods

All nodes have the following sc_has_* or sc_is_* methods to test whether an aspect of the node is true or false:

print "true" if $node->sc_is_space();          
print "true" if $node->sc_is_region();         
print "true" if $node->sc_is_field();          
print "true" if $node->sc_is_named();          
print "true" if $node->sc_is_typed();          
print "true" if $node->sc_is_first_child();          
print "true" if $node->sc_is_last_child();          
print "true" if $node->sc_has_properties();     
print "true" if $node->sc_has_property($key);  
print "true" if $node->sc_has_dimensions();    
print "true" if $node->sc_has_children();      

Iteration Methods

As the space is a hierarchy of regions, all nodes have the following methods to sequentially traverse the hierarchy.

To iterate bottom-up from child to parent, use sc_get_parent:

while ($parent = $child->sc_get_parent()) {
	...
}

And to iterate top-down from parent to each child, use sc_get_next_child:

while ($child = $parent->sc_get_next_child()) {
   ...
}

Note that as fields do not have children, calling sc_get_next_child on a field will return undef. This terminates the while loop before it starts and results in the desired behaviour.

Working with Properties

Each node can have a set of properties and each property is a key/value pair.

To check if the node has any properties:

$node->sc_has_properties();  # returns 1 if $node has any properties

To check if the node has a specific property:

$node->sc_has_property($key);  # returns 1 if $node has property $key

To get the value of the specific property:

$value = $node->sc_get_property($key);

To update the value of an existing property or to add a new property:

$node->sc_set_property($key);  
# - OR -
$node->sc_set_property($key,$value);

Note that a node can have a property that has no value (or a value of undef) because in some cases the existence or non-existence of a property is sufficient information. This capability introduces a couple of things to consider.

The first thing to consider is that the sc_get_property returns the property value and will return undef both when the property does not exist and when the property does not have a value. Hence you can not safely use sc_get_property to test for the existence of a property, since it will return falsy when the value is undef. For existence checking you must use sc_has_property.

For example, the following snippet:

$node->sc_set_property("exists");
print "exists\n" if $node->sc_get_property("exists");  # does not print
print "exists\n" if $node->sc_has_property("exists");  # prints

will print only once because the sc_get_property returns the value undef, which is falsy, while the sc_has_property returns truthy because it looks at the existence of the key, not the value.

The second thing to consider is that setting the value to undef does not remove the property. It simply turns the property into an existence boolean. To remove the property, you must use the sc_unset_property method.

$node->sc_set_property("key","value");
print "exists\n" if $node->sc_has_property("key");  # prints

$node->sc_set_property("key");
print "exists\n" if $node->sc_has_property("key");  # prints

$node->sc_unset_property("key");
print "exists\n" if $node->sc_has_property("key");  # does not print

Lastly, you can loop through the list of properties on a node with the sc_get_next_property method:

while ($key = $node->sc_get_next_property()) {
	$value = $node->sc_get_property($key);
	print $value ? "$key = $value\n" : "$key";
}

Working with Dimensions

Dimensions are a concise way to represent adjacent copies of a node, but with power comes responsibility. Dimensions transform redundancy in the definition into complexity in the engine. This is particularly true when nodes have multiple dimensions at multiple levels in the hierarchy.

All nodes can have zero or more dimensions.

To test where a node has any dimensions, use the sc_has_dimensions method:

$node->sc_has_dimensions();  # returns truthy if $node has any dimensions

To test how many dimensions a node has, use the sc_get_dimensions method:

print $node->sc_get_dimensions();  # returns N, the number of dimensions of $node

To iterate through the list of dimensions, if any, use the sc_get_next_dimension method:

while ($dim = $node->sc_get_next_dimension()) {
	...
}

Here the $dim variable returned is just the dimension number, and since dimensions are numbered starting from 1, the loop will iterate through the dimensions then return a 0 to terminate the loop.

To get the data member for a given dimension, use the sc_get_dimension_* methods:

$node->sc_get_dimension_label($dim);
$node->sc_get_dimension_from($dim);
$node->sc_get_dimension_to($dim);
$node->sc_get_dimension_size($dim);
$node->sc_get_dimension_span($dim);
$node->sc_get_dimension_count($dim);

To set the data member for a given dimension, use the sc_set_dimension_* methods:

$node->sc_set_dimension_label($dim,$label) or &sc_error("set failed");
$node->sc_set_dimension_from($dim,$from)   or &sc_error("set failed");
$node->sc_set_dimension_to($dim,$to)       or &sc_error("set failed");
$node->sc_set_dimension_size($dim,$size)   or &sc_error("set failed");

The sc_set_dimension_* methods behave like the rest of the sc_set_* methods, returning 1 when the set is successful, and otherwise returning 0 having left the dimension unchanged. Setting the dimension size, for example, to a value less than the node size, will cause the set to fail.

Note that you can’t set the span and count as these are calculated values.

Also note that you can only get/set dimensions that exist, and passing an invalid dimension number will issue a warning and have no effect.

Quite often an engine is assembling a dimension into a formatted string, which makes the sc_get_dimension_* methods cumbersome. To lighten the burden, you can use the sc_get_dimension method instead:

$node->sc_get_dimension($dim,$dimformat);

Where the $dimformat is a printf-like format string with % specifiers that extract dimension data members. For example:

# Assume label = 'x', from = 1, to = 3, snapping size

$node->sc_get_dimension(1,"%v");  # returns "[x:1:3]", the dimension vector

See the sc_get_dimension API for details.

To convert between unrolled & rolled representations, use the sc_unroll and sc_reroll APIs:

$node->sc_unroll();
$node->sc_reroll();

Note that these methods only make sense for nodes declared or constructed with dimensions and they have no effect on non-dimensioned nodes. Also note that this implies the initial state is rolled, which means you will always unroll first.

Working with Hierarchy

In addition to the iterative methods, the following two APIs are provided for working with hierarchy:

$space = $region->sc_detach();
$space->sc_reattach();

The sc_detach API moves all children from a region into a new separate space, leaving the origininal region childless, while the sc_reattach reverts the change.

For example, suppose we decide to instantiate two instances of a particular module:

0KB  8KB  EAST  EAST_* module;
8KB  8KB  WEST  WEST_* module;

If we run this through a documentation engine, we’ll get all EAST_* fields followed by all WEST_* fields. This is comprehensive but repetative.

A better approach is to the document the module type, then document the instances with cross references to the the type. This is done by detaching the space from the region, reruning the engine on the detached space, then reattaching the space:

sub subspaces {
  my $region = shift;
  my $list   = shift; $list = {} unless defined $list;

  while (my $node = $region->sc_get_next_child) {
    if ($node->sc_is_region) {
      my $type = $node->sc_get_type;
      if ($type) {
        #
        # Detach the space from the region and a copy of the type
        #
        $list->{$type} = $node->sc_detach unless $list->{$type};
      } else {
        &subspaces($node,$list);  
      }
    }
  }
  return $list;
}

my $space  = &sc_get_space
my $linked = &subspaces($space);

# Document the space
&document($space);

# Document the linked sub-spaces
foreach my $type (sort keys %{$linked}) {
  &document($linked->{$type});
  #
  # Reattach the detached space back into the model
  #
  $linked->{$type}->reattach;
}

Fueling

Though Rocket Fuel can be passed on the spacecraft command line, it doesn’t have to be and the model and be fueled through the API:

# Get the (empty) space
$space = &sc_get_space();

# Add "path" to the fuel supply (search list)
&sc_fuel_supply("path");

# Read the type (.rf file) into the space
$space->sc_fuel("type");    

which is equivalent to:

$ spacecraft -R -I path type.rf

but stores it in an engine. This can be a handy way to manage a long list of fuel supplies rather than passing them on the command line each time.

Though the previous example loaded the entire space, the fueling API can be used to fuel regions as well:

# Get the (empty) space
$space = &sc_get_space();

# Add a region
my $region = $space->sc_add_region(-offset => '0KB', -size => '8KB');

# Fuel the region fromt the "type.rf" file
$region->sc_fuel("type.rf");

The fueling API also supports the instantiation of a region type:

# Get the (empty) space
$space = &sc_get_space();

# Add some regions
my $east = $space->sc_add_region(
  -offset => '0KB', 
  -size   => '8KB', 
  -name   => 'EAST',
  -glob   => 'EAST_*'
  );

my $west = $space->sc_add_region(
  -offset => '8KB', 
  -size   => '8KB', 
  -name   => 'WEST',
  -glob   => 'WEST_*'
  );

# Load a region "type"
my $module = &sc_fuel("module.rf");

# Instantiate the type
$east->sc_set_children($module);
$west->sc_set_children($module);

Note that the same definition ($module) is used twice.

Mission Control

Engines have access to the logging system of spacecraft using the following printf-like functions:

&sc_note($level,"Just %s","information");
&sc_warn("Possible issue");
&sc_error("User issue");
&sc_fatal("Engine issue");

Utilities

&sc_bits();
&sc_dimensions();
&sc_import();

Engine Patterns

Most output engines follow a very simple pattern that recursively traverses the hierarchy of regions:

#
# Include the EngineAPI
#
use EngineAPI;

#
# Declare a function that works on one region at a time
#
sub work_on {
	my $region = shift;

	# iterate through all children in the region:

	while ($child = $region->sc_get_next_child) {

		if ($child->sc_is_field) {

			# do something useful with the field

		} else {

			# recursively work on the child region

			&work_on($child);
		}

	}
}

#
# Pass the space as the initial region
#
&work_on(&sc_get_space());