Add user object to runtime.
diff --git a/py/runtime.c b/py/runtime.c
index 76f194d..90c350c 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -68,6 +68,7 @@
     O_MAP,
     O_CLASS,
     O_OBJ,
+    O_USER,
 } py_obj_kind_t;
 
 typedef enum {
@@ -172,6 +173,11 @@
             py_obj_base_t *class; // points to a O_CLASS object
             py_map_t *members;
         } u_obj;
+        struct { // for O_USER
+            const py_user_info_t *info;
+            machine_uint_t data1;
+            machine_uint_t data2;
+        } u_user;
     };
 };
 
@@ -431,6 +437,34 @@
     return o;
 }
 
+py_obj_t py_obj_new_user(const py_user_info_t *info, machine_uint_t data1, machine_uint_t data2) {
+    py_obj_base_t *o = m_new(py_obj_base_t, 1);
+    o->kind = O_USER;
+    // TODO should probably parse the info to turn strings to qstr's, and wrap functions in O_FUN_N objects
+    // that'll take up some memory.  maybe we can lazily do the O_FUN_N: leave it a ptr to a C function, and
+    // only when the method is looked-up do we change that to the O_FUN_N object.
+    o->u_user.info = info;
+    o->u_user.data1 = data1;
+    o->u_user.data2 = data2;
+    return o;
+}
+
+void py_user_get_data(py_obj_t o, machine_uint_t *data1, machine_uint_t *data2) {
+    assert(IS_O(o, O_USER));
+    if (data1 != NULL) {
+        *data1 = ((py_obj_base_t*)o)->u_user.data1;
+    }
+    if (data2 != NULL) {
+        *data2 = ((py_obj_base_t*)o)->u_user.data2;
+    }
+}
+
+void py_user_set_data(py_obj_t o, machine_uint_t data1, machine_uint_t data2) {
+    assert(IS_O(o, O_USER));
+    ((py_obj_base_t*)o)->u_user.data1 = data1;
+    ((py_obj_base_t*)o)->u_user.data2 = data2;
+}
+
 py_obj_t rt_str_join(py_obj_t self_in, py_obj_t arg) {
     assert(IS_O(self_in, O_STR));
     py_obj_base_t *self = self_in;
@@ -812,6 +846,8 @@
                 assert(IS_O(qn->value, O_STR));
                 return qstr_str(((py_obj_base_t*)qn->value)->u_str);
             }
+            case O_USER:
+                return o->u_user.info->type_name;
             default:
                 assert(0);
                 return "UnknownType";
@@ -911,6 +947,9 @@
                 printf("}");
                 break;
             }
+            case O_USER:
+                o->u_user.info->print(o_in);
+                break;
             default:
                 printf("<? %d>", o->kind);
                 assert(0);
@@ -1826,6 +1865,22 @@
             }
         }
         goto no_attr;
+    } else if (IS_O(base, O_USER)) {
+        py_obj_base_t *o = base;
+        const py_user_method_t *meth = &o->u_user.info->methods[0];
+        for (; meth->name != NULL; meth++) {
+            if (strcmp(meth->name, qstr_str(attr)) == 0) {
+                if (meth->kind == 0) {
+                    dest[1] = rt_make_function_1(meth->fun);
+                } else if (meth->kind == 1) {
+                    dest[1] = rt_make_function_2(meth->fun);
+                } else {
+                    assert(0);
+                }
+                dest[0] = base;
+                return;
+            }
+        }
     }
 
 no_attr: