[Tutorial Home] [Lesson 1] [Lesson 2] [Lesson 3] [Lesson 4] [Lesson 5] [Lesson 6]

Lesson 5: Adding Functions

Last modified: Wed Jul 14 11:24:01 PDT 2004

Introduction

Lessons 1-3 show you how to install a Maté and write TinyScript programs. Lesson 4 shows you how to build a new Maté environment from existing functions and handlers. This lesson describes how to write new functions and how to incorporate them into your VMs. Doing so allows you to extend VMs to meet the computational requirements of particular applications.

Maté's dbg Statements

The existing Maté code uses TOSSIM's dbg functionality heavily; we strongly recommend that you test any new function or event in TOSSIM before running it on a mote. Maté uses three DBG settings: USR1, USR2, and USR3. Each has its own partilcuar use.

USR1 pertains to execution; almost every instruction has a USR1 statement to say that it's executing, so a tester can see what happens as a program executes. Also, events that trigger contexts to run or halt (as you'll see in Lesson 6) fall under USR1.

USR2 pertains to synchronization. USR2 statements instrumetn the Maté scheduler, its wait and ready queues, and system locks. Unless you are delving into Maté much more deeply than these tutorials do, you shouldn't need to add any USR2 statements.

USR3 pertains to code propagation and installation: the capsule propagation component, MVirus, uses USR3 for information on its timers and state. You shouldn't need to add any USR3 statements.

Adding a Simple Function

A Maté function is a TinyOS component that provides the MateBytecode interface, which has two commands:

interface MateBytecode {
  command result_t execute(uint8_t instr, MateContext* context);
  command uint8_t byteLength();
}
		    

byteLength() returns the length of an instruction in bytes. Function's byteLength must always return 1. execute is the actual logic of the function. execute takes two parameters. The first, instr is the opcode of the instruction that led to the function being called; this can be ignored. The second, context, is the execution context that is calling the function. context can be used to access execution state, such as parameters passed to the function.

In addition to a TinyOS component, a function must have an ODF file that describes it. The Maté toolchain uses the description to check that a program passes the right number of parameters, to figure out which component implements the function, and provide information to a user. An ODF file contains a FUNCTION element with several tags. For example, this is the ODF for the bfull function:


		    

The following table describes the set of tags that a FUNCTION element must have, and their meanings. There are also several optional tags, which you'll cover later.

name The name of the function as a user writing a script will use it.
opcode The name of the function as the toolchain and VM use it. While the name tag should be human readable, the opcode tag need not be. When VMBuilder generates the files for a VM, it generates a constant based on the opcode tag to identify the binary instruction.
numParams The number of parameters that the function takes. For example, bfull takes a single parameter. The TinyScript compiler checks that the number of parameters passed to a function match this value.
param1...N For each parameter (specified by numParams), there must be a tag of the form paramN, where N is the parameter number, starting at 1. A function with zero parameters has no param tags For example, a function that takes two parameters must have a param1 tag and a param2 tag. Each tag should say what type the corresponding parameter is. These types are currently unchecked by the TinyScript compiler; they serve only as documentation. For example, bfull's parameter is a buffer.
returnval Either true or false; denotes whether the function has a return value. bfull has a return value (an integer, denoting whether the buffer is full).
desc Text, describing the function. The Scripter and VMBuilder GUIs display this text.

To understand how writing a function works, you'll start by writing increment, a very simple new function. increment takes a single integer as a parameter and returns it incremented by one. The first step is to write the function description file. Make a new directory in tos/lib/VM, named extensions:

mkdir extensions

In the extensions directory, open a file named "OPincrement.odf". This will contain the FUNCTION element describing your function. The function takes a single parameter, an integer, and has a return value. Write the FUNCTION element describing increment. It should look something like this:

<FUNCTION name=increment opcode=incr numparams=1 param1=integer returnval=TRUE desc="Takes an integer as a parameter; returns that integer incremented by one.">

By default Maté toolchain looks for a function in a TinyOS component generated from the opcode tag. If the tag's value is incr, Maté looks for a component named OPincr (the nesC rules require this component to be in the file OPincr.nc). The component should provide a single instance of the MateBytecode interface, and nothing else: it will almost always be a configuration. For example, this is OPbfull.nc:

