6. Write the Smalltalk "Glue" method
OK, this is where it gets a bit more complicated. The Squeak Interpreter class is written in Smalltalk, but all of its instance methods get translated to C and form the core of the Squeak virtual machine (that huge file named "interp.c" on most platforms). There are class methods in this class that create the primitive index table (where element 614 will point to our code), and the instance methods whose names correspond to the names given in the primitive table are the actual bodies of the primitives. These "glue" methods typically unpack the arguments from the stack, call the actual C code of the primitive, and handle the return values.
There is a great deal of flexibility here, and interested readers are encouraged to read and analyze more of the Interpreter primitive methods (for example the sound I/O or network interface methods). Remember that these are all translated to C, so they cannot use all the language features of Smalltalk. (I'd give you the deatils of the Smalltalk-to-C translator if I understood them.)
The example that follows demonstrates the basic flow of the several stages in a typical glue method:
- 1) unpack the argument(s) from the stack;
- 2) test the arguments for validity (optional);
- 3) call the C function that implements the primitive (optional);
- 4) pop the arguments (and possible the receiver) off of the stack; and
- 5) push the return value onto the stack.
I have annotated the method below with these stages (in parentheses). Also note that I generally include both the Smalltalk method header and C function prototype as comments in this method; this makes debugging it much easier. In the Interpreter (and/or DynamicInterpreter) class, we have to write,
"Read a message (a MIDIPacket) from the MIDI interface."
"ST: PrimMIDIPort primReadPacket: packet data: data"
"C: int sqReadMIDIPacket (int packet, int data);"
| packet data answer |
"Get the arguments"
(1) data := self stackValue: 0.
(1) packet := self stackValue: 1.
"Make sure that 'data' is byte-like"
(2) self success: (self isBytes: data).
"Call the primitive"
(3) ifTrue: [answer := self cCode: 'sqReadMIDIPacket (packet, data + 4)'].
"Pop the args and rcvr object"
(4) ifTrue: [self pop: 3.
"Answer the number of data bytes read"
(5) self push: (self integerObjectOf: answer)]
For (1), note that the arguments are pushed onto the stack in reverse order (so the last argument is stack(0), the next-to-last is stack(1), etc.). There are methods (in ObjectMemory, the superclass of Interpreter) that allow you to get integers and other kind of things from the stack with automatic conversion. (Look at the other primitive methods in class Interpreter for lots of examples.) Since both of the arguments here are pointers, I use stackValue:.
Step (2) is a simple example of type-checking on primitive arguments. The success: message sets the primitive success/fail flag based on whether the second argument is a ByteArray. The method/function success: is used in the Smalltalk glue code and in C primitive implementations to signal primitive success or failure; to fail, set success to false, as in the test in step 2.
Step (3) uses the message "cCode: aString"; it takes a C function prototype as its argument and it is here that we actually call our C-language primitive. Note that I must use the actual variable names packet and data in the string. The "data + 4" means that the argument is a ByteArray but that the C code casts it as (unsigned char *); 4 is the size of the object header, so I skip it to pass the base address of the byte array's actual (char *) data. This is a hard-coded special value that implies that I know the object header is 32 bits.
In step (4), we pop the two arguments *and* the receiver object (a PrimMIDIPort instance) off of the stack if the primitive succeeded.
Step (5) pushes the C function's return value onto the stack as an integer. There are other coercion functions in ObjectMemory that can be found used in other primitive methods in class Interpreter.
I have not discussed data sharing between glue code and primitives, but there are some nifty and flexible facilities for it. (You can actually declare a temporary variable or argument to the glue method with the exact format it will have in C.) Look at John Maloney's sound primitives, or browse senders of var:declareC: as used in Interpreter >> primitiveSoundGetRecordingSampleRate (or pay me a really fat consulting fee to tell you about it :-) .
Because the VM is single-threaded, the garbage collector will not run while your glue code (and the primitive it calls) is active, so the objects you pass to C are safe for the duration of the primitive. If you want to pass an object pointer down to C code and have it held onto across primitive calls, you have to register it with the garbage collector as special so it will not be moved. (I've generated gigabytes of core dumps over the years with a whole array of VMs by forgetting this.) Look at the senders of SystemDictionary >> registerExternalObject: for places that do this.
The glue code method is translated to C when you generate a new interp.c file (see below) so it is important that you can't just send arbitrary Smalltalk messages from here. Look at the other primitive glue code methods in Interpreter (or DynamicInterpreter) for more examples.