The Do-It Yourself Guide to Squeak Primitives


9. Write the C Code

Now we have to actually write the C code for the primitive's implementation. In my case, I'm just taking some data out of a data structure I maintain in the VM (it's updated asynchronously from the MIDI driver call-back routines) and copying it into the objects I passed down as arguments to the primitive call. This is a good example for our tutorial because you don't need to understand (or care about) the domain, and it demonstrates Smalltalk/C data-passing. The source (somewhat simplified) for this primitive looks like the following.

/***************************************************************
* sqReadMIDIPacket -- Sent in response to the input semaphore
* This is to up-load MIDI messages into the arguments (a MIDIPacket
* and its data ByteArray -- both passed as ints and cast here).
* ST: PrimMIDIPort primReadPacket: packet data: data
*/

int sqReadMIDIPacket(int ipacket, int idata) {
// The ipacket object is defined as:
// Object subclass: #MIDIPacket
// instanceVariableNames: 'length time flags data '
// idata is a byte array (+4 to skip the object header)

sqMIDIEvent *outPtr;
unsigned char *cdata;
int len, i;
unsigned char *pdata = (unsigned char *)idata;

success(true); // Set the success flag to true.
if (itemsInInQ == 0) // Return immediately if there is no input.
return (0);
if (sqCallback == 0) // Answer an error code if input is off.
// success(true); // Set the success flag to false to fail.
return (-1);
        // Get a pointer to the MIDI event structure.
outPtr = &sqMIDIInQ[itemsInInQ2++];
        // Print a message for debugging--yes,
// you can use printf() in the VM!

if(debug_mode) printf("%x %x %x\n",
outPtr->data[0], outPtr->data[1], outPtr->data[2]);

len = outPtr->len; // Copy the response fields.
// Copy the driver data into the packet.
// (Inst vars are 1-based.)

instVarAtPutInt(ipacket, 1, len);  // Copy length, time, flags.
instVarAtPutInt(ipacket, 2, (int)(outPtr->timeStamp));
instVarAtPutInt(ipacket, 3, (int)(outPtr->flags));

cdata = &(outPtr->data[0]); // Copy MIDI message bytes into the packet.
for (i=0; i<len; i++)
*pdata++ = *cdata++;

return (len); // Answer len.
} // End of fcn

Most of this should be pretty obvious to the seasoned C programmer. The cast of the idata argument from int to (unsigned char *) will work because it's actually a ByteArray (+ 4) in Smalltalk. The instVarAtPutInt() macro is defined as,

#define longAtput(i, val)   (*((int *) (i)) = val)
#define instVarAtPutInt(obj, slot, val) \
longAtput(((char *)obj + (slot << 2)), (((int)val << 1) | 1))

This is nasty, but allows you to stuff 31-bit integers into SmallInteger instance variables with abandon. If you look into interp.c, there are more useful macros for primitive writers that would help you if you need to write floats, etc.

It's outside of the scope of this introduction to go into the details of object unpacking in C, but given the above macros, Dan Ingalls's notes on the Squeak object format (from their OOPSLA paper), and a good debugger, you can pretty much do anything. As I stated above, however, it's my opinion that it's easier to unpack objects in Smalltalk, so I have lots of primitives that take several arguments that are the components of one top-level Squeak object.

The last line of the function returns an integer to the glue code, which pushes it onto the stack explicitly after popping the arguments and receiver object.

Note that I also use printf() for debugging, On a Mac, printf() from the VM pops up an output window for the messages. I use the following macros for debugging primitives,

Boolean debug_mode = true; // Enable/Disable printfs (see macros below)
       // Debugging macros

#define dprint1(str) if(debug_mode) printf(str)
#define dprint2(str, val) if(debug_mode) printf(str, val)
#define dprint3(str, v1, v2) if(debug_mode) printf(str, v1, v2)
etc...

(The same could be done with #ifdef, of course.)