includes Mate;

configuration OPbfull {
  provides interface MateBytecode;
}

implementation {
  components OPbfullM, MStacksProxy, MTypesProxy;
  
  MateBytecode = OPbfullM;

  OPbfullM.Stacks -> MStacksProxy;
  OPbfullM.Types -> MTypesProxy;
}
		    

OPbfull takes the actual implementation of the function -- in OPbfullM -- and wires it to the needed components. The configuration then exports the function as a provided interface. This is OPbfullM.nc:

module OPbfullM {
  provides interface MateBytecode;
  uses interface MateStacks as Stacks;
  uses interface MateTypes as Types;
}

implementation {

  command result_t MateBytecode.execute(uint8_t instr,
                                            MateContext* context) {
    MateStackVariable* arg = call Stacks.popOperand(context);
    dbg(DBG_USR1, "VM (%i): Checking if buffer full.\n", (int)context->which);  
                
    if (!call Types.checkTypes(context, arg, MATE_TYPE_BUFFER)) {return FAIL;}
    call Stacks.pushValue(context, (arg->buffer.var->size == MATE_BUF_LEN)? 1: 0);
    return SUCCESS;
  }
  
  command uint8_t MateBytecode.byteLength() {return 1;}

		    

The implementation of the function, execute, has five lines. We'll step through them, one by one, but touch on the interfaces it uses first.

bfull uses two interfaces, MateStacks and MateTypes. Looking back at the configuration, the component MateStacksProxy provides the first, while MateTypesProxy provides the latter. MateStacks has a set of commands for manipulating the operand stack of a Maté context. Before performing an operation, the VM pushes operands onto the stack; the operation then pops the operands, using them as parameters. For example, to add two integers, a program pushes the two integers onto the operand stack, then executes the add instruction. The instruction's implementation pops the two integers off the stack, adds them, and pushes the result back onto the stack. TinyScript passes function parameters on the operand stack; the buffer a program is testing to see if it's full is on top of the stack.

The operands that MateStacks returns from popOperand commands must be handled carefully. Specifically, they can be corrupted or invalidated by any command that pushes something onto the operand stack. The pointer that popOperand returns is a pointer onto the actual stack. If something is pushed, the region of the stack where the return value of the pop points to may be overwritten. Therefore, when writing a function, you should process all operands before pushing anything back onto the stack.

MateTypes has commands for examining the types of operands. There are two types of commands, checks and queries. Checks begin with check, while queries begin with is. For example, MateTypes has a checkInteger and an isInteger. Both kinds of commands return TRUE if the operand matches the type. In addition to returning the result, check commands will trigger an error condition if the test fails. For example, if you call isInteger on a buffer, it will return FALSE; if you call checkInteger on a buffer, it will return FALSE and trigger a type check error in the core VM, halting execution.

The first line of execute pops the top of the operand stack, followed by a dbg statement for when using TOSSIM. Functions should always print a dbg message of type USR1 when they execute; this makes debugging in TOSSIM much easier.

MateStackVariable* arg = call Stacks.popOperand(context);
dbg(DBG_USR1, "VM (%i): Checking if buffer full.\n", (int)context->which);

If the programmer called bfull properly, then this argument should be a buffer; the third line checks that this is the case:

if (!call Types.checkTypes(context, arg, MATE_TYPE_BUFFER)) {return FAIL;}

If the operand is not a buffer, then checkTypes will trigger an error in the VM; the instruction exits immediately, and the VM halts execution.

Since the operand is a buffer, the function does the actual work, checking if the buffer is full:

call Stacks.pushValue(context, (arg->buffer.var->size == MATE_BUF_LEN)? 1: 0);

This line pushes an integer onto the operand stack: if the buffer is full, it pushes 1, if false it pushes 0. Operands (MateStackVariabless) are a union of their possible types: arg->buffer accesses the variable as a buffer, while arg->value accesses it as a value. In TinyScript, the actual structure is:

typedef struct {
  int16_t var;
} MateValueVariable;

typedef struct {
  uint8_t type;
  uint8_t size;
  int16_t entries[MATE_BUF_LEN];
} MateDataBuffer;

typedef struct {
  MateDataBuffer* var;
} MateBufferVariable;

typedef struct {
  uint8_t type;
  union {
    MateValueVariable value;
    MateBufferVariable buffer;
  };
} MateStackVariable;
		    

These and other Maté types can be found in tos/lib/VM/types/Mate.h.

The function has consumed its parameter (it popped the buffer off the operand stack), and produced a return value (it pushed the result onto the operand stack).

With increment, we want to do something similar to bfull: it takes a single parameter, and returns a value. In the extensions directory, open a file named OPincrementM.nc and type:

module OPincrementM {
  provides interface MateBytecode;
  uses interface MateStacks as Stacks;
  uses interface MateTypes as Types;
}

implementation {

  command result_t MateBytecode.execute(uint8_t instr,
                                            MateContext* context) {
    MateStackVariable* arg = call Stacks.popOperand(context);
    dbg(DBG_USR1, "VM (%i): Incrementing an integer.\n", (int)context->which);  
                
    if (!call Types.checkInteger(context, arg)) {return FAIL;}
    call Stacks.pushValue(context, (arg->value.var + 1));
    return SUCCESS;
  }
  
  command uint8_t MateBytecode.byteLength() {return 1;}
}
		    

It also needs a configuration. Open OPincrement.nc and type:

includes Mate;

configuration OPincrement {
  provides interface MateBytecode;
}

implementation {
  components OPincrementM, MStacksProxy, MTypesProxy;
  
  MateBytecode = OPincrementM;

  OPincrementM.Stacks -> MStacksProxy;
  OPincrementM.Types -> MTypesProxy;
}
		    

Now that you've written all three files (the ODF, the configuration, and the implementation), increment is ready to use. Go back to your specification file for SimpleVM, simple.vmsf, and add the following two lines:

<SEARCH PATH="../extensions">
<FUNCTION NAME="increment">			
		    

The first line tells VMBuilder to look in your new extensions directory, while the second tells it to include your new function. Rebuild the VM and install it on a mote. You can now write CntToLeds using the function instead of addition:

private counter;
counter = increment(counter);
led(counter % 8);
		    
Maté Abstractions and Split-Phase Operations

When writing the increment function, the configuration wired OPincrementM to a component named MateStacksProxy, which provided interfaces for manipulating the operand stack and type checking. You can find the full set of data abstraction components in tos/lib/VM/components. They include:

  • MateEngine: The core VM scheduler. It signals when the VM has rebooted (e.g., when new code arrives), which contexts and some functions need to know, to clear state properly.
  • MBufferProxy: Commands for accessing data buffers.
  • MContextSynchProxy: Commands and events for controlling context execution: the section on writing a new handler component goes into how to use this component's interface.
  • MErrorProxy: Commands for triggering an error condition in the VM.
  • MHandlerStoreProxy: Commands and events for monitoring and controlling code propagation: the section on writing a new handler component goes into how to use this component's interface.
  • MQueueProxy: Commands for manipulating context queues. Functions that encapsulate split-phase operations usually need to maintain a queue of pending requests, if more than one context wants to execute it at once.
  • MStacksProxy: Commands for manipulating the operand stack.
  • MTypesProxy: Commands for type checking.
  • MTypeManager:: Commands for transforming data types to and from network representations.

There are a few additional components, such as MateLocksProxy, but they are only relevant if you're modifying VM internals, as opposed to adding new functions and contexts. Using "Proxy" components allows components to refer to VM abstractions in terms of interfaces, instead of implementation. For example, MHandlerStore is a a component that provides an implementation of code storage; wiring to MHandlerStore specifies an implementation. If components wired to MHandlerStore, then changing the implementation used would either require changing all of the components that wire to it (to wire to something else), changing MHandlerStore itself, or trying to use search path tricks to use a different component also named MHandlerStore.

In contrast, MHandlerStoreProxy is a configuration wrapper around an implementation; by having components wire to MHandlerStoreProxy, one can change the implementation the entire VM uses by modifying only this component.

Earlier in the lesson, you wrote a simple function that incremented an integer. Functions can also encapsulate underlying TinyOS operation. For example, the rand() Maté function calls the standard Tiny)S RandomLFSR.Random.random() command to generate a random number:

