2005-12-16 14:27

Bound by Smoke I

This is what I understood of Smoke so far. I may be way off, since it is C++ sorcery of a higher level than I'm used to, but I really think I am getting the hang of it (and a bunch of thanks to Richard Dale and Ashley Winters who are the ones that made me understand so far. Any mistakes a re my fault, any good thing is theirs ;-).

This piece is only half of the story, though. Maybe one third.

Concept

Since Smoke's goal is to help you write bindings for languages other than C++, it provides a way to access Qt's API. The original thing about Smoke is that it does so by providing you with a smaller, more dynamic API that maps onto Qt's.

You could write a Qt C++ program using Smoke as the API instead of using Qt's. In fact, you can see it here written by Ashley Winters.

I had to rename it hello.cpp to make it work because that looks like C but may not really be C ;-)

As you can see, the Smoke version is quite a bit more complex than the Qt one. But that's ok, remember that the goal is a binding, which means that what you need to make your life simpler is less API... which is what makes the program more verbose.

Let's examine the Smoke hello.cpp in detail.

One key point is callMethod:

// call obj->method(args)
void callMethod(Smoke *smoke, void *obj, Smoke::Index method, Smoke::Stack args) {
        Smoke::Method *m = smoke->methods + method;
        Smoke::ClassFn fn = smoke->classes[m->classId].classFn;
        fn(m->method, obj, args);
}

If you have programmed in Python or Ruby or a number of other dynamic languages you may guess what this does already.

It takes as arguments a number of things which still have to be explained but the main gist is there is an object, there is a method, there are args, and it ends calling obj->method(args).

The first argument is a Smoke pointer , which is the big object in the Smoke library, created in our program by the init_smoke function.

A Smoke object is a strange beast. It contains a description (in this case, because we got qt_smoke) for the whole Qt API.

You can find classes in it indexed by their names, and methods for each class indexed by their names and types of arguments.

That is why you can have a generic callMethod that will work for any kind of object and for any method in the class, because all of them are somewhere in the Smoke object.

The second argument is void *obj which is the object itself we are manipulating. So, if you are trying to call QLabel::setText, it will have to be a Qlabel* casted as void*.

In the hello.cpp example, we even create these objects using Smoke's API (see later).

The third argument is a Smoke::Index which is what Smoke uses to find the requested method in its method table. This Index we get using the getMethod function, which is the second key piece of code:

// given class-name and mangled function-name, return an unambiguous method ID
Smoke::Index getMethod(Smoke *smoke, const char* c, const char* m) {
        Smoke::Index method = smoke->findMethod(c, m);
        Smoke::Index i = smoke->methodMaps[method].method;
        if(i <= 0) {
                // ambiguous method have i < 0; it's possible to resolve them, see the other bindings
                fprintf(stderr, "%s method %s::%s\n",
                i ? "Ambiguous" : "Unknown", c, m);
                exit(-1);
        }
        return i;
}

Here is an example of a getMethod call, where we are getting QApplication::setMainWidget

Smoke::Index method = getMethod(smoke, "QApplication", "setMainWidget#");

As you can see, we search for the method using strings of the class name and method name. Excapt for that pesky # at the end of setMainWidget#.

That is a basic argument-type-mangling scheme, since there can be more than one QApplication::setMainWidget on the Qt side of the fence, we are saying we want the one that has an object as the first and only argument. Here is the key to the mangling taken from smoke.h:

* The munging works this way:
* $ is a plain scalar
* # is an object
* ? is a non-scalar (reference to array or hash, undef)
*
* e.g. QApplication(int &, char **) becomes QApplication$?

I am not yet completely clear on how this is enough to do all the work (for example, what happens if you have two methods that take different objects as only argument?) but it's what I saw :-)

The last argument, args is of Smoke::Stack type, and it's the tricky one, at least for me.

Here's how it's used in the previous example, QApplication::setMainWidget

