7.9 KiB
name, description
| name | description |
|---|---|
| structive-development | Use when defining, extending, modifying, debugging, or reviewing Structive property schemas, Type_Descriptor, Accessors, Attributes, Constraints, managed read/write, guards, synchronization topology, runtime type-erased access, Presentation/extensions, tests, CMake, install/export, or when integrating a C++ type with Structive. |
Structive Development
Goal
Express structural facts about ordinary C++ objects without replacing the native C++ model. Keep compile-time facts compile-time, keep external policy outside Core, and make managed synchronization pay only for mutable state that actually needs it.
Read before editing
Use these as the source of truth:
AGENTS.md.docs/DESIGN.zh-CN.mdordocs/DESIGN.mdfor architecture.docs/CORE_GUIDE.zh-CN.mdfor Core API usage.docs/EXTENSIONS.zh-CN.mdfor extension ownership.- The focused header and tests for the feature being changed.
When Structive is embedded under Adminive, also respect the parent Adminive instructions, but this nested file owns Structive-specific decisions.
Define a described object
Keep fields as ordinary C++ members and inherit Property_Object<T> only for managed APIs:
struct Device : structive::Property_Object<Device> {
double temperature{};
int serial_number{};
};
template <>
struct structive::Type_Descriptor<Device> {
static auto get() {
using namespace structive;
return object<Device>(
field<&Device::temperature>(key<"temperature">),
field<&Device::serial_number>(key<"serial_number">, read_only)
);
}
};
Do not wrap each member in a new storage type just to make it participate in Structive.
Choose the correct mechanism
| Requirement | Mechanism |
|---|---|
| Register a stored member | field<&T::member>(...) |
| Stable runtime/schema name | key<"name"> |
| Narrow intrinsic capability | read_only or capability metadata supported by the descriptor |
| Unit/sensitive/custom structural metadata | Attribute |
| Reusable object-level inheritable metadata | defaults(...) with an inheritable Attribute |
| Candidate value validity | Constraint + explicit validation helper |
| Managed typed read/write | Property_Object::read / write |
| Atomic multi-property consistency | shared synchronization group + typed guard |
| Runtime-selected property access | Property_Object_Base::runtime_read/runtime_write |
| UI/persistence/RPC-specific meaning | Extension-owned Attribute/logic, not Core |
Managed access rules
Prefer typed member/key access in normal C++ code:
auto value = device.read<&Device::temperature>();
device.write<&Device::temperature>(42.0);
Use guards for a consistent multi-property operation:
auto guard = device.lock_unique<&Device::minimum, &Device::maximum>();
guard.set<&Device::minimum>(20);
guard.set<&Device::maximum>(120);
Do not replace direct raw C++ access. Raw access intentionally bypasses the managed contract and is the caller's responsibility.
Synchronization workflow
Before changing synchronization code, preserve these invariants:
- Only intrinsically writable stored properties contribute mutable lock domains.
- Stored read-only properties resolve to
unsynchronized_slotand managed reads do not acquire a lock. independent,shared,unsynchronized, groups, compile-time member rules, runtime-key rules, and per-instance overrides all resolve through one lock-slot topology.- Multiple locks are acquired in stable order.
No_Lock_Policyremoves real mutex storage/locking work instead of simulating mutexes.- Computed read-only properties may require a synchronized dependency view when they derive from mutable state; do not give their immutable storage a fake lock.
Synchronization protects in-process consistency only. Do not add transaction or authorization semantics here.
Runtime adapter workflow
Runtime access is a dynamic bridge. It must keep exact type semantics:
- Unknown key -> the appropriate not-found result.
- Intrinsically unreadable/unwritable -> capability result.
- Runtime type mismatch -> type-mismatch result.
- Intrinsically writable but not copy-writable ->
unsupported_runtime_write. - Successful runtime write reuses the managed synchronization path.
runtime_write copies from a const type-erased input. Do not steal/move from that pointer to make move-only types appear runtime-copy-writable. Typed write remains the path for move-only values.
Attribute and Extension workflow
Core owns the generic Attribute protocol; the domain that defines a category owns its interpretation.
For presentation metadata, use the extension:
field<&Device::temperature>(
key<"temperature">,
structive::presentation::label<"Temperature">,
structive::presentation::description<"Current temperature">,
structive::presentation::group<"Environment">,
structive::presentation::order<1>
)
Read it through presentation::describe<...>(schema). Presentation fallback from missing label to property key belongs to the extension, not Core.
For a new metadata domain, create a category and Attribute in the extension. Do not add an enum/switch to Core merely because an adapter wants a new hint.
Schema and compile-time contracts
Structural mistakes should fail during compilation/configuration whenever they are statically knowable. Existing compile-fail coverage includes duplicate keys, duplicate storage, missing keys, capability/accessor mismatch, incompatible constraints, and foreign synchronization members.
When adding another compile-time invariant:
- Express it in concepts/
requires/static_assertat the schema boundary. - Add a source under
core/tests/compile_fail/. - Register it with the existing
structive_expect_compile_failuremechanism. - Do not replace compile-time rejection with a runtime error for typed APIs.
Empty schemas are valid. Do not reintroduce pack/CTAD assumptions that require at least one property unless the specific API semantically requires a non-empty set.
Testing workflow
Use focused targets first:
- Property/schema/attribute/validation:
structive_property_core_test. - Runtime type-erased API:
structive_property_runtime_api_test. - Synchronization/guard/concurrency:
structive_property_synchronization_test. - Presentation extension:
structive_property_extensions_test. - Public headers:
structive_header_*tests. - Installed consumer:
structive_install_consumer_test.
Then run the standalone suite:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DSTRUCTIVE_BUILD_TESTS=ON -DSTRUCTIVE_INSTALL=ON
cmake --build build
ctest --test-dir build --output-on-failure
For synchronization changes, run TSan when the compiler/platform supports it. For Core public API changes, run Debug and Release and then verify the Adminive consumer suite.
CMake/install rules
- Keep
structive::property_coreheader-only andstructive::property_extensionsas the extension target unless a deliberate design change says otherwise. - Examples/tests default based on top-level use; embedding Structive as a subdirectory must not force examples into the parent build.
- Standalone install/export must continue to support
find_package(Structive CONFIG REQUIRED COMPONENTS Core Extensions). - Use paths relative to the Structive CMake file that owns them.
Review checklist
Before handoff, verify:
- No second object model or external policy leaked into Core.
- Intrinsic capability still matches Accessor capability.
- Stored read-only properties still contribute zero lock slots.
- Move-only typed write and runtime copy-write semantics remain distinct.
- Validation is still explicit.
- Extension metadata uses the shared Attribute protocol and Core does not interpret it.
- New static invariants have compile-fail coverage.
- Public headers compile alone and the standalone install consumer works.
- Adminive still builds/tests if the public Structive contract changed.
- Only checks actually run are reported as passing.