Shop OBEX P1 Docs P2 Docs Learn Events
Procedure variables or callbacks in spin? — Parallax Forums

Procedure variables or callbacks in spin?

ManAtWorkManAtWork Posts: 2,076
edited 2013-11-04 21:49 in Propeller 1
Hello,

is it possible to write a "generic" method in spin that takes another method as an argument? For example I'd like to write a universal measurement function that calculates·an average of the value to measure. It·takes the address of the input variable and the number of values to take for the average as arguments.

PUB
average (inPtr, num): avg | sum
  repeat num
    waitNextVal
    sum+=long[noparse][[/noparse]inPtr]
  avg:= sum/num

Now, because of different update rates I'd like to make the "wait for next value" method variable. This way I could pass another method as argument that waits for a pin to toggle, for a varible to change or for a lock to be set. If this is not possible I'd have to rewrite several versions of the method or use lots of if or case statements. This would also be possible but is not nearly as elegant and prevents the creation of independent and reusable library objects.

I know I can jump to arbitrary destination adresses in assembler but this can't be mixed with spin. Is there some hack that allows patching of the method call vector in spin? Since all spin code resides in RAM it should be possible·somehow, like self modifying code in assembler. Of course, we have to assure that stack usage (number of arguments passed) is compatible.

·

Comments

  • Dave HeinDave Hein Posts: 6,347
    edited 2010-03-02 14:16
    I've just starting looking at the calling technique that is used by spincode, so I don't know a lot of the details.· However, it seems like a placeholder method could be defined to provide a function pointer in the call table.· The code would look something like what I show below.· The function index table could be manually created, or a pre-processor could automatically generate it.· The method "CallFunction" is a dummy routine, and it's information in the call table is over-written by the method SetFunctionPointer.· The tricky part is handling a variable number of arguments.

    Edit:· I thought about this some more, and realized that a dummy CallFunction would be needed for each cog so there wouldn't be any collisions.· Multiple CallFunction's would also be needed to handle each variation of argument count that is used.· A pre-processor could hide all these nasty details from the programmer.

    con
    · main_index 0
    · sub1_index 1
    · CallFunction_index 2
    · SetFunctionPointer_index 3

    pub main | FunctionIndex
    · FunctionIndex := sub1_index
    · SetFunctionPointer(FunctionIndex)
    · CallFunction(arg1, arg2)

    pub sub1(x, y)
    · ...

    pub CallFunction(x, y)

    pub SetFunctionPointer(func_index)
    · ' Copy function info for func_index to CallFunction table entry

    Post Edited (Dave Hein) : 3/2/2010 3:05:13 PM GMT
  • MagIO2MagIO2 Posts: 2,243
    edited 2010-03-02 14:59
    You don't need a variable number of arguments. Just give every function a pointer to the array where you store the parameters.
  • Dave HeinDave Hein Posts: 6,347
    edited 2010-03-02 15:06
    MagIO2 said...
    You don't need a variable number of arguments. Just give every function a pointer to the array where you store the parameters.
    Yes, that would work also.· But that means that any function that is called by a function pointer would need to use that calling convention.
  • ManAtWorkManAtWork Posts: 2,076
    edited 2010-03-02 16:02
    Hello Dave,

    ok, let's just ignore the problems of multitasking and variable number of arguments at the moment. I think there must be a simpler solution.·Something like...
    PUB 
    CallStub (x, y)
      CallDummy (x, y)
     
    PUB
    CallFunction (funcPtr, x, y)
      long[noparse][[/noparse]CallStub@+offset]:=funcPtr
      CallStub (x, y)
    

    Assuming that CallDummy is the first (and only) instruction of CallStub, there should be a·fixed offset to add where the adress of the CallDummy destination is located in memory. So the CallDummy would be overwritten with the desired destination. I know very little about the internals of the spin interpreter, but it's in ROM and therefore, at least, it shouldn't change.smilewinkgrin.gif

    Cheers
  • Phil Pilgrim (PhiPi)Phil Pilgrim (PhiPi) Posts: 23,514
    edited 2010-03-02 16:14
    Here's a link that provides some info on callbacks:

    http://forums.parallax.com/showthread.php?p=593662

    It's dated and crude, but may be useful nonetheless.

    -Phil
  • MagIO2MagIO2 Posts: 2,243
    edited 2010-03-02 16:18
    1. The address operator does not support functions. So, there is no easy way to find the entry point of a function.
    2. Function calls are translated to an index inside of a function address table by the SPIN compiler. Such an function address table exists for each object.

    So, in the end you need a lot of hardcore memory access if you want to implement something like that. So, it's much easier to use a case statement instead.
  • jazzedjazzed Posts: 11,803
    edited 2010-03-02 17:08
    Function pointers (method pointers) can be implemented by taking advantage of Spin's lack of object array bound checking.

    The only caveats are:
    • all objects must have the same placement of methods for the user to call.
    • the approach is considered an obfuscating hack
    • you are responsible for bounds checking, etc...
    'ObjectBoundHack.spin
    obj
      fp[noparse][[/noparse] 1 ]: "Object0"
      fp1 : "Object1"
      fp2 : "Object2"
      fp3 : "Object3"
    pub main | n
      repeat n from 0 to 2
        result := fp[noparse][[/noparse]n].method ' call methods
      repeat ' halt
    
    'Object0.spin
    pub method
       return 0
    
    'Object1.spin
    pub method
       return 1
    
    'Object2.spin
    pub method
       return 2
    
    'Object3.spin
    pub method
       return 3
    
    


    Variable args can be implemented using an object with a queue datastructure.
    Syntax would be similar to Java's System.out.print(...). For example:

    PUB p(v)
    {{
    This is the "parameter" method. The user will string together a
    list of method calls to push parameters onto the print queue.
    
    For example: snprintf(@buf, len, string("values %d %d %d\n"),p(1)+p(2)+p(3))
    This will print "values 1 2 3\n" to the buffer.
    
    @param v - value that will replace the sprintf % format specifier.
    @returns 1. Using a list of p(v)+p(v)+p(v) as the arg to snprintf gives arg count. 
    }}
      enqueue(v)
      return 1
    
    CON
      QLEN = $3f     ' up to 15 parameters
      
    VAR
      long queue[noparse][[/noparse]QLEN+1]
      long tail, head
      
    PRI enqueue(val)
      long[noparse][[/noparse]@queue+head] := val
      head+=4
      head&= QLEN
    
    PRI dequeue : rc
      rc := long[noparse][[/noparse]@queue+tail]
      tail+=4
      tail&= QLEN
      return rc
      
    PRI count
      if head => tail
        result := head-tail
      else
        result := head+(QLEN-tail)
          
    PRI flush
      head := tail
    
    

    Post Edited (jazzed) : 3/2/2010 9:24:15 PM GMT
  • Dave HeinDave Hein Posts: 6,347
    edited 2010-03-02 17:17
    ManAtWork,

    Spin does not allow for directly accessing a method's address.· However, you can get it from the call table, and modify it to call another method.

    Method calls are described in the wiki at http://propeller.wikispaces.com/Method+Calls .· Method calls use a table that contains the address and local variable size for each PUB method followed by each PRI method.· I believe the program shown below would call sub5 using sub2's table entry.· I haven't tried it myself.

    CON
    · ' Define an index for each PUB method
    · sub1_index = 1
    · sub2_index = 2
    · sub5_index = 3
    · ' Define an index for each PRI method
    · sub3_index = 4
    · sub4_index = 5
    PUB sub1 | MethodTablePtr, MethodAddr, VariableSpace
    · ' Initialize to the address of the Method Table
    · MethodTablePtr := $10
    · ' Get the address and local variable size for sub2
    · MethodAddr := word[noparse][[/noparse]MethodTablePtr][noparse][[/noparse]sub2_index*2]
    · VariableSpace := word[noparse][[/noparse]MethodTablePtr][noparse][[/noparse]sub2_index*2 + 1]
    · ' Copy sub2's info to the entry for sub5
    · word[noparse][[/noparse]MethodTablePtr][noparse][[/noparse]sub5_index*2] := MethodAddr
    · word[noparse][[/noparse]MethodTablePtr][noparse][[/noparse]sub5_index*2 + 1] := VariableSpace
    · ' Call sub2 using sub5's table entry
    · sub5(1, 2)
    PUB sub2(x, y) | z
    PRI sub3(x, y, z)
    PRI sub4 | x
    PUB sub5(x, y)
  • ManAtWorkManAtWork Posts: 2,076
    edited 2010-03-02 21:21
    Hello Dave, Jazzed and Phil,

    thanks a lot. At least it seams to be possible somehow. But I think here applies the proverb "Most time is wasted by trying to save time". The "unelegant" way with lots of case statements which is disapproved in true object oriented languages·seems to be more clearly and easier, here.

    Cheers
  • Dave HeinDave Hein Posts: 6,347
    edited 2010-03-03 03:55
    I wrote a short program to test out the function pointer technique.· It is listed below.··The method "SetFunctionPtr"·replaces the entry in the call table for "CallFunction" with the information for another function.

    Since Spin does not provide direct access to the method address or its index number in the table I had to create a set of constants for the method indexes.· The test program sets the function pointer for sub1, sub2 and sub3, and calls "CallFunction" to execute the function.

    I determined the location of the call table by using a value in the DAT section that stores its own address.· The stored value does not contain the relocation offset.· The address obtained at run time does contain the offset.· The call table is stored at the first location after the offset, so I can compute the function table address by computing the difference @memaddr - memaddr.· I believe this technique can be used for additional objects, but I did not test it.

    CON
      _clkmode = XTAL1 + PLL16x
      _xinfreq = 5_000_000
      
    obj
      ser : "fullduplexserial"
    CON
      ' Define an index for each PUB method
      main_index = 1
      sub1_index = 2
      sub2_index = 3
      sub3_index = 4
      CallFunction_index = 5
      SetFunctionPtr_index = 6
      ' Define an index for each PRI method
      
    PUB main
      ser.start(31, 30, 0, 19200)
      waitcnt(clkfreq*5+cnt)
      SetFunctionPtr(sub1_index)
      CallFunction
      SetFunctionPtr(sub2_index)
      CallFunction
      SetFunctionPtr(sub3_index)
      CallFunction
      
    PUB sub1
     ser.str(string("sub1", 13))
     
    PUB sub2
     ser.str(string("sub2", 13))
     
    PUB sub3
     ser.str(string("sub3", 13))
     
    PUB CallFunction
     ser.str(string("CallFunction", 13))
     
    DAT
      memaddr long @memaddr
     
    PUB SetFunctionPtr(function_index) | offset
      offset := @memaddr - memaddr
      long[noparse][[/noparse]offset][noparse][[/noparse]CallFunction_index] := long[noparse][[/noparse]offset][noparse][[/noparse]function_index]
    
  • mparkmpark Posts: 1,305
    edited 2010-03-03 04:40
    I think you can just say offset := @@0
  • Dave HeinDave Hein Posts: 6,347
    edited 2010-03-03 14:43
    mpark,

    Thanks for the tip.· So the call to SetFunctionPtr can be replaced by the following line:

    long[noparse][[/noparse]@@0][noparse][[/noparse]CallFunction_index] := long[noparse][[/noparse]@@0][noparse][[/noparse]function_index]

    Dave
  • jazzedjazzed Posts: 11,803
    edited 2010-03-03 16:35
    Nice work Dave. Commit your example to the OBEX when you're ready.
    Just one concern: I don't think the method address is a long.

    Added: A method pointer with no parameters and no local variables will look like a long since the
    upper word would be 0 or $0000_xxxx. It seems method with 1 local variable would be $0004_xxxx.

    There are various references to Spin object structure. Here is a thread that may help:
    http://forums.parallax.com/showthread.php?p=736449

    Post Edited (jazzed) : 3/3/2010 4:55:54 PM GMT
  • Dave HeinDave Hein Posts: 6,347
    edited 2010-03-03 17:29
    Phil Pilgrim (PhiPi) said...
    Here's a link that provides some info on callbacks:

    http://forums.parallax.com/showthread.php?p=593662

    It's dated and crude, but may be useful nonetheless.

    -Phil
    Phil,

    I looked at your code, and I think I understand the basic idea.· However, there are many details in your code that I still haven't quite figured out.· I know that having multiple objects complicates things quite a bit.· Your dummy routines are in a different object than the routines that actually get executed.· This requires adjusting the object offset and the VAR offset for the dummy routine.· You are also modifying the index within the spin bytes instead of in the call table.· It seems like things would simplify if the dummy routines are in the same object as the routines that are called.· It is also easier to figure out where the call table entry is, and modify that rather than modifying the index number·within the spin bytes.

    I haven't figured out how the call table works for multiple objects, but it seems like the technique I'm using should be extendable to multiple objects.· I think it can also be extended to multiple cogs by using multiple dummy routines, with one dedicated to each cog.

    Dave
  • Dave HeinDave Hein Posts: 6,347
    edited 2010-03-03 20:21
    jazzed said...
    Nice work Dave. Commit your example to the OBEX when you're ready.
    Just one concern: I don't think the method address is a long.

    Added: A method pointer with no parameters and no local variables will look like a long since the
    upper word would be 0 or $0000_xxxx. It seems method with 1 local variable would be $0004_xxxx.

    There are various references to Spin object structure. Here is a thread that may help:
    http://forums.parallax.com/showthread.php?p=736449
    Jazzed,

    Thanks for the link to the thread.· I copy the local variable count because I assume the spn code will add this to the SP when it calls a method.· I'll have to add some prints to verify that it works correctly.· I am assuming the 2 words used for a method table entry are long aligned, so I copy them together as a single long.· If this is not always the case then they need to be copied seperately as 2 words.

    Dave
  • jazzedjazzed Posts: 11,803
    edited 2010-03-03 22:50
    Dave Hein said...
    I copy the local variable count because I assume the spn code will add this to the SP when it calls a method. I'll have to add some prints to verify that it works correctly. I am assuming the 2 words used for a method table entry are long aligned, so I copy them together as a single long. If this is not always the case then they need to be copied seperately as 2 words.
    I think you're Ok with that. If I remember correctly, the object base is always long aligned because the 2 LSBs have significance to the interpreter. This can be seen in a Spin stack dump and the interpreter source. Here is the stack interpreter method I use. Notice "Object Base | Flags" is printed once I get a Frame.

    pri DisplayStackAddrs | c, n, frame, addr
    {{
    On entry this method assumes the VMM is stalled with post.
    }}
      'display the stack
      io.str(string("sp+"))                              
      c := (dsp - sbstart) / 2                     'count of items on stack
      io.hex(c, 2)
      io.str(string("("))
      io.hex(dsp, 4)
      io.str(string(") "))
    
      io.str(string($d))
      frame := sbstart
      repeat n from 0 to c-1                         'display stack items
        addr := sbstart + (n * 2)
        if isFrame(addr, @frame)
          frame := addr+4
          io.hex(addr,4)              
          io.str(string(" "))
          io.hex(word[noparse][[/noparse]addr],4)
          io.str(string(" Object Base | Flags",$d))
          io.hex(addr+2,4)              
          io.str(string(" "))
          io.hex(word[noparse][[/noparse]addr+2],4)
          io.str(string(" Object Var Base",$d))
          io.hex(addr+4,4)              
          io.str(string(" "))
          io.hex(word[noparse][[/noparse]addr+4],4)
          io.str(string(" Stack Frame",$d))                     
          io.hex(addr+6,4)              
          io.str(string(" "))
          io.hex(word[noparse][[/noparse]addr+6],4)
          io.str(string(" Return Address"))
          n+=3
          frame := sbstart + (n * 2) + 2
          io.str(string($d))
        elseifnot addr // 4
          io.hex(addr,4)              
          io.str(string(" "))
          io.hex(frame,4)
          io.str(string(" "))
          io.hex(word[noparse][[/noparse]addr+2],4)
          io.hex(word[noparse][[/noparse]addr],4)
          n++
          io.str(string($d))
      io.sum
    
    
  • mr23mr23 Posts: 21
    edited 2013-11-04 21:38
    Jazzed,Is the fp1,2,3 method incompatible with BST when using the eliminate-unused-code option?The fp1,2,3 method works with Prop Tool, and BST w/o this mentioned option, in my testing.Seems to make sense, there is no direct reference to fp1,2,3, so BST would optimize it out.Is there a known workaround for this, such as a compiler directive or something for BST to override the eliminate-unused-code for a specific object file ?-Chris
    jazzed wrote: »
    Function pointers (method pointers) can be implemented by taking advantage of Spin's lack of object array bound checking.

    The only caveats are:
    • all objects must have the same placement of methods for the user to call.
    • the approach is considered an obfuscating hack
    • you are responsible for bounds checking, etc...
    'ObjectBoundHack.spin
    obj
      fp[noparse][[/noparse] 1 ]: "Object0"
      fp1 : "Object1"
      fp2 : "Object2"
      fp3 : "Object3"
    pub main | n
      repeat n from 0 to 2
        result := fp[noparse][[/noparse]n].method ' call methods
      repeat ' halt
    
    'Object0.spin
    pub method
       return 0
    
    'Object1.spin
    pub method
       return 1
    
    'Object2.spin
    pub method
       return 2
    
    'Object3.spin
    pub method
       return 3
    
    


    Variable args can be implemented using an object with a queue datastructure.
    Syntax would be similar to Java's System.out.print(...). For example:

    PUB p(v)
    {{
    This is the "parameter" method. The user will string together a
    list of method calls to push parameters onto the print queue.
    
    For example: snprintf(@buf, len, string("values %d %d %d\n"),p(1)+p(2)+p(3))
    This will print "values 1 2 3\n" to the buffer.
    
    @param v - value that will replace the sprintf % format specifier.
    @returns 1. Using a list of p(v)+p(v)+p(v) as the arg to snprintf gives arg count. 
    }}
      enqueue(v)
      return 1
    
    CON
      QLEN = $3f     ' up to 15 parameters
      
    VAR
      long queue[noparse][[/noparse]QLEN+1]
      long tail, head
      
    PRI enqueue(val)
      long[noparse][[/noparse]@queue+head] := val
      head+=4
      head&= QLEN
    
    PRI dequeue : rc
      rc := long[noparse][[/noparse]@queue+tail]
      tail+=4
      tail&= QLEN
      return rc
      
    PRI count
      if head => tail
        result := head-tail
      else
        result := head+(QLEN-tail)
          
    PRI flush
      head := tail
    
    

    Post Edited (jazzed) : 3/2/2010 9:24:15 PM GMT
  • jazzedjazzed Posts: 11,803
    edited 2013-11-04 21:49
    Hi mr23,

    What you're seeing makes sense. There may be some combination of -O options that would work though.
    I'm not sure if BST has controls for all of the options or not; I always used BSTC from the command line.

    bstc gives this if you haven't seen it ...
    sh-3.1$ bstc
    Brads Spin Tool Compiler v0.15.4-pre3 - Copyright 2008,2009 All rights reserved
    Compiled for i386 Win32 at 19:57:58 on 2010/01/15
    Program Usage :- bstc (Options) Filename[.spin]
     -a            - Create Propeller object archive zipfile
     -b            - Write .binary file
     -c            - Write .dat file for C-Compiler (Drops a <filename.dat> file)
     -d <device>   - Device to load to (Default : )
     -D <define>   - Define a pre-processor symbol (may be used multiple times)
     -e            - Write .eeprom file
     -f            - Double download baud rate
     -h            - Display this help information
     -l[sm]        - Generate listfile (s) For source code / (m) for Machine readable - Debugger style listing
     -L <Lib Path> - Add a library path or file holding library path(s) to the searchpath (may be used multiple times)
     -o <filename> - Output [.list/.eeprom/.binary/.zip] Filename (Defaults to input Filename without .spin)
     -O <options>  - Optimise Binary (HIGHLY EXPERIMENTAL!!!!!)
        a          - Enable all optmisations (Be careful! No, really)
        b          - Bigger constants (should be slightly faster at the expense of code size)
        c          - Fold Constants
        g          - Generic "safe" size optimisations for smaller/faster code, however not what the Parallax compiler will
    generate
        r          - Remove unused Spin Methods
        u          - Fold Unary "-" Operations on Constants if it will make the code smaller
        x          - Non-Parallax compatible extensions
     -p[012]       - Program Chip on device (-d)
        0          - Load Ram and run
        1          - Load EEProm and shutdown
        2          - Load EEProm and run
     -w[012]       - Error/Warning level - 0 - Errors only / 1 - Error/Warning / 2 - Error/Warning/Information (Default 0)
     -q            - Be silent except for GCC style errors and warnings
     -v            - Get program version information
    
Sign In or Register to comment.