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 with Q_PROPERTY and the moc-generated meta-object, RTTR with a run-time registration block, refl-cpp with a compile-time member list.

In JaiScript a property is a member declared with JAI_PROPERTY on a jai::property_owner, and that one declaration serves run-time reflection by name, serialization, script access and, through JAI_OBSERVABLE_PROPERTY and JAI_SIGNAL_PROPERTY, change signals. The trade is scope: no user metadata on a property, no compile-time member list, no property added at run time (What has no counterpart).

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::metaType() (Qt 6; type() is deprecated there), 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. In Qt:

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; }
        m_heat = heat;
        emit heatChanged(heat);
    }
signals:
    void heatChanged(int heat);
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 heat) { qDebug() << heat; });

RTTR registers the same members of a plain struct, with the range as metadata; refl-cpp describes them beside the class and walks them in templates:

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);
C++ (refl-cpp)
REFL_AUTO(
    type(Turret),
    field(name),
    field(ammo),
    field(range, Range(0.0f, 50.0f))     // Range: a user attribute deriving refl::attr::usage::field
)

for_each(refl::reflect<Turret>().members, [&](auto member) {
    std::cout << get_display_name(member) << " = " << member(t) << "\n";
});
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, 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 hands its receivers the old and the new value, where a Qt notify signal conventionally carries the new value or nothing. It 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 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.
  • 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.