366 lines
12 KiB
Markdown
366 lines
12 KiB
Markdown
# Structive
|
|
|
|
**Structive enhances ordinary C++ structs with explicit structural metadata and managed property behavior without replacing the native C++ data model.**
|
|
|
|
[中文文档](README.zh-CN.md) · [Design Philosophy](docs/DESIGN.md) · [Core Guide](docs/CORE_GUIDE.md) · [Extension Guide](docs/EXTENSIONS.md)
|
|
|
|
## What Structive is
|
|
|
|
Structive is a C++20 structural property system built around two separate layers:
|
|
|
|
```text
|
|
C++ object model
|
|
ordinary members and member functions
|
|
│
|
|
├── Type_Descriptor<T> → Object_Schema
|
|
│ type-level structure, keys, intrinsic capabilities,
|
|
│ attributes, constraints and synchronization description
|
|
│
|
|
└── Property_Object<T>
|
|
instance-level managed read/write, synchronization,
|
|
traversal and type-erased runtime access
|
|
```
|
|
|
|
A type remains ordinary C++:
|
|
|
|
```cpp
|
|
#include <structive/property/property.hpp>
|
|
using namespace structive;
|
|
struct Device : Property_Object<Device> {
|
|
double temperature{25.0};
|
|
int serial_number{1001};
|
|
};
|
|
template <>
|
|
struct structive::Type_Descriptor<Device> {
|
|
static auto get() {
|
|
return object<Device>(
|
|
field<&Device::temperature>(key<"temperature">, unit<"C">),
|
|
field<&Device::serial_number>(key<"serial_number">, read_only)
|
|
);
|
|
}
|
|
};
|
|
```
|
|
|
|
The members are still real members. Structive only adds explicit structural meaning around them.
|
|
|
|
## Core idea
|
|
|
|
Structive follows one central rule:
|
|
|
|
> **Enhance the struct; do not replace the struct.**
|
|
|
|
That means:
|
|
|
|
- registered fields remain ordinary C++ members;
|
|
- unregistered members remain outside Structive;
|
|
- member pointers are preferred compile-time identities;
|
|
- string keys exist for dynamic and adapter boundaries;
|
|
- metadata does not force storage wrappers such as `Property<T>`;
|
|
- raw C++ access remains possible;
|
|
- Structive does not try to enforce a security boundary around a public member;
|
|
- external systems decide for themselves what they expose or allow;
|
|
- Core only describes what the property itself can intrinsically do.
|
|
|
|
## No access-control subsystem
|
|
|
|
Structive intentionally has no built-in `internal`, `external`, `persistence`, role, context or policy access modes.
|
|
|
|
Core does not expose domain-specific access views, permission enums or persistence-specific access modes.
|
|
|
|
A GUI, RPC service, serializer, plugin system or persistence layer is responsible for deciding which properties it exposes and which operations it permits. Structive does not own that policy.
|
|
|
|
The Core only answers intrinsic structural questions:
|
|
|
|
```text
|
|
Can this property be read?
|
|
Can this property be written?
|
|
What is its key?
|
|
What metadata and constraints are attached?
|
|
What synchronization is required for managed access?
|
|
```
|
|
|
|
## Intrinsic property capability
|
|
|
|
Every property has one intrinsic capability:
|
|
|
|
```text
|
|
none
|
|
read
|
|
write
|
|
read_write
|
|
```
|
|
|
|
For normal accessors Structive derives this from the accessor automatically. A member field is normally `read_write`; a getter-only computed property is naturally `read`.
|
|
|
|
The schema can explicitly narrow the capability:
|
|
|
|
```cpp
|
|
field<&Device::serial_number>(
|
|
key<"serial_number">,
|
|
read_only
|
|
)
|
|
```
|
|
|
|
The predefined capability attributes are:
|
|
|
|
```cpp
|
|
read_only
|
|
write_only
|
|
read_write
|
|
inaccessible
|
|
```
|
|
|
|
They are structural contracts, not user permissions.
|
|
|
|
For a read-only property:
|
|
|
|
```cpp
|
|
auto id = device.read<&Device::serial_number>();
|
|
```
|
|
|
|
is valid, while:
|
|
|
|
```cpp
|
|
device.write<&Device::serial_number>(1002);
|
|
```
|
|
|
|
is unavailable at compile time.
|
|
|
|
The raw C++ member remains accessible if the C++ type itself makes it accessible:
|
|
|
|
```cpp
|
|
device.serial_number = 1002;
|
|
```
|
|
|
|
That raw write deliberately bypasses the Structive contract and its synchronization guarantees.
|
|
|
|
## Read-only means no property lock
|
|
|
|
Property metadata is used for optimization, not only documentation.
|
|
|
|
A stored property whose intrinsic capability is read-only does not participate in the synchronization topology:
|
|
|
|
```text
|
|
read-only stored property
|
|
↓
|
|
no lock slot
|
|
↓
|
|
no mutex contribution
|
|
↓
|
|
managed read performs no lock lookup
|
|
↓
|
|
direct accessor read
|
|
```
|
|
|
|
For example:
|
|
|
|
```cpp
|
|
struct Device : Property_Object<Device> {
|
|
int id{1};
|
|
int value{0};
|
|
};
|
|
template <>
|
|
struct structive::Type_Descriptor<Device> {
|
|
static auto get() {
|
|
return object<Device>(
|
|
synchronization(sync_all_shared),
|
|
field<&Device::id>(key<"id">, read_only),
|
|
field<&Device::value>(key<"value">)
|
|
);
|
|
}
|
|
};
|
|
```
|
|
|
|
`id` resolves to `unsynchronized_slot`. Only `value` contributes a mutex to the managed object.
|
|
|
|
This relies on the Structive managed contract. If another thread deliberately writes `device.id` through raw C++ access while a managed read is occurring, that code has bypassed Structive and owns the resulting synchronization responsibility.
|
|
|
|
## Computed read-only properties
|
|
|
|
A computed property is usually intrinsically read-only, but it may read writable dependencies.
|
|
|
|
```cpp
|
|
computed_property<Device, int>([](const auto& view) {
|
|
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
|
}, key<"speed_span">)
|
|
```
|
|
|
|
The computed value has no writable storage of its own. Its synchronized view protects the consistency domain of writable dependencies. Read-only stored dependencies can be read directly because they cannot change through the managed path.
|
|
|
|
Writable dependencies that must form one snapshot should be placed in the same synchronization group as the computed property.
|
|
|
|
## Managed access and raw access
|
|
|
|
These operations are deliberately different:
|
|
|
|
```cpp
|
|
device.temperature = 30.0;
|
|
device.write<&Device::temperature>(30.0);
|
|
```
|
|
|
|
The first is raw C++ access. The second is the managed Structive path.
|
|
|
|
Managed access provides the behavior described by the schema, including intrinsic capability checks and synchronization. Raw access bypasses that behavior.
|
|
|
|
Structive follows a cooperative model: it helps correct code express and use structure efficiently; it does not attempt to stop code that intentionally bypasses the system.
|
|
|
|
## Schema and managed object are separate
|
|
|
|
`Type_Descriptor<T>` describes the type. `Property_Object<T>` adds per-instance managed behavior.
|
|
|
|
The default synchronization topology is resolved once per type. Instances do not keep a per-property vector for the default layout. An instance only stores real mutex state required by writable synchronization domains and a compact override layout when that instance explicitly supplies `Property_Synchronization`.
|
|
|
|
`No_Lock_Policy` removes real mutex storage entirely.
|
|
|
|
## Unified Attribute model
|
|
|
|
Core and extensions use one Attribute protocol.
|
|
|
|
```cpp
|
|
struct Label_Category {};
|
|
template <Fixed_String Value>
|
|
struct Label_Attribute {
|
|
using attribute_category = Label_Category;
|
|
static constexpr bool single_valued = true;
|
|
static constexpr bool inheritable = false;
|
|
static constexpr auto value = Value;
|
|
};
|
|
```
|
|
|
|
An extension can attach its metadata to the same descriptor:
|
|
|
|
```cpp
|
|
field<&Device::temperature>(
|
|
key<"temperature">,
|
|
presentation::label<"Temperature">
|
|
)
|
|
```
|
|
|
|
Core stores extension metadata but does not interpret extension-owned categories.
|
|
|
|
## Validation is explicit
|
|
|
|
Constraints are metadata. `write()` does not automatically execute them.
|
|
|
|
```cpp
|
|
auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0);
|
|
```
|
|
|
|
Validation, transactions, rollback and synchronization are separate concerns.
|
|
|
|
## Synchronization
|
|
|
|
Synchronization is a topology layer, not access control. It defines which intrinsically mutable properties share a managed consistency domain. Stored read-only properties are removed before lock slots are created, so metadata directly reduces runtime synchronization cost.
|
|
|
|
Structive provides three defaults:
|
|
|
|
```cpp
|
|
sync_all_independent
|
|
sync_all_shared
|
|
sync_all_unsynchronized
|
|
```
|
|
|
|
and typed or runtime-key overrides/groups:
|
|
|
|
```cpp
|
|
synchronization(
|
|
sync_all_independent,
|
|
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
|
)
|
|
```
|
|
|
|
The several entry forms are intentional: compile-time member rules serve typed C++ code, runtime-key plans serve adapters, per-instance overrides serve exceptional objects, and guards express temporary multi-property consistency. They all resolve to the same compact lock-slot model.
|
|
|
|
Multi-property guards deduplicate lock domains and acquire them in stable slot order:
|
|
|
|
```cpp
|
|
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
|
|
auto old_min = guard.get<&Device::min_speed>();
|
|
auto old_max = guard.get<"maximum_speed">();
|
|
guard.set<"minimum_speed">(20);
|
|
guard.set<&Device::max_speed>(120);
|
|
```
|
|
|
|
Typed guard capability is constrained at compile time. Runtime-key guards validate keys and capability at runtime. See [Core Guide: Synchronization](docs/CORE_GUIDE.md#14-synchronization-plans) and `core/tests/synchronization_test.cpp` for topology, validation, blocking and lock-order coverage.
|
|
|
|
## Runtime access
|
|
|
|
`Property_Object_Base` is deliberately a low-level type-erased adapter boundary for code that discovers keys only at runtime:
|
|
|
|
```cpp
|
|
Property_Object_Base& erased = device;
|
|
auto type = erased.runtime_object_type();
|
|
auto count = erased.runtime_property_count();
|
|
```
|
|
|
|
Runtime operations are key based:
|
|
|
|
```text
|
|
runtime_read(key, context, callback)
|
|
runtime_write(key, type_info, value)
|
|
```
|
|
|
|
They return `ok`, `unknown_property`, `not_readable`, `not_writable` or `type_mismatch`. Runtime write requires an exact type match and performs no implicit conversion. The read callback receives a borrowed pointer that is valid only during the callback; synchronized writable state remains read-locked while the callback executes. Stored read-only properties retain the same zero-lock fast path as typed reads.
|
|
|
|
Core intentionally does not impose `variant`, `any`, conversion registries or serialization ownership on this boundary. Higher-level adapters may wrap it. There is no runtime access mode or access-control policy: an external system decides what it exposes, while Structive reports only intrinsic property capability. See [Core Guide: Runtime access](docs/CORE_GUIDE.md#22-runtime-type-erased-access) and `core/tests/runtime_api_test.cpp`.
|
|
|
|
## Current Core metadata
|
|
|
|
Core currently defines:
|
|
|
|
- `key<"...">`
|
|
- `read_only`
|
|
- `write_only`
|
|
- `read_write`
|
|
- `inaccessible`
|
|
- `unit<"...">`
|
|
- `sensitive<>`
|
|
- `min_value<...>`
|
|
- `max_value<...>`
|
|
- `finite`
|
|
- `constraint<"code">(...)`
|
|
|
|
`read_only`, `write_only`, `read_write` and `inaccessible` describe the property itself. They are not access-control policies.
|
|
|
|
## Current extension metadata
|
|
|
|
The presentation extension defines:
|
|
|
|
- `presentation::label<"...">`
|
|
- `presentation::description<"...">`
|
|
- `presentation::group<"...">`
|
|
- `presentation::order<N>`
|
|
|
|
A presentation consumer may independently decide whether a property should be visible or editable. That policy is outside Property Core.
|
|
|
|
## Build
|
|
|
|
```cmake
|
|
add_subdirectory(path/to/Structive)
|
|
target_link_libraries(my_target PRIVATE structive::property_core)
|
|
```
|
|
|
|
For linked extensions:
|
|
|
|
```cmake
|
|
target_link_libraries(my_target PRIVATE structive::property_extensions)
|
|
```
|
|
|
|
Build and test:
|
|
|
|
```bash
|
|
cmake -S . -B build -DBUILD_TESTING=ON
|
|
cmake --build build
|
|
ctest --test-dir build --output-on-failure
|
|
```
|
|
|
|
## Documentation
|
|
|
|
- [Design Philosophy and Principles](docs/DESIGN.md)
|
|
- [Core Guide](docs/CORE_GUIDE.md)
|
|
- [Extension Architecture](docs/EXTENSIONS.md)
|
|
- [中文首页](README.zh-CN.md)
|
|
- [设计理念与原则(中文)](docs/DESIGN.zh-CN.md)
|
|
- [核心使用指南(中文)](docs/CORE_GUIDE.zh-CN.md)
|
|
- [扩展体系(中文)](docs/EXTENSIONS.zh-CN.md)
|