MOO Structure Binding

The MOO Structure Binding (MSB) is a way to tie C-style structs to the MOO by presenting them with an object-like interface to the database.

Other References to Have Handy

There are comments in msb.c and msb.h which have more information about using MSB. There is an MSB type used internally by MSB to reflect the definitions of MSB types implemented in msb.c which can serve as an example for how to create a new type.

Creating a new Structure Binding

  1. Pick just one C struct to bind to an MSB type. If it has internal structures, make each one a different MSB type.

    For the example I'm using a "classical" struct stat because it is familiar and the binding can be simple or complex. I wouldn't really recommend putting stat in your server.

    Modern struct stat is a bit complex but here's what it looked like in SVR4. You don't actually have to know what your structure looks like internally, you just need to know what members it has and what their types are. If you look in /usr/include/sys/stat.h you'll probably see more fields and some of these may even be provided as macros, like st_atime, st_mtime and st_ctime. They should still work for our example.

    struct stat {
            dev_t   st_dev;
            ino_t   st_ino;
            mode_t  st_mode;
            nlink_t st_nlink;
            uid_t   st_uid;
            gid_t   st_gid;
            dev_t   st_rdev;
            off_t   st_size;
            time_t  st_atime;
            time_t  st_mtime;
            time_t  st_ctime;
    };
    
  2. Choose a name for your type. I'm calling this "stat" and the C variables will have the prefix msb_stat. You can choose whatever you like. New types have a string name which is available in MOO code as var.class.name.
  3. Define the properties associated with your type.

    There are several type accessors provided with MSB, so use those if you can. They all start with msbp_ followed by a typename. The types in your structure may be masked by typedefs, but if you can figure out what they actually are you can choose to ignore that distinction. In /usr/include/sys/stat.h you can see that a ino_t is really an int, or close enough for our purposes. If you want to maintain the abstraction, you can create your own type accessors (described later) or use a #define to add an alias to the property accessor:

    /*
     * This isn't strictly necessary but it clarifies how you are
     * overloading the property accessors.
     */
    #define msbp_ino_t	msbp_int
    #define msbp_dev_t	msbp_int
    #define msbp_mode_t	msbp_ushort
    #define msbp_nlink_t	msbp_int
    #define msbp_uid_t	msbp_int
    #define msbp_gid_t	msbp_int
    #define msbp_off_t	msbp_int
    #define msbp_time_t	msbp_int
    
    msb_property_t msb_stat_properties[] = {
    	/* name, offset in structure, accessor func, flags */
    	{ "st_dev", MsbOffset(struct stat, st_dev), msbp_dev_t, MSBP_F_R },
    	{ "st_ino", MsbOffset(struct stat, st_ino), msbp_ino_t, MSBP_F_R },
    	{ "st_mode", MsbOffset(struct stat, st_mode), msbp_mode_t, MSBP_F_R },
    	{ "st_nlink", MsbOffset(struct stat, st_nlink), msbp_nlink_t, MSBP_F_R },
    	{ "st_uid", MsbOffset(struct stat, st_uid), msbp_uid_t, MSBP_F_R },
    	{ "st_gid", MsbOffset(struct stat, st_gid), msbp_gid_t, MSBP_F_R },
    	{ "st_size", MsbOffset(struct stat, st_size), msbp_off_t, MSBP_F_R },
    	{ "st_atime", MsbOffset(struct stat, st_atime), msbp_time_t, MSBP_F_R },
    	{ "st_mtime", MsbOffset(struct stat, st_mtime), msbp_time_t, MSBP_F_R },
    	{ "st_ctime", MsbOffset(struct stat, st_ctime), msbp_time_t, MSBP_F_R },
    	{ NULL }
    };
    

    Notes:

  4. Define the verbs associated with your type.

    I can't think of a natural operation on a struct stat so I offer a slightly contrived one: finding the age of the file.

    msb_verb_t msb_stat_verbs[] = {
    	/* name, function, flags, min args, max args, prototype... */
    	{ "age", stat_age, MSBV_F_ALLX, 0, 0 },
    	{ NULL }
    };
    

    Notes:

  5. Implement the verbs in your list.

    There is a convenience macro MSBV_DECL(name) which declares a function named name with the right arguments for a msbv_handler_t. It actually looks like this:

    package name(MSB *m, void *ptr, const char *verbname, Objid progr, Var arglist)
    Using the macro protects you from interface changes. The function is called with the following values: It returns a package like a builtin function. It must not return a BI_CALL package! This means that MSB verbs cannot call back into the interpreter. This may be supported eventually.

    Our example takes no args, so the argument checking is complete before the function is even called:

    MSBV_DECL(stat_age)
    {
    	struct stat *sbp = (struct stat *)ptr;
    	Var result;
    
    	/* no args to extract */
    	/* must always free arglist */
    	free_var(arglist);
    
    	result.type = TYPE_INT;
    	result.v.num = time(0) - sbp->st_mtime;
    	return make_var_pack(result);
    }
    
  6. Define the ops associated with your type.

    Currently this is incompletely implemented.

  7. Implement the ops associated with your type.

    Currently this is incompletely implemented.

  8. Define the msb_t which gathers the last three things together:
    msb_t msb_stat = {
    	"stat",
    	msb_stat_properties,
    	msb_stat_verbs,
    	&msb_stat_ops,
    	sizeof(struct stat),
    	MSB_F_WRITABLE
    };
    
    There is an MSB-supplied MSB for this structure, and MOO references to yourmsb.class will return it. The class definition is reflected in-MOO as:
    yourmsb.class.name
    yourmsb.class:properties()
    yourmsb.class:verbs()
  9. Arrange for your type to be initialized at MOO startup or just before it is needed:
    msb_init_type(&msb_stat);
    
  10. Create a way for new instances of your type to get into the database. Usually this will be a builtin that returns instances of your type. If this structure is a sub-structure of a larger MSB, then it will be a msbp_handler_t for the entries of that larger structure. You could also introduce values by using them as arguments to verbs which you call with run_server_task().

    Somehow your code must introduce the values into the database, as there is no syntax or MSB-supplied builtin for creating instances of your new type.

    Here we take the builtin approach. This code makes stat("file") return the stat MSB we're making:

    static package
    bf_stat(Var arglist, Byte next, void *vdata, Objid progr)
    {
            const char *filename = arglist.v.list[1].v.str;
    	struct stat sb;
    
    	/* your perms check may not be so strict */
    	if (!is_wizard(progr)) {
    		free_var(arglist);
    		return make_error_pack(E_PERM);
    	}
    	if (-1 == stat(filename, &sb)) {
    		Var bad = var_ref(arglist.v.list[1]);
    
    		free_var(arglist);
    		return make_raise_pack(E_INVARG, strerror(errno), bad);
    	}
    	free_var(arglist);
    	return make_var_pack(msb_new_copy(&msb_stat, progr, &sb));
    }
    
  11. And somewhere arrange for this builtin to be registered:
    register_function("stat", 1, 1, bf_stat, TYPE_STR);
    