command result_t MateBytecode.execute(uint8_t instr,
                                      MateContext* context) {
  uint16_t rval = call Random.rand();
  dbg(DBG_USR1, "VM (%i): Pushing random number: %hu.\n", (int)context->which,
 rval);
  call Stacks.pushValue(context, rval);
  return SUCCESS;
}

Functions can also encapsule split-phase operations, such as sending a packet or sampling a sensor. This is a bit more complex than calling a TinyOS command, and requires using several of the abstractions described above. A function that encapsulates a split-phase operation has to interact with the Maté scheduler and synchronization subsystem, in order to suspend and resume the executing context properly. Some split-phase operations require the function component to allocate some state; for example, the send function component (OPsendM) allocates a TOS_Msg for sending. The implementing module has to ensure atomic access to this state, to make sure that it doesn't corrupt it if multiple contexts execute the function concurrently.

Finally, the function must keep track of when the VM reboots; if new code arrives while it's in the middle of its split-phase operation, then it must keep the lock on any state, but can release the executing context. This is the basic pseudocode for a function that encapsulates a split-phase operation. There are three entry points: MateBytecode.execute(), the event signalling completion of the split-phase event, and the reboot event signalling that the VM has rebooted.

MateQueue queue;      // A wait queue
MateContext* execing; // The currently executing context
bool busy;            // Whether the state is in use
State state;              // State that requires atomic access

