Although much of SuperCollider's functionality is implemented in the SuperCollider language itself, for reasons of efficiency, some functionality is implemented in 'back end' C++ functions called 'primitives'. This document provides guidance to members of the SC development community on writing primitives.
Primitive calls are preceded with an underscore, so for example _myPrimitiveName. Here is an example call to a primitive in an SC class:
In the example above the primitive is dispatched at _Cocoa_GetPathsDialog. If it is successful, it will return without executing the code below that. Primitive functions (see below) return an integer, which should be one of the values defined in the file PyrErrors.h:
enum { // primitive errors errReturn = -1, // not really an error.. primitive executed a non-local return errNone, errFailed = 5000, errBadPrimitive, errWrongType, errIndexNotAnInteger, errIndexOutOfRange, errImmutableObject, errNotAnIndexableObject, errStackOverflow, errOutOfMemory, errCantCallOS, errException, errPropertyNotFound = 6000, errLastError };
If your primitive can return any value besides errNone
then you will need to provide handling code after the primitive call. In most cases this will be ^this.primitiveFailed
. This will throw an Error giving the user information about what went wrong.
In some cases, you may wish to execute fallback SC code instead of throwing an Error. This can be useful in cases where for example a primitive provides an optimised version of a method which is not usable in all instances. Here is an example of how this can be done:
Note that returning anything besides errNone will result in executing the SC method ignoring the primitive call. For this reason, if you need to do some preparatory work in SC before calling the primitive, it is best practice to do this in a separate method to avoid duplication. For example:
In your primitive source code define the primitive:
void initCocoaFilePrimitives() { int base, index; base = nextPrimitiveIndex(); index = 0; definePrimitive(base, index++, "_Cocoa_GetPathsDialog", prGetPathsDialog, 2, 0); // further primitives can be laid in... //definePrimitive(base, index++, "_Cocoa_SaveAsPlist", prSaveAsPlist, 3, 0); }
Here is the prototype for definePrimitive:
int definePrimitive(int base, int index, char *name, PrimitiveHandler handler, int numArgs, int varArgs);
The numArgs is the number of arguments that were passed into the SuperCollider method that calls the primitive, plus one to include the receiver which is passed in as the first argument.
(TODO varArgs ...)
g->sp
is the top of the stack and is the last argument pushed. g->sp - inNumArgsPushed + 1
is the receiver and where the result goes.
In this example, the numArgsPushed will be 2 (as specified in definePrimitive)
int prGetPathsDialog(struct VMGlobals *g, int numArgsPushed) { if (!g->canCallOS) return errCantCallOS; //if its deferred, does this matter ? PyrSlot *receiver = g->sp - 1; // an instance of Cocoa PyrSlot *array = g->sp; // an array // ... the body return errNone; }
This example does not set the receiver, so the primitive returns the original receiver unchanged (still an instance of Cocoa). Or set the object at receiver
which again is at (g->sp - numArgsPushed + 1)
.
SuperCollider uses a garbage collector to manage memory allocation and collection where needed.1 In order to meet the requirements of good real time performance, a small and bounded amount of garbage collection may be triggered each time an object is created. This consists of incrementally examining all objects and determining if they are reachable (see below) or not. Unreachable objects may have their memory reallocated to new objects.
The following points are important to understanding how the GC works, and how to avoid bugs:
SC provides a number of functions which create new objects. These include instantiateObject
, newPyrObject
, newPyrString
, and newPyrArray
. Before any calls to such functions it is crucial that all previously created objects have been made reachable. If this is not done, it is possible that such objects will be marked as free. Since a freed object's memory may not be immediately reused, problems may not arise at the time your primitive is called, leading to extremely hard to find bugs.
Alternatively, most object creation functions include a bool runGC
argument. If set to false, this will guarantee that the garbage collector does not run on this allocation. While not ideal, as it is best that GC activity is amortised to the extent possible, this option is safe, since the status of any previously created objects will not be changed.
The following two examples are both safe:
PyrSlot *arg = g->sp; PyrObject *array1 = newPyrArray(g->gc, 2, 0, true); // runGC = true SetObject(arg, array1); // make the array reachable on the stack PyrObject *array2 = newPyrArray(g->gc, 2, 0, true); ...
runGC
to false:PyrSlot *arg = g->sp; // runGC = true PyrObject *array1 = newPyrArray(g->gc, 2, 0, true); // runGC = false so GC is not triggered, and array1 can't be freed PyrObject *array2 = newPyrArray(g->gc, 2, 0, false); ...
PyrSlot *receiver = g->sp; // get the receiver PyrObject *result = newPyrArray(g->gc, 2, 0, true); // create the result ++g->sp; SetObject(g->sp, result); // push the result array on the stack, so both it and rec are reachable ... // further allocations which make use of the receiver to populate result --g->sp; // pop the stack back to the receiver slot since we stored result there above SetObject(receiver, result); // now set the receiver
prArrayMultiChanExpand
in lang/LangPrimSource/PyrListPrim.cpp gives an example of this approach. Setting runGC
to false is another possible solution.
Similarly, care must be taken when writing utility functions which themselves create new objects, since this may happen somewhat opaquely and the calling context may not be known. Functions which may call themselves recursively also need special attention. In such cases setting runGC
to false may be the safest option, or including a runGC
arg so that GC behaviour is explicit. MsgToInt8Array
is one example of such a function.
static PyrInt8Array* MsgToInt8Array ( sc_msg_iter& msg, bool runGC ) { int size = msg.getbsize() ; VMGlobals *g = gMainVMGlobals ; PyrInt8Array *obj = newPyrInt8Array ( g->gc , size , 0 , runGC ) ; obj->size = size ; msg.getb ( (char *)obj->b , obj->size ) ; return obj ; }
Setting an object into another object's internal slot (e.g. with SetObject
or slotCopy
) also requires care. If the parent object is black (reachable and examined), the GC needs to be notified of the change. For this reason, you must usually call g->gc->GCWrite(parentObject, childObject)
after using one of these methods. The only exceptions to this rule are cases in which the parent object is known to be white (unexamined). This will be true if:
runGC = false
(i.e. the GC cannot have run in the interim), andThe following two examples are both safe:
PyrSlot *arg = g->sp; PyrObject *array = newPyrArray(g->gc, 2, 0, true); // runGC = true SetObject(arg, array); // make the array reachable on the stack PyrObject *str = newPyrString(g->gc, "Hello", 0, true); // runGC = true SetObject(array->slots, str); // we must call GCWrite, since array may not be white g->gc->GCWrite(array, str); ...
PyrObject *array = newPyrArray(g->gc, 2, 0, true); // runGC = true PyrObject *str = newPyrString(g->gc, "Hello", 0, false); // runGC = false SetObject(array->slots, str); // we don't need GCWrite, since array must still be white ...
GCWriteNew
instead of GCWrite
. The child object will still be white if the GC has not been triggered since it was created, and you have not previously called GCWrite on it.
If placing an object inside another has modified its size (e.g. adding an object to an array), you must correctly adjust its size by parent->size = newSize
. Both this and calling GCWrite (if necessary) should be done before any further object allocations. It is best practice to do them immediately if possible.
PyrSlot *arg = g->sp; int size = 10; PyrObject *array = newPyrArray(g->gc, size, 0, true); // runGC = true SetObject(arg, array); for(i=0; i<numLists; ++i) { PyrObject *str = newPyrString(g->gc, "Hello", 0, true); // runGC = true SetObject(array->slots + i, str); // str must still be white so we can use GCWriteNew g->gc->GCWriteNew(array, str); // increment size immediately //so it is accurate on next allocation array->size++; } ...
PyrSlot *arg = g->sp; int size = 10; PyrObject *array = newPyrArray(g->gc, size, 0, true); // runGC = true // setting size to final value here means // it is *not* accurate on next allocation below array->size = size; SetObject(arg, array); for(i=0; i<numLists; ++i) { PyrObject *str = newPyrString(g->gc, "Hello", 0, true); // runGC = true SetObject(array->slots + i, str); g->gc->GCWriteNew(array, str); } ...
newPyr*
) you must make sure these criteria are fulfilled:g->sp
stack (taking care not to overwrite any objects which will be needed later)child
) was put inside a slot of another object ( parent
), you must haveg->gc->GCWrite(parent, child)
afterwards unless you know that the parent is still white (unexamined), or GCWriteNew if you also know that the child is whiteparent->size
to the correct valueHere's an example of how a complete primitive might look:
int prMyPrimitive(struct VMGlobals* g, int numArgsPushed) { PyrSlot *arg = g->sp; float number; int err; err = slotFloatVal(arg, &number); // get one float argument if(err) return err; PyrObject *array = newPyrArray(g->gc, 2, 0, true); // array->size = 0 at creation; max size is 2 SetObject(arg, array); // return value // NOTE: array is now reachable on the stack, since arg refers to g->sp PyrObject *str1 = newPyrString(g->gc, "Hello", 0, true); SetObject(array->slots, str1); array->size++; // immediately increment array's size // array may not be white, so call GCWrite // but we know str is white, so can use GCWriteNew instead g->gc->GCWriteNew(array, str1); // NOTE: str1 is now reachable in array, which is reachable on the stack SetFloat(array->slots+1, number); array->size++; // A float is not an allocated object, so no need for anything special here return errNone; }
If we would have put SetObject(arg, array);
at the end of this function, array
would not have been reachable at the call to newPyrString
, and thus may have been marked free
, resulting in a hard to track down bug.
Since SC is dynamically typed, you cannot rely on any of the arguments being of the class you expect. You should check every argument to make sure it is the correct type.
One way to do this is by using isKindOfSlot
. If you just want a numeric value, you can use slotIntVal
, slotFloatVal
, or slotDoubleVal
which will return an error if the value is not a numeric type. Similarly there is slotStringVal
.
It is safe to assume that the receiver will be of the correct type because this is ensured by the method dispatch mechanism.
g->sp - inNumArgsPushed + 1
(In most primitives this is referred to by the variable a
).