Skip navigation.
KDE Developer's Journals

Using custom C++ classes with QtRuby

richard dale's picture

I've recently been having a discussion with Eric Landuyt on the Korundum site help forum about wrapping custom C++ classes in QtRuby. I told Eric that you just needed to create a QObject derived class with the slots and properties you wanted to expose, give it a name via a QObject::setObjectName() call, and create it with qApp as the parent. Then wrap the class in a Ruby extension using an extconf.rb script to generate the makefile to build it. Once your new extension is loaded, you can find the instance of the C++ class by using Qt::Object.findChild() with the object name you gave it.

Here is a simple example class with a property and a slot. The header is defined like this:

#include <QtCore/qobject.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/qstring.h>

class TestObject : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int value READ value WRITE setValue)
public:
    TestObject(QObject *parent = 0);

    void setValue(int value);
    int value();

public slots:
    int foobar(bool yn, QString text);

private:
	int m_value;
};

And then the implementation with a C Init_testqobject() function as an entry point. It creates an instance of a C++ TestObject with QCoreApplication::instance() as parent, and an objectName of "QtRuby TestObject":

#include "testobject.h"

TestObject::TestObject(QObject *parent)
    : QObject(parent), m_value(0)
{
}

int TestObject::value()
{
    return m_value;
}

void TestObject::setValue(int value)
{
    m_value = value;
}

int TestObject::foobar(bool yn, QString text)
{
    qDebug("in foobar yn: %s text: %s\n", yn ? "true" : "false", (const char *) text.toLatin1());
    return 123;
}

void init() 
{
    TestObject * test = new TestObject(QCoreApplication::instance());
    test->setObjectName("QtRuby TestObject");
}

extern "C" {

void
Init_testqobject()
{
    init();
}

};

The extconf.rb to build the 'testqobject' extension creates the makefile, and execs the moc utility. It looks like this:

require 'mkmf'
$CPPFLAGS += " -I/opt/kde4/include/QtCore -I/opt/kde4/include "
$LOCAL_LIBS += '-L/opt/kde4/lib -lQtCore -lstdc++'
create_makefile("testqobject")
exec "/opt/kde4/bin/moc #{$CPPFLAGS} testobject.h -o moc_testobject.cpp"

In your ruby code you can get hold of the instance of you TestObject like this:

irb -rQt
irb(main):001:0> app = Qt::Application.new(ARGV)
=> #<Qt::Application:0xb6ab4f08 objectName="irb">
irb(main):002:0> require 'testqobject'
=> true
irb(main):003:0> test = app.findChild(Qt::Object, "QtRuby TestObject")
=> #<TestObject:0xb6aaf134 objectName="QtRuby TestObject">
irb(main):004:0> test.value = 456
=> 456
irb(main):005:0> test.value
=> 456

Note that you need to do the require statement for 'testqobject' after you've created the Qt::Application so it has a non null 'qApp' as a parent. Retrieve the instance with 'app.findChild()' and it returns a Ruby instance of 'TestObject' even though the QtRuby runtime knew nothing about that class in advance. Then you can set and get properties from ruby directly, or you can invoke slots by connecting them to a QtRuby signal and emitting it.

That does work fine, but Eric asked why he couldn't just invoke the slots directly. He said:

I created a custom C++ Qt class, named TestObject, derivating from QObject,
with a custom mouseClick() Qt slot. Obtaining the C++ instance by using 'test
= findChild(TestObject, "test")' works properly. However, if I try to invoke
a Qt slot directly, such as test.MouseClick(), it fails with a NoMethodError.
...

As you suggested it, direct support in the runtime would be really perfect!
Smiling In fact, I really like the idea to use C++ classes throught Qt (as it avoid
to use something like SWIG or a C Ruby extension only to generate a wrapper).
Basically, I derivate all C++ classes from QObject, replace 'public:' by 'public
slots:' and I get all my C++ classes accessible from the Ruby side for free,
with introspection as an unexpected benefit! Smiling If required, the only missing
thing to write at the C++ side would be a small object factory to directly
instanciate C++ classes from the Ruby side.

So I went ahead and did what he said and checked the code into the svn today, and it will be in the next release of QtRuby. You can invoke you custom slot directly like this:

irb(main):006:0> test.foobar(false, "hi there")
in foobar yn: false text: hi there
=> 123

This is actually really useful for KDE programming because it means you can now invoke slots as methods directly on KPart instances, as well as setting properties on them with really simple ruby code.

If you want to do something similar with the current QtRuby release, Eric posted this sample code to achieve the same effect:

class ObjectWrapper < Qt::Object
  def initialize(parent)
    super(parent)
    slots = parent.metaObject.slotNames
    self.class.signals *slots.collect { |slot| slot = '_' + slot }
    slots.each do |slot|
      connect(self, SIGNAL('_' + slot), parent, SLOT(slot))
      method = slot.split('(')[0]
      instance_eval <<-EOS
        def #{method}(*args)
          emit _#{method}(*args)
        end
      EOS
    end
  end