// Only execute if busy == FALSE, try starting the split-phase op
// If you can't start it, put the context on the wait queue
try_exec(context) {
  // access state, etc
  if (split_phase_command(state) == SUCCESS) {
    execing = context;
    mark context as executing operation
    busy = TRUE;
    call Synch.yieldContext(context); // We've started the operation,
                                      // Let the scheduler know
  }
  else { // For some reason we can't start the op, put on wait queue
    mark context as waiting
    put context on queue
  }
}

// MateBytecode.execute: a context is trying to execute the function
execute(context) {
  // Somebody is using the shared state: put the context on a wait queue
  if (busy) {
    context->state = WAITING;
    enqueue(queue, context);
  }
  // Noboody is using the shared state: try starting the op
  else {
    try_exec(context);
  }
}

// Split phase op complete: if it's for us, resume who was executing,
// try letting another context execute the operation
split_phase_event(State s) {
  if (s != state) {return;} // Not for us
  if (execing != NULL) {
    call Synch.resumeContext(execing);
    execing = NULL;
  }
  busy = FALSE;

  if (!empty(queue)) {
    pull next context off queue
    try_exec(context);
  }
}

// Rebooting means that nobody is execing the op any longer,
// and nobody is waiting for it; if a completion event comes in,
// execing must be null or it may try to resume somebody who isn't waiting.
reboot() {
  execing = NULL;
  empty queue
}
		    

There's a good deal of pseudocode, because the function has to deal with many corner cases. The function needs to keep track of whether anyone is accessing the shared state (the busy flag), whether there is a context waiting to resume when the operation completes (the execing pointer), and whether any contexts are waiting for the shared state to be available or the underling resource (the wait queue).

A function which has a wait queue must handle reboot events from the VM. Otherwise, system state can become inconsistent if the VM reboots (due to new code arriving) in the middle of the operation. Rebooting requires all of the executing contexts to halt and reset, waiting for their triggering event. However, when the split-phase op completes, then the function component will try to resume the context that was executing it. This could cause a halted context to start execution when it shouldn't. When the VM reboots, the function component must clear its wait queue and executing context. The components that implement individual contexts are responsible for clearing their state; all the function needs to do is clear its queue.

However, as the operation is still executing, it should not clear the busy flag on the shared state. If, after reboot, a context tries to execute the function before the underlying operation completes, then the component should put the context on a wait queue. When the split-phase operation completes, releasing the shared state, the component can let a context on the wait queue execute. If the function component wants a context to stop executing Maté instructions(e.g., wait on a queue), it must change the context's state variable from MATE_STATE_RUN; otherwise, the scheduler will continue to have the context issue instructions.

Here is the full code of the uart function, which sends a packet to the UART. Its shared state is the message buffer it passes to SendMsg.send():

