Friday, August 20, 2010

QMetaEnum Magic - Serializing C++ Enums - Take 2

A few posts ago I described two methods of serializing C++ enums. Of these, method 2 serialized the Qt::Key enum. The approach, however, relied on some behind the scenes magic that I wasn't fully aware of nor did I fully document. This approach remedies that and describes in full the requirements for serializing C++ enums.

Method 2 - Reevaluated

The goal for this method is to take an enum called MyKey in the MyNS namespace and serialize it. In brief, the code for that looks like the following:

namespace MyNS
{
    enum MyKey {
        MyKey_Return = 0,
        MyKey_Enter = 1
        };
}

Since we want Qt to be able to serialize the above enum, it needs to know about the enum, so we're going to need Q_ENUMS(MyKey). But unless moc sees a Q_OBJECT or Q_GADGET macro, our Q_ENUMS macro will result in a compilation error. To get around this, we need to convince moc to process the file as if it were a class. We can do that by adding some preprocessor defines:

#ifndef Q_MOC_RUN
namespace MyNS
#else
class MyNS
#endif
{
#if defined(Q_MOC_RUN)
    Q_GADGET
    Q_ENUMS(MyKey)
public:
#endif
    enum MyKey {
        MyKey_Return = 0,
        MyKey_Enter = 1
    };
}

At this point we've convinced moc to look at and process the file, but that alone isn't enough. The code that moc generates assumes that a const staticMetaObject has been declared, but at this point one hasn't been declared. Although some compilers will let us get away with this, we'll declare it as follows:

    // ... continuing at the enum
    enum MyKey {
        MyKey_Return = 0,
        MyKey_Enter = 1
    };
    extern const QMetaObject staticMetaObject;
}

With that in place, we're ready to serialize the enum. The first step is to get a copy of the QMetaEnum object. We do so by accessing the static reference directly and then calling indexOfEnumerator to get the appropriate index:

    // get the QMetaEnum object
    const QMetaObject &mo = MyNS::staticMetaObject;
    int enum_index = mo.indexOfEnumerator("MyKey");
    QMetaEnum metaEnum = mo.enumerator(enum_index);

With the QMetaEnum instance in hand, we can now serialize the enum as demonstrated in my prior post:

    // convert to a string
    MyNS::MyKey key = MyNS::MyKey_Return;
    QByteArray str = metaEnum.valueToKey(key);
    qDebug() << "Value as str:" << str;

    // convert from a string
    int value = metaEnum.keyToValue("MyKey_Enter");
    key = static_cast(value);
    qDebug() << "key is MyKey_Enter? : " << (key == MyNS::MyKey_Enter);

With all the above in place, we've used a bit of magic to trick moc into thinking MyNS was a class. This causes moc to generate a MyNS::staticMetaObject instance and store the necessary serialization meta data. With everything in place, we get the following output:

Value as str: "MyKey_Return" 
key is MyKey_Enter? :  true 

References:

Saturday, August 14, 2010

Learning Ruby Symbols

Ruby has at least a couple of different ways of referencing variables. Class instance variables can be created with the @ prefix, like @my_var = "test", there's also an @@ prefix for class-level variables.

For the first few days I was learning Ruby and Ruby on Rails, I kept stumbling upon "variables" like :name. But when I tried to assign one, I quickly discovered that it wasn't a variable -- it was a symbol. My attempt resulted in a failure:

irb(main):001:0> :test = 4
SyntaxError: compile error
(irb):1: syntax error, unexpected '=', expecting $end
:test = 4
       ^
        from (irb):1

The ruby compiler didn't expect anything after the '=', and it complains.

What are Symbols?

According to the Symbol documentation:

Symbol objects represent names and some strings inside the Ruby interpreter. They are generated using the :name and :"string" literals syntax, and by the various to_sym methods. The same Symbol object will be created for a given name or string for the duration of a program's execution, regardless of the context or meaning of that name. Thus if Fred is a constant in one context, a method in another, and a class in a third, the Symbol :Fred will be the same object in all three contexts.

A symbol acts as an alias to a name such as a variable, class, or method. As a simple example, consider the following two hashes:

u1 = { "name" => "Kaleb", "phone" => "private" }
u2 = { "name" => "Larry", "phone" => "private" }

And here's a similarly structured hash using symbols:

u1 = { :name => "Kaleb", :phone => "private" }
u2 = { :name => "Larry", :phone => "private" }

For each hash the compiler keeps copies of the keys "name" and "phone". For a couple of values this doesn't matter, but for thousands of records it adds up to a lot of duplication and wasted memory. A post on glu.ttono.us provides more concrete details on both symbols and memory usage differences.

Other Resources on Symbols

Thursday, August 12, 2010

rspec: undefined method route_for error

I'm now in the process of learning Ruby on Rails and its friends, like rspec. Firmly believing in TDD, I jumped on the rpsec bandwagon as soon as I could. The second thing I started to test was my routes. My test looked something like this:

  describe "should create :page param for / url" do
    route_for(:controller => 'pages', :action => 'index').should == "/"
  end

But sadly this resulted in the following error:

pages_controller_routing_spec.rb:14: undefined method `route_for' for # (NoMethodError)

After some googling I found a thread on the rspec mailing list that revealed a few different causes:

  1. spec/controllers in their path (i.e. spec/controllers/foo_controller_spec)
  2. describe "route generation", :type => :controller do

My spec class was a controller, so that wasn't the issue. The second option is only required when the controller test is not in spec/controllers, so that wasn't the issue. I had scoured the rspec docs, and everything I read said it should work.

I had missed the most obvious thing of all -- I had said describe when I meant it:

  it "should create :page param for / url" do
    route_for(:controller => 'pages', :action => 'index').should == "/"
  end

So, if you happen to encounter a similar error... don't forget to check the obvious.