Updated 2018-07-15 14:11:48 by AMG

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 edit

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);

AMG: Where in the documentation is there any warning against passing a nameObj with zero refcount? Where else is this unsafe or forbidden? If it's generally unsafe, where is it permissible?

Notes edit

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).

Classes edit

AMG: How does one go about defining an entire TclOO class in C? I imagine Tcl_NewObjectInstance() is used to create a new instance of oo::class, then Tcl_NewMethod() is used (as above) to actually define the methods that will be available in instances of the class. Do I have this right? Not a lot of examples to go around...

A bit more detail. Tcl_NewObjectInstance() needs a Tcl_Class to instantiate, so how does one get the Tcl_Class for oo::class? I'm thinking call Tcl_GetObjectFromObj() with its second argument being Tcl_NewStringObj("oo::class", -1), then pass the returned Tcl_Object to Tcl_GetObjectAsClass() to get the corresponding Tcl_Class. That so?

I'll try actually coding up the above guesses and will report back with success or further questions.
DKF: That's pretty much it. Get the parent object, get it as a class, ask for a new instance of that class, populate the new class with methods. If we had overloads, I'd also have functions that did the lookup internally, but we don't so I'd have to think of new function names...

Update: Actually an example was written just yesterday: oo::widget

AMG: Okay, here's my update: now that I got it working (only took about a half hour), I'm actually leaning heavily toward dismantling it all in favor of exposing all the data via nested dicts and lists. I couldn't do that at first because the underlying C API being wrapped didn't provide access to most of the data, but I'm in talks with the developer about fixing that and am contributing patches of my own to that end. Once all the data becomes available at the C level, it makes little sense to hide it at the Tcl level since I'll just end up having to provide more Tcl API to get at it piece by piece. It would be much easier to instead leverage the existing data access commands.