Shop OBEX P1 Docs P2 Docs Learn Events
Callbacks and Method Pointers in Spin — Parallax Forums

Callbacks and Method Pointers in Spin

Dave HeinDave Hein Posts: 6,347
edited 2011-01-06 05:41 in Propeller 1
I figured out a way to implement method pointers in Spin. I use a 4-word array that contains the set up information about a method, which is the object base address, variable base address, method starting address and the stack variable size.

This is easy to set up if you know the object number and method number in the method table. The trick was to figure out a way to do it without knowing the method and object numbers up front. The technique I use can be used with multiple cogs and it doesn't use self-modifying code (except for writing some Spin bytecodes during initialization that the Spin tool won't generate).

The code is in the attached zip file. Let me know if you have any questions or comments.

Dave

EDIT: I found a bug in CallMethod0 and fixed it in the attached zip file, which is also posted at the OBEX. I removed the original zip file so it doesn't get used accidentally.

Comments

  • sssidneysssidney Posts: 64
    edited 2011-01-01 15:15
    Way cool. My state machines will look much neater with this. Any way to get this to also work with cognew/coginit?
  • sssidneysssidney Posts: 64
    edited 2011-01-01 15:23
    sssidney wrote: »
    Way cool. My state machines will look much neater with this. Any way to get this to also work with cognew/coginit?

    oh... I should be able to do this?

    cognew(mp.CallMethod1(1, @methodstruct1), @stack1[0])
  • jazzedjazzed Posts: 11,803
    edited 2011-01-01 15:40
    sssidney wrote: »
    oh... I should be able to do this?

    cognew(mp.CallMethod1(1, @methodstruct1), @stack1[0])

    No. You can not cognew a method in a separate object.
    Create and cognew a method in your object that uses mp.CallMethod.
  • sssidneysssidney Posts: 64
    edited 2011-01-01 16:01
    This will work for local methods
    con
      _clkfreq = 80_000_000
      _clkmode = xtal1+pll16x
    
      serXmit   = 30                      ' Serial Transmit
      serRecv   = 31                      ' Serial Receive
      speed     = 9600
    
    var
      long stack1[256]
      byte debugSemID
    
    obj
      mp : "MethodPointer"
      debug:      "MultiCogSerialDebug" 'debugport serial driver
    
    pub main | methodstruct1[2], methodstruct2[2], methodstruct3[2], arg1
      waitcnt(clkfreq*4 + cnt)
      debugSemID   := locknew
      debug.start(serRecv,serXmit,0,speed,debugSemID)
    
      debug.cprintf(string("main - start\r"),0,false)
    
      ' Initialize the method pointer routines
      mp.Initialize
    
      ' Set up the method structs for local methods
      mp.SetMethodPtr(@methodstruct1, 0, 2)
    
      arg1 := 123456
      cognew(cogfunc1(@methodstruct1, arg1), @stack1[0])
    
      debug.cprintf(string("main - finished\r"),0,false)
    
    
    pub Func1(parm1)
      debug.cprintf(string("Hello from Func1 - %d\r"),parm1,false)
    
    pub cogfunc1(methodptr, arg1)
       debug.cprintf(string("Hello from cogfunc - %d\r"),arg1,false)
       mp.CallMethod1(arg1, methodptr)
       debug.cprintf(string("goodbye from cogfunc\r"),0,false)
    
  • Cluso99Cluso99 Posts: 18,069
    edited 2011-01-01 16:02
    Dave: Congratulations. Another piece added :)
  • sssidneysssidney Posts: 64
    edited 2011-01-01 16:25
    OK this work for methods in other objects. COOL!!! Dave you are the best!!!!
    con
      _clkfreq = 80_000_000
      _clkmode = xtal1+pll16x
    
      serXmit   = 30                      ' Serial Transmit
      serRecv   = 31                      ' Serial Receive
      speed     = 9600
    
    var
    
      long stack1[256]
      long stack2[256]
      long stack3[256]
    
      byte debugSemID
    
    obj
      mp : "MethodPointer"
      test1 : "test1"
      debug:  "MultiCogSerialDebug" 'debugport serial driver
    
    pub main | methodstruct1[2], methodstruct2[2], methodstruct3[2], arg1
      waitcnt(clkfreq*4 + cnt)
      debugSemID   := locknew
      debug.start(serRecv,serXmit,0,speed,debugSemID)
    
      debug.cprintf(string("main - start\r"),0,false)
    
      ' Initialize the method pointer routines
      mp.Initialize
    
      ' Set up method structs using SetMethodPtrX
      if mp.SetMethodPtrEx(@methodstruct2)
        test1.Func2(0)
    
      arg1 := 7890
      cognew(cogfunc1(@methodstruct2, arg1), @stack2[0])
    
      debug.cprintf(string("main - finished\r"),0,false)
    
    pub cogfunc1(methodptr, arg1)
       debug.cprintf(string("Hello from cogfunc1 - %d\r"),arg1,false)
       mp.CallMethod1(arg1, methodptr)
       debug.cprintf(string("goodbye from cogfunc1 %d\r"),arg1,false)
    
  • Dave HeinDave Hein Posts: 6,347
    edited 2011-01-03 11:00
    After adding a few more comments and a readme file I posted the method pointer object to the OBEX at http://obex.parallax.com/objects/697/ .
  • StefanL38StefanL38 Posts: 2,292
    edited 2011-01-03 15:52
    Hi Dave,

    Maybe my questions are all caused from not knowing how method-pointers work.
    As far as the documentation and explanation goes right now
    I guess I have to become an expert about method-calls using another programming-language
    like C/C++ get some months of experience coding woth it and then coming back to your SPIN-object.

    I guess this is not your intention. I guess it is caused by the effect that I call the

    "expert-blindness for beginner-problems"

    I haven't worked with method-pointers or callbacks yet.
    I guess the example you provided works but to me the principles of how do I have to make changes
    in the example to make it work in other code is not clear at all.
    (This is something a lot of other objects suffer from too).
    To understand the principles you have to provide more than one example. Through the differencies between the examples the principles get much more obvious.

    Could you please add more exampes and comments of what is happening?

    mp.SetMethodPtr(@methodstruct1, 0, 2) 'pub SetMethodPtr(methodptr, objnum, methnum)

    why is objnum 0 and methnum 2?
    how would the code look different if I use more objects?
    how would the code look different if the call would be
    mp.SetMethodPtr(@methodstruct1, 1, 2)

    from your short comment in the readme.txt
    objectnum is determined by counting the number of PUB and PRI methods,
    and the number of objects before the target object, including the target object.

    I would guess as
    ser : "fds1"
    mp : "MethodPointer"
    are defined BEFORE test1
    objnum would have to be 3

    or does it mean call method 2 in object fds1?? (as fds1 is the first obj?)

    mp.SetMethodPtr(@methodstruct1, 7, 2)
    mp.SetMethodPtr(@methodstruct2, 8, 3)
    mp.SetMethodPtr(@methodstruct3, 9, 4)

    does not make any sense to me. There are just three objects

    obj
    ser : "fds1"
    mp : "MethodPointer"
    test1[3] : "test1"

    or do I have to count through all the objects that refer or use other objects?

    does the sequence how the objects are defined in the OBJ-section play a role?

    could you provide some kind of a graphic that shows where is stored what and who is related to whom?


    what is the meaning of "pbase" and "vbase" "dbase"?? These names are NOT selfexplaining to me


    And last but not least where in the world is THIS construction more clear or saves memory or makes code more elegant than something that is not using it????

    I guess if I have different methods that I want to call through method-pointers and there argument list differs from your example-code
    I have to change the code of the mp-object before I can use the method-calls.

    So where is the "writing it down in five minutes"-example where this object saves memory or makes code more elegant?


    best regards

    Stefan
  • Dave HeinDave Hein Posts: 6,347
    edited 2011-01-03 18:17
    Stefan,

    You have asked some very good questions, and I'll try to answer them. To fully understand how method pointers work you would have to understand some basic details about the spin interpreter, and how spin programs are executed. The spin interpreter contains 5 basic state variables that control how data and opcodes are addressed. The 5 state variables are:

    pbase - This is the starting address of an object
    vbase - This is the starting address of the VAR section of an object
    dbase - This is the starting address of a method's stack variables
    pcurr - This is the address of the next opcode to be executed
    dcurr - This is the address of the next variable to be stored on the stack

    These 5 state variables are located in the cog's memory at address $1eb to $1ef. Under normal circumstances the spin interpreter performs a method call by looking up the values of pbase, vbase and pcurr for a method, and then replacing the values of the state variables in cog memory with the new values.

    Just before the call is made the spin interpreter stores the current values of pbase, vbase, dbase and pcurr on the stack so that they can be restored after the method returns. These four values define a stack frame. dbase is updated to point to the next long after the stack frame. dcurr is offset from the new dbase to provide space for the method parameters, the method's stack variables, and the return value. After the interpreter does all of these operations it sets pcurr to the starting address of the called method.

    The method's starting address and local stack space are stored in a table at the beginning of an object called a method table. The method table also contains the pbase and vbase for every object that is referenced by an object. The spin compiler organizes the method table with all the PUB methods first, followed by all the PRI method, and then followed by the OBJ information. As an example, consider an object that contains 5 PUB methods, 4 PRI methods and 3 OBJ references. The PUB methods will be numbered in order from 1 through 5. The PRI methods will be numbered in order from 6 through 9. The objects will be numbered in order from 10 to 12.

    The spin interpreter calls a local method by looking up it's entry in the local table. A method in another object is called by first looking up the object entry in the local table, and then looking up the method entry in the referenced object's method table. The SetMethodPtr routine does the same lookup functions as the spin interpreter based on the object and method numbers that are passed to it. An object number of zero indicates that the method is contained within the calling object. A non-zero object number references a method in another object. SetMethodPtr stores the values of pbase, vbase, pcurr and the stack variable offset in the method struct.

    SetMethodPtrEx is used to automatically determine the object number and method number for a target method by placing a dummy call after the call to SetMethodPtrEx. After SetMethodPtrEx extracts the object and method numbers it performs the same operations as SetMethodPtr.

    CallMethodN uses the information in the method struct to replace the contents of pbase, vbase, pcurr and dcurr. This causes the spin interpreter to begin executing the code in the target method with the proper object and variable bases.

    I hope this answers most of your questions.

    Dave
  • mparkmpark Posts: 1,305
    edited 2011-01-04 22:57
    For folks who are unfamiliar with method pointers and their uses:

    A method pointer is an indirect method call. In Spin, when you say "foo(1, 2)", your program executes the method called "foo". If Spin supported method pointers, you could declare a variable (let's call it "p") as a method pointer and then say something like "p := @foo". Then when you said "p(1, 2)" your program would execute not a method named "p" but rather the method that p points to (i.e., foo).

    With method pointers, you can implement jump tables, for example. Suppose you want to do various actions (say move forward, turn left, turn right, etc.) depending on user input, a number from 0 to 9. In plain Spin you would have to use CASE or a bunch of IFs, but with method pointers you can build a table: pointer to MoveForward, pointer to TurnLeft, pointer to TurnRight, etc.

    You can also use method pointers for callbacks, which means you can call a method and pass it a pointer to another method. Maybe you are building a robot and when it hits a wall you want it to stop or back up or turn left or explode. Instead of writing an OnCollisionStop method and OnCollisionBackUp and OnCollisionTurnLeft and OnCollisionExplode, you can write one OnCollision method and call it with a pointer to the action you want: e.g. OnCollision( @Explode ).

    Now Spin does not have first-class support for method pointers, and in my examples I've been glossing over many details and just making up syntax, but what Dave has managed to do, rather ingeniously, is provide method pointer functionality within the confines of standard Spin.
  • Dave HeinDave Hein Posts: 6,347
    edited 2011-01-05 07:51
    Method pointers could be added to the spin language by implementing methodset and methodcall intrinsic functions. The compiler knows the object numbers and method numbers for all the methods, and it could automatically set up the method struct when methodset is used. methodcall would allow for a variable number of parameters, and the compiler would generate a method call based on the information in the method struct instead of using the method table. A spin program would look something like this if these extensions were added to the compiler.

    pub start | method[2]
    methodset(sub1, @method)
    methodcall(1, 2, @method)

    pub sub1(x, y)
    ...
  • ErNaErNa Posts: 1,742
    edited 2011-01-06 05:41
    I was waiting for such an function for some time to start processes with a pointer to the process. I hope to find the time to integrate your solution into my modules. But I have to do it in timesharing with other things to do. :-( And I hope that Chip is listening. Hi Chip!
Sign In or Register to comment.