HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Scale-able API development with macro definitions

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
withdevelopmentmacroscaleapiabledefinitions

Problem

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:

  • 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?



  • If not, how should I go about building something that achieves the same but lets me skip all the repetitive cruft?



  • 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 

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_ 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

struct SPerson
{
unsigned int Age;

std::string getName( void ) {

Solution

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_ 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

Code Snippets

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();
}
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")
PLUGIN_BEGIN(Person)
    PLUGIN_MAP_FIELD(Age)
    PLUGIN_MAP_PROPERTY(Name)
PLUGIN_END
#define FOO \
        bar \
        baz \
/* warning: this blank line is semantically significant */
#define FOO \
        bar \
        baz

Context

StackExchange Code Review Q#115043, answer score: 2

Revisions (0)

No revisions yet.