From Qt Properties, RTTR and refl-cpp​

C++ reflection libraries answer one question: what fields does this object have, and how do I read and write them by name? Qt answers it with Q_PROPERTY and the moc-generated meta-object. RTTR registers properties at run time from a registration block. refl-cpp describes members at compile time for templates to walk. In JaiScript a property is a member declared with JAI_PROPERTY on a jai::property_owner, and that one declaration serves four systems: run-time reflection by name, serialization, script access, and, through JAI_OBSERVABLE_PROPERTY and JAI_SIGNAL_PROPERTY, change signals. The trade is scope. There is no user metadata on a property, no compile-time member list, and no property added at run time. This page maps the three libraries, converts a class, and lists what each has with no counterpart here. The declaration is documented in dynamic_binder, observable values and signals in Signals & Observable Properties, and script-side reflection in the guide's Reflection.

The concept map​

Headers: <jaiscript/properties/macros.hpp> and <jaiscript/properties/property_serialization.hpp> for the declarations, <jaiscript/properties/observable_property.hpp> for observable values, <jaiscript/signals/signal_property.hpp> for JAI_SIGNAL_PROPERTY, and <jaiscript/core/registrar.hpp> to make the class scriptable.

Declaring​

Qt / RTTR / refl-cppJaiScript
a QObject with Q_OBJECT; RTTR and refl-cpp work on any classclass Turret : public jai::property_owner<Turret> (property_owner<Derived, Base> for a derived level)
Q_PROPERTY(int ammo MEMBER m_ammo); RTTR .property("ammo", &Turret::ammo); refl-cpp field(ammo)JAI_PROPERTY((int64_t), ammo, 10);, a jai::property<int64_t> that reads and assigns like the member
Q_PROPERTY(int ammo READ ammo WRITE setAmmo NOTIFY ammoChanged)JAI_OBSERVABLE_PROPERTY((int64_t), ammo, 10);, whose on_change(old, new) fires on every change; or, for a plain class, .property("ammo", &Turret::ammo, &Turret::setAmmo) on its binder
Q_PROPERTY(... CONSTANT), RTTR registration::property_readonlyscript-side, a binder .property(name, getter, nullptr); a JAI_PROPERTY is writable
Q_PROPERTY(... STORED false)JAI_TRANSIENT_PROPERTY: reflected and scripted, never saved
a different public name for a memberJAI_NAMED_PROPERTY((int64_t), "ammo_count", ammo, 10);
a signals: member, RTTR has noneJAI_SIGNAL_PROPERTY(void(int64_t), onFire);: a signal scripts connect to, whose script-text receivers save with the object
the class name from Q_OBJECT; registration::class_<T>("Turret"); type(Turret)the bare C++ class name, or JAI_PROPERTY_OWNER_AS(Turret, "Turret") to pin one
moc, a registration block, REFL_AUTOnothing else: the macros are the declaration

Reading and writing by name​

Qt / RTTR / refl-cppJaiScript
meta->property(i) for i up to propertyCount(); type::get<T>().get_properties(); for_each(refl::reflect<T>().members, ...)obj->visit_reflected_properties([](const std::string& name, jai::property_base* p) { ... });, base levels first and virtual, so a base pointer sees the whole object
the property list without an instanceTurret::all_property_names(), and Turret::schema() for each name's C++ type and flags
obj.property("ammo").toInt(); prop.get_value(obj).to_int(); member(obj)obj->property_value<int64_t>("ammo"), a pointer to the value, null for another name or type
obj.setProperty("ammo", 3); prop.set_value(obj, 3)obj->set_property_value<int64_t>("ammo", 3), which goes through the property, so an observable one fires on_change
QMetaProperty::type(), prop.get_type()p->value_type_id(), a std::type_index; p->allow_save() tells whether it is saved
QVariant, rttr::varianttyped access (p->value_as<T>()), or a jai::script_value through script
Q_INVOKABLE with QMetaObject::invokeMethod; RTTR .method(...) with invokemethods are binder .method(...) entries; script calls one by name with reflect::invoke(obj, "fire")

From script​

Qt (QML / QJSEngine)JaiScript
turret.ammo in QMLturret.ammo: a property of int, float, bool or string type binds to script by itself, through auto_bind; another type takes a binder .property line
onAmmoChanged: ...turret.ammo.on_change([=](int old_value, int new_value) -> void { ... }); on an observable property
enumerating properties from JavaScriptreflect::fields(turret) (name, type, kind), reflect::get(turret, "ammo"), reflect::set(turret, "ammo", 3)

A class converted​

A turret with a name, ammunition, a range limited to 0..50, a heat level other systems watch, and a cooldown nobody saves. Qt and RTTR, the way each is usually written:

C++ (Qt)
class Turret : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString name MEMBER m_name)
    Q_PROPERTY(int ammo MEMBER m_ammo)
    Q_PROPERTY(double range MEMBER m_range)
    Q_PROPERTY(int heat READ heat WRITE setHeat NOTIFY heatChanged)
    Q_PROPERTY(double cooldown MEMBER m_cooldown STORED false)
public:
    int heat() const { return m_heat; }
    void setHeat(int heat) {
        if (heat == m_heat) { return; }
        int old = m_heat;
        m_heat = heat;
        emit heatChanged(old, heat);
    }
signals:
    void heatChanged(int oldHeat, int newHeat);
private:
    QString m_name = "north";
    int m_ammo = 10;
    double m_range = 12.5;
    int m_heat = 0;
    double m_cooldown = 0.0;
};