includes Mate;

module OPuartM {

  provides {
    interface MateBytecode;
    interface StdControl;
    event result_t sendDone();
  }

  uses {
    interface MateQueue as Queue;
    interface MateContextSynch as Synch;
    interface MateError as Error;
    interface MateTypes as TypeCheck;
    interface MateType as Type[uint8_t typeID];
    interface MateStacks as Stacks;
    interface MateEngineStatus as EngineStatus;

    interface SendMsg as SendPacket;
  }
}

implementation {
  MateQueue sendWaitQueue;
  MateContext* sendingContext;
  bool busy;
  TOS_Msg msg;
    
  command result_t StdControl.init() {
    call Queue.init(&sendWaitQueue);
    sendingContext = NULL;
    busy = FALSE;
    return SUCCESS;
  }
  
  command result_t StdControl.start() {
    return SUCCESS;
  }
  
  command result_t StdControl.stop() {
    return SUCCESS;
  }

  result_t trySend(MateContext* context) {
    MateStackVariable* arg = call Stacks.popOperand(context);
    if (!call Type.supported[arg->type]()) {
      call Error.error(context, MATE_ERROR_TYPE_CHECK);
      dbg(DBG_USR1|DBG_ERROR, 
          "VM (%i): UART tried to send data type %i, \
          doesn't have a network representation.\n", 
          (int)context->which, (int)arg->type);
      return FAIL;
    }
    else {
      uint16_t maxLen = TOSH_DATA_LENGTH;
      uint8_t len = call Type.length[arg->type]((void*)arg->buffer.var);
      MateStructMsg* destMsg = (MateStructMsg*)msg.data;
      if (len >= maxLen) {
        call Error.error(context, MATE_ERROR_BUFFER_OVERFLOW);
        return FAIL;
      }
                                                                                
      destMsg->type = arg->type;
                                                                                
      call Type.encode[arg->type](&(destMsg->data), arg->buffer.var);
                                                                                
      if (call SendPacket.send(TOS_UART_ADDR, TOSH_DATA_LENGTH, &msg) == SUCCESS) {
        dbg(DBG_USR1, 
            "VM (%i): OPuartM sending data of length %i to uart.\n", 
            (int)context->which, (int)len);
        busy = TRUE;
        sendingContext = context;
        context->state = MATE_STATE_BLOCKED;
        call Synch.yieldContext(context);
      }
      else {
        call Stacks.pushOperand(context, arg);
        context->state = MATE_STATE_WAITING;
        call Queue.enqueue(context, &sendWaitQueue, context);
      }
    }
    return SUCCESS;
  }
  
  command result_t MateBytecode.execute(uint8_t instr,
					MateContext* context) {
    if (busy) {
      dbg(DBG_USR1,
          "VM (%i): Executing OPuart, but UART busy, put on queue.\n",
          (int)context->which);
      context->state = MATE_STATE_SEND_WAITING;
      call Queue.enqueue(context, &sendWaitQueue, context);
      return SUCCESS;
    }
    else {
      dbg(DBG_USR1, "VM (%i): Executing OPuart.\n", (int)context->which);
      return trySend(context);
    }
  }
  command uint8_t MateBytecode.byteLength() {return 1;}
  
  event result_t SendPacket.sendDone(TOS_MsgPtr mesg, result_t success) {
    if (mesg != &msg) {return SUCCESS;}

    busy = FALSE;
    if (sendingContext != NULL) {
      call Synch.resumeContext(sendingContext, sendingContext);
      sendingContext = NULL;
    }

    if (!call Queue.empty(&sendWaitQueue)) {
      MateContext* context = call Queue.dequeue(NULL, &sendWaitQueue);
      trySend(context);
    }
    dbg(DBG_USR1, "VM: UART send completed with code. %i\n", (int)success);
    return SUCCESS;
  }

  // It may be that we couldn't send a packet because another component was.
  // If someone is waiting and we're not sending, try sending.
  event result_t sendDone() {
    if (!busy &&
	sendingContext == NULL &&
	!call Queue.empty(&sendWaitQueue)) {
      MateContext* context = call Queue.dequeue(NULL, &sendWaitQueue);
      trySend(context);
    }
    return SUCCESS;
  }
  
  event void EngineStatus.rebooted() {
    sendingContext = NULL;
    call Queue.init(&sendWaitQueue);
  }
}
		    

