Version 3 of Writing a TclOO Method in C

Updated 2016-06-28 19:45:45 by dkf

Ever wanted to make a TclOO method, yet found doing it in Tcl just a bit awkward (perhaps because it requires access to some low level API that would suck to do in Tcl directly)? This page is for you.

Writing a Method

TclOO methods consist of several parts. Firstly, they have at least an implementation function. They may also have a function used to delete a clientData (just as in normal commands) and they could have a function used to copy the method (since there's a copymethod command inside an oo::define context). These pieces are assembled into a method type descriptor record, in part to allow for future flexibility, and in part because passing all those functions into any use of the type of method would suck. Finally, when the method is actually used, it's installed with Tcl_NewMethod or Tcl_NewInstanceMethod; that's also when the clientData would be passed in (those functions are very much like Tcl_CreateObjCommand).

The Implementation Function

First, we need a method function.

static int
ExampleMethod(
    ClientData clientData,        /* Same as usual for commands. */
    Tcl_Interp *interp,           /* Interpreter context. */
    Tcl_ObjectContext context,    /* The object/call context. */
    int objc,                     /* Number of arguments. */
    Tcl_Obj *const *objv)         /* The actual arguments. */
{
    /*
     * Get the object instance we're applied to.
     */

    Tcl_Object object = Tcl_ObjectContextObject(context);

    /*
     * Get the number of arguments to "skip" when doing argument parsing.
     * If we did this for ordinary Tcl commands, the # skipped args would be 1 for the
     * command name, but we need to subtract a *VARIABLE* number of arguments since
     * we can be called as [$obj exampleName …] and also as [next …] from a subclass.
     *
     * The context knows what's really going on, so we ask it.
     */

    const int skip = Tcl_ObjectContextSkippedArgs(context);

    if (objc - skip != 2) {
        Tcl_WrongNumArgs(interp, skip, objv, "foo bar");
        return TCL_ERROR;
    }

    /*
     * Usual argument parsing. Note the offset from 'skip'.
     */

    int foo, bar;
    if (Tcl_GetIntFromObj(interp, objv[skip+0], &foo) != TCL_OK ||
            Tcl_GetIntFromObj(interp, objv[skip+1], &bar) != TCL_OK) {
        return TCL_ERROR;
    }

    /*
     * I'll skip the rest; it's usual API usage now…
     */

    return TCL_OK;
}

The Method Type Descriptor

Next, we need a method definition record.

static Tcl_MethodType exampleMethodType = {
    TCL_OO_METHOD_VERSION_CURRENT,    /* Allow future versioning. */
    "exampleMethod",                  /* The name of the method TYPE; useful when debugging. */
    ExampleMethod,                    /* The implementation function. */
    NULL,                             /* The function for deleting the clientData, or NULL for "don't bother". */
    NULL                              /* The function for copying the clientData, or NULL for "mustn't copy". */
};

Installing the Method

Finally, we can register the method on a class and at the same time set up a clientData if necessary (not this time, but very useful for standard methods which are all the same type). The only tricky wrinkle here that is different to creating a normal command is that the name of the method is taken as a Tcl_Obj, and the command doesn't guarantee to retain a reference (or not) to the name object; don't pass in a refcount-zero object.

    Tcl_Class cls = …;
    Tcl_Obj *nameObj = Tcl_NewStringObj("example", -1);

    Tcl_IncrRefCount(nameObj);
    Tcl_NewMethod(interp, cls, nameObj, 1, &exampleMethodType, (ClientData) NULL);
    Tcl_DecrRefCount(nameObj);

Notes

The API is a bit more complicated than for Tcl commands, though it's as simple as possible. You can also register a method on an individual instance object using Tcl_NewInstanceMethod, and the 1 in the call above is whether the method is public (1) or private (0).