Skip to content
rosco-pc edited this page Feb 11, 2015 · 1 revision

Method calls ( subroutine and function calls ) in Spin are a little complicated from a bytecode and run-time execution perspective.

Rather than have a 'call' to a specific address where the method's executable code is, as is used in PASM and other programming languages, Spin creates a 'method pointer table' which holds the addresses of the methods, and a method call is done through that table using an index of the number of the method to call. This is a technique which was used with the Parallax Basic Stamp and may be the basis for the decision to use the same technique in Spin, however the reasoning behind the choice of architecture is not known.

The method table is placed at the start of the main program or object it relates to and is a list of 32-bit longs, each entry split into two 16-bit words. The first long is at index +0, the second at +1 and so on.

The first entry (+0) is used to link all objects in an application together and is not discussed further in this document. The full puropose of this entry and how it has been used by the Spin interpreter has not yet been determined.

The second entry (+1) is the one relating to the first or only PUB method defined in the program or object. All PUB method entries are placed in the 'method pointer table' followed by any PRI method entries if any exist, and finally come entries to any sub-objects that may have been included.

A call to a method is a call via an index into the 'method pointer table'. A call to the first PUB method of the program would see bytecode generated in the form of "CALL +1", a call to a second PUB method if it existed ( or first PRI method if it did not ) would be a bytecode sequence of "CALL +2". In each case the Spin Interpreter will locate the 'method pointer table', find the second (+1) or third (+2) entry respectively and use the entry to invoke the method called.

The two words which make up the related entry in the 'method pointer table' are, frstly, the actual address of the method's code to execute, and the second a number of bytes by which to increase the stack pointer when the call is made. This allows space to be allocated for local variables used within the called method. As local variables are all 32-bit longs, the number to increase the stack pointer by will be zero ( no local variables ) or a multiple of four bytes. Note that the stack pointer is incremented but there is no clearing of the data on the stack. This is why local variables are not initialised when a method is entered.

Actually making a call to a method is a little more complicated and is a three stage affair. Firstly the 'call stack framing information' is created; this determines what should be done when the call returns, whether any value returned from the method should be discarded or left on the stack and whether an abort from within the method should be caught or allowed to fall through.

Once the 'call stack framing information' has been determined, any paramaters to pass to the routine are evaluated and the resultsof each evaluation is left on the stack. Finally, the actual call to the method is made. At the point the method is entered it should be noted that the passed-in parameter values are stored on the stack immediately preceding any local variables used. Within the method itself both parameters and local variables are treated the same by the Spin Interpreter, the Spin Compiler of the Propeller Tool having determined where each will respectively be on the stack.

When the method completes, reaches the end of its code or encounters a 'return' or 'abort', the Spin Interpreter unravels the stack, handles the returned value and deals with any abort processing necessary. The Spin Interpreter then continues executing the bytecode after the method call.

Note that all methods return a value which can be one of three things; a return value specified by a 'return' statement, an abort value specified by an 'abort' statement, or whatever value is held in the 'result' variable when the end of method code, or a 'return' or 'abort' statement with no expression is encountered. The 'result' variable exists for all methods and can be considered as the first local variable used within the method, before any parameters and any explicitly defined local variables. Unlike explicit local variables, the 'result' variable is always initialised to zero when a method is called.

Function Pointers

It has been noted that the Spin Language does not support 'function pointers', for example the address of a 'PRI GetCounter' cannot be obtained by using an '@GetCounter' expression even though its address is known at both compile time and run-time.

The lack of function pointers seems inexplicable to those expecting a call to a function to be simply implemented as a call to the function's executable code, but because calls do not make sense with the Spin Bytecode architecture a function pointer could not be used in the same way; a 'function pointer' in Spin would need to be an index into the 'method pointer table' and not the address of the method's executable code.

Spin methods do not include a mechanism within themselves for making local stack space available ( the necessary information for that is held in the 'method pointer table' ) so even if the address of a method's executable code could be found, it could not be reliably executed unless it used no local variables. Because the stack pointer would not have been adjusted to take account of those local variables, any executing code would interfere with the stack space those local variables would effectively be sharing at that time leading to all manner of peculiar results.

Because the Spin architecture was not designed to allow methods to be called directly it makes no real sense in most cases to provide any means to obtain the address of a method's executable code and there is no easy way to use function pointers as there is in other languages.

This is also the reason that there are no simple 'call-back mechanisms' because they intrinsically rely upon function pointers to work.

Unfortunately the Spin language provides no known mechanism to identify the index of a particular method in the 'method pointer table' so there is no easy mechanism to call particular methods programmatically at run-time nor to provide call-back mechanisms. Dynamic selection of methods to call at run-time can only really be done using 'if-else' or 'case' statements.

It is possible to 'fix-up' or 'patch' the bytecode at run-time ( either in-line code or the 'method pointer table' ) to dynamically select a method to call, but this is no simple task and also relies on having to manually determine the indexing of the methods being called which can easily and frequently change as code is developed. It also requires considerable understanding of teh Spin bytecode itself. It is therefore complicated and potentially error prone to use such a strategy, and the mechanism needs additional protections to allow it to be used with re-enterent code which is shared by one or more parallel operating Cog programs.

The Future

There is no reason that the Spin Language could not provide a means of obtaining an index for a method into the 'method pointer table' nor not provide a means to call a method using that information.

There has been no indication that either ability will be provided by Parallax but it is something which could potentially be provided for by a third-party Spin Compiler.

Inter-Object Method Calls

Inter-object method calls, and consequently 'call back methods', have an additional level of complication requiring additional 'Object Base' and 'Variable Base' pointers to be updated along with the stack pointer when a call is made.

This is all handled by the Spin Interpreter again using information held in the 'method pointer table'. The bytecode for a call into another object's method is in the form of 'CALLOBJ +N,+M' where the "+N" is an index into the 'method pointer table' as with a normal method call but the entry held there is different.

The first word of such an entry is the 'Object Base' of the object referenced and the second word is an amount of bytes to update the 'Variable Base' by when that object's methods are called. Updating the 'Variable Base' is what allows each object to have their own entirely independant VAR sections, and the VAR sections of each object instantiation to be be unique to that object.

The first step of an inter-object method call is therefore to update the 'Object Base' and 'Variable Base' pointers and thereafter the '+M' reference to the method to be called relates to the 'method pointer table' of the object referenced and can be handled just as a method call within the same object can be.

For anyone undertaking the development of a Spin Interpreter, it must be noted that the alteration of the 'Object Pointer' and 'Variable Base' must be done at the last instant, just as control is passed to the called method. Changes cannot be made to either at the time framing information is calculated as this is before parameters for the call is evaluated and both need to remain correct for those evaluatons to take place.

When an inter-object call completes, the Spin Interpreter again unravals the stack as for a call within an object and restores the 'Object Base' and 'Variable Base' pointers as they need to be for the object returned to. To simplify handling of method returns, the framing information pushed to the stack appears to contains the same informaton regardless of whether a method call is inter-object or within the same object. A return from a same-object method call will simply 'restore' both 'Object base' and 'Variable Base' to what they were.

This means more framing information is pushed to the stack than is absolutely necessary on a same-object method call but it does mean that method return handling is common regardless of how the call was made and the stack frame will be the same when seen from within a method regardless of how it were called for a method which is able to use information pushed onto the stack before it was called.

Clone this wiki locally