OPuartM introduces another Maté framework service, MateType. This interface has commands for components to transform an on-mote data structure into a network-friendly representation. This is important for functions to be language independent. To take a program type and convert it to a network representation, a component calls the encode command. To convert a network type back into an on-mote representation, the component calls the decode command. A component, MTypeManagerProxy, provides a parameterized interface whose parameter is a type ID of the type.

To include an additional type into a VM, a component must wire to MTypeManager. This is how, for example, TinyScript includes the buffer type. The MBuffer component provides the MateType interface, and wires to MTypeManager so that calls which go to MTypeManagerProxy for encoding/decoding buffers pass through to MBuffer. If any component (such as the one which implements the opcode for putting an element in a buffer) wires to the MBuffer component, that is, uses buffers, then full support for the buffer type will be included.

Function components can interact with the Maté synchronization component (MContextSynchProxy) to improve parallelism. When a context executes, it has exclusive access to any shared resources (such as shared variables or buffers) that it uses. This prevents race conditions between concurrent contexts. A context can release shared resources it holds as it executes; this allows other contexts that need them to execute. The most useful time to release resources is during split-phase operations, as the context executing the operation cannot use the resources and is not running (it's waiting for the operation to complete).

As a context executes, it may mark some resources as safe to release. A function encapsulating a split-phase operation should have the context actually release the resources it has marked by calling MateContextSynch.yield(). If the function calls this, then it must call MateContextSynch.resume() when the context resumes (the operation completes). yield() makes the context yield any resources it has marked, and resume contexts that were waiting for those resources. resume() tells the synchronization component that the context is ready to run again. If there are any resources the context needs to reacquire but are held by another context, then resume() will cause the context to wait until those resources are available (either through another yield, or the holder halting).

In the above uart function, the buffer passed on the operand stack is a shared variable. The function cannot yield a context until its state has been copied into the TOS_Msg to send (otherwise, it's possible someone else may modify it).

A Complex Function

Now that you've seen a complex Maté function, you'll write your own. Instead of encapsulating a split-phase operation, you'll take increment and have it perform its computation in a separate task: posting the task is equivalent to starting a split-phase operation, while the task running is equivalent to a completion event. However, by using a task, it will be easier to debug and test what's going on. We strongly recommend that you first run and test your function in TOSSIM, before trying a mote.

Return to OPincrementM.nc. Because it will increment an integer in a task, the component needs to store the value in its frame. It also needs a wait queue, a context pointer, and a busy flag. Add the following variables to OPincrementM's frame:

MateContext* executingContext;
MateQueue waitQueue;
bool busy;
int16_t value;

Some of these variables need initialization; OPincrementM must provide StdControl. Also, the component must monitor when the VM reboots, so it must use MateEngineStatus. Finally, it manages a queue, so needs to use MateQueue. Change the module declaration to:

module OPincrementM {
  provides {
    interface StdControl;
    interface MateBytecode;
  }
  uses {
    interface MateStacks as Stacks;
    interface MateTypes as Types;
    interface MateQueue as Queue;
    interface MateEngineStatus as EngineStatus;
  }
}
		    

The next step is to initialize the variables in StdControl.init(). Add the following code to the module implementation:

  command result_t StdControl.init() {
    call Queue.init(&waitQueue);
    executingContext = NULL;
    busy = FALSE;
    return SUCCESS;
  }
  command result_t StdControl.start() {return SUCCESS;}
  command result_t StdControl.stop() {return SUCCESS;}
		    

Next, add the handler for when the VM reboots:

  event void EngineStatus.rebooted() {
    executingContext = NULL;
    call Queue.init(&waitQueue);
  }
		    

All that's left to write are the execute and completion code. Most complex functions in the standard Maté release have a "try to execute" function in the component, as both execute and the completion event need to get contexts to execute the operation. Putting it in a separate C function allows both of them to share the same code and reduces the chances of bugs. The component only calls try_execute if the local state is free. Change MateBytecode.execute to:

  command result_t MateBytecode.execute(uint8_t instr, MateContext* context) {
    if (busy) {
      dbg(DBG_USR1,
          "VM (%i): Executing increment, but busy, put on queue.\n",
          (int)context->which);
      context->state = MATE_STATE_WAIT;
      call Queue.enqueue(context, &waitQueue, context);
      return SUCCESS;
    }
    else {
      dbg(DBG_USR1, "VM (%i): Executing increment.\n", (int)context->which);
      return try_execute(context);
    }
  }
		    

Next, add the code for actually trying to execute the function: this involves type-checking the parameter, saving the local state and posting the task:

  result_t try_execute(MateContext* context) {
    MateStackVariable* arg = call Stacks.popOperand(context);
    if (!call Types.checkInteger(context, arg)) {return FAIL;}
    else {
      executingContext = context;
      busy = TRUE;
      value = arg->value.var;
      post incrementTask();
      context->state = MATE_STATE_BLOCKED;
      call Synch.yieldContext(context);
      return SUCCESS;
    }
  }
		    

Finally, there's the task itself, which acts as the completion event:

  task void incrementTask() {
    busy = FALSE;
    if (executingContext != NULL) {
      call Stacks.pushInteger(executingContext, value + 1);
      call Synch.resumeContext(executingContext, executingContext);
      executingContext = NULL;
    }
    if (!call Queue.empty(&waitQueue)) {
      MateContext* context = call Queue.dequeue(NULL, &waitQueue);
      try_execute(context);
    }
    dbg(DBG_USR1, "VM: Increment completed.\n");
  }
		    

Many Maté data abstractions -- queues, for example -- require a context as their first argument. This parameter represents the context performing the action (if there is one); this is so that, if an error occurs, the VM can report which context caused the error. Passing NULL means there isn't really a context responsible for the call (as is the case, for example, in dequeueing a context off of the wait queue in incrementTask): if an error occurs, Maté will report that it does not know who is responsible. Commands that actually modify or access the context, however, such as operand stack manipulation, cannot take NULL as a parameter. Calling Queue.dequeue with NULL as the first parameter is safe, calling Stacks.pushInteger with NULL as the first parameter will most probably cause the node to crash.

Because your SimpleVM already includes increment as a function, all you need to do is recompile it. Run the VM in TOSSIM and inject a handler that calls increment. If you've set DBG to include usr1, you'll see messages showing you that increment has executed as a split-phase operation (the separate execute and task messages).

Each function must have a configuration which wires to the appropriate interface on a module; this means that a particular module can implement more than one function. This is useful for functions that share state. For example, if you wanted to write a set of functions for accessing non-volatile storage, you could implement them in a single module, allowing you to easily keep track of state relevant to all of them (such as a seek pointer).

Optional Tags

You've implemented increment as a split-phase operation, which now interacts with the Maté scheduler. For the TinyScript compiler to be aware that the function blocks, you have to add an additional tag to the ODF file. Open OPincrement.odf and add the following tag to the FUNCTION element:

schedpoint=true

If you forget to include this tag, nothing bad will happen: the compiler will just think it is a regular instruction, and not try to have a program release shared resources which are safe to release.

As described in Lesson 4, a FUNCTION element in a VM specification file causes VMBuilder to look for a ODF. For example, <FUNCTION name=increment> causes VMBuilder to look for a file named OPincrement.odf. When VMBuilder loads this file, it uses the opcode tag of the FUNCTION element to determine what component implements the function. For example, in your ODF for increment, opcode=increment leads VMBuilder to wire to the nesC component OPincrement, contained in OPincrement.nc, for the function implementation. You can tell VMBuilder to wire to a different component. This is useful when the opcode name is not a good name for a component, as functions are case insensitive in TinyScript. For example, if you add the tag

component=IncrementInteger

to the FUNCTION element, then VMBuilder will wire the increment function to a component named OPIncrementInteger, which is implemented in OPIncrementInteger.nc.

Conclusion

You implemented a new function, increment, and incorporated it into your SimpleVM. You then re-implemented it as a split-phase operation, and learned how to let the TinyScript compiler know that it is a blocking call. The next and final lesson describes how to add new execution events to a VM.


< Previous Lesson | Next Lesson > | Top