4
\$\begingroup\$

In a couple of months I'll be part of a large(ish) project, and there's good chances my team will be a lot bigger, with at least 2 programmers (probably 3) and some 3-5, or more, consumers of the API we'll build (we're building a game, and they'll script high level logic using a scripting language we've yet to choose).

Now, last project I was in I used the below mentioned technique with "great" success when generating structures for use in mapping configuration files. Problem is, this will be a first timer for me in building an API with several developers and multiple consumers, and I have no idea how well it will scale.

tl;dr:

  1. Is this system intuitive enough and scale-able enough for someone (you!), without pre-existent knowledge of the system, to pick it up and extend it without much issue?
  2. If not, how should I go about building something that achieves the same but lets me skip all the repetitive cruft?
  3. Can you spot any gotchas I've overlooked?

These examples uses python 2.7 and pybind11 (for binding structures to python). When properly linked against python they're buildable and runnable.

#include <pybind11/pybind11.h>

namespace py = pybind11;

#define PLUGIN PYBIND11_PLUGIN

#define PLUGIN_BEGIN(TYPE, NAME, DESCR)         \
    PLUGIN(TYPE)                                \
    {                                           \
        typedef TYPE Type;                      \
        py::module Module (NAME, DESCR);        \
        py::class_<Type> Binding(Module, #NAME);\

#define PLUGIN_END                              \
        return Module.ptr();                    \
    }                                           \

#define PLUGIN_MAP_FIELD(NAME)                \
    Binding.def_readwrite(#NAME, &Type::NAME);\

#define PLUGIN_MAP_PROPERTY(NAME)                                   \
    Binding.def_property(#NAME, &Type::get##NAME, &Type::set##NAME);\

And then you use it as so:

#include <string>

struct SPerson
{
    unsigned int Age;

    std::string getName( void ) { return m_Name; }
    void setName( const std::string name ) { m_Name = name; }

private:
    std::string m_Name;
};

PLUGIN_BEGIN(SPerson, "Person", "A person descriptor")
    PLUGIN_MAP_FIELD(Age)
    PLUGIN_MAP_PROPERTY(Name)
PLUGIN_END
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

What you've currently got expands to something like this:

PYBIND11_PLUGIN(SPerson)
{
    typedef SPerson Type;
    py::module Module("Person", "A person descriptor")
    py::class_<SPerson> Binding(Module, "\"Person\"");
    Binding.def_readwrite("Age", &Type::Age);
    Binding.def_property("Name", &Type::getName, &Type::setName);
    return Module.ptr();
}

I think you've got a stray # in front of NAME in one place.


Anyway, is there a technical reason you need SPerson and Person to be different identifiers? That seems like it would increase confusion (two names for the same entity, depending on what language you were writing) rather than decreasing it.

And the string "A person descriptor" is just internal documentation, right? It's not important to the code's semantics, i.e., it's just a comment? If so, it would almost certainly be better to omit it. Remember, you're aiming this code at people who will cut-and-paste it and change just what they need in order to make it work; I foresee a lot of

PLUGIN_BEGIN(SPerson, "Person", "A person descriptor")
PLUGIN_BEGIN(SDog, "Dog", "A dog descriptor")
PLUGIN_BEGIN(SVehicle, "Vehicle", "A vehicledescriptor")
PLUGIN_BEGIN(SWeapon, "Weapan", "A person descriptor")
PLUGIN_BEGIN(SEnemy, "Enemy", "A person descriptor")

in your future, unless you take steps now to eliminate those redundant fields. Remember Murphy's Law: Whatever can go wrong, will go wrong. So if you don't want anything to go wrong, you should try really hard to eliminate any element of the system that can go wrong.

PLUGIN_BEGIN(Person)
    PLUGIN_MAP_FIELD(Age)
    PLUGIN_MAP_PROPERTY(Name)
PLUGIN_END

If your users are likely to show any personal initiative whatsoever — if they're likely to want to understand the tool you give them, instead of just using it passively and/or asking you personally if they have questions — then I would also advise not monkeying with pybind11's names for things. Don't say FIELD if what you mean is READWRITE. This will help the user-programmer when something inevitably breaks and he has to go google the name of the thing; it'll help because there will be one name (readwrite) instead of two (readwrite and field), which means his googling can be twice as effective in the same amount of time.

This will also help you, the library writer, when it comes time to extend the library. Under your current system, how would a user expose a const data member to pybind11? He can't. You need a new macro to deal with const fields. pybind11 has an existing name for such things (readonly), but you haven't made up your own name for them yet. Do you go with FIELD_READONLY, or CONST_FIELD, or suck it up and go with READONLY even though you didn't originally use READWRITE...? It's a bit of a mess. Whereas, if you make a design decision to stick with pybind11's names for things from the get-go, you'll never have to waste your own time on such questions again; the answer is always "go with what pybind11 called it."


Your macro system limits you to one Python class per Python module. That's okay, right?


Syntactical nitpick: Those trailing backslashes are going to bite you one day. (I don't even know if putting a backslash as the last character of a source file is well-defined!) Instead of

#define FOO \
        bar \
        baz \
/* warning: this blank line is semantically significant */

I'd strongly recommend

#define FOO \
        bar \
        baz
\$\endgroup\$
2
  • \$\begingroup\$ I'm a strong believer in staying as close to the naming of the underlying library as possible, so excellent take on the naming standards! In regards to the differing types (e.g. SPerson/Person): the prefix is there mainly because the rest of our codebase uses prefixes (C for class, S for struct, p for pointer, etc.) and we'd rather have a unified standard for all our code. The meaning of these prefixes kind of disappears in dynamic languages like python, lua etc, and I'd rather have us programmers use multiple names than have confused designers and artists. \$\endgroup\$ Commented Feb 17, 2016 at 18:01
  • \$\begingroup\$ Also, the description strings are part of the public api. I'd absolutely love to toss them out, but I'm not sure yet. On the one hand, it's nice to have the documentation close to the modules. On the other, bloat. So much bloat. \$\endgroup\$ Commented Feb 17, 2016 at 18:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.