const QMetaObject* meta = turret.metaObject();
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
    qDebug() << meta->property(i).name();
}
turret.setProperty("ammo", 3);
QObject::connect(&turret, &Turret::heatChanged, [](int oldHeat, int newHeat) { qDebug() << oldHeat << newHeat; });
C++ (RTTR)
RTTR_REGISTRATION {
    rttr::registration::class_<Turret>("Turret")
        .constructor<>()
        .property("name", &Turret::name)
        .property("ammo", &Turret::ammo)
        .property("range", &Turret::range)(rttr::metadata("min", 0.0), rttr::metadata("max", 50.0));
}

rttr::type type = rttr::type::get<Turret>();
for (auto& prop : type.get_properties()) {
    std::cout << prop.get_name() << "\n";
}
type.get_property("ammo").set_value(turret, 3);

refl-cpp describes the members beside the class and walks them in templates, attribute included:

C++ (refl-cpp)
#include <refl.hpp>

struct Range : refl::attr::usage::field {
    float min, max;
    constexpr Range(float a_min, float a_max) : min(a_min), max(a_max) {}
};

struct Turret {
    std::string name = "north";
    int ammo = 10;
    float range = 12.5f;
};

REFL_AUTO(
    type(Turret),
    field(name),
    field(ammo),
    field(range, Range(0.0f, 50.0f))
)

for_each(refl::reflect<Turret>().members, [&](auto member) {
    std::cout << get_display_name(member) << " = " << member(t) << "\n";
});

JaiScript:

C++ (JaiScript)
#include <jaiscript/core/registrar.hpp>
#include <jaiscript/properties/macros.hpp>
#include <jaiscript/properties/property_serialization.hpp>
#include <jaiscript/properties/observable_property.hpp>

class Turret : public jai::property_owner<Turret> {
public:
    JAI_PROPERTY((std::string), name, "north");
    JAI_PROPERTY((int64_t), ammo, 10);
    JAI_PROPERTY((double), range, 12.5);
    JAI_OBSERVABLE_PROPERTY((int64_t), heat, 0);
    JAI_TRANSIENT_PROPERTY((double), cooldown, 0.0);
};

static jai::registrar<Turret, void> turretRegistrar("Turret");

auto turret = std::make_shared<Turret>();
turret->visit_reflected_properties([](const std::string& name, jai::property_base* p) {
    std::cout << name << (p->allow_save() ? "" : " (transient)") << "\n";
});
turret->set_property_value<int64_t>("ammo", int64_t{3});

auto watch = turret->heat.on_change.connect([](const int64_t& oldHeat, const int64_t& newHeat) {
    std::cout << "heat " << oldHeat << " -> " << newHeat << "\n";
});
turret->heat = 5;
turret->set_property_value<int64_t>("heat", int64_t{8});
output
ammo
cooldown (transient)
heat
name
range
heat 0 -> 5
heat 5 -> 8
  • One declaration, four uses. The same five lines are what jai::to_json(*eng, turret) saves ("ammo": 3,"heat": 8,"name": "north","range": 12.5, the transient cooldown left out), what script reads and writes (turret.ammo, reflect::fields(turret)), and what an editor walks. There is no registration block to keep in step with the class.
  • The walk is in name order within a level and starts with base levels. Turret::all_property_names() lists the names in declaration order without an instance.
  • The notify signal is part of the property. on_change fires only when the value changes, from C++ assignment, compound operators, set_property_value or a script store; heat.set_silent(v) writes without it, for loading.
  • The range limit has no place to go. A property carries its name, its C++ type and its save and observe flags, nothing else. A limit is enforced by a setter (a binder .property(name, getter, setter)) and described to an editor by the host's own table.

From script, the same object:

jaiscript (with the host above)
for (auto f : reflect::fields(turret)) { print(f["name"], " ", f["type"], " ", f["kind"]); }
print(reflect::get(turret, "ammo"));
reflect::set(turret, "range", 20.0);
turret.heat.on_change([=](int old_value, int new_value) -> void { print("script sees heat ", old_value, " -> ", new_value); });
turret.heat = turret.heat + 1;
output
name string field
ammo int field
range float field
heat int observable
cooldown float field
3
heat 8 -> 9
script sees heat 8 -> 9

The C++ receiver from the block above fires too (heat 8 -> 9): one emitter, receivers on both sides.

What has no counterpart​

  • Metadata and attributes. RTTR's metadata(key, value), refl-cpp's attributes and Qt's DESIGNABLE, SCRIPTABLE and USER flags have nothing to attach to: a property is a name, a type and its save and observe flags.
  • Compile-time iteration. refl-cpp's member list is a compile-time sequence templates expand over. JaiScript's is a run-time walk; generic code takes a property_base* and asks it for its type.
  • Dynamic properties. Qt's setProperty with an undeclared name adds one to that object. set_property_value finds declared properties only, and returns false for any other name.
  • RESET, REVISION, BINDABLE. No reset function, revisions or bindable properties; a binding that follows another value connects to its on_change.
  • Enumerating methods from C++. Qt and RTTR list and invoke methods through the meta-object. A JaiScript class's methods are its binder entries in an engine; script lists them with reflect::methods and calls them with reflect::invoke.
  • Properties on any class. RTTR and refl-cpp reflect a class without touching it. A JaiScript property lives in a property_owner; a class you cannot edit is bound with dynamic_binder .property lines, which script sees but the C++ walk does not.

Where to next​

dynamic_binder follows one declaration through script, reflection, serialization and hot reload. Signals & Observable Properties covers observable_property and JAI_SIGNAL_PROPERTY, and the guide's Reflection covers every reflect:: call.