SpinWrap - a tool for allowing C or C++ to use Spin Objects
David Betz
Posts: 14,516
SpinWrap is a C program that reads a Spin source file, finds all of the PUB declarations, and writes C or C++ wrappers for allowing the Spin functions to be called from C or C++. The current source is at the URL below.
Revision history (most to least recent):
2014-03-02 Added support for floating point values for CON symbols.
2014-03-02 Switched from dumb internal Spin parser to the OpenSpin -s option. Moved sources to GitHub.
2014-02-27 Added a README.txt file and also made the Makefile use payload for loading Catalina binaries.
2014-02-27 Add a "zip" target to the Makefile and fix an output formatting problem with Catalina.
2014-02-26 Add compile time option "CATALINA" to use "spinnaker" instead of "openspin" for compiling Spin programs.
2014-02-26 Handle Spin objects that have no variables and add a test for using the return value of a Spin function.
2014-02-25 Added reference counting to avoid stopping the COG when instances still exist.
2014-02-25 Rearranged the compiler conditionals and added a "runcatalina" target to the Makefile.
The spinwrap sources are now hosted on GitHub:
https://github.com/dbetz/spinwrap
************ ORIGINAL TOP MESSAGE ************
I'm trying to piece together the information I need to start a Spin binary from C code. Here is a compilation of information that Dave Hein posted to Dr_Acula's thread on Spin overlays:
Thanks!
David
usage: spinwrap [ -c ] [ -d ] [ -p <output-path-root> ] [ -s <stack-size> ] [ -S,<openspin option> ]... <spin-file>...
Revision history (most to least recent):
2014-03-02 Added support for floating point values for CON symbols.
2014-03-02 Switched from dumb internal Spin parser to the OpenSpin -s option. Moved sources to GitHub.
2014-02-27 Added a README.txt file and also made the Makefile use payload for loading Catalina binaries.
2014-02-27 Add a "zip" target to the Makefile and fix an output formatting problem with Catalina.
2014-02-26 Add compile time option "CATALINA" to use "spinnaker" instead of "openspin" for compiling Spin programs.
2014-02-26 Handle Spin objects that have no variables and add a test for using the return value of a Spin function.
2014-02-25 Added reference counting to avoid stopping the COG when instances still exist.
2014-02-25 Rearranged the compiler conditionals and added a "runcatalina" target to the Makefile.
The spinwrap sources are now hosted on GitHub:
https://github.com/dbetz/spinwrap
************ ORIGINAL TOP MESSAGE ************
I'm trying to piece together the information I need to start a Spin binary from C code. Here is a compilation of information that Dave Hein posted to Dr_Acula's thread on Spin overlays:
Also, from reading Dave's overlay demonstration code it seems that a Spin binary file starts like this:A Spin object can be loaded anywhere in hub RAM and called by properly setting up PBASE, VBASE, DBASE, PCURR and DCURR. The MethodPointer object does this for resident objects by storing the values of the first 4 state variables in a 4-word array, and then setting up DCURR based on the current stack pointer. So it would be fairly easy to implement overlays using this technique. I'm not sure how efficient this would be. It depends on how often you would have to switch overlays.
Two instances of the same object will have the same PBASE and PCURR values, since they are both executing the same code. PBASE is just the absolute starting address of the object, and PCURR is the Spin program counter. Each method within the object uses a value for PCURR that points to the first bytecode in the method.
VBASE is the absolute starting address of the object's VAR variables. This is different for each instance of the object. DBASE is the absolute address of the RESULT variable when calling a method. This is dependent on the location in the stack at the time that a method is called. DCURR is the stack pointer. When a method is called, DCURR is set to the current stack pointer value plus space for the stack frame, local variables and calling parameters.
The stack frame is a 4-word structure that is placed on the stack when calling another method. It contains the values of PBASE, VBASE, DBASE and PCURR just before a method is to be called. The stack frame is used by the RETURN and ABORT bytecodes to restore the Spin state variables. The stack frame also contains two additional bits to indicate whether the return value should be pushed to the stack, and whether an ABORT should stop returning at this point or cause a return to the previous method.
Jumps in Spin are relative. Variables and code positions are all relative to PBASE, VBASE and DBASE. This makes it very easy to relocate Spin objects.
$0000-$0003 CLKFREQ $0004 CLKMODE $0005 ???? $0006-$0007 PBASE (object base address) $0008-$0009 VBASE (address of object's variables) $000a-$000b DBASE (address of the result variable)I assume that when I load a Spin object PBASE, VBASE, and DBASE are set based on the object being loaded at the start of hub memory. Does that mean that I can relocate the object elsewhere by just adding the new base address onto PBASE, VBASE, and DBASE? Or maybe I have to add the new base address minus the size of the Spin binary header containing CLKFREQ, CLKMODE, and ????)? In other words, do something like this:
PBASE += new_base - 6; VBASE += new_base - 6; DBASE += new_base - 6;Is this correct? If so, how do I start this code? RossH's Catalina does this using this function:
int _coginit_Spin(void *code, void *data, void *stack, int start, int offs) { short params[6]; int cog; ((unsigned long *)stack)[0] = 0xFFF9FFFF; ((unsigned long *)stack)[1] = 0xFFF9FFFF; params[0] = 0; params[1] = (short)((long)code); params[2] = (short)((long)data); params[3] = (short)((long)stack + 8); params[4] = (short)((long)code + start); params[5] = (short)((long)stack + 8 + offs); // start the Spin interpreter contained in the ROM cog = _coginit((int)¶ms>>2, 0xF004>>2, ANY_COG); // small delay to allow cog to start properly before we re-use the RAM! _waitcnt(_cnt() + (_clockfreq()/20)); return cog; }This will, I assume, start the main method. What if I wanted to start some method other than the main method? Also, can someone describe the format of the parameter block passed to coginit? What I'd love to see is a complete description of the Spin binary file format as well as the layout of a Spin binary image. Does that exist anywhere?
Thanks!
David
Comments
Byte 5 in the header is the Spin binary file checksum. The sum of the bytes in a Spin binary file should be $14, or if you add in the bytes in $FFF9FFFF twice you get zero.
A Spin cog is started by specifying the address of the Spin interpreter in ROM, which is $F004. The PAR is set to point to the PBASE minus 2. The minus 2 is used because PAR must be set to a long address, and PBASE is on an odd word address. The Spin interpreter adds 2 to PAR before it begins to copy PBASE, VBASE, DBASE, PCURR and DCURR from the header.
You could start some other method instead of the first one. You would have to compute it's address from the method table plus PBASE, and write it in PCURR in the header. If the method uses calling parameters you would have to preload them in the stack, and then adjust DCURR to account for the number of parameters and local variables used by the method.
Thanks for all of your help!
David
As always on this or any forum you have to filter a through certain amount of junk.
The link that Steve posted should provide information about the method table. The method table is at the beginning of each object, and it consist of an array of words that provide the following:
- object size
- number of methods/objects
- starting address and local variable space for each pub
- starting address and local variable space for each pri
- starting address and VAR address for each object
1) Parse a Spin source file to find all of the PUB declarations and create C prototypes for each. I will also keep track of the method index associated with each PUB method.
2) Compile the object with a proxy stub that can be patched to call any of the object's methods by index similar to what Dave Hein does in his overlay example.
3) Create a C header file with a prototype for each of the PUB methods
4) Create C functions that communicate with the proxy stub through a mailbox to invoke the Spin object's methods
5) Use OpenSpin to compile the proxy and the Spin object into a Spin binary
6) Parse the resulting Spin binary to find the method table
I'd also like to be able to support multiple instances of the Spin object. This should be possible by passing the VBASE associated with the instance along with the method parameters. These instances could even be dynamically allocated by the C code.
Another question is whether to use C or C++. If I use C++ then I can make the C calls to the Spin methods look pretty much the same as Spin calls would look. The downside is that this program wouldn't work with Catalina. I may try to support both options based on a command line option.
For example:
Could produce: I'll also create C/C++ code to load the compiled Spin object into a COG and start it.
Just to be clear (I hope!), here is what I'd like to do:
The C stubs that are linked with the C program use a mailbox to communicate with a proxy for the Spin object. The mailbox has the following fields:
- method index
- object address
- argument count
- arrray of arguments
The proxy is supposed to call the method at the specified index with the specified arguments. This means that the proxy needs to setup the stack with the arguments and then execute the appropriate bytecode sequence to call the specified method. Without understanding at least some of the bytecodes I can't do this.
Spin2cpp is looking better all the time for this. It wouldn't waste any COG for a dispatcher for example. The main COG would be used for both the C code and the Spin equivalent.
The spin2cpp solution is probably better but it may involve peppering the Spin code with spin2cpp declarations to identify variables shared between COGs. This isn't necessary if you lelt the Spin compiler and Spin VM run the code. Anyway, until I dig up some documentation on the Spin bytecodes, I can't easily make any more progress.
Looking for the spin opcodes, is this the sort of thing you need? http://propeller.wikispaces.com/Spin+Byte+Code
I'm sure I've seen more documentation similar to this somewhere.
This was sort of where we were up to with the Overlay thread. We had typical objects split into the pasm and spin part. The next bit was loading in pure spin objects, running them and then discarding them. The technique ought to be similar for C.
I'm still trying to think through how it would work. Calling one method is pretty easy - call a method to add two numbers for instance and return a value. Where it might get complicated is a spin object with multiple methods. You might pass one value which is a pointer to an array which contains the PUB in the object to run and the data to pass. But it would get complicated coding because that object is a precompiled binary. Sort of like the mystery of DLL's. Coding it would be a bit complicated because you would need the object source open in another tab. Possible in the proptool but would require coffee and sharp thinking. More complicated if coding C and Spin at the same time.
It could be very handy though. Say there was a routine of string functions all collected up into one object. How handy and cool would it be to be able to call that same object from both C and from Spin, then discard it, and load in a floating point object etc etc?!
I'm wondering if an answer to this might be forming in the open source spin IDE thread, especially if there is an autocomplete function. Code in Spin or C. Add "MyObject". Then type "MyObject." and there is an autocomplete with all the methods. Select the correct one. Then send it to a compiler to sort out which method number you called without having to think about that, including precompiling self contained binary objects and sending them to an SD card. All entirely possible with a single F10, and it opens up ways of coding huge programs with the best features of C and Spin.
I'll ponder this some more
"Call Obj Sub 2 4" means call method 4 in object 2. Object 2 refers to an entry in the local method table. I forget if the "2" is an absolute long index into the table or whether it must be added to the number of local methods to get the table index. The "4" is used as an index into the called object's method table that contains the relative starting address of the method and the local variable space.
The space used by the calling parameters is not described in the method table, and is implicit in the definition of the method. The calling method must put the correct number of parameters on the stack before calling the method.
If I were you, I would just use the remote method call technique that is in the Spinix files I posted. This technique doesn't mess around with method tables. You can write a utility to generate the stub and kernel source files, which are compiled with the calling and called programs. For C, the utility would also generate a header with function prototypes for all of the PUB methods. Each entry would look like "int32_t method_name(int32_t arg1, ...)". You may also want to include the constants defined in the Spin code in the header file, but this could get messy if they reference other objects.
EDIT: The Spinix code uses a lock so that the kernel can be called from multiple cogs. I think for the purpose of calling Spin methods from C you will only be calling the code from one cog, so you don't need to use a lock. However, if you want to allow pthreads to call the Spin code you would need to use a lock.
You may want to prefix the Spin names with an object tag, such as "fds_" or "FullDuplexSerial_" to ensure that the stub doesn't contain conflicting names, such as start or stop from multiple objects. I'm not sure how to handle multiple instances of an object. Maybe you would need to pass an object index, or maybe there's a C++ way to do it.
There is no formal document that describes the Spin bytecodes. The Spin interpreter source written by Chip does contain a lot of comments that describe it. One problem is that formal mnemonics where never defined for the Spin bytecodes. It's a bit awkward to talk about a bytecode when all you have is the value, such as $36, or the text from BST "Constant 2 $00000001". I attempted to define a set of mnemonics, that are defined in the man entry for spasm, which is the Spinix Spin bytecode assembler. So I defined the bytecode $36 as ldli1, which stands for "LoaD Long Immediate 1". The spasm document is attached below.
You could generate nested IF/ELSE statement to perform a binary search on the index. Only 5 tests would be needed for 32 methods.
Just trying to understand what I'm seeing when I dump a binary file.
Thanks,
David
The generated C++ header file:
The generated C++ source file:
The generated Spin source file:
I compile the Spin code with OpenSpin and include a patched binary in the C++ source file. That's the big array of bytes you see in that file. The only value that I patch is the address of the mailbox in the DAT section.
I still haven't tried this because I haven't figured out how to handle multiple instances of the Spin object yet.
I've attached the C source code that generates this although please excuse the lack of comments. I'll add those once I get it working.
spinwrap.c
Another bit of trivia is that DBASE will be equal to (VBASE + 8) if there are no VAR variables. The 8 bytes consist of the initial stack frame, which contain $FFF9FFFF, $FFF9FFFF. If the program does contain a VAR section then DBASE will be (VBASE + VarSize + 8).
In the Prop Tool you can write a simple one-line Spin program containing only "pub main", and hit F8 to see the header bytes. The 5 variables will be $10, $1C, $24, $18 and $28. You can then add calling parameters, local variables and VAR variables to see the affect on the header bytes.
Current:
Should I change to to this?
Thanks! I guess I'll just leave my code the way it is then if that will work as well as the version with the $FFFF values.
Here are my suggested changes.