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-cpp | JaiScript |
|---|---|
a QObject with Q_OBJECT; RTTR and refl-cpp work on any class | class 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_readonly | script-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 member | JAI_NAMED_PROPERTY((int64_t), "ammo_count", ammo, 10); |
a signals: member, RTTR has none | JAI_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_AUTO | nothing else: the macros are the declaration |
Reading and writing by name
| Qt / RTTR / refl-cpp | JaiScript |
|---|---|
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 instance | Turret::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::variant | typed access (p->value_as<T>()), or a jai::script_value through script |
Q_INVOKABLE with QMetaObject::invokeMethod; RTTR .method(...) with invoke | methods are binder .method(...) entries; script calls one by name with reflect::invoke(obj, "fire") |
From script
| Qt (QML / QJSEngine) | JaiScript |
|---|---|
turret.ammo in QML | turret.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 JavaScript | reflect::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:
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; });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:
#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:
#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});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_changefires only when the value changes, from C++ assignment, compound operators,set_property_valueor 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:
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;name string field
ammo int field
range float field
heat int observable
cooldown float field
3
heat 8 -> 9
script sees heat 8 -> 9The 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'sDESIGNABLE,SCRIPTABLEandUSERflags 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
setPropertywith an undeclared name adds one to that object.set_property_valuefinds declared properties only, and returnsfalsefor any other name. RESET,REVISION,BINDABLE. No reset function, revisions or bindable properties; a binding that follows another value connects to itson_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::methodsand calls them withreflect::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 withdynamic_binder.propertylines, 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.