User defined classes with TinyPy

I recently stumbled across TinyPy by Phil Hassey, what an amazing little project - a Python compiler & virtual machine that weighs in at under 64k.

So after playing around with it, updating the build system a bit and seeing if I could get everything down to as small as possible (around 17k for the VM with -Os, strip then upx) I realized that the possibilities for TinyPy are endless.

Among the things I want to add to the engine its self are transparent zip file access via the zziplib library, and then micro-tasks and channels in a similar way to the Stackless Python project.

However, taking baby steps first this article covers creating classes and making them available to code running in the VM. I'm creating a very simple container class to hold any value, along with a method.

An example of it's use would be:

example = test_class("World")
greeting = example.hello("Hi,")
print(greeting)  # "Hi, Hello World"
print(example.value)  # "World"

example.value = "Phil Hassey"  # Error - read-only property

Just as a warning - I have little to no experience with Python so I'm not really sure if everything I'm doing is the norm in Python or not.

Creating the constructor

The constructor is just a regular function which returns the object, we initialize the value we're holding internally and setup the get, set and delete operations for the object.

struct test
{
    tp_obj value;
};

// Create a "test" object
// test() or test(value)
tp_obj test_new(TP)
{
    struct test* me = tp_malloc(sizeof(struct test));
    tp_obj obj;

    obj = tp_data(tp, me);
    obj.data.meta->get = test_get;
    obj.data.meta->set = test_set;
    obj.data.meta->free = test_free;

    me->value = TP_OBJ();

    return obj;
}

A new 'data' type is created (consider it similar to PHP's resource type, but better) with the 'test' struct as the value it holds, then test_get, test_set and test_free operations are attributed to it (see below).

'Free' operation

Everything apart from the test struct will be cleaned up by the garbage collector, but when it comes to our object it notifies it using the free operation to perform custom cleanup.

void test_free(TP, tp_obj self)
{
    struct test* me = self.data.val;
    tp_free(me);
}

TinyPy can be used with the Boehm GC, but I much prefer explicitly freeing data.

'Get' operation

All property accesses with be made through either this operation or the 'set' operation, providing a simple interface to access virtual properties. We can also return wrapped functions as methods using tp_method as opposed to tp_fnc.

tp_obj test_m_hello(TP)
{
    struct test* me;

    tp_obj s = TP_OBJ();    
    tp_obj world = TP_DEFAULT(tp_string("World"));

    me = s.data.val;

    return tp_printf(tp, "%s Hello %s", STR(world), STR(me->value));
}

The hello method is provided by test_m_hello, with self being the first parameter on the stack - from which we can retrieve a test structure via s.data.val

// Get/call a value in test
tp_obj test_get(TP, tp_obj s, tp_obj k)
{
    struct test* me = s.data.val;
    char* key = STR(k);

    // Access the "value" property
    if( strcmp(key, "value") == 0 )
    {
        return me->value;
    }

    // "hello" method
    if( strcmp(key, "hello") == 0 )
    {
        return tp_method(tp,s,test_m_hello);
    }

    tp_raise(None, "test.%s: unknown key", key);
}

At the moment we're just doing a manual comparison to return either the value associated with the object or the 'hello' method. In future I suppose you could bastardize the dictionary type to provide this, although I haven't looked into it yet.

'Set' operation

And finally our unimplemented 'set' operation, the value the key is to be set to can be accessed via the 4th parameter, but instead we're raising an exception because we're forcing everything to be read only.

One quirk I'm abusing here is the tp_raise macro, by default it tries to return a parameter (causing a compile error because the function has a void signature) but the leniency of the preprocessor allows me to just omit it completely.

void test_set(TP, tp_obj self, tp_obj k, tp_obj v)
{
    char* key = STR(k);

    tp_raise(, "test.%s: unknown key", key);
}

Bringing it all together

The only step remaining is to expose the test_new function to the VM, so somewhere in your init code:

    tp_obj context = tp->builtins;
    tp_set(tp,context,tp_string("test_class"),tp_fnc(tp,test_new));

And the example code at the top should work as expected.


Leave a Reply



About

Harry is a professional developer and sysadmin from London, UK.

He's an atheist, employed at PixelMags LLC, a socialist and has a pragmatic outlook on life, love and religion.

Bookmarks

I'm constantly finding interesting stuff, here are some of the things I've bookmarked recently:

HarryR on Faves.com