Sample Session with msb_stat

> ;stat("/etc/motd");
=> <<class.name->stat owner->#2 st_dev->160800 st_ino->46 st_mode->33188 
    st_nlink->1 st_uid->0 st_gid->0 st_size->373 st_atime->984080406 
    st_mtime->983836012 st_ctime->983836012>>
> @prop me.tmp
Property added with value 0.
> ;me.tmp = stat("/homes/bjj/Mail")
=> <<class.name->stat owner->#2 st_dev->18954 st_ino->26880 st_mode->16832 
    st_nlink->459341865 st_uid->7009 st_gid->1233 st_size->2048 
    st_atime->984104214 st_mtime->984111545 st_ctime->984111545>>
> ;me.tmp:age()
=> 30
> ;me.tmp.class
=> <<class.name->msb_class owner->#2 name->"stat">>
> ;me.tmp.class:verbs()
=> {"age"}
> ;me.tmp.class:properties()
=> {"st_dev", "st_ino", "st_mode", "st_nlink", "st_uid", "st_gid", "st_size", 
    "st_atime", "st_mtime", "st_ctime"}

Advanced Topics

Here I want to cover:
  1. Making msb_verb_t entries with more interesting prototypes
  2. Making a special msbv_handler_t for st_mode that makes strings like `rwx'
  3. Making a special msbv_handler_t for st_uid that looks up usernames
  4. Live data (not copies)
  5. Forced invalidation of MSB instances
  6. Example of a nested struct value accessor
  7. Example of a wildcard verb like set_*

Junk I Threw In From xpbinding.c

{ "score", xp_player_score, 0, 4, 4, TYPE_INT, TYPE_INT, TYPE_INT, TYPE_STR },

static
MSBV_DECL(xp_player_score)
{
        player *pl_copy = (player *)ptr, *pl;
        int points = arglist.v.list[1].v.num;
        int x = arglist.v.list[2].v.num;
        int y = arglist.v.list[3].v.num;
        const char *msg = str_ref(arglist.v.list[4].v.str);
        package err;
        Var newscore;

        free_var(arglist);
        if (NULL == (pl = xp_player_copy_to_real(pl_copy, &err))) {
                free_str(msg);
                return err;
        }
        /* XPilot now locked */

        SCORE(GetInd[pl->id], points, x, y, msg);
        newscore.type = TYPE_INT;
        newscore.v.num = pl->score;
        UNLOCK_XPILOT();
        free_str(msg);
        return make_var_pack(newscore);
}
msb_property_t msb_xp_ivec_properties[] = {
        { "x", MsbOffset(ivec, x), msbp_int, MSBP_F_R },
        { "y", MsbOffset(ivec, y), msbp_int, MSBP_F_R },
        { NULL }
};

msb_t msb_xp_ivec = {
        "xp_ivec",
        msb_xp_ivec_properties,
        msb_no_verbs,
        &msb_null_ops,
        sizeof(ivec),
        MSB_F_WRITABLE
};

MSBP_DECL(ivec)
{
        if (write)
                return E_NACC;
        else {
                *inout = msb_new_parent(&msb_xp_ivec, msb_owner(m), ptr, m);
                return E_NONE;
        }
}
MSBP_DECL(modifiers)
{
        modifiers mods = *((modifiers *) ptr);

        if (write)
                return E_NACC;
        else {
                static Stream *s;
                char *modstr;

                if (!s)
                        s = new_stream(20);
                if (BIT(mods.nuclear, FULLNUCLEAR))
                        stream_add_char(s, 'F');
                if (BIT(mods.nuclear, NUCLEAR))
                        stream_add_char(s, 'N');
                if (BIT(mods.warhead, CLUSTER))
                        stream_add_string(s, " C");
                if (BIT(mods.warhead, IMPLOSION))
                        stream_add_string(s, " I");
                if (mods.velocity)
                        stream_printf(s, " V%d", mods.velocity);
                if (mods.mini)
                        stream_printf(s, " X%d", mods.mini);
                if (mods.spread)
                        stream_printf(s, " Z%d", mods.spread);
                if (mods.power)
                        stream_printf(s, " B%d", mods.power);
                if (mods.laser) {
                        stream_add_string(s, " L");
                        stream_add_char(s, (BIT(mods.laser, STUN)) ? 'S':'B');
                }
                modstr = reset_stream(s);
                if (modstr[0] == ' ')
                        ++modstr;
                inout->type = TYPE_STR;
                inout->v.str = str_dup(modstr);
                return E_NONE;
        }
}

        { "set_*", xp_player_set_star, 0, 1, 1, TYPE_ANY },

static
MSBV_DECL(xp_player_set_star)
{
        Var value = arglist.v.list[1], changed;
        player *pl_copy = (player *)ptr, *pl;
        package err;
        enum error res, cres;
        Var live;

        if (NULL == (pl = xp_player_copy_to_real(pl_copy, &err))) {
                free_var(arglist);
                return err;
        }
        /* XPilot now locked */

        /*
         * Create a new MSB that points to the *active* pl data.
         * This must not escape into the wild!  It will only be used
         * as a handle to perform a live property update.
         */
        live = msb_new(&msb_xp_player, msb_owner(m), pl);
        /*
         * msb_put_prop will handle perms checking for us.  The live
         * instance has the same ownership as the original.  The
         * msbp_handler_t for this value will handle typechecking.
         */
        res = msb_put_prop(live.v.msb, verbname + 4, value, progr);
        /*
         * If that worked, re-get the property to return to the caller,
         * who might want to notice if there was some kind of roundoff
         * error.  However, failure of this step is not an error, it
         * just means we return 0.
         */
        if (res == E_NONE) {
                cres = msb_get_prop(live.v.msb, verbname + 4, &changed, progr);
        }
        UNLOCK_XPILOT();
        free_var(live);
        free_var(arglist);

        if (res != E_NONE)
                return make_error_pack(res);
        else if (cres == E_NONE)
                return make_var_pack(changed);
        else
                return no_var_pack();
}