// qapp->setMainWidget(l)
Smoke::Index method = getMethod(smoke, "QApplication", "setMainWidget#");
Smoke::StackItem args[2];
smokeCast(smoke, method, args, 1, l, "QLabel");
smokeCastThis(smoke, method, args, qapp, "QApplication");
callMethod(smoke, args[0].s_class, method, args);

A Smoke::Stack is a way to pass the arguments to be used with the method getMethod gave us.

We first create an array of 2 StackItems:

Smoke::StackItem args[2];

Then we assign a value to the second of them:

smokeCast(smoke, method, args, 1, l, "QLabel");

Here l is a pointer to a QLabel. ( Ok, it is really declared as a void* because, remember, we are not using the Qt API, so we have no clue what a QLabel is ;-) and what we are doing is storing in args[1] a casted version of l.

The exact details of why you have to pass smoke and method are not that important, and they seem pretty involved, so I won't try to go there, at least not yet. This has to be done for each argument for the method.

Then we have a similar, yet different line:

smokeCastThis(smoke, method, args, qapp, "QApplication");

This puts the qapp void * in args[0], casted to QApplication. There are tricky C++ reasons why this is done slightly different here than on smokeCast, which I am not 100% sure I get right, so I will keep quiet ;-)

This special case is only for the object to which the method belongs (the this object).

Here is the code for smokeCast and smokeCastThis

// cast argument pointer to the correct type for the specified method argument
// args[i].s_class = (void*)(typeof(args[i]))(className*)obj
void smokeCast(Smoke *smoke, Smoke::Index method, Smoke::Stack args, Smoke::Index i, void *obj, const char *className) {
        // cast obj from className to the desired type of args[i]
        Smoke::Index arg = smoke->argumentList[
                smoke->methods[method].args + i - 1
        ];
        // cast(obj, from_type, to_type)
        args[i].s_class = smoke->cast(obj, smoke->idClass(className), smoke->types[arg].classId);
}

// cast obj to the required type of this, which, dur to multiple-inheritance, could change the pointer-address
// from the one returned by new. Puts the pointer in args[0].s_class, even though smoke doesn't do it that way
void smokeCastThis(Smoke *smoke, Smoke::Index method, Smoke::Stack args, void *obj, const char *className) {
        args[0].s_class = smoke->cast(obj, smoke->idClass(className), smoke->methods[method].classId);
}

But where did we get l or qapp? You can use these same mechanisms to create an object:

void *qapp;
{
    // new QApplication(argc, argv)
    Smoke::Index method = getMethod(smoke, "QApplication", "QApplication$?");
    Smoke::StackItem args[3];
    args[1].s_voidp = (void*)&argc;
    args[2].s_voidp = (void*)argv;
    callMethod(smoke, 0, method, args);

    qapp = args[0].s_class;
}

You get QApplication::QApplication(scalar,undef) which should hopefully map to QApplication::QApplication(argc,argv). You create a Smoke::Stack of 3 items. The first is unset because this is a constructor, so it has no this yet, and the other two are argc and argv.

Then you call it through callMethod, and you get the resulting object via args[0].s_class.

Later you repeat this sort of operation for every method call you want, and you got yourself an application.

The binding side of things

So, how do you use this to bind your language to Qt?

Well, you will have to create an object in your language called, for example, QApplication, and map the "missing method" mechanism (your scripting language probably has one) which is used when a method is undefined so that somehow it finds the right Smoke method (mangling the name correctly should be the hard part), then create the Smoke::Stack with the passed arguments, call, get the result, wrap it in some language construct you can use of the side of your language, and pass it back.

It looks involved, and I am sure it's tricky, but at least it only has to be done once unlike on traditional bindings, where you had to do it for every class and for every method.

The traditional solution was to automatically generate the code for such wrapping (like SWIG does). I think Smoke is less error prone.

If I keep on understanding things, there may be a second part of this article, explaining the SmokeBinding class, and perhaps a short example of how to bind Qt into a language (Io is a strong candidate).

The regularity of Io's syntax is probably going to make the binding simpler than most.

Comments

Comments powered by Disqus

Contents © 2000-2018 Roberto Alsina