end

app  = Qt::Application::instance
test = ObjectWrapper.new(app.findChild(Qt::Object, 'TestObject'))

Another really good thing with QtRuby happened today; thanks to the efforts of Guillaume Laurent and Thomas Moenicke you can now build both qtruby, and the smoke library it uses, entirely with cmake. This is a huge improvement over autoconf/automake which often barely worked on Linux let alone Windows or Mac OS X where it was utterly useless. So I'm now hoping that QtRuby will be relatively easy to build on any platform, and the next step will be packaging it as a gem built by cmake, and that will allow a lot more people to use it.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
illissius's picture

yesh!

This is awesome stuff. My first choice for most applications would be QtRuby, but nearly everything has some parts which are extremely sensitive to speed (e.g. filling and sorting huge listviews -- whether it happens in half a second or half a minute is not at all insignificant). This would have pretty much forced you to write the whole thing in C++, but if there's a good way to write only select bits of a QtRuby application in C++, well, that changes.

Is there any way to do the reverse -- write libraries in QtRuby, and call them from C++? (Not sure what I'd want to use this for; just curious).

richard dale's picture

Re: yesh!

Yes, you can do exactly the same thing going the other way. Change the init() method in the C++ TestObject to look like this:

void init() 
{
    TestObject * test = new TestObject(QCoreApplication::instance());
    test->setObjectName("QtRuby TestObject");
    QObject * rubyObject = QCoreApplication::instance()->findChild<QObject*>("RubyTestObject");
    if (rubyObject == 0) {
        qDebug("Failed to find RubyTestObject\n");
    }

    int retVal;
    if (    QMetaObject::invokeMethod(  rubyObject, 
                                        "foobar", 
                                        Qt::DirectConnection, 
                                        Q_RETURN_ARG(int, retVal),
                                        Q_ARG(bool, true),
                                        Q_ARG(QString, "hi there ruby!") ) )
    {
        qDebug("Called ruby foobar result: %d\n", retVal);
    } else {
        qDebug("Failed to call ruby foobar\n");
    }
}

And write a ruby program called 'doit.rb' with a slot called 'foobar' to invoke:

require 'Qt'

class RubyTestObject < Qt::Object
    slots 'int foobar(bool, QString)'

    def initialize(parent)
        super(parent)
        self.objectName = "RubyTestObject"
    end

    def foobar(yn, text)
        puts "In RubyTestObject.foobar %s %s" % [yn, text]
        return 123
    end
end

app = Qt::Application.new(ARGV)

testobj = RubyTestObject.new(app)
require 'testqobject'

And the ruby slot will be called:

ruby doit.rb
In RubyTestObject.foobar true hi there ruby!
Called ruby foobar result: 123

The QtRuby integration with the C++/Qt runtime is 'turtles all the way down', in that there is nothing faked there. When you define a QtRuby slot a real QMetaObject is created at runtime that looks just the same as a C++ one to the C++ world.

illissius's picture

Re: yesh!

Cool stuff; thanks. Although having to use QMetaObject::invokeMethod() sort of defeats what might have been the purpose (convenience). (I recognize that C++ is like this and there's probably no better way).

One more thing: Is there any way to do threads? I remember you can't really use ruby threads with Qt because they're not really threads, but how about the reverse, QThreads in ruby?

richard dale's picture

UPDATE: Q_ENUMS are now mapped onto ruby constants

I've just added this nice feature - here is the commit message:

* If a new Ruby class is created for a custom C++ QObject derived class,
  then create Ruby constants in the class for any enums defined via 
  Q_ENUMS which are in the same scope as the new class. For instance:

        class TestObject : public QObject
        {
                Q_OBJECT
                Q_ENUMS(Priority)
                public:
                        enum Priority { High, Low, VeryHigh, VeryLow };

        ...

        irb(main):001:0> app = Qt::Application.new(ARGV)
        => #<Qt::Application:0xb6aaaf58 objectName="irb">
        irb(main):002:0> require 'testqobject'
        => true
        irb(main):003:0> test = app.findChild(Qt::Object, "QtRuby TestObject")
        => #<TestObject:0xb6aa5184 objectName="QtRuby TestObject">
        irb(main):004:0> TestObject::High
        => 0
        irb(main):005:0> TestObject::Low
        => 1
        irb(main):006:0> TestObject::VeryHigh
        => 2
aseigo's picture

add to developer wiki?

this would make a fine tutorial on devnew =)

richard dale's picture

Re: add to developer wiki?

Yes, ok sure. I've been meaning to do some more documentation for QtRuby for sometime on the new wiki.

This is a very powerful capability that is also very simple to implement. It demonstates than the Qt/C++ foundation of KDE is hardly a problem for language bindings. In fact quite the reverse.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.