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