From 3205dd84e867ac09fe16785da4bbee5b2d9492ec Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 7 Aug 2026 17:50:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E6=89=80=E6=9C=89=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E7=BA=A7=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 334 ++++----- README.zh-CN.md | 354 +++++----- core/README.md | 13 +- core/README.zh-CN.md | 13 +- core/example/main.cpp | 40 +- .../include/structive/property/attributes.hpp | 36 +- .../include/structive/property/descriptor.hpp | 20 +- .../structive/property/property_object.hpp | 428 ++++-------- core/include/structive/property/schema.hpp | 55 +- core/tests/property_core_test.cpp | 187 ++++-- docs/CORE_GUIDE.md | 631 ++++++++---------- docs/CORE_GUIDE.zh-CN.md | 625 ++++++++--------- docs/DESIGN.md | 565 ++++++++-------- docs/DESIGN.zh-CN.md | 569 ++++++++-------- docs/EXTENSIONS.md | 315 ++++----- docs/EXTENSIONS.zh-CN.md | 313 ++++----- extensions/tests/presentation_test.cpp | 12 +- 17 files changed, 2086 insertions(+), 2424 deletions(-) diff --git a/README.md b/README.md index 470dc0d..d0af72a 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,47 @@ # Structive -**Structive enhances ordinary C++ structs with an explicit structural metadata and managed-property layer without replacing their native data model.** +**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 property and structural-description system built around two deliberately separate layers: +Structive is a C++20 structural property system built around two separate layers: ```text C++ object model - ordinary members, member functions, native layout and direct access + ordinary members and member functions │ ├── Type_Descriptor → Object_Schema - │ compile-time structure, keys, attributes, constraints, access capabilities + │ type-level structure, keys, intrinsic capabilities, + │ attributes, constraints and synchronization description │ └── Property_Object - per-instance managed access, synchronization, traversal and runtime access + instance-level managed read/write, synchronization, + traversal and type-erased runtime access ``` -A type can remain recognizably ordinary C++: +A type remains ordinary C++: ```cpp #include using namespace structive; struct Device : Property_Object { double temperature{25.0}; - double pressure{101.3}; + int serial_number{1001}; }; template <> struct structive::Type_Descriptor { static auto get() { return object( - defaults( - external_access, - persistence_access - ), - field<&Device::temperature>( - key<"temperature">, - unit<"C">, - min_value<-50.0>, - max_value<200.0> - ), - field<&Device::pressure>( - key<"pressure">, - unit<"kPa"> - ) + field<&Device::temperature>(key<"temperature">, unit<"C">), + field<&Device::serial_number>(key<"serial_number">, read_only) ); } }; ``` -The members are still real members. Structive adds a second, explicit layer that generic systems can understand. +The members are still real members. Structive only adds explicit structural meaning around them. ## Core idea @@ -59,77 +49,172 @@ Structive follows one central rule: > **Enhance the struct; do not replace the struct.** -This has several consequences: +That means: -- A registered field remains an ordinary C++ member. -- Unregistered members remain completely outside the property system. -- Member pointers are the preferred compile-time identity for business code. -- String keys exist for runtime and adapter boundaries. -- Metadata does not force storage wrappers such as `Property` into every field. -- Validation, synchronization, persistence capability and presentation metadata remain separate concerns. +- 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`; +- 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. -The result is intended to let the same C++ type participate in UI, persistence, serialization, RPC or tooling layers without making the core object depend on those systems. +## No access-control subsystem -## Project layers +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 -core/ -└── structive::property_core - ├── INTERFACE target - ├── Type_Descriptor and Object_Schema - ├── member/computed accessors - ├── unified Attribute protocol - ├── constraints and explicit validation - ├── access capability metadata - ├── synchronization plans and guards - ├── typed and runtime access - └── traversal - -extensions/ -└── structive::property_extensions - ├── STATIC target - ├── depends on property_core - ├── defines extension-owned Attribute categories - └── currently provides presentation metadata interpretation +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? ``` -The dependency direction is one-way: **extensions depend on core; core never includes or links extensions.** +## Intrinsic property capability -## Why not `Property` members? +Every property has one intrinsic capability: -Structive intentionally does not require this: +```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 -struct Device { - Property temperature; -}; +field<&Device::serial_number>( + key<"serial_number">, + read_only +) ``` -Instead, the storage stays native: +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 { - double temperature; + int id{1}; + int value{0}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + synchronization(sync_all_shared), + field<&Device::id>(key<"id">, read_only), + field<&Device::value>(key<"value">) + ); + } }; ``` -and the structural meaning is declared separately with `Type_Descriptor`. +`id` resolves to `unsynchronized_slot`. Only `value` contributes a mutex to the managed object. -This preserves normal C++ member semantics, keeps raw object access available when it is intentionally needed, and lets Structive remain an enhancement layer rather than a replacement object model. +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. -## Schema and managed object are different concepts +## Computed read-only properties -`Type_Descriptor` describes the **type**. `Property_Object` adds state and behavior to an **instance**. +A computed property is usually intrinsically read-only, but it may read writable dependencies. -The schema contains the registered property tuple, object defaults and the default synchronization plan. `Property_Object` shares one resolved default lock topology per type and keeps only instance synchronization state that is actually required: real mutex storage for locking policies and a compact override layout only when an instance explicitly supplies `Property_Synchronization`. +```cpp +computed_property([](const auto& view) { + return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); +}, key<"speed_span">) +``` -This distinction matters for performance and architecture: structural description is type-level information; managed synchronization is instance-level state. +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` describes the type. `Property_Object` 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 -There is one Attribute protocol. Core metadata and extension metadata use the same mechanism. - -A single-valued Attribute category can be defined as: +Core and extensions use one Attribute protocol. ```cpp struct Label_Category {}; @@ -142,7 +227,7 @@ struct Label_Attribute { }; ``` -Then it can be attached directly to a property: +An extension can attach its metadata to the same descriptor: ```cpp field<&Device::temperature>( @@ -151,69 +236,21 @@ field<&Device::temperature>( ) ``` -Core stores and traverses the Attribute but does not interpret categories it does not own. The owning extension interprets its own category. - -This is the primary extension boundary of Structive. - -## Managed access is not forced encapsulation - -Both of these are valid but mean different things: - -```cpp -device.temperature = 30.0; -device.write<&Device::temperature>(30.0); -``` - -The first is the **raw C++ path**. It bypasses Structive-managed synchronization, access capabilities and other managed behavior. - -The second is the **managed path**. It resolves the registered property and uses the configured synchronization behavior. - -Structive intentionally keeps both. A codebase should choose the appropriate path according to its ownership and concurrency rules. - -## Intrinsic capability and boundary projections - -A property first has an **intrinsic capability** derived from its accessor: - -- readable if the accessor can read it; -- writable if the accessor can write it. - -Normal managed application code uses that intrinsic capability directly: - -```cpp -device.write<&Device::temperature>(30.0); -auto value = device.read<&Device::temperature>(); -``` - -Core then defines two boundary projections over the same property definition: - -- `external`: controlled by `External_Access` metadata; -- `persistence`: controlled by `Persistence_Access` metadata. - -```cpp -device.external().write<&Device::temperature>(31.0); -device.persistence().load<&Device::temperature>(32.0); -auto stored = device.persistence().store<&Device::temperature>(); -``` - -There is no separate `internal` capability mode. The default managed API is the intrinsic property capability itself. `Managed_Access_Mode` therefore identifies only boundary projections. A projection can only narrow abilities that the accessor actually provides; it cannot make an intrinsically unreadable or unwritable property readable or writable. +Core stores extension metadata but does not interpret extension-owned categories. ## Validation is explicit -Constraints are metadata. `write()` does **not** automatically execute them. +Constraints are metadata. `write()` does not automatically execute them. ```cpp auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0); -if (error) { - // error->property_key - // error->code -} ``` -This is intentional. Field validation, cross-field invariants, transactions and rollback are different concerns and should not be hidden inside a generic setter. +Validation, transactions, rollback and synchronization are separate concerns. -## Synchronization is explicit and composable +## Synchronization -The default modes are: +Structive provides: ```cpp sync_all_independent @@ -221,7 +258,7 @@ sync_all_shared sync_all_unsynchronized ``` -Properties may then be overridden or grouped: +and per-property/group rules: ```cpp synchronization( @@ -230,33 +267,21 @@ synchronization( ) ``` -A group means those properties resolve to the same lock slot. Multi-property guards deduplicate lock slots and acquire them in stable order. +Only properties that require synchronization contribute lock slots. Stored read-only properties are removed from the resolved lock topology even if a broad default rule would otherwise include them. + +Multi-property guards deduplicate lock slots and acquire them in stable order: ```cpp auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); auto old_min = guard.get<&Device::min_speed>(); -guard.set<&Device::min_speed>(20.0); +guard.set<&Device::min_speed>(20); ``` -Synchronization does not imply validation, transaction, rollback or event emission. - -## Computed properties - -Structive supports synchronized computed properties: - -```cpp -computed_property([](const auto& view) { - return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); -}, key<"speed_span">, external_access) -``` - -A synchronized computed property reads dependencies through the synchronization view. Those dependencies must resolve to the same synchronization slot as the computed property. This makes the dependency relationship explicit in the synchronization plan. - -Advanced trusted accessor forms also exist for member-function getters and getter/setter pairs. They intentionally operate through trusted object access and therefore should be used only when their synchronization semantics are understood by the caller. +A typed unique guard can only be requested for intrinsically writable properties. ## Runtime access -`Property_Object_Base` provides type-erased runtime access for adapter-style code: +`Property_Object_Base` exposes only intrinsic dynamic access: ```cpp Property_Object_Base& erased = device; @@ -264,7 +289,14 @@ auto type = erased.runtime_object_type(); auto count = erased.runtime_property_count(); ``` -Runtime reads and writes use property keys, access modes and `std::type_info`, and report one of: +Runtime operations are key based: + +```text +runtime_read(key, ...) +runtime_write(key, type_info, value) +``` + +They return: ```text ok @@ -274,15 +306,17 @@ not_writable type_mismatch ``` -This path is intended for boundaries such as generic serialization, HTTP/RPC adapters, scripting bridges or tooling. Compile-time business code should generally prefer member pointers. +There is no runtime access mode. An external adapter decides whether it should call `runtime_read` or `runtime_write` for a given property. -## Current core-owned metadata +## Current Core metadata -The current core defines: +Core currently defines: - `key<"...">` -- `external_access<...>` -- `persistence_access<...>` +- `read_only` +- `write_only` +- `read_write` +- `inaccessible` - `unit<"...">` - `sensitive<>` - `min_value<...>` @@ -290,23 +324,21 @@ The current core defines: - `finite` - `constraint<"code">(...)` -`external_access`, `persistence_access` and `sensitive` are inheritable and can be supplied through `defaults(...)`. Property-level declarations override object defaults for the same category. +`read_only`, `write_only`, `read_write` and `inaccessible` describe the property itself. They are not access-control policies. ## Current extension metadata -The presentation extension currently defines: +The presentation extension defines: - `presentation::label<"...">` - `presentation::description<"...">` - `presentation::group<"...">` - `presentation::order` -`presentation::describe()` interprets those attributes and falls back to the property key when no label is declared. That fallback belongs to the presentation extension, not to Property Core. +A presentation consumer may independently decide whether a property should be visible or editable. That policy is outside Property Core. ## Build -Structive currently exposes CMake targets for use through the project tree: - ```cmake add_subdirectory(path/to/Structive) target_link_libraries(my_target PRIVATE structive::property_core) @@ -318,7 +350,7 @@ For linked extensions: target_link_libraries(my_target PRIVATE structive::property_extensions) ``` -Build and test the repository with: +Build and test: ```bash cmake -S . -B build -DBUILD_TESTING=ON diff --git a/README.zh-CN.md b/README.zh-CN.md index 0a9f02e..fcb3bd8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,135 +1,226 @@ # Structive -**Structive 的目标是在不替换 C++ 原生数据模型的前提下,为普通 `struct` 增加显式的结构描述、属性元数据和受管理访问能力。** +**Structive 在不替代 C++ 原生数据模型的前提下,为普通 struct 增加显式结构元数据和受管理属性行为。** -[English](README.md) · [设计理念](docs/DESIGN.zh-CN.md) · [核心指南](docs/CORE_GUIDE.zh-CN.md) · [扩展体系](docs/EXTENSIONS.zh-CN.md) +[English](README.md) · [设计理念](docs/DESIGN.zh-CN.md) · [Core 指南](docs/CORE_GUIDE.zh-CN.md) · [Extension 指南](docs/EXTENSIONS.zh-CN.md) ## Structive 是什么 -Structive 是一个 C++20 属性与结构描述系统。它刻意把“类型结构描述”和“对象实例管理”分成两层: +Structive 是一个 C++20 结构属性系统,明确分成两层: ```text -C++ 原生对象模型 - 普通成员、成员函数、原生布局、直接访问 +C++ 对象模型 + 普通成员、成员函数和原生布局 │ ├── Type_Descriptor → Object_Schema - │ 类型级结构、key、Attribute、约束、访问能力 + │ 类型级结构、key、属性固有能力、Attribute、Constraint、同步描述 │ └── Property_Object - 实例级受管理访问、同步、遍历、运行时访问 + 实例级 managed read/write、同步、遍历和 runtime access ``` -业务类型仍然可以保持非常普通: +类型本身仍然是普通 C++: ```cpp #include using namespace structive; struct Device : Property_Object { double temperature{25.0}; - double pressure{101.3}; + int serial_number{1001}; }; template <> struct structive::Type_Descriptor { static auto get() { return object( - defaults( - external_access, - persistence_access - ), - field<&Device::temperature>( - key<"temperature">, - unit<"C">, - min_value<-50.0>, - max_value<200.0> - ), - field<&Device::pressure>( - key<"pressure">, - unit<"kPa"> - ) + field<&Device::temperature>(key<"temperature">, unit<"C">), + field<&Device::serial_number>(key<"serial_number">, read_only) ); } }; ``` -成员仍然是真实的 C++ 成员。Structive 只是额外建立一层可以被泛型系统理解的结构语义。 +成员仍是真实成员。Structive 只在它们旁边增加一层显式结构语义。 ## 核心思想 -Structive 最核心的原则只有一句: +Structive 的第一原则: > **增强 struct,而不是替代 struct。** -因此它坚持: +因此: -- 注册后的字段仍然是普通 C++ 成员。 -- 未注册成员完全不进入属性系统。 -- 业务代码优先使用成员指针作为编译期属性身份。 -- 字符串 `key` 主要服务运行时和适配器边界。 -- 不要求把每个成员改造成 `Property` 之类的包装类型。 -- Validation、Synchronization、Persistence、Presentation 等语义相互分离。 +- 注册字段仍然是普通 C++ 成员; +- 未注册成员完全不进入 Structive; +- C++ 业务代码优先使用 member pointer 作为编译期身份; +- string key 用于动态系统和 adapter 边界; +- 不要求把字段替换成 `Property` 包装器; +- 保留 raw C++ access; +- Structive 不试图给 public member 建立安全边界; +- 外部系统是否暴露、允许读还是允许写,由外部系统自己决定; +- Core 只描述属性本身固有能做什么。 -这样,同一个业务结构体可以被 UI、持久化、序列化、RPC、脚本或工具系统理解,但业务类型本身不需要依赖这些系统。 +## Core 完全不做访问控制 -## 项目分层 +Structive 不再内建 `internal`、`external`、`persistence`、role、context 或 policy 访问模式。 + +Core 不暴露领域专用访问 View、权限枚举或持久化专用访问模式。 + +GUI、RPC、序列化器、插件系统、持久化系统都自己决定: ```text -core/ -└── structive::property_core - ├── INTERFACE target - ├── Type_Descriptor / Object_Schema - ├── member / computed accessor - ├── 统一 Attribute 协议 - ├── constraint 与显式 validation - ├── access capability 元数据 - ├── synchronization plan 与 guard - ├── typed / runtime access - └── traversal - -extensions/ -└── structive::property_extensions - ├── STATIC target - ├── 只依赖 property_core - ├── 定义扩展自己拥有的 Attribute category - └── 当前实现 presentation 元数据解释 +我要不要暴露这个属性? +我要不要允许用户修改? +我要不要保存它? ``` -依赖方向必须保持单向:**Extension 依赖 Core;Core 永远不 include、不 link Extension。** +Structive Core 只回答结构事实: -## 为什么不使用 `Property` 成员 +```text +这个属性自身能不能读? +这个属性自身能不能写? +它的 key 是什么? +它有哪些 Attribute 和 Constraint? +managed access 是否需要同步? +``` -Structive 不要求这样定义对象: +## 属性自身的 Intrinsic Capability + +每个 Property 只有一套固有能力: + +```text +none +read +write +read_write +``` + +正常情况下由 Accessor 自动推导。普通可写成员天然是 `read_write`,getter-only computed property 天然是 `read`。 + +Schema 可以显式收窄能力: ```cpp -struct Device { - Property temperature; -}; +field<&Device::serial_number>( + key<"serial_number">, + read_only +) ``` -而是保留原生存储: +预定义能力 Attribute: + +```cpp +read_only +write_only +read_write +inaccessible +``` + +这些是**属性自身契约**,不是用户权限。 + +只读属性可以: + +```cpp +auto id = device.read<&Device::serial_number>(); +``` + +但: + +```cpp +device.write<&Device::serial_number>(1002); +``` + +在编译期就不可用。 + +如果 C++ 成员本身是 public,raw path 仍然可以: + +```cpp +device.serial_number = 1002; +``` + +这代表调用方主动绕过 Structive,同时也绕过 Structive 的同步保证。Structive 采用“君子不防小人”的协作模型,不把自己伪装成 C++ 内存保护机制。 + +## Read-Only 必须带来真正的优化 + +Property metadata 不只是文档,而应该影响实现。 + +一个 intrinsic read-only 的**存储属性**不会进入同步拓扑: + +```text +read-only stored property + ↓ +不分配 lock slot + ↓ +不贡献 mutex + ↓ +managed read 不查询 slot + ↓ +不构造 shared_lock + ↓ +直接执行 accessor.read() +``` + +例如: ```cpp struct Device : Property_Object { - double temperature; + int id{1}; + int value{0}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + synchronization(sync_all_shared), + field<&Device::id>(key<"id">, read_only), + field<&Device::value>(key<"value">) + ); + } }; ``` -然后通过 `Type_Descriptor` 单独声明结构语义。 +即使默认是 `sync_all_shared`,`id` 仍然固定解析成 `unsynchronized_slot`。只有 `value` 会为对象贡献 mutex。 -这样可以保留真实成员指针、原生成员语义和必要时的直接访问能力。Structive 是增强层,不建立第二套替代 C++ 的对象模型。 +前提是调用方遵守 managed contract。如果另一个线程直接写 `device.id`,那么它已经绕过 Structive,相关 data race 由调用方负责。 -## Schema 和 Managed Object 是两种概念 +## Computed Read-Only Property -`Type_Descriptor` 描述的是**类型**;`Property_Object` 管理的是**实例**。 +Computed Property 通常自身不可写,但它可能依赖可写字段: -Schema 保存注册属性列表、对象默认 Attribute 和默认同步计划。`Property_Object` 对同一类型共享一份解析后的默认 lock topology;实例只保留真正需要的同步状态:真实锁策略需要 mutex storage,只有显式传入 `Property_Synchronization` 的实例才额外持有紧凑的覆盖布局。 +```cpp +computed_property([](const auto& view) { + return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); +}, key<"speed_span">) +``` -这个边界必须长期保持:结构描述属于类型级;同步状态属于实例级。 +Computed value 本身没有可写存储。它的 synchronized view 保护的是**可写依赖的一致性域**。 -## 只有一套 Attribute 协议 +- read-only 存储依赖可以直接读取,不需要锁; +- writable 依赖如果需要同一快照,应与 computed property 放在同一个 synchronization group; +- computed property 的同步语义不是“给只读值创建一个成员锁”,而是描述其依赖一致性边界。 -Core Attribute 和 Extension Attribute 使用同一机制,不存在额外的 `hint` 通道。 +## Managed Access 与 Raw Access -例如一个单值 Attribute category 可以这样定义: +下面两句语义不同: + +```cpp +device.temperature = 30.0; +device.write<&Device::temperature>(30.0); +``` + +第一句是 raw C++ path,第二句是 Structive managed path。 + +Managed path 使用 Schema 描述的 intrinsic capability 和 synchronization。Raw path 完全绕过这些行为。 + +## Schema 与 Managed Object 分层 + +`Type_Descriptor` 描述类型,`Property_Object` 给实例增加 managed behavior。 + +默认同步拓扑每个类型只解析并共享一次。实例不再保存默认的 per-property `vector`。实例只保存真正需要的 mutex storage;只有显式传入 `Property_Synchronization` 时才保存紧凑的实例级覆盖布局。 + +`No_Lock_Policy` 完全不保存真实 mutex。 + +## 统一 Attribute 模型 + +Core 和 Extension 使用同一套 Attribute 协议: ```cpp struct Label_Category {}; @@ -142,7 +233,7 @@ struct Label_Attribute { }; ``` -然后直接挂到属性上: +Extension metadata 可以直接挂在 Property 上: ```cpp field<&Device::temperature>( @@ -151,69 +242,21 @@ field<&Device::temperature>( ) ``` -Core 负责统一保存和遍历,但只解释自己拥有的 category。Presentation、JSON、RPC 等扩展应该自己解释自己的 category。 - -这是 Structive 最主要的扩展边界。 - -## Managed Access 不是强制封装 - -下面两种写法都合法,但语义不同: - -```cpp -device.temperature = 30.0; -device.write<&Device::temperature>(30.0); -``` - -第一种是 **raw C++ path**,不会经过 Structive 的同步和 capability 管理。 - -第二种是 **managed path**,会定位注册属性并按该对象实例的同步计划执行。 - -Structive 有意保留两条路径。业务代码应该根据对象所有权和并发规则选择,而不是假装所有成员访问都能被框架强制拦截。 - -## 固有能力与边界投影 - -一个 Property 首先拥有由 accessor 自身决定的 **固有能力(intrinsic capability)**: - -- accessor 能读,则 Property intrinsically readable; -- accessor 能写,则 Property intrinsically writable。 - -应用内部的普通 managed code 直接使用这套固有能力: - -```cpp -device.write<&Device::temperature>(30.0); -auto value = device.read<&Device::temperature>(); -``` - -Core 在同一份 Property definition 之上只定义两种边界投影: - -- `external`:由 `External_Access` 元数据控制; -- `persistence`:由 `Persistence_Access` 元数据控制。 - -```cpp -device.external().write<&Device::temperature>(31.0); -device.persistence().load<&Device::temperature>(32.0); -auto stored = device.persistence().store<&Device::temperature>(); -``` - -Structive 不再定义单独的 `internal` capability mode。默认 managed API 本身就是 Property 的固有能力,因此 `Managed_Access_Mode` 只用于标识边界投影。投影只能收窄 accessor 原本具备的能力,不能把 intrinsically 不可读或不可写的 Property 变成可读或可写。 +Core 负责保存和遍历,但不解释 Extension 自己拥有的 category。 ## Validation 必须显式 -Constraint 是结构元数据;`write()` **不会自动执行 constraint**。 +Constraint 是元数据,`write()` 不自动执行 validation: ```cpp auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0); -if (error) { - // error->property_key - // error->code -} ``` -这是设计选择,不是缺功能。单字段校验、跨字段不变量、事务和回滚是不同概念,不应该偷偷塞进一个通用 setter。 +Validation、transaction、rollback、synchronization 是不同问题,不隐藏在一个 setter 里。 -## Synchronization 明确且可组合 +## Synchronization -默认同步模式包括: +Structive 提供: ```cpp sync_all_independent @@ -221,7 +264,7 @@ sync_all_shared sync_all_unsynchronized ``` -也可以覆盖单个字段或建立同步组: +以及 Property 和 group 规则: ```cpp synchronization( @@ -230,33 +273,21 @@ synchronization( ) ``` -同一 group 的属性会解析到同一个 lock slot。多属性 Guard 会对 lock slot 去重,并按稳定顺序获取锁。 +只有真正需要同步的 Property 才进入最终 lock topology。read-only 存储属性即使被宽泛默认规则覆盖,也会在 resolve 阶段被裁掉。 + +多属性 Guard 对 slot 去重,并使用稳定顺序获取锁: ```cpp auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); auto old_min = guard.get<&Device::min_speed>(); -guard.set<&Device::min_speed>(20.0); +guard.set<&Device::min_speed>(20); ``` -Synchronization 只负责同步,不自动等价于 validation、transaction、rollback 或事件系统。 +typed `lock_unique` 只能用于 intrinsically writable property。 -## Computed Property +## Runtime Access -Structive 支持基于同步视图读取依赖项的 computed property: - -```cpp -computed_property([](const auto& view) { - return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); -}, key<"speed_span">, external_access) -``` - -这种 computed property 的依赖字段必须和 computed property 本身落在同一个同步 slot,否则运行时会拒绝跨 slot 读取。同步计划因此显式表达了 computed value 的一致性边界。 - -Core 还提供 `trusted_computed_property` 和 `trusted_accessor_property` 作为高级入口。这类 accessor 直接通过受信任对象访问执行成员函数,不经过 synchronized view 的依赖检查,因此只应该在调用方明确掌握同步语义时使用。 - -## 运行时访问 - -`Property_Object_Base` 提供 type-erased runtime access: +`Property_Object_Base` 只提供属性自身的动态访问: ```cpp Property_Object_Base& erased = device; @@ -264,7 +295,14 @@ auto type = erased.runtime_object_type(); auto count = erased.runtime_property_count(); ``` -Runtime read/write 使用 property key、access mode 和 `std::type_info`,返回: +动态接口只有: + +```text +runtime_read(key, ...) +runtime_write(key, type_info, value) +``` + +返回: ```text ok @@ -274,15 +312,17 @@ not_writable type_mismatch ``` -它适合 JSON、HTTP/RPC、脚本桥接、动态工具等边界。普通编译期业务代码仍然应优先使用成员指针。 +没有 runtime access mode。外部 adapter 自己决定是否应该对某个属性调用 `runtime_read` 或 `runtime_write`。 ## Core 当前元数据 -当前 Core 提供: +Core 当前定义: - `key<"...">` -- `external_access<...>` -- `persistence_access<...>` +- `read_only` +- `write_only` +- `read_write` +- `inaccessible` - `unit<"...">` - `sensitive<>` - `min_value<...>` @@ -290,35 +330,33 @@ type_mismatch - `finite` - `constraint<"code">(...)` -其中 `external_access`、`persistence_access`、`sensitive` 支持继承,可以放入 `defaults(...)`。同 category 的 property 级 Attribute 会覆盖 object default。 +其中四种 capability 只描述 Property 自身,不承担访问控制职责。 -## 当前 Presentation Extension +## 当前 Extension 元数据 -当前扩展层定义: +Presentation Extension 定义: - `presentation::label<"...">` - `presentation::description<"...">` - `presentation::group<"...">` - `presentation::order` -`presentation::describe()` 解释这些 Attribute;未提供 label 时由 Presentation Extension 使用 property key 作为 fallback。这个 fallback 不属于 Core。 +Presentation consumer 是否显示、是否允许编辑,由 consumer 自己决定,不属于 Property Core。 ## 构建 -当前 CMake 目标适用于作为子目录接入: - ```cmake add_subdirectory(path/to/Structive) target_link_libraries(my_target PRIVATE structive::property_core) ``` -使用链接型扩展: +使用 linked extension: ```cmake target_link_libraries(my_target PRIVATE structive::property_extensions) ``` -仓库构建测试: +构建与测试: ```bash cmake -S . -B build -DBUILD_TESTING=ON @@ -330,8 +368,8 @@ ctest --test-dir build --output-on-failure - [设计理念与原则](docs/DESIGN.zh-CN.md) - [Core 完整指南](docs/CORE_GUIDE.zh-CN.md) -- [Extension 架构指南](docs/EXTENSIONS.zh-CN.md) -- [English README](README.md) +- [Extension 架构](docs/EXTENSIONS.zh-CN.md) +- [English](README.md) - [Design Philosophy](docs/DESIGN.md) - [Core Guide](docs/CORE_GUIDE.md) - [Extension Architecture](docs/EXTENSIONS.md) diff --git a/core/README.md b/core/README.md index 5b75683..de7f0a3 100644 --- a/core/README.md +++ b/core/README.md @@ -1,14 +1,9 @@ # Structive Property Core -The Core is the C++20 header-only structural and managed-property layer of Structive. +`structive::property_core` describes intrinsic property structure and provides managed read/write, synchronization, traversal, validation metadata and type-erased runtime access. -It provides `Type_Descriptor`, `Object_Schema`, member/computed property descriptors, the unified Attribute protocol, explicit constraints and validation, access capability views, synchronization plans/guards, typed traversal and type-erased runtime access. +Core has no access-control modes. A property only reports its own intrinsic `readable` / `writable` capability. External systems own their own exposure and authorization policy. -The core design rule is that registered properties remain ordinary C++ members. Structive adds a managed structural layer; it does not replace the native object model. +Stored `read_only` properties do not receive lock slots or contribute mutexes, and managed reads use the no-lock fast path. -See the complete documentation: - -- [Core Guide](../docs/CORE_GUIDE.md) -- [Design Philosophy](../docs/DESIGN.md) -- [中文 Core 指南](../docs/CORE_GUIDE.zh-CN.md) -- [中文设计理念](../docs/DESIGN.zh-CN.md) +See [Core Guide](../docs/CORE_GUIDE.md) and [Design Philosophy](../docs/DESIGN.md). diff --git a/core/README.zh-CN.md b/core/README.zh-CN.md index 81c8b9c..4a64a45 100644 --- a/core/README.zh-CN.md +++ b/core/README.zh-CN.md @@ -1,14 +1,9 @@ # Structive Property Core -Core 是 Structive 的 C++20 header-only 结构描述与 managed-property 核心层。 +`structive::property_core` 描述 Property 自身的固有结构,并提供 managed read/write、synchronization、traversal、validation metadata 和 type-erased runtime access。 -它提供 `Type_Descriptor`、`Object_Schema`、member/computed property descriptor、统一 Attribute 协议、显式 constraint/validation、access capability view、synchronization plan/guard、typed traversal,以及 type-erased runtime access。 +Core 完全没有访问控制 mode。Property 只报告自身 intrinsic `readable` / `writable`;外部系统自己拥有 exposure 与 authorization policy。 -Core 的基本原则是:注册后的 property 仍然是普通 C++ 成员。Structive 增加受管理结构层,但不替代原生对象模型。 +Stored `read_only` Property 不分配 lock slot、不贡献 mutex,managed read 直接走 no-lock fast path。 -完整文档: - -- [Core 完整指南](../docs/CORE_GUIDE.zh-CN.md) -- [设计理念与原则](../docs/DESIGN.zh-CN.md) -- [Core Guide](../docs/CORE_GUIDE.md) -- [Design Philosophy](../docs/DESIGN.md) +详见 [Core 完整指南](../docs/CORE_GUIDE.zh-CN.md) 与 [设计理念](../docs/DESIGN.zh-CN.md)。 diff --git a/core/example/main.cpp b/core/example/main.cpp index e66b7d0..e4200e3 100644 --- a/core/example/main.cpp +++ b/core/example/main.cpp @@ -1,51 +1,27 @@ #include #include -#include using namespace structive; struct Device : Property_Object { double temperature{25.0}; double pressure{101.3}; - double min_speed{10.0}; - double max_speed{100.0}; - std::string name{"device-1"}; + int serial_number{1001}; }; template <> struct structive::Type_Descriptor { static auto get() { return object( - defaults(external_access, persistence_access), - synchronization(sync_all_independent, sync_group<&Device::min_speed, &Device::max_speed>("speed_range")), + synchronization(sync_all_independent), field<&Device::temperature>(key<"temperature">, unit<"C">, min_value<-50.0>, max_value<200.0>), field<&Device::pressure>(key<"pressure">, unit<"kPa">), - field<&Device::min_speed>(key<"min_speed">), - field<&Device::max_speed>(key<"max_speed">), - field<&Device::name>(key<"name">) + field<&Device::serial_number>(key<"serial_number">, read_only) ); } }; -static bool update_speed_range(Device& device, double min_speed, double max_speed) { - auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); - auto old_min = guard.get<&Device::min_speed>(); - auto old_max = guard.get<&Device::max_speed>(); - guard.set < &Device::min_speed > (min_speed); - guard.set < &Device::max_speed > (max_speed); - if (min_speed <= max_speed) { - return true; - } - guard.set < &Device::min_speed > (old_min); - guard.set < &Device::max_speed > (old_max); - return false; -} int main() { Device device; - device.external().write < &Device::temperature > (30.0); - std::cout << "temperature=" << device.external().read<&Device::temperature>() << '\n'; - std::cout << "temperature lock slot=" << device.lock_slot<&Device::temperature>() << '\n'; - std::cout << "min/max shared slot=" << device.lock_slot<&Device::min_speed>() << '\n'; - std::cout << "max_speed key=" << device.schema().property<&Device::max_speed>().key() << '\n'; - if (!update_speed_range(device, 120.0, 80.0)) { - std::cout << "invalid range rolled back by business logic\n"; - } - device.temperature = 31.0; - std::cout << "raw temperature=" << device.temperature << '\n'; + device.write<&Device::temperature>(30.0); + std::cout << "temperature=" << device.read<&Device::temperature>() << '\n'; + std::cout << "serial_number=" << device.read<&Device::serial_number>() << '\n'; + std::cout << "lock_count=" << device.resolved_synchronization().lock_count << '\n'; + return 0; } diff --git a/core/include/structive/property/attributes.hpp b/core/include/structive/property/attributes.hpp index 2a43171..bcbe853 100644 --- a/core/include/structive/property/attributes.hpp +++ b/core/include/structive/property/attributes.hpp @@ -7,22 +7,15 @@ #include namespace structive { struct Key_Category {}; -struct Access_Category {}; -struct Persistence_Category {}; +struct Capability_Category {}; struct Unit_Category {}; struct Sensitive_Category {}; -enum class External_Access { +enum class Property_Capability { none, read, write, read_write }; -enum class Persistence_Access { - none, - load, - store, - load_store -}; template struct Key_Attribute { using attribute_category = Key_Category; @@ -30,18 +23,11 @@ struct Key_Attribute { static constexpr bool inheritable = false; static constexpr auto value = Value; }; -template -struct External_Access_Attribute { - using attribute_category = Access_Category; +template +struct Property_Capability_Attribute { + using attribute_category = Capability_Category; static constexpr bool single_valued = true; - static constexpr bool inheritable = true; - static constexpr auto value = Value; -}; -template -struct Persistence_Access_Attribute { - using attribute_category = Persistence_Category; - static constexpr bool single_valued = true; - static constexpr bool inheritable = true; + static constexpr bool inheritable = false; static constexpr auto value = Value; }; template @@ -100,10 +86,12 @@ struct Custom_Constraint { }; template inline constexpr Key_Attribute key{}; -template -inline constexpr External_Access_Attribute external_access{}; -template -inline constexpr Persistence_Access_Attribute persistence_access{}; +template +inline constexpr Property_Capability_Attribute capability{}; +inline constexpr auto read_only = capability; +inline constexpr auto write_only = capability; +inline constexpr auto read_write = capability; +inline constexpr auto inaccessible = capability; template inline constexpr Unit_Attribute unit{}; template diff --git a/core/include/structive/property/descriptor.hpp b/core/include/structive/property/descriptor.hpp index 030eee4..d5df3d3 100644 --- a/core/include/structive/property/descriptor.hpp +++ b/core/include/structive/property/descriptor.hpp @@ -25,11 +25,27 @@ struct Property_Descriptor { using value_type = typename Accessor::value_type; using storage_identity = typename Accessor::storage_identity; using attribute_types = Type_List; - static constexpr bool readable = Accessor::readable; - static constexpr bool writable = Accessor::writable; + using capability_type = find_attribute_in_list_t; + static constexpr Property_Capability intrinsic_capability = [] { + if constexpr (!std::same_as) { + return capability_type::value; + } else if constexpr (Accessor::readable && Accessor::writable) { + return Property_Capability::read_write; + } else if constexpr (Accessor::readable) { + return Property_Capability::read; + } else if constexpr (Accessor::writable) { + return Property_Capability::write; + } else { + return Property_Capability::none; + } + }(); + static constexpr bool readable = Accessor::readable && (intrinsic_capability == Property_Capability::read || intrinsic_capability == Property_Capability::read_write); + static constexpr bool writable = Accessor::writable && (intrinsic_capability == Property_Capability::write || intrinsic_capability == Property_Capability::read_write); static constexpr bool synchronized_view_read = Accessor::synchronized_view_read; static constexpr bool trusted_object_access = Accessor::trusted_object_access; static_assert(unique_single_value_categories()); + static_assert(!((intrinsic_capability == Property_Capability::read || intrinsic_capability == Property_Capability::read_write) && !Accessor::readable), "property capability requests read from a non-readable accessor"); + static_assert(!((intrinsic_capability == Property_Capability::write || intrinsic_capability == Property_Capability::read_write) && !Accessor::writable), "property capability requests write from a non-writable accessor"); using key_type = find_attribute_in_list_t; static_assert(!std::same_as); static_assert(key_type::value.view().size() > 0); diff --git a/core/include/structive/property/property_object.hpp b/core/include/structive/property/property_object.hpp index 234694f..e2f6f54 100644 --- a/core/include/structive/property/property_object.hpp +++ b/core/include/structive/property/property_object.hpp @@ -79,22 +79,6 @@ public: } }; } -enum class Managed_Access_Mode { - external, - persistence -}; -namespace detail { -enum class Access_Kind { - intrinsic, - external, - persistence -}; -constexpr Access_Kind access_kind(Managed_Access_Mode mode) noexcept { - return mode == Managed_Access_Mode::external ? Access_Kind::external : Access_Kind::persistence; -} -template -inline constexpr Access_Kind access_kind_v = Mode == Managed_Access_Mode::external ? Access_Kind::external : Access_Kind::persistence; -} enum class Runtime_Access_Result { ok, unknown_property, @@ -107,8 +91,8 @@ class Property_Object_Base { struct Runtime_Interface { const std::type_info& (*object_type)() noexcept; std::size_t (*property_count)() noexcept; - Runtime_Access_Result (*read)(const Property_Object_Base&, detail::Access_Kind, std::string_view, void*, Runtime_Read_Callback); - Runtime_Access_Result (*write)(Property_Object_Base&, detail::Access_Kind, std::string_view, const std::type_info&, const void*); + Runtime_Access_Result (*read)(const Property_Object_Base&, std::string_view, void*, Runtime_Read_Callback); + Runtime_Access_Result (*write)(Property_Object_Base&, std::string_view, const std::type_info&, const void*); }; const Runtime_Interface* runtime_interface_{}; protected: @@ -128,26 +112,12 @@ public: return runtime_interface_->property_count(); } Runtime_Access_Result runtime_read(std::string_view key, void* context, Runtime_Read_Callback callback) const { - return runtime_interface_->read(*this, detail::Access_Kind::intrinsic, key, context, callback); - } - Runtime_Access_Result runtime_read(Managed_Access_Mode mode, std::string_view key, void* context, Runtime_Read_Callback callback) const { - return runtime_interface_->read(*this, detail::access_kind(mode), key, context, callback); + return runtime_interface_->read(*this, key, context, callback); } Runtime_Access_Result runtime_write(std::string_view key, const std::type_info& value_type, const void* value) { - return runtime_interface_->write(*this, detail::Access_Kind::intrinsic, key, value_type, value); - } - Runtime_Access_Result runtime_write(Managed_Access_Mode mode, std::string_view key, const std::type_info& value_type, const void* value) { - return runtime_interface_->write(*this, detail::access_kind(mode), key, value_type, value); + return runtime_interface_->write(*this, key, value_type, value); } }; -namespace detail { -template -inline constexpr bool property_read_allowed_v = Mode == Access_Kind::intrinsic ? Schema::template property_type::readable : Mode == Access_Kind::external ? external_readable_v : persistence_storable_v; -template -inline constexpr bool property_write_allowed_v = Mode == Access_Kind::intrinsic ? Schema::template property_type::writable : Mode == Access_Kind::external ? external_writable_v : persistence_loadable_v; -template -inline constexpr bool property_visible_v = property_read_allowed_v || property_write_allowed_v; -} struct Property_Synchronization { Synchronization_Plan plan; }; @@ -249,23 +219,20 @@ private: } count = write; } - template static bool runtime_property_readable(std::size_t index) { using Schema = type_descriptor_schema_t; static const auto table = [](std::index_sequence) { - return std::array{detail::property_read_allowed_v...}; + return std::array{Schema::template property_type::readable...}; }(std::make_index_sequence{}); return table[index]; } - template - static bool runtime_property_visible(std::size_t index) { + static bool runtime_property_writable(std::size_t index) { using Schema = type_descriptor_schema_t; static const auto table = [](std::index_sequence) { - return std::array{detail::property_visible_v...}; + return std::array{Schema::template property_type::writable...}; }(std::make_index_sequence{}); return table[index]; } - template Dynamic_Lock_Targets collect_dynamic_lock_targets(std::span keys, bool shared_access) const { using Schema = type_descriptor_schema_t; Dynamic_Lock_Targets targets; @@ -276,9 +243,9 @@ private: if (!index) { throw std::invalid_argument("Unknown property: " + std::string(key)); } - bool allowed = shared_access ? runtime_property_readable(*index) : runtime_property_visible(*index); + bool allowed = shared_access ? runtime_property_readable(*index) : runtime_property_writable(*index); if (!allowed) { - throw std::invalid_argument("Property is not accessible through this view: " + std::string(key)); + throw std::invalid_argument(std::string(shared_access ? "Property is not readable: " : "Property is not writable: ") + std::string(key)); } auto lock_slot = slot(*index); if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { @@ -293,10 +260,10 @@ private: targets.unsynchronized_properties.erase(std::unique(targets.unsynchronized_properties.begin(), targets.unsynchronized_properties.end()), targets.unsynchronized_properties.end()); return targets; } - template + template Static_Lock_Targets collect_static_lock_targets() const { using Schema = type_descriptor_schema_t; - static_assert(((Shared_Access ? detail::property_read_allowed_v : detail::property_visible_v) && ...)); + static_assert(((Shared_Access ? Schema::template property_type::readable : Schema::template property_type::writable) && ...)); Static_Lock_Targets targets; auto collect = [&]() { auto lock_slot = slot(Property_Index); @@ -311,12 +278,12 @@ private: sort_unique_prefix(targets.unsynchronized_properties, targets.unsynchronized_count); return targets; } - template + template auto collect_all_static_lock_targets() const { using Schema = type_descriptor_schema_t; Static_Lock_Targets targets; auto collect = [&]() { - constexpr bool allowed = Shared_Access ? detail::property_read_allowed_v : detail::property_write_allowed_v; + constexpr bool allowed = Shared_Access ? Schema::template property_type::readable : Schema::template property_type::writable; if constexpr (allowed) { auto lock_slot = slot(Index); if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { @@ -349,7 +316,6 @@ private: const auto& descriptor = type_descriptor().template property(); descriptor.accessor.write(static_cast(*this), std::forward(value)); } - template class Single_Read_View { const Property_Object* owner_{}; std::size_t property_index_{}; @@ -361,6 +327,11 @@ private: using Schema = type_descriptor_schema_t; constexpr auto index = schema_member_property_index_v; static_assert(index < Schema::property_count); + using Property = typename Schema::template property_type; + static_assert(Property::readable); + if constexpr (!Property::writable && !Property::accessor_type::synchronized_view_read) { + return owner_->template read_unlocked(*this); + } if (owner_->slot(index) != lock_slot_ || (lock_slot_ == Resolved_Synchronization_View::unsynchronized_slot && index != property_index_)) { throw std::logic_error("Computed property reads outside its synchronization slot"); } @@ -371,23 +342,28 @@ private: using Schema = type_descriptor_schema_t; constexpr auto index = schema_property_index_v; static_assert(index < Schema::property_count); + using Property = typename Schema::template property_type; + static_assert(Property::readable); + if constexpr (!Property::writable && !Property::accessor_type::synchronized_view_read) { + return owner_->template read_unlocked(*this); + } if (owner_->slot(index) != lock_slot_ || (lock_slot_ == Resolved_Synchronization_View::unsynchronized_slot && index != property_index_)) { throw std::logic_error("Computed property reads outside its synchronization slot"); } return owner_->template read_unlocked(*this); } }; - template + template auto read_one() const { using Schema = type_descriptor_schema_t; - static_assert(detail::property_read_allowed_v); + static_assert(Schema::template property_type::readable); using Property = typename Schema::template property_type; using Value = typename Property::value_type; - if constexpr (!uses_real_mutexes && !Property::accessor_type::synchronized_view_read) { + if constexpr ((!Property::writable && !Property::accessor_type::synchronized_view_read) || (!uses_real_mutexes && !Property::accessor_type::synchronized_view_read)) { return Value(read_unlocked(*this)); } auto lock_slot = slot(Index); - Single_Read_View view{*this, Index, lock_slot}; + Single_Read_View view{*this, Index, lock_slot}; if constexpr (!uses_real_mutexes) { return Value(read_unlocked(view)); } @@ -397,10 +373,10 @@ private: std::shared_lock lock{mutex(lock_slot)}; return Value(read_unlocked(view)); } - template + template void write_one(Value&& value) { using Schema = type_descriptor_schema_t; - static_assert(detail::property_write_allowed_v); + static_assert(Schema::template property_type::writable); if constexpr (!uses_real_mutexes) { write_unlocked(std::forward(value)); return; @@ -414,7 +390,6 @@ private: write_unlocked(std::forward(value)); } private: - template class Basic_Read_Guard { const Property_Object* owner_{}; std::vector slots_; @@ -441,7 +416,7 @@ private: decltype(auto) get_index() const { using Schema = type_descriptor_schema_t; static_assert(Index < Schema::property_count); - static_assert(detail::property_read_allowed_v); + static_assert(Schema::template property_type::readable); if (!holds(Index)) { throw std::logic_error("Property is outside the held synchronization set"); } @@ -462,7 +437,6 @@ private: return get_index(); } }; - template class Basic_Write_Guard { Property_Object* owner_{}; std::vector slots_; @@ -489,7 +463,7 @@ private: decltype(auto) get_index() const { using Schema = type_descriptor_schema_t; static_assert(Index < Schema::property_count); - static_assert(detail::property_read_allowed_v); + static_assert(Schema::template property_type::readable); if (!holds(Index)) { throw std::logic_error("Property is outside the held synchronization set"); } @@ -513,7 +487,7 @@ private: void set_index(Value&& value) { using Schema = type_descriptor_schema_t; static_assert(Index < Schema::property_count); - static_assert(detail::property_write_allowed_v); + static_assert(Schema::template property_type::writable); if (!holds(Index)) { throw std::logic_error("Property is outside the held synchronization set"); } @@ -534,7 +508,7 @@ private: set_index(std::forward(value)); } }; - template + template class Static_Read_Guard { const Property_Object* owner_{}; Static_Lock_Targets targets_; @@ -559,7 +533,7 @@ private: decltype(auto) get_index() const { using Schema = type_descriptor_schema_t; static_assert(Index < Schema::property_count); - static_assert(detail::property_read_allowed_v); + static_assert(Schema::template property_type::readable); if (!holds(Index)) { throw std::logic_error("Property is outside the held synchronization set"); } @@ -580,7 +554,7 @@ private: return get_index(); } }; - template + template class Static_Write_Guard { Property_Object* owner_{}; Static_Lock_Targets targets_; @@ -605,7 +579,7 @@ private: decltype(auto) get_index() const { using Schema = type_descriptor_schema_t; static_assert(Index < Schema::property_count); - static_assert(detail::property_read_allowed_v); + static_assert(Schema::template property_type::readable); if (!holds(Index)) { throw std::logic_error("Property is outside the held synchronization set"); } @@ -629,7 +603,7 @@ private: void set_index(Value&& value) { using Schema = type_descriptor_schema_t; static_assert(Index < Schema::property_count); - static_assert(detail::property_write_allowed_v); + static_assert(Schema::template property_type::writable); if (!holds(Index)) { throw std::logic_error("Property is outside the held synchronization set"); } @@ -651,42 +625,38 @@ private: } }; public: - using Intrinsic_Read_Guard = Basic_Read_Guard; - using Intrinsic_Write_Guard = Basic_Write_Guard; - template - using Read_Guard = Basic_Read_Guard>; - template - using Write_Guard = Basic_Write_Guard>; + using Read_Guard = Basic_Read_Guard; + using Write_Guard = Basic_Write_Guard; private: - template + template void for_each_readable_impl(Function&& function) const { using Schema = type_descriptor_schema_t; const auto& schema = type_descriptor(); schema.for_each_property([&](auto index, const auto& descriptor) { constexpr std::size_t property_index = decltype(index)::value; - if constexpr (detail::property_read_allowed_v) { - auto value = read_one(); + if constexpr (Schema::template property_type::readable) { + auto value = read_one(); std::invoke(function, index, descriptor, value); } }); } - template + template void for_each_readable_locked_impl(Function&& function) const { using Schema = type_descriptor_schema_t; - auto targets = collect_all_static_lock_targets(); - Static_Read_Guard guard{*this, std::move(targets)}; + auto targets = collect_all_static_lock_targets(); + Static_Read_Guard guard{*this, std::move(targets)}; type_descriptor().for_each_property([&](auto index, const auto& descriptor) { constexpr std::size_t property_index = decltype(index)::value; - if constexpr (detail::property_read_allowed_v) { + if constexpr (Schema::template property_type::readable) { std::invoke(function, index, descriptor, guard.template get_index()); } }); } - template + template void with_all_writable_locked_impl(Function&& function) { using Schema = type_descriptor_schema_t; - auto targets = collect_all_static_lock_targets(); - Static_Write_Guard guard{*this, std::move(targets)}; + auto targets = collect_all_static_lock_targets(); + Static_Write_Guard guard{*this, std::move(targets)}; std::invoke(std::forward(function), guard); } static const std::type_info& runtime_object_type_impl() noexcept { @@ -696,63 +666,64 @@ private: using Schema = type_descriptor_schema_t; return Schema::property_count; } - template - Runtime_Access_Result runtime_read_mode(std::string_view key, void* context, Runtime_Read_Callback callback) const { + Runtime_Access_Result runtime_read_impl_local(std::string_view key, void* context, Runtime_Read_Callback callback) const { using Schema = type_descriptor_schema_t; auto index = schema_property_index(key); if (!index) { return Runtime_Access_Result::unknown_property; } - if (!runtime_property_readable(*index)) { + if (!runtime_property_readable(*index)) { return Runtime_Access_Result::not_readable; } Runtime_Access_Result result = Runtime_Access_Result::unknown_property; visit_schema_property(type_descriptor(), key, [&](auto property_index_constant, const auto&) { constexpr std::size_t property_index = decltype(property_index_constant)::value; - using Value = typename Schema::template property_type::value_type; - auto lock_slot = slot(property_index); - Single_Read_View view{*this, property_index, lock_slot}; - auto emit = [&] { - if constexpr (std::is_reference_v(view))>) { - auto&& value = read_unlocked(view); + using Property = typename Schema::template property_type; + using Value = typename Property::value_type; + if constexpr (!Property::writable && !Property::accessor_type::synchronized_view_read) { + if constexpr (std::is_reference_v(*this))>) { + auto&& value = read_unlocked(*this); callback(context, property_index, key, typeid(Value), std::addressof(value)); } else { - Value value = read_unlocked(view); + Value value = read_unlocked(*this); callback(context, property_index, key, typeid(Value), std::addressof(value)); } - }; - if constexpr (!uses_real_mutexes) { - emit(); - } else if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { - emit(); } else { - std::shared_lock lock{mutex(lock_slot)}; - emit(); + auto lock_slot = slot(property_index); + Single_Read_View view{*this, property_index, lock_slot}; + auto emit = [&] { + if constexpr (std::is_reference_v(view))>) { + auto&& value = read_unlocked(view); + callback(context, property_index, key, typeid(Value), std::addressof(value)); + } else { + Value value = read_unlocked(view); + callback(context, property_index, key, typeid(Value), std::addressof(value)); + } + }; + if constexpr (!uses_real_mutexes) { + emit(); + } else if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) { + emit(); + } else { + std::shared_lock lock{mutex(lock_slot)}; + emit(); + } } result = Runtime_Access_Result::ok; }); return result; } - static Runtime_Access_Result runtime_read_impl(const Property_Object_Base& base, detail::Access_Kind mode, std::string_view key, void* context, Runtime_Read_Callback callback) { + static Runtime_Access_Result runtime_read_impl(const Property_Object_Base& base, std::string_view key, void* context, Runtime_Read_Callback callback) { const auto& self = static_cast(base); - switch (mode) { - case detail::Access_Kind::intrinsic: - return self.template runtime_read_mode(key, context, callback); - case detail::Access_Kind::external: - return self.template runtime_read_mode(key, context, callback); - case detail::Access_Kind::persistence: - return self.template runtime_read_mode(key, context, callback); - } - return Runtime_Access_Result::not_readable; + return self.runtime_read_impl_local(key, context, callback); } - template - Runtime_Access_Result runtime_write_mode(std::string_view key, const std::type_info& value_type, const void* value) { + Runtime_Access_Result runtime_write_impl_local(std::string_view key, const std::type_info& value_type, const void* value) { using Schema = type_descriptor_schema_t; auto index = schema_property_index(key); if (!index) { return Runtime_Access_Result::unknown_property; } - if (!runtime_property_visible(*index)) { + if (!runtime_property_writable(*index)) { return Runtime_Access_Result::not_writable; } Runtime_Access_Result result = Runtime_Access_Result::unknown_property; @@ -761,28 +732,20 @@ private: using Property = typename Schema::template property_type; using Accessor = typename Property::accessor_type; using Value = typename Property::value_type; - if constexpr (!detail::property_write_allowed_v || !requires(const Accessor& accessor, Derived& object, const Value& candidate) { accessor.write(object, candidate); }) { + if constexpr (!Schema::template property_type::writable || !requires(const Accessor& accessor, Derived& object, const Value& candidate) { accessor.write(object, candidate); }) { result = Runtime_Access_Result::not_writable; } else if (value_type != typeid(Value)) { result = Runtime_Access_Result::type_mismatch; } else { - write_one(*static_cast(value)); + write_one(*static_cast(value)); result = Runtime_Access_Result::ok; } }); return result; } - static Runtime_Access_Result runtime_write_impl(Property_Object_Base& base, detail::Access_Kind mode, std::string_view key, const std::type_info& value_type, const void* value) { + static Runtime_Access_Result runtime_write_impl(Property_Object_Base& base, std::string_view key, const std::type_info& value_type, const void* value) { auto& self = static_cast(base); - switch (mode) { - case detail::Access_Kind::intrinsic: - return self.template runtime_write_mode(key, value_type, value); - case detail::Access_Kind::external: - return self.template runtime_write_mode(key, value_type, value); - case detail::Access_Kind::persistence: - return self.template runtime_write_mode(key, value_type, value); - } - return Runtime_Access_Result::not_writable; + return self.runtime_write_impl_local(key, value_type, value); } static const Property_Object_Base::Runtime_Interface* runtime_interface() noexcept { static const Property_Object_Base::Runtime_Interface value{ @@ -830,32 +793,32 @@ public: return static_cast(*this); } template - auto read() const { + auto read() const requires Schema_Readable_Property_Member, Member> { using Schema = type_descriptor_schema_t; constexpr auto index = schema_member_property_index_v; static_assert(index < Schema::property_count); - return read_one(); + return read_one(); } template - auto read_key() const { + auto read_key() const requires Schema_Readable_Property_Key, Key> { using Schema = type_descriptor_schema_t; constexpr auto index = schema_property_index_v; static_assert(index < Schema::property_count); - return read_one(); + return read_one(); } template - void write(Value&& value) { + void write(Value&& value) requires Schema_Writable_Property_Member, Member> { using Schema = type_descriptor_schema_t; constexpr auto index = schema_member_property_index_v; static_assert(index < Schema::property_count); - write_one(std::forward(value)); + write_one(std::forward(value)); } template - void write_key(Value&& value) { + void write_key(Value&& value) requires Schema_Writable_Property_Key, Key> { using Schema = type_descriptor_schema_t; constexpr auto index = schema_property_index_v; static_assert(index < Schema::property_count); - write_one(std::forward(value)); + write_one(std::forward(value)); } template std::size_t lock_slot() const noexcept { @@ -865,209 +828,64 @@ public: return slot(index); } private: - template - Basic_Read_Guard lock_shared_mode(std::span keys) const { - return Basic_Read_Guard{*this, collect_dynamic_lock_targets(keys, true)}; + Basic_Read_Guard lock_shared_impl(std::span keys) const { + return Basic_Read_Guard{*this, collect_dynamic_lock_targets(keys, true)}; } - template - Basic_Read_Guard lock_shared_mode(std::initializer_list keys) const { - return lock_shared_mode(std::span{keys.begin(), keys.size()}); + Basic_Read_Guard lock_shared_impl(std::initializer_list keys) const { + return lock_shared_impl(std::span{keys.begin(), keys.size()}); } - template - Basic_Write_Guard lock_unique_mode(std::span keys) { - return Basic_Write_Guard{*this, collect_dynamic_lock_targets(keys, false)}; + Basic_Write_Guard lock_unique_impl(std::span keys) { + return Basic_Write_Guard{*this, collect_dynamic_lock_targets(keys, false)}; } - template - Basic_Write_Guard lock_unique_mode(std::initializer_list keys) { - return lock_unique_mode(std::span{keys.begin(), keys.size()}); + Basic_Write_Guard lock_unique_impl(std::initializer_list keys) { + return lock_unique_impl(std::span{keys.begin(), keys.size()}); } - template - auto lock_shared_mode() const { + template + auto lock_shared_impl() const { using Schema = type_descriptor_schema_t; static_assert((Schema_Property_Member && ...)); - auto targets = collect_static_lock_targets...>(); - return Static_Read_Guard{*this, std::move(targets)}; + auto targets = collect_static_lock_targets...>(); + return Static_Read_Guard{*this, std::move(targets)}; } - template - auto lock_unique_mode() { + template + auto lock_unique_impl() { using Schema = type_descriptor_schema_t; static_assert((Schema_Property_Member && ...)); - auto targets = collect_static_lock_targets...>(); - return Static_Write_Guard{*this, std::move(targets)}; + auto targets = collect_static_lock_targets...>(); + return Static_Write_Guard{*this, std::move(targets)}; } public: - Intrinsic_Read_Guard lock_shared(std::span keys) const { - return lock_shared_mode(keys); + Read_Guard lock_shared(std::span keys) const { + return lock_shared_impl(keys); } - Intrinsic_Read_Guard lock_shared(std::initializer_list keys) const { - return lock_shared_mode(keys); + Read_Guard lock_shared(std::initializer_list keys) const { + return lock_shared_impl(keys); } - Intrinsic_Write_Guard lock_unique(std::span keys) { - return lock_unique_mode(keys); + Write_Guard lock_unique(std::span keys) { + return lock_unique_impl(keys); } - Intrinsic_Write_Guard lock_unique(std::initializer_list keys) { - return lock_unique_mode(keys); + Write_Guard lock_unique(std::initializer_list keys) { + return lock_unique_impl(keys); } template - auto lock_shared() const { - return lock_shared_mode(); + auto lock_shared() const requires (Schema_Readable_Property_Member, Members> && ...) { + return lock_shared_impl(); } template - auto lock_unique() { - return lock_unique_mode(); + auto lock_unique() requires (Schema_Writable_Property_Member, Members> && ...) { + return lock_unique_impl(); } template void for_each_readable(Function&& function) const { - for_each_readable_impl(std::forward(function)); + for_each_readable_impl(std::forward(function)); } template void for_each_readable_locked(Function&& function) const { - for_each_readable_locked_impl(std::forward(function)); + for_each_readable_locked_impl(std::forward(function)); } template void with_all_writable_locked(Function&& function) { - with_all_writable_locked_impl(std::forward(function)); - } - template - class Capability_View { - Property_Object* owner_{}; - public: - explicit Capability_View(Property_Object& owner) : owner_(&owner) {} - template - auto read() const { - using Schema = type_descriptor_schema_t; - constexpr auto index = schema_member_property_index_v; - static_assert(index < Schema::property_count); - return owner_->template read_one, index>(); - } - template - auto read_key() const { - using Schema = type_descriptor_schema_t; - constexpr auto index = schema_property_index_v; - static_assert(index < Schema::property_count); - return owner_->template read_one, index>(); - } - template - void write(Value&& value) requires (Mode != Managed_Access_Mode::persistence) { - using Schema = type_descriptor_schema_t; - constexpr auto index = schema_member_property_index_v; - static_assert(index < Schema::property_count); - owner_->template write_one, index>(std::forward(value)); - } - template - void write_key(Value&& value) requires (Mode != Managed_Access_Mode::persistence) { - using Schema = type_descriptor_schema_t; - constexpr auto index = schema_property_index_v; - static_assert(index < Schema::property_count); - owner_->template write_one, index>(std::forward(value)); - } - template - void load(Value&& value) requires (Mode == Managed_Access_Mode::persistence) { - using Schema = type_descriptor_schema_t; - constexpr auto index = schema_member_property_index_v; - static_assert(index < Schema::property_count); - owner_->template write_one, index>(std::forward(value)); - } - template - void load_key(Value&& value) requires (Mode == Managed_Access_Mode::persistence) { - using Schema = type_descriptor_schema_t; - constexpr auto index = schema_property_index_v; - static_assert(index < Schema::property_count); - owner_->template write_one, index>(std::forward(value)); - } - template - auto store() const requires (Mode == Managed_Access_Mode::persistence) { - return read(); - } - template - auto store_key() const requires (Mode == Managed_Access_Mode::persistence) { - return read_key(); - } - template - auto lock_shared() const { - return owner_->template lock_shared_mode, Members...>(); - } - template - auto lock_unique() { - return owner_->template lock_unique_mode, Members...>(); - } - Read_Guard lock_shared(std::span keys) const { - return owner_->template lock_shared_mode>(keys); - } - Write_Guard lock_unique(std::span keys) { - return owner_->template lock_unique_mode>(keys); - } - template - void for_each_readable(Function&& function) const { - owner_->template for_each_readable_impl>(std::forward(function)); - } - template - void for_each_readable_locked(Function&& function) const { - owner_->template for_each_readable_locked_impl>(std::forward(function)); - } - template - void with_all_writable_locked(Function&& function) requires (Mode != Managed_Access_Mode::persistence) { - owner_->template with_all_writable_locked_impl>(std::forward(function)); - } - template - void with_all_loadable_locked(Function&& function) requires (Mode == Managed_Access_Mode::persistence) { - owner_->template with_all_writable_locked_impl>(std::forward(function)); - } - }; - template - class Const_Capability_View { - const Property_Object* owner_{}; - public: - explicit Const_Capability_View(const Property_Object& owner) : owner_(&owner) {} - template - auto read() const { - using Schema = type_descriptor_schema_t; - constexpr auto index = schema_member_property_index_v; - static_assert(index < Schema::property_count); - return owner_->template read_one, index>(); - } - template - auto read_key() const { - using Schema = type_descriptor_schema_t; - constexpr auto index = schema_property_index_v; - static_assert(index < Schema::property_count); - return owner_->template read_one, index>(); - } - template - auto store() const requires (Mode == Managed_Access_Mode::persistence) { - return read(); - } - template - auto store_key() const requires (Mode == Managed_Access_Mode::persistence) { - return read_key(); - } - template - auto lock_shared() const { - return owner_->template lock_shared_mode, Members...>(); - } - Read_Guard lock_shared(std::span keys) const { - return owner_->template lock_shared_mode>(keys); - } - template - void for_each_readable(Function&& function) const { - owner_->template for_each_readable_impl>(std::forward(function)); - } - template - void for_each_readable_locked(Function&& function) const { - owner_->template for_each_readable_locked_impl>(std::forward(function)); - } - }; - Capability_View external() { - return Capability_View{*this}; - } - Capability_View persistence() { - return Capability_View{*this}; - } - Const_Capability_View external() const { - return Const_Capability_View{*this}; - } - Const_Capability_View persistence() const { - return Const_Capability_View{*this}; + with_all_writable_locked_impl(std::forward(function)); } }; } diff --git a/core/include/structive/property/schema.hpp b/core/include/structive/property/schema.hpp index f4a83ce..e46e7bc 100644 --- a/core/include/structive/property/schema.hpp +++ b/core/include/structive/property/schema.hpp @@ -211,14 +211,8 @@ public: }; template using effective_attribute_t = typename Effective_Attribute::type; -using Default_Access_Attribute = External_Access_Attribute; -using Default_Persistence_Attribute = Persistence_Access_Attribute; using Default_Sensitive_Attribute = Sensitive_Attribute; template -inline constexpr External_Access effective_external_access_v = effective_attribute_t::value; -template -inline constexpr Persistence_Access effective_persistence_access_v = effective_attribute_t::value; -template inline constexpr bool effective_sensitive_v = effective_attribute_t::value; template using property_declared_attribute_t = typename Schema::template property_type::template attribute_type; @@ -236,37 +230,18 @@ constexpr decltype(auto) declared_effective_attribute(const Schema& schema) requ return std::get(schema.object_defaults().attributes); } } -template -inline constexpr bool external_readable_v = effective_external_access_v == External_Access::read || effective_external_access_v == External_Access::read_write; -template -inline constexpr bool external_writable_v = effective_external_access_v == External_Access::write || effective_external_access_v == External_Access::read_write; -template -inline constexpr bool persistence_loadable_v = effective_persistence_access_v == Persistence_Access::load || effective_persistence_access_v == Persistence_Access::load_store; -template -inline constexpr bool persistence_storable_v = effective_persistence_access_v == Persistence_Access::store || effective_persistence_access_v == Persistence_Access::load_store; -template -consteval bool property_capability_semantics_valid() { - using property_type = typename Schema::template property_type; - if constexpr ((external_readable_v || persistence_storable_v) && !property_type::readable) { - return false; - } - if constexpr ((external_writable_v || persistence_loadable_v) && !property_type::writable) { - return false; - } - return true; -} -template -consteval bool schema_capability_semantics_valid_impl(std::index_sequence) { - return (property_capability_semantics_valid() && ...); -} template -consteval bool schema_capability_semantics_valid() { - return schema_capability_semantics_valid_impl(std::make_index_sequence{}); -} -template -concept Valid_Property_Schema = Property_Schema && requires { - requires schema_capability_semantics_valid(); -}; +concept Valid_Property_Schema = Property_Schema; +template +inline constexpr bool property_requires_synchronization_v = Schema::template property_type::writable || Schema::template property_type::synchronized_view_read; +template +concept Schema_Readable_Property_Member = Schema_Property_Member && Schema::template property_type>::readable; +template +concept Schema_Writable_Property_Member = Schema_Property_Member && Schema::template property_type>::writable; +template +concept Schema_Readable_Property_Key = Schema_Property_Key && Schema::template property_type>::readable; +template +concept Schema_Writable_Property_Key = Schema_Property_Key && Schema::template property_type>::writable; template Resolved_Synchronization_Plan resolve_synchronization_plan(const Schema&, const Synchronization_Plan& plan) { using resolved_type = Resolved_Synchronization_Plan; @@ -318,6 +293,14 @@ Resolved_Synchronization_Plan resolve_synchronization_pl logical[index] = token; } } + constexpr auto requires_synchronization = [](std::index_sequence) { + return std::array{property_requires_synchronization_v...}; + }(std::make_index_sequence{}); + for (std::size_t index = 0; index < Schema::property_count; ++index) { + if (!requires_synchronization[index]) { + logical[index] = unsynchronized_slot; + } + } resolved_type resolved; resolved.lock_slots.fill(unsynchronized_slot); std::vector> token_slots; diff --git a/core/tests/property_core_test.cpp b/core/tests/property_core_test.cpp index 86be2da..25000f6 100644 --- a/core/tests/property_core_test.cpp +++ b/core/tests/property_core_test.cpp @@ -34,31 +34,31 @@ template <> struct structive::Type_Descriptor { static auto get() { return object( - defaults(external_access, persistence_access), - synchronization(sync_all_independent, sync_group < &Device::min_speed, &Device::max_speed > ("speed_range"), sync_unsynchronized<&Device::immutable_id>()), - field < &Device::temperature > (key < "temperature" >, min_value < -50 >, max_value < 200 >, unit < "C" >, test_tag<7>), - field < &Device::pressure > (key < "pressure" >), - field < &Device::min_speed > (key < "minimum_speed" >), - field < &Device::max_speed > (key < "maximum_speed" >), - field < &Device::immutable_id > (key < "immutable_id" >, external_access, persistence_access) + synchronization(sync_all_independent, sync_group<&Device::min_speed, &Device::max_speed>("speed_range")), + field<&Device::temperature>(key<"temperature">, min_value<-50>, max_value<200>, unit<"C">, test_tag<7>), + field<&Device::pressure>(key<"pressure">), + field<&Device::min_speed>(key<"minimum_speed">), + field<&Device::max_speed>(key<"maximum_speed">), + field<&Device::immutable_id>(key<"immutable_id">, read_only) ); } }; struct Computed_Device : Property_Object { int min_speed{10}; int max_speed{100}; + int fixed_offset{5}; }; template <> struct structive::Type_Descriptor { static auto get() { return object( - defaults(external_access), synchronization(sync_all_independent, sync_group("speed", "min_speed", "max_speed", "speed_span")), - field < &Computed_Device::min_speed > (key < "min_speed" >), - field < &Computed_Device::max_speed > (key < "max_speed" >), + field<&Computed_Device::min_speed>(key<"min_speed">), + field<&Computed_Device::max_speed>(key<"max_speed">), + field<&Computed_Device::fixed_offset>(key<"fixed_offset">, read_only), computed_property([](const auto& view) { - return view.template get<&Computed_Device::max_speed>() - view.template get<&Computed_Device::min_speed>(); - }, key < "speed_span" >, external_access) + return view.template get<&Computed_Device::max_speed>() - view.template get<&Computed_Device::min_speed>() + view.template get<&Computed_Device::fixed_offset>(); + }, key<"speed_span">) ); } }; @@ -68,10 +68,7 @@ struct Non_Copyable_Device : Property_Object { template <> struct structive::Type_Descriptor { static auto get() { - return object( - defaults(external_access), - field < &Non_Copyable_Device::payload > (key < "payload" >) - ); + return object(field<&Non_Copyable_Device::payload>(key<"payload">)); } }; struct Lockless_Device : Property_Object { @@ -85,29 +82,63 @@ struct structive::Type_Descriptor { static auto get() { return object( synchronization(sync_all_independent, sync_group("sum", "left", "right", "sum")), - field < &Lockless_Device::left > (key < "left" >), - field < &Lockless_Device::right > (key < "right" >), + field<&Lockless_Device::left>(key<"left">), + field<&Lockless_Device::right>(key<"right">), computed_property([](const auto& view) { return view.template get<&Lockless_Device::left>() + view.template get<&Lockless_Device::right>(); - }, key < "sum" >) + }, key<"sum">) ); } }; -template -concept Has_Write_Temperature = requires(View view) { - view.template write<&Device::temperature>(1); +struct Read_Only_Device : Property_Object { + int id{11}; + int version{3}; + int value{9}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + synchronization(sync_all_shared), + field<&Read_Only_Device::id>(key<"id">, read_only), + field<&Read_Only_Device::version>(key<"version">, read_only), + field<&Read_Only_Device::value>(key<"value">) + ); + } +}; +struct Pure_Read_Only_Device : Property_Object { + int id{21}; + int version{4}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object( + synchronization(sync_all_shared), + field<&Pure_Read_Only_Device::id>(key<"id">, read_only), + field<&Pure_Read_Only_Device::version>(key<"version">, read_only) + ); + } +}; +template +concept Can_Write_Immutable = requires(Object& object) { + object.template write<&Device::immutable_id>(1); +}; +template +concept Can_Lock_Immutable_Unique = requires(Object& object) { + object.template lock_unique<&Device::immutable_id>(); }; static bool update_speed_range(Device& device, int min_speed, int max_speed) { auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); int old_min = guard.get<&Device::min_speed>(); int old_max = guard.get<&Device::max_speed>(); - guard.set < &Device::min_speed > (min_speed); - guard.set < &Device::max_speed > (max_speed); + guard.set<&Device::min_speed>(min_speed); + guard.set<&Device::max_speed>(max_speed); if (min_speed <= max_speed) { return true; } - guard.set < &Device::min_speed > (old_min); - guard.set < &Device::max_speed > (old_max); + guard.set<&Device::min_speed>(old_min); + guard.set<&Device::max_speed>(old_max); return false; } static void runtime_read_int(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) { @@ -116,10 +147,15 @@ static void runtime_read_int(void* context, std::size_t, std::string_view, const } int main() { static_assert(Property_Described_Object); + static_assert(!Can_Write_Immutable); + static_assert(!Can_Lock_Immutable_Unique); const auto& schema = type_descriptor(); using Schema = type_descriptor_schema_t; static_assert(Valid_Property_Schema); static_assert(Schema::property_count == 5); + using Immutable_Property = typename Schema::template property_type<4>; + static_assert(Immutable_Property::readable); + static_assert(!Immutable_Property::writable); REQUIRE(schema.template property<0>().key() == "temperature"); REQUIRE(schema.template property<&Device::temperature>().key() == "temperature"); REQUIRE(schema.template property<&Device::min_speed>().key() == "minimum_speed"); @@ -128,20 +164,24 @@ int main() { static_assert(Temperature_Property::template has_attribute); REQUIRE(Temperature_Property::template attribute_type::value == 7); Device device; + REQUIRE(device.resolved_synchronization().lock_count == 3); + REQUIRE(device.lock_slot<&Device::immutable_id>() == Resolved_Synchronization_View::unsynchronized_slot); REQUIRE(device.temperature == 20); device.temperature = 21; REQUIRE(device.read<&Device::temperature>() == 21); - device.write < &Device::temperature > (22); + device.write<&Device::temperature>(22); REQUIRE(device.temperature == 22); + REQUIRE(device.read<&Device::immutable_id>() == 7); REQUIRE(&device.unsafe_object() == &device); REQUIRE(&device.schema() == &schema); REQUIRE(device.lock_slot<&Device::min_speed>() == device.lock_slot<&Device::max_speed>()); REQUIRE(device.lock_slot<&Device::temperature>() != device.lock_slot<&Device::pressure>()); - REQUIRE(device.lock_slot<&Device::immutable_id>() == Resolved_Synchronization_View::unsynchronized_slot); Device shared_device{property_synchronization(synchronization(sync_all_shared))}; + REQUIRE(shared_device.resolved_synchronization().lock_count == 1); REQUIRE(shared_device.lock_slot<&Device::temperature>() == shared_device.lock_slot<&Device::pressure>()); REQUIRE(shared_device.lock_slot<&Device::pressure>() == shared_device.lock_slot<&Device::min_speed>()); - Device grouped_device{property_synchronization(synchronization(sync_all_independent, sync_group < &Device::temperature, &Device::pressure > ("environment")))}; + REQUIRE(shared_device.lock_slot<&Device::immutable_id>() == Resolved_Synchronization_View::unsynchronized_slot); + Device grouped_device{property_synchronization(synchronization(sync_all_independent, sync_group<&Device::temperature, &Device::pressure>("environment")))}; REQUIRE(grouped_device.lock_slot<&Device::temperature>() == grouped_device.lock_slot<&Device::pressure>()); Device shared_copy{shared_device}; REQUIRE(shared_copy.lock_slot<&Device::temperature>() == shared_copy.lock_slot<&Device::pressure>()); @@ -151,11 +191,7 @@ int main() { auto assignment_target_slot = assignment_target.lock_slot<&Device::pressure>(); assignment_target = shared_device; REQUIRE(assignment_target.lock_slot<&Device::pressure>() == assignment_target_slot); - device.external().write < &Device::temperature > (30); - REQUIRE(device.external().read<&Device::temperature>() == 30); - device.persistence().load < &Device::pressure > (101); - REQUIRE(device.persistence().store<&Device::pressure>() == 101); - auto validation = validate_property_value < &Device::temperature > (device.schema(), 500); + auto validation = validate_property_value<&Device::temperature>(device.schema(), 500); REQUIRE(validation.has_value()); REQUIRE(validation->code == "max_value"); REQUIRE(!update_speed_range(device, 200, 100)); @@ -170,24 +206,25 @@ int main() { }); REQUIRE(schema_visits == 5); std::size_t value_visits = 0; - device.external().for_each_readable_locked([&](auto, const auto&, const auto&) { + device.for_each_readable_locked([&](auto, const auto&, const auto&) { ++value_visits; }); REQUIRE(value_visits == 5); Computed_Device computed; - REQUIRE(computed.external().read_key<"speed_span">() == 90); - computed.write < &Computed_Device::min_speed > (20); - REQUIRE(computed.external().read_key<"speed_span">() == 80); + REQUIRE(computed.lock_slot<&Computed_Device::fixed_offset>() == Resolved_Synchronization_View::unsynchronized_slot); + REQUIRE(computed.read_key<"speed_span">() == 95); + computed.write<&Computed_Device::min_speed>(20); + REQUIRE(computed.read_key<"speed_span">() == 85); Lockless_Device lockless; REQUIRE(lockless.read<&Lockless_Device::left>() == 1); - lockless.write < &Lockless_Device::right > (4); + lockless.write<&Lockless_Device::right>(4); REQUIRE(lockless.read_key<"sum">() == 5); auto lockless_guard = lockless.lock_shared<&Lockless_Device::left, &Lockless_Device::right>(); REQUIRE(lockless_guard.get<&Lockless_Device::left>() == 1); REQUIRE(lockless_guard.get<&Lockless_Device::right>() == 4); Non_Copyable_Device non_copyable; bool non_copyable_visited = false; - non_copyable.external().for_each_readable_locked([&](auto, const auto&, const auto& value) { + non_copyable.for_each_readable_locked([&](auto, const auto&, const auto& value) { REQUIRE(*value == 42); non_copyable_visited = true; }); @@ -197,27 +234,41 @@ int main() { REQUIRE(erased.runtime_property_count() == 5); int runtime_value = 0; REQUIRE(erased.runtime_read("temperature", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok); - REQUIRE(runtime_value == 30); - int intrinsic_runtime_write_value = 34; - REQUIRE(erased.runtime_write("immutable_id", typeid(int), &intrinsic_runtime_write_value) == Runtime_Access_Result::ok); - REQUIRE(device.immutable_id == 34); - REQUIRE(erased.runtime_read(Managed_Access_Mode::external, "temperature", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok); - REQUIRE(runtime_value == 30); + REQUIRE(runtime_value == 22); + REQUIRE(erased.runtime_read("immutable_id", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok); + REQUIRE(runtime_value == 7); int runtime_write_value = 35; - REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "temperature", typeid(int), &runtime_write_value) == Runtime_Access_Result::ok); + REQUIRE(erased.runtime_write("temperature", typeid(int), &runtime_write_value) == Runtime_Access_Result::ok); REQUIRE(device.temperature == 35); - REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "immutable_id", typeid(int), &runtime_write_value) == Runtime_Access_Result::not_writable); - int persistence_write_value = 102; - REQUIRE(erased.runtime_write(Managed_Access_Mode::persistence, "pressure", typeid(int), &persistence_write_value) == Runtime_Access_Result::ok); - REQUIRE(device.pressure == 102); - REQUIRE(erased.runtime_read(Managed_Access_Mode::persistence, "immutable_id", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok); - REQUIRE(runtime_value == 34); - REQUIRE(erased.runtime_write(Managed_Access_Mode::persistence, "immutable_id", typeid(int), &persistence_write_value) == Runtime_Access_Result::not_writable); + REQUIRE(erased.runtime_write("immutable_id", typeid(int), &runtime_write_value) == Runtime_Access_Result::not_writable); double wrong_type = 1.0; - REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "temperature", typeid(double), &wrong_type) == Runtime_Access_Result::type_mismatch); - REQUIRE(erased.runtime_write(Managed_Access_Mode::external, "missing", typeid(int), &runtime_write_value) == Runtime_Access_Result::unknown_property); - const Device& const_device = device; - static_assert(!Has_Write_Temperature); + REQUIRE(erased.runtime_write("temperature", typeid(double), &wrong_type) == Runtime_Access_Result::type_mismatch); + REQUIRE(erased.runtime_write("missing", typeid(int), &runtime_write_value) == Runtime_Access_Result::unknown_property); + Read_Only_Device read_only_device; + REQUIRE(read_only_device.resolved_synchronization().lock_count == 1); + REQUIRE(read_only_device.lock_slot<&Read_Only_Device::id>() == Resolved_Synchronization_View::unsynchronized_slot); + REQUIRE(read_only_device.lock_slot<&Read_Only_Device::version>() == Resolved_Synchronization_View::unsynchronized_slot); + REQUIRE(read_only_device.lock_slot<&Read_Only_Device::value>() == 0); + Pure_Read_Only_Device pure_read_only; + REQUIRE(pure_read_only.resolved_synchronization().lock_count == 0); + REQUIRE(pure_read_only.lock_slot<&Pure_Read_Only_Device::id>() == Resolved_Synchronization_View::unsynchronized_slot); + REQUIRE(pure_read_only.lock_slot<&Pure_Read_Only_Device::version>() == Resolved_Synchronization_View::unsynchronized_slot); + REQUIRE(pure_read_only.read<&Pure_Read_Only_Device::id>() == 21); + REQUIRE(pure_read_only.read<&Pure_Read_Only_Device::version>() == 4); + Property_Object_Base& read_only_erased = read_only_device; + std::binary_semaphore read_only_done{0}; + std::jthread read_only_reader; + { + auto guard = read_only_device.lock_unique<&Read_Only_Device::value>(); + read_only_reader = std::jthread([&] { + int value = 0; + REQUIRE(read_only_erased.runtime_read("id", &value, &runtime_read_int) == Runtime_Access_Result::ok); + REQUIRE(value == 11); + read_only_done.release(); + }); + REQUIRE(read_only_done.try_acquire_for(std::chrono::milliseconds(100))); + } + read_only_reader.join(); std::binary_semaphore runtime_blocked_done{0}; std::jthread runtime_writer; { @@ -236,25 +287,33 @@ int main() { { auto guard = device.lock_unique({"temperature"}); std::jthread writer([&] { - device.write < &Device::pressure > (200); + device.write<&Device::pressure>(200); independent_done.release(); }); REQUIRE(independent_done.try_acquire_for(std::chrono::seconds(2))); } + bool dynamic_read_only_unique_rejected = false; + try { + auto guard = device.lock_unique({"immutable_id"}); + (void) guard; + } catch (const std::invalid_argument&) { + dynamic_read_only_unique_rejected = true; + } + REQUIRE(dynamic_read_only_unique_rejected); std::barrier gate(2); std::atomic completed{0}; std::jthread first([&] { gate.arrive_and_wait(); auto guard = device.lock_unique({"temperature", "pressure"}); - guard.set < &Device::temperature > (40); - guard.set < &Device::pressure > (140); + guard.set<&Device::temperature>(40); + guard.set<&Device::pressure>(140); completed.fetch_add(1, std::memory_order_release); }); std::jthread second([&] { gate.arrive_and_wait(); auto guard = device.lock_unique({"pressure", "temperature"}); - guard.set < &Device::pressure > (141); - guard.set < &Device::temperature > (41); + guard.set<&Device::pressure>(141); + guard.set<&Device::temperature>(41); completed.fetch_add(1, std::memory_order_release); }); first.join(); @@ -262,7 +321,7 @@ int main() { REQUIRE(completed.load(std::memory_order_acquire) == 2); Device copied = device; REQUIRE(copied.temperature == device.temperature); - copied.write < &Device::temperature > (99); + copied.write<&Device::temperature>(99); REQUIRE(device.read<&Device::temperature>() != copied.read<&Device::temperature>()); return 0; } diff --git a/docs/CORE_GUIDE.md b/docs/CORE_GUIDE.md index 17addf2..6280736 100644 --- a/docs/CORE_GUIDE.md +++ b/docs/CORE_GUIDE.md @@ -2,227 +2,235 @@ [中文](CORE_GUIDE.zh-CN.md) -## 1. Include and target - -Include the complete Core surface with: +## 1. Include and CMake target ```cpp #include ``` -CMake target: - ```cmake target_link_libraries(my_target PRIVATE structive::property_core) ``` -The Core target is header-only and requires C++20. +Property Core is C++20. ## 2. Define a managed object -A managed object normally derives from `Property_Object`: - ```cpp using namespace structive; struct Device : Property_Object { double temperature{25.0}; double pressure{101.3}; - double min_speed{10.0}; - double max_speed{100.0}; - std::string name{"device-1"}; + int serial_number{1001}; }; ``` -The members remain ordinary C++ members. +`Property_Object` adds managed operations. The fields remain ordinary members. ## 3. Define the type descriptor -Specialize `Type_Descriptor` and return an `Object_Schema` through `object(...)`: - ```cpp template <> struct structive::Type_Descriptor { static auto get() { return object( - defaults( - external_access, - persistence_access - ), - synchronization( - sync_all_independent, - sync_group<&Device::min_speed, &Device::max_speed>("speed_range") - ), + synchronization(sync_all_independent), field<&Device::temperature>( key<"temperature">, unit<"C">, min_value<-50.0>, max_value<200.0> ), - field<&Device::pressure>(key<"pressure">, unit<"kPa">), - field<&Device::min_speed>(key<"min_speed">), - field<&Device::max_speed>(key<"max_speed">), - field<&Device::name>(key<"name">) + field<&Device::pressure>( + key<"pressure">, + unit<"kPa"> + ), + field<&Device::serial_number>( + key<"serial_number">, + read_only + ) ); } }; ``` -`field(...)` is an alias of `property(...)` and produces a member-backed `Property_Descriptor`. +The descriptor is the structural definition of `Device`. ## 4. Schema guarantees -The schema enforces several structural conditions: +A valid schema guarantees: -- every property has a non-empty `key`; -- property keys are unique; -- the same member storage identity cannot be registered twice; -- single-valued Attribute categories cannot appear more than once on the same declaration; -- constraints must be compatible with the property value type; -- object defaults may contain only inheritable Attributes; -- external/persistence capabilities cannot claim operations unsupported by the accessor. +- every registered property has a non-empty key; +- keys are unique; +- member selectors belong to the described object type; +- single-valued Attribute categories are not duplicated on the same property; +- capability metadata cannot request operations unsupported by the underlying accessor; +- constraints are compatible with the property value type. -Many schema errors are therefore compile-time errors. +Use: + +```cpp +static_assert(Property_Described_Object); +using Schema = type_descriptor_schema_t; +static_assert(Valid_Property_Schema); +``` ## 5. Access the schema -For a described type: - ```cpp const auto& schema = type_descriptor(); +const auto& same_schema = device.schema(); ``` -For an instance: - -```cpp -Device device; -const auto& schema = device.schema(); -``` - -Property lookup supports a numeric compile-time index or a member pointer: +Properties can be selected by index or member pointer: ```cpp const auto& first = schema.property<0>(); const auto& temperature = schema.property<&Device::temperature>(); ``` -Use member pointers in typed business code. Use indexes mainly in generic traversal. - ## 6. Property descriptor information -A property descriptor exposes: +A property descriptor exposes compile-time structural facts: ```cpp -auto key_value = temperature.key(); -``` - -and compile-time traits such as: - -```cpp -using Property = std::remove_cvref_t; +using Property = std::remove_cvref_t())>; static_assert(Property::readable); static_assert(Property::writable); +using Value = Property::value_type; +using Accessor = Property::accessor_type; ``` -Category-based Attribute lookup is available through: +A `read_only` field reports: ```cpp -static_assert(Property::has_attribute); -const auto& attribute = temperature.attribute(); +using Serial = std::remove_cvref_t())>; +static_assert(Serial::readable); +static_assert(!Serial::writable); ``` -All declared Attributes can be traversed: +The key is available at runtime without dynamic allocation: ```cpp -temperature.for_each_attribute([](const auto& attribute) { - // inspect attribute type/value -}); +auto key_value = schema.property<&Device::temperature>().key(); ``` -Constraints can be traversed separately: +## 7. Intrinsic capability + +Core capability values are: ```cpp -temperature.for_each_constraint([](const auto& constraint_value) { - // inspect or evaluate a constraint -}); +Property_Capability::none +Property_Capability::read +Property_Capability::write +Property_Capability::read_write ``` -## 7. Object defaults and effective Attributes - -`defaults(...)` provides object-wide values for inheritable Attribute categories: +Convenience Attributes are: ```cpp -defaults( - external_access, - persistence_access, - sensitive -) +read_only +write_only +read_write +inaccessible ``` -A property may override a default: +The capability belongs to the property itself. It is not an authorization rule. -```cpp -field<&Device::name>( - key<"name">, - external_access -) +If capability metadata is absent, Structive derives capability from the accessor. + +For a normal non-const member field, the default is read/write. + +For a getter-only computed property, the default is read-only. + +Capability metadata can narrow the accessor, but it cannot create an operation the accessor does not support. + +## 8. No access-control API + +Property Core intentionally does not provide access modes or domain views. + +There is no built-in distinction between: + +```text +internal +external +persistence ``` -Core exposes effective access traits such as `effective_external_access_v`, `external_readable_v`, `external_writable_v`, `persistence_loadable_v`, `persistence_storable_v` and `effective_sensitive_v` for generic code. +A consumer decides its own policy. For example, a GUI may choose to expose only selected properties even though all of them are structurally readable. -Extensions can use `declared_effective_attribute(schema)` for extension-owned inheritable categories when either the property or object defaults declare that category. +Core only exposes intrinsic `readable` and `writable` facts. -## 8. Core Attributes +## 9. Core Attributes -### 8.1 Key +### 9.1 Key + +Every property requires one key: ```cpp key<"temperature"> ``` -Required for every property. Non-empty and unique per schema. +The key is the structural protocol identifier used by runtime lookup and adapters. -### 8.2 External access +### 9.2 Capability ```cpp -external_access -external_access -external_access -external_access +read_only +write_only +read_write +inaccessible ``` -This Attribute is inheritable through `defaults(...)`. +Capability Attributes are single-valued and non-inheritable. -### 8.3 Persistence access - -```cpp -persistence_access -persistence_access -persistence_access -persistence_access -``` - -This Attribute is also inheritable. - -### 8.4 Unit +### 9.3 Unit ```cpp unit<"C"> +unit<"kPa"> ``` -This is descriptive metadata and is not inheritable. +Core stores unit metadata but does not perform conversion. -### 8.5 Sensitive +### 9.4 Sensitive ```cpp sensitive<> sensitive ``` -This is inheritable metadata. Core exposes its effective value but does not automatically redact data. +`sensitive` is inheritable metadata. It does not implement authorization. A consumer may use it as one input to its own policy. -## 9. Constraints and validation +## 10. Custom Attributes and defaults -Built-in constraints: +Any type following the Attribute protocol can be attached to a property. + +```cpp +struct Group_Category {}; +template +struct Group_Attribute { + using attribute_category = Group_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = true; + static constexpr auto value = Value; +}; +``` + +Inheritable Attributes can be supplied by `defaults(...)`: + +```cpp +object( + defaults(sensitive<>), + field<&Device::temperature>(key<"temperature">) +) +``` + +Capability is intentionally non-inheritable because each property owns its intrinsic operation set. + +## 11. Constraints and validation + +Built-in constraints include: ```cpp min_value<0> @@ -230,7 +238,7 @@ max_value<100> finite ``` -Custom constraint: +Custom constraints: ```cpp constraint<"even">([](int value) { @@ -238,291 +246,241 @@ constraint<"even">([](int value) { }) ``` -Validation by member pointer: +Validation is explicit: ```cpp -auto error = validate_property_value<&Device::temperature>(device.schema(), candidate); +auto result = validate_property_value<&Device::temperature>(device.schema(), candidate); +if (result) { + auto key_value = result->property_key; + auto code = result->code; +} ``` -Validation by compile-time key: +`write()` does not automatically call validation. -```cpp -auto error = validate_property_key_value<"temperature">(device.schema(), candidate); -``` +## 12. Typed managed read/write -A failure returns: - -```cpp -struct Validation_Error { - std::string_view property_key; - std::string_view code; -}; -``` - -Validation is explicit and is not automatically executed by managed writes. - -## 10. Intrinsic managed read/write - -Typed member-pointer access: +Member-pointer access is the preferred C++ API: ```cpp auto temperature = device.read<&Device::temperature>(); device.write<&Device::temperature>(30.0); ``` -Compile-time key access: +Compile-time key access is also available: ```cpp auto temperature = device.read_key<"temperature">(); device.write_key<"temperature">(30.0); ``` -These operations use the property's intrinsic capability directly. There is no separate `internal` capability mode; readability and writability come from the accessor itself. - -`read()` returns a value object, not a reference to storage. The read is performed while the configured shared lock is held when synchronization is enabled. - -## 11. External capability view - -Object-level defaults may expose properties externally: +The typed interfaces are constrained by intrinsic capability. A write to `read_only` does not participate in overload resolution. ```cpp -defaults(external_access) +template +concept Can_Write_Serial = requires(Object& object) { + object.template write<&Device::serial_number>(1); +}; +static_assert(!Can_Write_Serial); ``` -Use: +## 13. Read-only fast path + +Stored read-only properties are statically removed from managed locking. + +Given: ```cpp -device.external().write<&Device::temperature>(30.0); -auto value = device.external().read<&Device::temperature>(); +field<&Device::serial_number>(key<"serial_number">, read_only) ``` -A const object produces a const capability view and therefore has no write API. +Structive resolves: -The typed API rejects statically inaccessible operations at compile time. - -## 12. Persistence capability view - -Use persistence terminology instead of generic write/read: - -```cpp -device.persistence().load<&Device::temperature>(30.0); -auto value = device.persistence().store<&Device::temperature>(); +```text +serial_number -> unsynchronized_slot ``` -Compile-time key forms are also available: +and `read<&Device::serial_number>()` does not query a lock slot or construct a `shared_lock`. -```cpp -device.persistence().load_key<"temperature">(30.0); -auto value = device.persistence().store_key<"temperature">(); -``` +This remains true even when the broad default is `sync_all_shared`. -The persistence view is controlled by `Persistence_Access` metadata. +If an object has only stored read-only properties, those properties contribute zero mutexes to `resolved_synchronization().lock_count`. -## 13. Synchronization plans +## 14. Synchronization plans -### 13.1 Default independent - -The default `Synchronization_Plan` is independent: each synchronized property receives its own logical lock domain. - -Explicit form: +### 14.1 Independent ```cpp synchronization(sync_all_independent) ``` -### 13.2 Shared +Each property that actually requires synchronization gets its own lock domain. + +### 14.2 Shared ```cpp synchronization(sync_all_shared) ``` -All properties use the same lock slot unless overridden. +All properties that require synchronization share one lock domain. -### 13.3 Unsynchronized +Read-only stored properties are excluded before lock slots are materialized. + +### 14.3 Unsynchronized ```cpp synchronization(sync_all_unsynchronized) ``` -Properties use the `unsynchronized_slot` and no real Structive mutex protects them. +Managed access performs no real locking even for writable properties. -### 13.4 Per-property override - -By member pointer: +### 14.4 Per-property override ```cpp -sync_independent<&Device::temperature>() -sync_unsynchronized<&Device::immutable_id>() +synchronization( + sync_all_independent, + sync_unsynchronized<&Device::temperature>() +) ``` -Runtime string forms also exist: +Dynamic-key rule forms are also available. + +### 14.5 Groups ```cpp -sync_independent("temperature") -sync_unsynchronized("immutable_id") +synchronization( + sync_all_independent, + sync_group<&Device::min_speed, &Device::max_speed>("speed_range") +) ``` -Prefer member-pointer rules when the property is statically known. +Members in one group resolve to the same lock slot if they require synchronization. -### 13.5 Groups +## 15. Per-instance synchronization override -Member-pointer form: +An object can explicitly override the type default: ```cpp -sync_group<&Device::min_speed, &Device::max_speed>("speed_range") -``` - -String form: - -```cpp -sync_group("speed_range", "min_speed", "max_speed") -``` - -All properties in a group share one lock slot. - -The resolver rejects unknown runtime keys, duplicate group names, empty groups and multiple explicit configurations of the same property. - -## 14. Per-instance synchronization override - -The type descriptor supplies a default synchronization plan, but an instance may override it through `Property_Synchronization`: - -```cpp -struct Device : Property_Object { - Device() = default; - explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} -}; -Device shared_device{ - property_synchronization(synchronization(sync_all_shared)) -}; -``` - -For schema-aware compile-time synchronization specifications: - -```cpp -auto policy = property_synchronization( - synchronization( - sync_all_independent, - sync_group<&Device::temperature, &Device::pressure>("environment") +Device device{ + property_synchronization( + synchronization(sync_all_shared) ) -); +}; ``` -The schema remains the same; only that instance uses an override lock topology. Default instances share one resolved topology per type, so the default plan is not re-resolved for every object. +For typed member rules: -## 15. Inspect resolved synchronization +```cpp +Device device{ + property_synchronization( + synchronization( + sync_all_independent, + sync_group<&Device::temperature, &Device::pressure>("environment") + ) + ) +}; +``` -For a typed member: +The default topology is shared per type. Only an object with an explicit override stores its compact override layout. + +## 16. Inspect resolved synchronization + +```cpp +auto view = device.resolved_synchronization(); +auto count = view.lock_count; +``` + +Inspect a member slot: ```cpp auto slot = device.lock_slot<&Device::temperature>(); ``` -For the complete view: +No-lock properties use: ```cpp -auto resolved = device.resolved_synchronization(); -auto count = resolved.lock_count; -auto slot = resolved.slot(0); -auto uses_lock = resolved.uses_lock(0); +Resolved_Synchronization_View::unsynchronized_slot ``` -`Resolved_Synchronization_View::unsynchronized_slot` identifies unsynchronized properties. +For a read-only stored property this is automatic. -## 16. Multi-property static guards +## 17. Static multi-property guards -Shared guard: +Read guard: ```cpp auto guard = device.lock_shared<&Device::temperature, &Device::pressure>(); auto temperature = guard.get<&Device::temperature>(); -auto pressure = guard.get<&Device::pressure>(); ``` -Unique guard: +Write guard: ```cpp auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); -auto old_min = guard.get<&Device::min_speed>(); -guard.set<&Device::min_speed>(20.0); -guard.set<&Device::max_speed>(120.0); +guard.set<&Device::min_speed>(20); +guard.set<&Device::max_speed>(120); ``` -The guard only permits access to properties inside its held synchronization set. +Static guards deduplicate slots and acquire them in stable slot order. -Capability views provide equivalent static guards constrained by their access mode: +A typed unique guard requires all selected properties to be intrinsically writable. + +## 18. Dynamic-key guards ```cpp -auto guard = device.external().lock_shared<&Device::temperature>(); +auto read_guard = device.lock_shared({"temperature", "pressure"}); +auto write_guard = device.lock_unique({"temperature", "pressure"}); ``` -## 17. Dynamic-key guards +Unknown keys throw `std::invalid_argument`. -When a property set is known only at runtime: +A dynamic unique guard rejects a read-only property because the key is only known at runtime. + +## 19. Traversal + +Schema-only traversal: ```cpp -std::array keys{"temperature", "pressure"}; -auto guard = device.lock_shared(keys); -``` - -or: - -```cpp -auto guard = device.lock_unique({"min_speed", "max_speed"}); -``` - -Dynamic guards validate keys and capability visibility at runtime. Lock slots are sorted and deduplicated before acquisition. - -Read/write through a dynamic guard can still use compile-time members/keys after the guard is acquired; access outside the held set throws `std::logic_error`. - -## 18. Traversal - -Schema traversal: - -```cpp -schema.for_each_property([](auto index, const auto& property) { - // compile-time index and descriptor +schema.for_each_property([&](auto index, const auto& descriptor) { }); ``` -Managed value traversal: +Readable value traversal: ```cpp -device.for_each_readable([](auto index, const auto& descriptor, const auto& value) { - // one managed read per property +device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) { }); ``` -Locked traversal acquires the complete readable synchronization set first: +Locked readable traversal: ```cpp -device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) { - // all selected readable properties are held under the guard +device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) { }); ``` -For coordinated writable access: +The locked traversal acquires only actual synchronization slots. Read-only stored properties do not introduce locks. + +Writable transaction-style guard acquisition: ```cpp -device.with_all_writable_locked([](auto& guard) { - // use guard.get / guard.set +device.with_all_writable_locked([&](auto& guard) { }); ``` -The persistence view provides `with_all_loadable_locked(...)`. +This acquires all writable managed lock domains; it does not perform validation or rollback automatically. -## 19. Computed property +## 20. Computed properties A synchronized computed property receives a read view: ```cpp -computed_property([](const auto& view) { +computed_property([](const auto& view) { return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); -}, key<"speed_span">, external_access) +}, key<"speed_span">) ``` -The computed property itself and every dependency read through the view must resolve to the same lock slot. - -For example: +Writable dependencies that must form one snapshot should share the computed synchronization domain: ```cpp synchronization( @@ -531,27 +489,27 @@ synchronization( ) ``` -Computed properties are read-only. +A read-only stored dependency may be read through the computed view without joining a lock slot, because it has no managed writer. -## 20. Trusted accessor properties +## 21. Trusted accessor properties -Core also supports member-function-based trusted access: +Trusted getter: ```cpp -trusted_computed_property<&Device::get_temperature>(key<"temperature">) +trusted_computed_property<&Device::value>(key<"value">) ``` -and getter/setter pairs: +Trusted getter/setter: ```cpp -trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">) +trusted_accessor_property<&Device::get_value, &Device::set_value>(key<"value">) ``` -These accessors invoke the object member functions directly and mark themselves as trusted object access. They do not use the synchronized computed-view dependency restriction. Use them when the member functions themselves define the correct synchronization/consistency contract. +These forms access the object directly rather than using synchronized dependency views. They should only be used when the caller understands the synchronization contract of those member functions. -## 21. Runtime type-erased access +## 22. Runtime type-erased access -Any `Property_Object` is also a `Property_Object_Base`: +Use `Property_Object_Base` for dynamic adapters: ```cpp Property_Object_Base& erased = device; @@ -564,112 +522,75 @@ erased.runtime_object_type(); erased.runtime_property_count(); ``` -Intrinsic runtime write does not require a mode: +Read: ```cpp -double value = 35.0; -auto result = erased.runtime_write( - "temperature", - typeid(double), - &value -); +auto result = erased.runtime_read("temperature", context, callback); ``` -A boundary projection is selected explicitly when an adapter must respect external or persistence capability metadata: +Write: ```cpp -auto external_result = erased.runtime_write( - Managed_Access_Mode::external, - "temperature", - typeid(double), - &value -); -``` - -Runtime read uses the same overload model and returns the value through a callback: - -```cpp -static void read_double(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) { - if (type == typeid(double)) { - *static_cast(context) = *static_cast(value); - } -} -double output = 0.0; -auto result = erased.runtime_read( - "temperature", - &output, - &read_double -); -auto external_result = erased.runtime_read( - Managed_Access_Mode::external, - "temperature", - &output, - &read_double -); +auto result = erased.runtime_write("temperature", typeid(double), &value); ``` Possible results: -```cpp -Runtime_Access_Result::ok -Runtime_Access_Result::unknown_property -Runtime_Access_Result::not_readable -Runtime_Access_Result::not_writable -Runtime_Access_Result::type_mismatch +```text +ok +unknown_property +not_readable +not_writable +type_mismatch ``` -The intrinsic runtime overload uses the accessor's intrinsic capability and the same synchronization topology as typed `read()`/`write()`. The overload taking `Managed_Access_Mode` applies the selected external or persistence projection before using the same synchronization rules. +Runtime access has no external/persistence mode. An adapter owns its own policy and chooses whether it calls the intrinsic read/write operation. -## 22. Lock policy +Runtime read of a stored read-only property follows the same no-lock fast path as typed read. + +## 23. Lock policies Default: ```cpp -struct Device : Property_Object { -}; +Property_Object ``` -Equivalent shorthand: +No-lock: ```cpp -struct Device : Property_Object { -}; +Property_Object ``` -A no-op lock policy exists: +`No_Lock_Policy` stores no real mutex array and avoids real lock objects on managed hot paths. -```cpp -struct Device : Property_Object { -}; -``` - -`No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct. Its default managed-object path stores no mutex array and performs no per-instance heap allocation for synchronization. A custom `Property_Synchronization` may still allocate a compact override topology because computed-property synchronization-domain checks must preserve the selected per-instance layout. - -## 23. Raw object access - -`unsafe_object()` exposes the derived object directly: +## 24. Raw object access ```cpp Device& raw = device.unsafe_object(); ``` -Direct field access is also normal C++: +or normal public member access: ```cpp -device.temperature = 40.0; +device.temperature = 30.0; ``` -These paths intentionally bypass Structive-managed access. They are useful when the caller already owns the required synchronization or when a field is being handled outside Structive’s managed contract. +Both bypass the Structive managed contract. -## 24. Recommended usage rules +This is intentional. Structive is cooperative structural infrastructure, not forced encapsulation. -- Use member pointers for typed business access. -- Use string keys at runtime integration boundaries. -- Treat keys as protocol identifiers. -- Keep object-wide access policy in `defaults(...)` and override only exceptions. -- Use synchronization groups for fields that must be observed consistently. -- Validate explicitly at the business operation boundary. -- Use static multi-property guards for cross-field invariants. -- Prefer synchronized computed properties over trusted accessors. -- Use runtime access for adapters, not ordinary typed code. -- Use raw access only when the caller intentionally owns the missing managed guarantees. +## 25. Recommended usage rules + +Use these defaults: + +1. Use ordinary C++ members for storage. +2. Register only fields that belong to the structural model. +3. Prefer member-pointer typed APIs in C++ business code. +4. Use `read_only` when the Structive managed model must never write a stored property. +5. Rely on the resulting zero-lock optimization for stored read-only data. +6. Keep raw writes to read-only properties outside concurrent managed code. +7. Use synchronization groups for mutable cross-field consistency. +8. Keep validation explicit. +9. Use runtime access only for genuinely dynamic adapters. +10. Let GUI/RPC/persistence/authorization layers own their own exposure and access policy. diff --git a/docs/CORE_GUIDE.zh-CN.md b/docs/CORE_GUIDE.zh-CN.md index 62f7529..0ebd9b3 100644 --- a/docs/CORE_GUIDE.zh-CN.md +++ b/docs/CORE_GUIDE.zh-CN.md @@ -4,225 +4,227 @@ ## 1. Include 与 CMake Target -完整 Core 接口: - ```cpp #include ``` -CMake: - ```cmake target_link_libraries(my_target PRIVATE structive::property_core) ``` -Core 是 header-only,要求 C++20。 +Property Core 使用 C++20。 ## 2. 定义 Managed Object -通常让业务对象继承 `Property_Object`: - ```cpp using namespace structive; struct Device : Property_Object { double temperature{25.0}; double pressure{101.3}; - double min_speed{10.0}; - double max_speed{100.0}; - std::string name{"device-1"}; + int serial_number{1001}; }; ``` -这些成员仍然都是普通 C++ 成员。 +`Property_Object` 增加 managed operation,字段本身仍是普通成员。 ## 3. 定义 Type Descriptor -特化 `Type_Descriptor`,通过 `object(...)` 返回 `Object_Schema`: - ```cpp template <> struct structive::Type_Descriptor { static auto get() { return object( - defaults( - external_access, - persistence_access - ), - synchronization( - sync_all_independent, - sync_group<&Device::min_speed, &Device::max_speed>("speed_range") - ), + synchronization(sync_all_independent), field<&Device::temperature>( key<"temperature">, unit<"C">, min_value<-50.0>, max_value<200.0> ), - field<&Device::pressure>(key<"pressure">, unit<"kPa">), - field<&Device::min_speed>(key<"min_speed">), - field<&Device::max_speed>(key<"max_speed">), - field<&Device::name>(key<"name">) + field<&Device::pressure>( + key<"pressure">, + unit<"kPa"> + ), + field<&Device::serial_number>( + key<"serial_number">, + read_only + ) ); } }; ``` -`field(...)` 是 `property(...)` 的别名,产生 member-backed `Property_Descriptor`。 +Descriptor 就是 `Device` 的结构定义。 -## 4. Schema 提供的静态保证 +## 4. Schema 静态保证 -Schema 会检查: +合法 Schema 保证: -- 每个 property 都必须拥有非空 `key`; -- 同一 Schema 内 key 唯一; -- 同一个成员存储不能重复注册; -- single-valued Attribute category 不能在同一声明中重复; -- constraint 必须能作用于 property value type; -- `defaults(...)` 只能放 inheritable Attribute; -- External/Persistence capability 不能声明 accessor 实际不支持的读写能力。 +- 每个 Property 都有非空 key; +- key 唯一; +- member selector 属于对应 object type; +- 同一 Property 上 single-valued Attribute category 不重复; +- capability metadata 不会要求底层 Accessor 不支持的操作; +- Constraint 与 Property value type 兼容。 -因此大量结构错误会直接成为编译期错误。 +```cpp +static_assert(Property_Described_Object); +using Schema = type_descriptor_schema_t; +static_assert(Valid_Property_Schema); +``` ## 5. 获取 Schema -类型级: - ```cpp const auto& schema = type_descriptor(); +const auto& same_schema = device.schema(); ``` -实例级: - -```cpp -Device device; -const auto& schema = device.schema(); -``` - -可以通过编译期 index 或成员指针定位 property: +按 index 或 member pointer 获取 Property: ```cpp const auto& first = schema.property<0>(); const auto& temperature = schema.property<&Device::temperature>(); ``` -业务 typed code 优先成员指针;index 主要用于泛型遍历。 - ## 6. Property Descriptor -获取 key: +Descriptor 暴露编译期结构事实: ```cpp -auto key_value = temperature.key(); -``` - -静态能力: - -```cpp -using Property = std::remove_cvref_t; +using Property = std::remove_cvref_t())>; static_assert(Property::readable); static_assert(Property::writable); +using Value = Property::value_type; +using Accessor = Property::accessor_type; ``` -按 category 查询 Attribute: +`read_only` 字段: ```cpp -static_assert(Property::has_attribute); -const auto& attribute = temperature.attribute(); +using Serial = std::remove_cvref_t())>; +static_assert(Serial::readable); +static_assert(!Serial::writable); ``` -遍历全部声明 Attribute: +运行时可直接取得 key: ```cpp -temperature.for_each_attribute([](const auto& attribute) { - // 根据 attribute 类型处理 -}); +auto key_value = schema.property<&Device::temperature>().key(); ``` -单独遍历 constraint: +## 7. Intrinsic Capability + +Core capability: ```cpp -temperature.for_each_constraint([](const auto& constraint_value) { - // 检查 constraint -}); +Property_Capability::none +Property_Capability::read +Property_Capability::write +Property_Capability::read_write ``` -## 7. Object Defaults 与 Effective Attribute - -`defaults(...)` 给 inheritable Attribute 提供对象级默认值: +便捷 Attribute: ```cpp -defaults( - external_access, - persistence_access, - sensitive -) +read_only +write_only +read_write +inaccessible ``` -Property 可以覆盖: +Capability 属于 Property 自身,不是 authorization rule。 -```cpp -field<&Device::name>( - key<"name">, - external_access -) +不声明 capability 时,Structive 根据 Accessor 自动推导。 + +普通非 const member 默认 read/write;getter-only computed property 默认 read-only。 + +Capability 可以收窄 Accessor,但不能创造 Accessor 本来不存在的操作。 + +## 8. Core 不存在访问控制 API + +Property Core 不提供 domain access mode,也不区分: + +```text +internal +external +persistence ``` -Core 提供 `effective_external_access_v`、`external_readable_v`、`external_writable_v`、`persistence_loadable_v`、`persistence_storable_v`、`effective_sensitive_v` 等有效属性计算入口。 +Consumer 自己决定 policy。例如 GUI 可以只展示部分 Property,即使这些 Property 在结构上都 readable。 -Extension 自己的 inheritable category 可以通过 `declared_effective_attribute(schema)` 读取 property 声明或 object default 中的有效值。 +Core 只给出 intrinsic `readable` / `writable`。 -## 8. Core Attribute +## 9. Core Attribute -### 8.1 Key +### 9.1 Key + +每个 Property 必须有 key: ```cpp key<"temperature"> ``` -每个 property 必须存在,不能为空,同一 Schema 内唯一。 +Key 是 runtime lookup 与 adapter 使用的结构协议身份。 -### 8.2 External Access +### 9.2 Capability ```cpp -external_access -external_access -external_access -external_access +read_only +write_only +read_write +inaccessible ``` -支持在 `defaults(...)` 中继承。 +Capability Attribute 是 single-valued、non-inheritable。 -### 8.3 Persistence Access - -```cpp -persistence_access -persistence_access -persistence_access -persistence_access -``` - -同样支持继承。 - -### 8.4 Unit +### 9.3 Unit ```cpp unit<"C"> +unit<"kPa"> ``` -描述元数据,不支持 object default 继承。 +Core 保存单位信息,但不负责换算。 -### 8.5 Sensitive +### 9.4 Sensitive ```cpp sensitive<> sensitive ``` -支持继承。Core 会计算 effective value,但不会自动执行脱敏或隐藏输出。 +`sensitive` 是可继承 metadata,不实现访问控制。Consumer 可以把它作为自己 policy 的一个输入。 -## 9. Constraint 与 Validation +## 10. 自定义 Attribute 与 Defaults -内置 constraint: +任何遵守 Attribute protocol 的类型都可以挂在 Property 上。 + +```cpp +struct Group_Category {}; +template +struct Group_Attribute { + using attribute_category = Group_Category; + static constexpr bool single_valued = true; + static constexpr bool inheritable = true; + static constexpr auto value = Value; +}; +``` + +Inheritable Attribute 可以放进 `defaults(...)`: + +```cpp +object( + defaults(sensitive<>), + field<&Device::temperature>(key<"temperature">) +) +``` + +Capability 故意不可继承,因为每个 Property 的固有操作集合必须独立成立。 + +## 11. Constraint 与 Validation + +内置 Constraint: ```cpp min_value<0> @@ -230,7 +232,7 @@ max_value<100> finite ``` -自定义 constraint: +自定义 Constraint: ```cpp constraint<"even">([](int value) { @@ -238,291 +240,239 @@ constraint<"even">([](int value) { }) ``` -按成员指针验证: +显式验证: ```cpp -auto error = validate_property_value<&Device::temperature>(device.schema(), candidate); +auto result = validate_property_value<&Device::temperature>(device.schema(), candidate); +if (result) { + auto key_value = result->property_key; + auto code = result->code; +} ``` -按编译期 key 验证: +`write()` 不自动调用 Validation。 -```cpp -auto error = validate_property_key_value<"temperature">(device.schema(), candidate); -``` +## 12. Typed Managed Read/Write -失败结果: - -```cpp -struct Validation_Error { - std::string_view property_key; - std::string_view code; -}; -``` - -Validation 是显式操作,不会在 managed write 中自动执行。 - -## 10. Intrinsic Managed Read/Write - -成员指针形式: +C++ 业务代码优先 member pointer: ```cpp auto temperature = device.read<&Device::temperature>(); device.write<&Device::temperature>(30.0); ``` -编译期 key 形式: +也支持编译期 key: ```cpp auto temperature = device.read_key<"temperature">(); device.write_key<"temperature">(30.0); ``` -这些操作直接使用 Property 的固有能力,不再存在单独的 `internal` capability mode;可读、可写能力由 accessor 本身决定。 - -`read()` 返回值对象而不是底层存储引用。启用同步时,读取发生在配置的 shared lock 持有期间。 - -## 11. External Capability View - -可以通过 object defaults 默认开放 external access: +Typed API 由 intrinsic capability 约束。对 `read_only` 调用 write 时,函数在 overload resolution 阶段就不可用。 ```cpp -defaults(external_access) +template +concept Can_Write_Serial = requires(Object& object) { + object.template write<&Device::serial_number>(1); +}; +static_assert(!Can_Write_Serial); ``` -使用: +## 13. Read-Only Fast Path + +Stored read-only property 在编译期和同步解析阶段都会被裁掉。 ```cpp -device.external().write<&Device::temperature>(30.0); -auto value = device.external().read<&Device::temperature>(); +field<&Device::serial_number>(key<"serial_number">, read_only) ``` -Const object 返回 const capability view,因此没有 write API。 +最终: -Typed API 对静态不可访问操作直接在编译期拒绝。 - -## 12. Persistence Capability View - -Persistence 使用 load/store 语义: - -```cpp -device.persistence().load<&Device::temperature>(30.0); -auto value = device.persistence().store<&Device::temperature>(); +```text +serial_number -> unsynchronized_slot ``` -编译期 key 版本: +`read<&Device::serial_number>()` 不查询 lock slot,也不构造 `shared_lock`。 -```cpp -device.persistence().load_key<"temperature">(30.0); -auto value = device.persistence().store_key<"temperature">(); -``` +即使默认是 `sync_all_shared` 也一样。 -能力由 `Persistence_Access` 元数据决定。 +如果一个对象只有 stored read-only property,这些 Property 对 `resolved_synchronization().lock_count` 的贡献为 0。 -## 13. Synchronization Plan +## 14. Synchronization Plan -### 13.1 Independent - -默认 `Synchronization_Plan` 就是 independent:每个同步 property 拥有独立逻辑锁域。 - -显式写法: +### 14.1 Independent ```cpp synchronization(sync_all_independent) ``` -### 13.2 Shared +每个真正需要同步的 Property 各自一个 lock domain。 + +### 14.2 Shared ```cpp synchronization(sync_all_shared) ``` -除显式 override 外,全部 property 共用一个 lock slot。 +所有真正需要同步的 Property 共用一个 lock domain。 -### 13.3 Unsynchronized +Read-only stored property 在 slot materialization 前就被排除。 + +### 14.3 Unsynchronized ```cpp synchronization(sync_all_unsynchronized) ``` -property 使用 `unsynchronized_slot`,Structive 不提供真实 mutex 保护。 +即使 writable property 也不执行真实锁。 -### 13.4 单 Property Override - -成员指针形式: +### 14.4 单 Property Override ```cpp -sync_independent<&Device::temperature>() -sync_unsynchronized<&Device::immutable_id>() +synchronization( + sync_all_independent, + sync_unsynchronized<&Device::temperature>() +) ``` -运行时字符串形式: +也支持动态 key 规则。 + +### 14.5 Group ```cpp -sync_independent("temperature") -sync_unsynchronized("immutable_id") +synchronization( + sync_all_independent, + sync_group<&Device::min_speed, &Device::max_speed>("speed_range") +) ``` -静态已知 property 优先成员指针形式。 +同组、且真正需要同步的 Property 解析到同一个 lock slot。 -### 13.5 Group +## 15. 每实例 Synchronization Override -成员指针形式: +对象可以显式覆盖类型默认策略: ```cpp -sync_group<&Device::min_speed, &Device::max_speed>("speed_range") -``` - -字符串形式: - -```cpp -sync_group("speed_range", "min_speed", "max_speed") -``` - -同一 group 的 property 共用 lock slot。 - -Resolver 会拒绝未知 key、重复 group name、空 group,以及同一 property 被重复显式配置。 - -## 14. 每实例覆盖同步策略 - -Type Descriptor 提供默认同步计划,但单个实例可以通过 `Property_Synchronization` 覆盖: - -```cpp -struct Device : Property_Object { - Device() = default; - explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} -}; -Device shared_device{ - property_synchronization(synchronization(sync_all_shared)) -}; -``` - -如果使用成员指针同步规则,可以显式提供对象类型完成 Schema materialization: - -```cpp -auto policy = property_synchronization( - synchronization( - sync_all_independent, - sync_group<&Device::temperature, &Device::pressure>("environment") +Device device{ + property_synchronization( + synchronization(sync_all_shared) ) -); +}; ``` -Schema 本身不变,只有这个实例使用覆盖后的 lock topology。默认实例按类型共享一份解析结果,因此不会为每个对象重复解析默认同步计划。 +Typed member rule: -## 15. 查看解析后的同步拓扑 +```cpp +Device device{ + property_synchronization( + synchronization( + sync_all_independent, + sync_group<&Device::temperature, &Device::pressure>("environment") + ) + ) +}; +``` -单字段: +默认 topology 每类型共享。只有显式 override 的实例才保存紧凑 override layout。 + +## 16. 查看 Resolved Synchronization + +```cpp +auto view = device.resolved_synchronization(); +auto count = view.lock_count; +``` + +成员 slot: ```cpp auto slot = device.lock_slot<&Device::temperature>(); ``` -完整 view: +无锁 Property 使用: ```cpp -auto resolved = device.resolved_synchronization(); -auto count = resolved.lock_count; -auto slot = resolved.slot(0); -auto uses_lock = resolved.uses_lock(0); +Resolved_Synchronization_View::unsynchronized_slot ``` -`Resolved_Synchronization_View::unsynchronized_slot` 表示无同步 property。 +Stored read-only property 自动得到这个值。 -## 16. 静态多属性 Guard +## 17. 静态多属性 Guard -Shared guard: +Read Guard: ```cpp auto guard = device.lock_shared<&Device::temperature, &Device::pressure>(); auto temperature = guard.get<&Device::temperature>(); -auto pressure = guard.get<&Device::pressure>(); ``` -Unique guard: +Write Guard: ```cpp auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); -auto old_min = guard.get<&Device::min_speed>(); -guard.set<&Device::min_speed>(20.0); -guard.set<&Device::max_speed>(120.0); +guard.set<&Device::min_speed>(20); +guard.set<&Device::max_speed>(120); ``` -Guard 只允许访问自己持有同步集合中的 property。 +Static Guard 会对 slot 去重,并使用稳定 slot 顺序获取锁。 -Capability view 也提供受其 access mode 限制的静态 guard: +Typed unique guard 要求所有目标 Property intrinsically writable。 + +## 18. Dynamic-Key Guard ```cpp -auto guard = device.external().lock_shared<&Device::temperature>(); +auto read_guard = device.lock_shared({"temperature", "pressure"}); +auto write_guard = device.lock_unique({"temperature", "pressure"}); ``` -## 17. 动态 Key Guard +未知 key 抛 `std::invalid_argument`。 -运行时才知道属性集合时: +Dynamic unique guard 如果遇到 read-only property,也会因为 key 只能运行期确定而运行时拒绝。 + +## 19. Traversal + +只遍历 Schema: ```cpp -std::array keys{"temperature", "pressure"}; -auto guard = device.lock_shared(keys); -``` - -或者: - -```cpp -auto guard = device.lock_unique({"min_speed", "max_speed"}); -``` - -动态 guard 会在运行时检查 key 和 capability。获取锁前会对 lock slot 排序、去重。 - -Guard 获取以后仍然可以通过编译期成员或 key 读写,但访问未包含在 held set 中的 property 会抛出 `std::logic_error`。 - -## 18. Traversal - -Schema 遍历: - -```cpp -schema.for_each_property([](auto index, const auto& property) { - // compile-time index + descriptor +schema.for_each_property([&](auto index, const auto& descriptor) { }); ``` -Managed value 遍历: +遍历 readable value: ```cpp -device.for_each_readable([](auto index, const auto& descriptor, const auto& value) { - // 每个 property 单独 managed read +device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) { }); ``` -Locked traversal 会先获取完整 readable 同步集合: +一次性锁定后遍历: ```cpp -device.external().for_each_readable_locked([](auto index, const auto& descriptor, const auto& value) { - // 全部选中 property 在 guard 下访问 +device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) { }); ``` -集中 writable 操作: +Locked traversal 只获取真实存在的同步 slot,read-only stored property 不会引入锁。 + +获取全部 writable lock domain: ```cpp -device.with_all_writable_locked([](auto& guard) { - // guard.get / guard.set +device.with_all_writable_locked([&](auto& guard) { }); ``` -Persistence view 对应提供 `with_all_loadable_locked(...)`。 +它不会自动执行 validation 或 rollback。 -## 19. Computed Property +## 20. Computed Property -同步 computed property 接收 read view: +Synchronized computed property 接收 read view: ```cpp -computed_property([](const auto& view) { +computed_property([](const auto& view) { return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>(); -}, key<"speed_span">, external_access) +}, key<"speed_span">) ``` -Computed property 自身与通过 view 读取的每个 dependency 必须解析到同一个 lock slot。 - -例如: +如果 writable dependency 需要同一快照,应共享 computed synchronization domain: ```cpp synchronization( @@ -531,145 +481,106 @@ synchronization( ) ``` -Computed property 是只读属性。 +Read-only stored dependency 可以直接通过 computed view 读取,不需要加入 lock slot,因为它没有 managed writer。 -## 20. Trusted Accessor Property +## 21. Trusted Accessor Property -Core 还支持基于成员函数的 trusted access: +Trusted getter: ```cpp -trusted_computed_property<&Device::get_temperature>(key<"temperature">) +trusted_computed_property<&Device::value>(key<"value">) ``` -以及 getter/setter: +Trusted getter/setter: ```cpp -trusted_accessor_property<&Device::get_temperature, &Device::set_temperature>(key<"temperature">) +trusted_accessor_property<&Device::get_value, &Device::set_value>(key<"value">) ``` -它们直接调用对象成员函数,并标记为 trusted object access,不受 synchronized computed view 的 dependency slot 检查。只有当成员函数本身拥有明确同步/一致性契约时才应该使用。 +这类入口直接访问对象,而不是通过 synchronized dependency view。只有调用方明确掌握成员函数同步语义时才应该使用。 -## 21. Runtime Type-Erased Access +## 22. Runtime Type-Erased Access -任何 `Property_Object` 同时也是 `Property_Object_Base`: +动态 Adapter 使用: ```cpp Property_Object_Base& erased = device; ``` -Runtime introspection: +类型信息: ```cpp erased.runtime_object_type(); erased.runtime_property_count(); ``` -Intrinsic runtime write 不需要 mode: +读取: ```cpp -double value = 35.0; -auto result = erased.runtime_write( - "temperature", - typeid(double), - &value -); +auto result = erased.runtime_read("temperature", context, callback); ``` -当 adapter 需要遵守 external 或 persistence capability metadata 时,再显式选择边界投影: +写入: ```cpp -auto external_result = erased.runtime_write( - Managed_Access_Mode::external, - "temperature", - typeid(double), - &value -); +auto result = erased.runtime_write("temperature", typeid(double), &value); ``` -Runtime read 使用同样的 overload 模型,并通过 callback 返回值: +结果: -```cpp -static void read_double(void* context, std::size_t, std::string_view, const std::type_info& type, const void* value) { - if (type == typeid(double)) { - *static_cast(context) = *static_cast(value); - } -} -double output = 0.0; -auto result = erased.runtime_read( - "temperature", - &output, - &read_double -); -auto external_result = erased.runtime_read( - Managed_Access_Mode::external, - "temperature", - &output, - &read_double -); +```text +ok +unknown_property +not_readable +not_writable +type_mismatch ``` -结果枚举: +Runtime access 没有 external/persistence mode。Adapter 自己拥有 policy,然后决定是否调用 intrinsic runtime read/write。 -```cpp -Runtime_Access_Result::ok -Runtime_Access_Result::unknown_property -Runtime_Access_Result::not_readable -Runtime_Access_Result::not_writable -Runtime_Access_Result::type_mismatch -``` +Stored read-only property 的 runtime read 同样走 no-lock fast path。 -Intrinsic runtime overload 使用 accessor 的固有能力,并与 typed `read()`/`write()` 使用同一套 synchronization topology。带 `Managed_Access_Mode` 的 overload 会先应用 external 或 persistence 投影,再使用同样的同步规则。 - -## 22. Lock Policy +## 23. Lock Policy 默认: ```cpp -struct Device : Property_Object { -}; +Property_Object ``` -等价简写: +NoLock: ```cpp -struct Device : Property_Object { -}; +Property_Object ``` -也提供 no-op lock policy: +`No_Lock_Policy` 不保存真实 mutex array,并从 managed hot path 去除真实 lock object。 -```cpp -struct Device : Property_Object { -}; -``` - -`No_Lock_Policy` 使用 `Null_Shared_Mutex`,它取消真实互斥;只有外部所有权规则能够保证正确性时才应该使用。默认 managed-object 路径不保存 mutex 数组,也不会为同步状态产生每实例堆分配。显式使用自定义 `Property_Synchronization` 时仍可能分配一份紧凑覆盖 topology,因为 computed property 的同步域检查必须保留该实例选择的布局。 - -## 23. Raw Object Access - -`unsafe_object()` 可以拿到底层 derived object: +## 24. Raw Object Access ```cpp Device& raw = device.unsafe_object(); ``` -普通成员访问当然也仍然存在: +或直接 public member: ```cpp -device.temperature = 40.0; +device.temperature = 30.0; ``` -这些入口故意绕过 Structive managed access。适合调用方已经持有正确同步、或者明确在 Structive 管理契约之外操作的情况。 +都绕过 Structive managed contract。 -## 24. 推荐使用规则 +这是刻意设计。Structive 是协作式结构基础设施,不是强制封装。 -- Typed 业务访问优先 member pointer。 -- Runtime integration boundary 使用 string key。 -- Key 按协议标识对待。 -- 稳定的对象级 capability 放 `defaults(...)`,特殊字段单独 override。 -- 需要一致观察的字段放同一个 synchronization group。 -- Validation 在业务操作边界显式执行。 -- 跨字段 invariant 使用静态 multi-property guard。 -- 默认优先 synchronized computed property。 -- Runtime access 用于 adapter,不要把普通 typed code 动态化。 -- Raw access 只在调用方明确拥有被绕过的管理保证时使用。 +## 25. 推荐规则 + +1. 存储保持普通 C++ member。 +2. 只注册真正属于结构模型的字段。 +3. C++ 业务代码优先 member-pointer typed API。 +4. 当 managed model 永远不应该写某个存储字段时,用 `read_only`。 +5. 利用 stored read-only 的 zero-lock 优化。 +6. 并发 managed code 中不要通过 raw path 修改 read-only 字段。 +7. 跨字段 mutable consistency 使用 synchronization group。 +8. Validation 保持显式。 +9. Runtime access 只用于真正动态的 Adapter。 +10. GUI/RPC/Persistence/Authorization 自己拥有 exposure 与 access policy。 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 3989307..a28fba4 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -4,434 +4,397 @@ ## 1. Purpose -Structive exists to give ordinary C++ object types a machine-readable structural layer without forcing those types into a framework-specific storage model. +Structive exists to give ordinary C++ structs explicit structural meaning while preserving the native C++ object model. -The library should make it possible to ask questions such as: +It is not a replacement language, reflection runtime, security boundary, ORM or object framework. It is a structural metadata and managed-property layer that remains close to normal C++. -- Which members are part of the public structural model? -- What is the stable runtime key of a property? -- Which properties are externally readable or writable? -- Which properties participate in persistence? -- Which metadata belongs to presentation, validation or another extension? -- Which properties share a synchronization domain? -- How can generic runtime code read or write a property safely with respect to declared capabilities? +The design target is: -It should answer those questions while leaving the underlying type recognizably C++. +```text +ordinary C++ data + + explicit schema + + intrinsic property capability + + metadata + + optional managed synchronization + + optional runtime adaptation +``` ## 2. Primary principle: enhance, do not replace -Structive is not a replacement object model. It is a structural layer attached to an existing C++ type. +The first rule is: -The preferred shape is: +> **Enhance the struct; do not replace the struct.** -```cpp -struct Device : structive::Property_Object { - double temperature; - std::string name; -}; -``` +A registered field remains a real member. Unregistered state remains normal C++. Raw access remains possible when the C++ type itself permits it. -not: +Structive must not require every field to become a wrapper such as `Property`, nor should it force application objects into a second object model. -```cpp -struct Device { - Framework_Property temperature; - Framework_Property name; -}; -``` +### Rule -This principle protects several properties of normal C++ code: +If a Structive feature can be implemented as metadata or a thin managed layer without changing the native member model, prefer that design. -1. Members remain real members. -2. Member pointers remain meaningful identities. -3. Raw access remains possible where ownership rules permit it. -4. Unregistered members can remain implementation details. -5. Generic Structive metadata can evolve independently from storage representation. +## 3. Type information and instance behavior are separate -The cost of this principle is intentional: Structive cannot intercept raw member access. Managed behavior only applies when callers use the managed path. - -## 3. Two layers, not one - -Structive separates type-level description from instance-level management. +Structive has two architectural layers. ### 3.1 Type layer -`Type_Descriptor` produces an `Object_Schema` describing: +`Type_Descriptor` and `Object_Schema` contain structural facts: - registered properties; -- declared property keys; -- accessors; +- keys; +- intrinsic readable/writable capability; - Attributes; -- constraints; -- object-level inheritable defaults; -- a default synchronization plan. +- Constraints; +- default synchronization description. -This is the structural definition of the type. +This information belongs to the type. ### 3.2 Instance layer -`Property_Object` provides: +`Property_Object` provides instance behavior: -- a type-shared resolved default lock-slot topology; -- per-instance mutex storage only when the selected lock policy requires real mutexes; -- a compact per-instance topology only for explicit synchronization overrides; -- managed typed reads and writes; -- capability views; -- static and runtime multi-property guards; -- traversal over managed values; -- type-erased runtime access through `Property_Object_Base`. +- managed read/write; +- lock storage for writable synchronization domains; +- multi-property guards; +- traversal; +- type-erased runtime access; +- optional per-instance synchronization override. -This is instance behavior, not schema identity. +### Rule -### 3.3 Design rule - -Do not move mutable instance synchronization state into the schema, and do not make schema metadata depend on one particular instance-management policy. Immutable topology derived from the type-level default plan may be shared across all instances of that type. - -A future user may want to describe a large number of plain objects without paying per-object synchronization cost. The architecture should continue to leave that possibility open. +Do not move type-level facts into every object instance unless the fact genuinely varies per instance. ## 4. Registration is explicit -Structive does not assume every C++ member belongs to the structural model. +Structive does not assume every member is a property. ```cpp -struct Device : structive::Property_Object { - int id; - double temperature; - mutable int internal_cache; +struct Device : Property_Object { + int temperature; + int internal_cache; }; ``` -If only `id` and `temperature` are registered, `internal_cache` is invisible to Structive. - -This is deliberate. Structural exposure is an API decision and should not be inferred from physical layout. +If only `temperature` is registered, `internal_cache` does not exist in the Structive schema. ### Rule -**The schema is the public structural contract; the struct layout is not automatically the schema.** +Registration defines participation. Absence from the schema means absence from Structive. -## 5. Prefer compile-time identity inside C++ +## 5. Intrinsic capability belongs to the property itself -Business C++ code should normally identify member-backed properties by member pointer: +Structive has no built-in access-control subsystem. -```cpp -device.read<&Device::temperature>(); -device.write<&Device::temperature>(30.0); -device.schema().property<&Device::temperature>(); -``` - -This gives the compiler the strongest relationship between the object type and the selected field. - -Numeric indexes are useful for generic compile-time traversal. String keys are useful at runtime boundaries. - -### Identity hierarchy +A property only describes what it intrinsically supports: ```text -business C++ code → member pointer -compile-time generic code → property index -runtime/adapters → declared string key +none +read +write +read_write ``` -Do not unnecessarily convert compile-time code to strings merely because a key exists. - -## 6. Keys are structural protocol identifiers - -Every `Property_Descriptor` requires a non-empty `key` Attribute. Keys must be unique inside one schema. - -A key is not merely a UI label. It is the runtime identity used by schema lookup, dynamic lock selection and runtime access. - -Changing a key may therefore change an external protocol or persistence contract even when the C++ member name stays unchanged. - -### Rule - -**Treat property keys as protocol-level names. Rename them deliberately.** - -## 7. One Attribute system - -Structive has one Attribute protocol. It should not grow parallel metadata channels such as “hint”, “annotation”, “UI metadata” and “serializer metadata” with separate storage machinery. - -An Attribute may declare: - -- `attribute_category` for category-based lookup; -- `single_valued` when only one Attribute of that category may appear in a declaration; -- `inheritable` when it may participate in object defaults; -- any payload required by the category owner. - -Core and extensions use this same mechanism. - -### Rule - -**Add semantics by adding an Attribute category and an interpreter, not by adding a second metadata framework.** - -## 8. Category ownership - -Core should interpret only categories that belong to Core. - -For example, Core understands access and persistence capability because those affect Core-managed views. It does not need to understand `presentation::label`. - -Presentation owns presentation semantics. A future JSON extension should own JSON-specific semantics. An RPC extension should own RPC-specific semantics. - -The Core still stores and traverses all Attributes uniformly. - -### Dependency rule - -```text -extension implementation - ↓ -Structive Property Core -``` - -Never reverse this dependency merely to make an extension convenient. - -## 9. Defaults are inheritance, not hidden mutation - -`defaults(...)` supports inheritable Attribute categories. Property declarations remain able to override those defaults. - -Current Core inheritable categories include external access, persistence access and sensitivity. - -This mechanism should be used for stable object-wide policy defaults, not as a general mechanism for implicit behavior. - -### Rule - -**Defaults should reduce repetition without making a property’s effective policy impossible to determine from schema rules.** - -## 10. Managed access and raw access are distinct contracts - -Raw access: +The capability is normally derived from the accessor. Metadata may explicitly narrow it. ```cpp -device.temperature = 30.0; +field<&Device::serial_number>(key<"serial_number">, read_only) ``` -Managed access: +This says: -```cpp -device.write<&Device::temperature>(30.0); -``` +> Within the Structive managed model, this property is readable and not writable. -The raw path is normal C++. It does not acquire Structive locks or enforce Structive capability views. - -The managed path uses the descriptor and synchronization topology. - -This duality is intentional rather than accidental. +It does not say which user, service, GUI or process is allowed to see it. ### Rule -Do not pretend that inheriting `Property_Object` turns public C++ members into encapsulated properties. If a subsystem requires managed synchronization, its coding rules must require the managed path. +Property capability describes structure, not authorization. -## 11. Intrinsic capability comes before boundary projections +## 6. Core must not own external access policy -Every property first has capabilities defined by its accessor itself: +Structive deliberately does not define: ```text -intrinsic -├── read -└── write +internal +external +persistence +role +context +permission ``` -Normal managed application code uses those intrinsic capabilities directly through `read()`, `write()`, locks and traversal. `internal` is therefore not a separate capability mode. +as managed access modes. -External and persistence behavior are boundary projections over that same property definition: +A GUI can decide which properties are editable. An RPC service can decide which fields are exposed. A persistence layer can decide which fields it saves. Those decisions belong to those systems. -```text -Property -├── intrinsic: read / write -├── external: read / write projection -└── persistence: load / store projection -``` - -Core combines projection Attributes with the accessor's actual abilities. A projection may narrow intrinsic capability, but it must never invent an ability the accessor does not provide. - -An external view should not become a second schema. A persistence view should not become a second schema. They are projections over one schema. +Core exposes structural facts; consumers define policy. ### Rule -**One property definition, intrinsic capabilities, explicit boundary projections.** +Do not add access policy to Core merely because an adapter needs a policy. The adapter owns that policy. -## 12. Validation is metadata plus an explicit operation +## 7. Raw access and managed access are distinct contracts -Structive constraints describe candidate validity, but `write()` does not automatically run them. - -This separation is essential because these are different concerns: - -```text -single-property constraint -cross-property invariant -locking -transaction boundary -rollback strategy -error reporting -side effects -``` - -A generic `write()` cannot correctly guess all of them. - -### Example +These are intentionally different: ```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<&Device::max_speed>(); -guard.set<&Device::min_speed>(candidate_min); -guard.set<&Device::max_speed>(candidate_max); -if (candidate_min > candidate_max) { - guard.set<&Device::min_speed>(old_min); - guard.set<&Device::max_speed>(old_max); -} +device.temperature = 30; +device.write<&Device::temperature>(30); ``` -The business operation owns the invariant and rollback semantics. +Raw access follows normal C++ rules. Managed access follows the Structive schema and synchronization model. + +If code writes a `read_only` public member directly, it has intentionally bypassed the Structive contract. ### Rule -**Do not turn `write()` into a hidden transaction engine.** +Structive protects cooperative managed code. It does not pretend to prevent deliberate raw C++ access. -## 13. Synchronization is synchronization only +## 8. Read-only metadata must produce a real optimization -A synchronization plan maps properties to lock slots. It supports independent, shared, unsynchronized and grouped configurations. +A stored property that cannot be written through Structive cannot race with another Structive managed writer, because no managed writer exists. -The purpose of a group is to define a consistency domain: properties in the group share a mutex slot. +Therefore a stored intrinsic read-only property: -Multi-property lock operations deduplicate slots and acquire them in stable slot order. +- receives no lock slot; +- contributes no mutex; +- ignores broad synchronization defaults; +- performs managed reads without lock lookup; +- performs managed reads without `shared_lock` construction. -### Synchronization does not mean +This is a structural optimization derived from schema information. -- validation; -- transaction; -- rollback; -- event emission; -- dirty tracking; -- persistence commit. +### Rule -Those may be built above Structive, but should not be silently coupled to locking. +If the schema proves that synchronization state is unnecessary, do not allocate or execute it. -## 14. Computed properties must have explicit consistency boundaries +## 9. Compile-time knowledge should remove runtime work -`computed_property` reads through a synchronized view. The view only permits access to properties in the same resolved lock slot. - -Therefore a computed property that depends on `min_speed` and `max_speed` should share their synchronization group: +Typed APIs know the selected property at compile time: ```cpp -synchronization( - sync_all_independent, - sync_group("speed", "min_speed", "max_speed", "speed_span") -) +device.read<&Device::serial_number>(); ``` -This makes consistency explicit rather than relying on a getter that casually reads unrelated fields. +For a stored read-only property, the compiler-visible implementation path bypasses synchronization entirely. -### Trusted accessors +Similarly, typed writes to a read-only property are removed by constraints rather than accepted and rejected at runtime. -`trusted_computed_property` and `trusted_accessor_property` invoke trusted member functions on the object. They bypass synchronized-view dependency checking by design. +Dynamic key APIs use runtime checks because the key is not known until runtime. ### Rule -**Use synchronized computed properties by default. Use trusted accessors only when the accessor itself owns or guarantees the required synchronization semantics.** +Static facts should become `constexpr`, `requires` or `if constexpr`, not runtime branches. -## 15. Runtime access is a boundary feature +## 10. Synchronization describes mutable consistency domains -`Property_Object_Base` intentionally provides a type-erased runtime interface using: +Synchronization exists to coordinate mutable managed state. -- string key; -- intrinsic access when no mode is supplied; -- `Managed_Access_Mode` only when selecting the `external` or `persistence` projection; -- `std::type_info`; -- explicit result codes. +The default rules are: -This is appropriate for adapters that do not know the concrete type at compile time. +```text +independent +shared +unsynchronized +``` -It is not a reason to make normal typed C++ code dynamic. +Groups allow several mutable properties to share one lock domain. + +Read-only stored properties are removed from the final resolved lock topology even if a broad rule names them. ### Rule -**Keep compile-time code compile-time; cross dynamic boundaries only where the application actually has a dynamic boundary.** +Synchronization topology is about mutable consistency, not property visibility. -## 16. Runtime errors and compile-time errors have different jobs +## 11. Computed read-only properties are a special case -Structive uses compile-time rejection where the selection is statically known: +A computed property may itself be read-only while depending on writable fields. -- missing member registration; -- duplicate keys; -- duplicate member storage identity; -- invalid Attribute category duplication; -- non-inheritable Attributes inside `defaults(...)`; -- impossible access capability for an accessor. +The computed property does not contain writable storage. However, its read may require a stable snapshot of mutable dependencies. -Runtime failures are reserved for runtime inputs and runtime configuration: +`Synchronized_Computed_Accessor` therefore uses a synchronized read view. Writable dependencies must share the computed consistency domain when the computed expression needs an atomic snapshot. -- unknown dynamic key; -- inaccessible dynamic property; -- invalid synchronization plan key; -- duplicate runtime synchronization configuration; -- runtime type mismatch. +Read-only stored dependencies are safe to read directly through the synchronized view because they have no managed writer. ### Rule -**Do not defer a statically knowable schema error to runtime. Do not force dynamic adapter input into compile-time machinery.** +Do not give a read-only stored field a mutex. A computed read uses synchronization only for mutable dependencies that require consistency. -## 17. Synchronization policy should remain replaceable +## 12. One Attribute protocol -`Property_Object` supports a lock policy, with `Shared_Mutex_Policy` as the default and `No_Lock_Policy` available. +Core and extensions share one Attribute mechanism. -This is an important architectural seam. Core semantics should not become inseparable from one specific mutex type or one global scheduler. +An Attribute owns a category and may define whether it is single-valued and inheritable. -`No_Lock_Policy` removes actual mutual exclusion; it does not magically make concurrent access safe. +Core must not create parallel metadata systems for UI, serialization, diagnostics or domain-specific features. -## 18. Extension design rules +### Rule -A good Structive extension should: +New metadata domains should extend the Attribute protocol instead of adding a second metadata framework. -1. Define categories that belong to its own domain. -2. Reuse the Core Attribute protocol. -3. Interpret only the categories it owns or explicitly depends on. -4. Depend on Core, never require Core to depend on it. -5. Keep domain-specific fallback behavior inside the extension. -6. Prefer reading the existing schema over duplicating a parallel descriptor tree. -7. Avoid changing Core behavior merely because an adapter needs a convenience rule. +## 13. Category ownership must be clear -## 19. Non-goals +The component that defines an Attribute category owns its semantics. -Structive Core should not become all of the following at once: +Presentation owns presentation categories. Core can store them, traverse them and expose them generically, but it must not interpret them. +This keeps dependency direction one-way: + +```text +Extension → Core +Core -X→ Extension +``` + +### Rule + +Core stores unknown extension metadata without learning extension semantics. + +## 14. Defaults are metadata inheritance only + +`defaults(...)` is for inheritable Attributes. It should not mutate objects or hide procedural behavior. + +Property-level metadata can override an inheritable default for the same single-valued category. + +Intrinsic capability is not inheritable because the capability describes each property itself. + +### Rule + +Defaults may reduce metadata repetition, but they must not become an invisible behavior engine. + +## 15. Validation is explicit + +Constraints describe valid values. They do not automatically execute inside every write. + +```cpp +auto error = validate_property_value<&Device::temperature>(schema, candidate); +``` + +Field validation, cross-field invariants, transaction boundaries and rollback are separate operations. + +### Rule + +Do not turn `write()` into an implicit workflow containing validation, events, transactions and rollback. + +## 16. Runtime access is adaptation, not the primary programming model + +`Property_Object_Base` provides key-based type-erased access for dynamic systems. + +Runtime access only uses intrinsic capability. There is no runtime permission mode. + +An adapter decides whether it should expose a property and whether it should invoke runtime read or write. + +### Rule + +Use member-pointer typed access inside normal C++ business code. Use runtime access at dynamic boundaries. + +## 17. Runtime and compile-time errors have different jobs + +Compile-time typed operations should reject impossible structural operations through constraints. + +Examples: + +- writing an intrinsic read-only property; +- requesting a typed unique guard for a read-only property; +- reading a write-only property. + +Runtime key APIs report dynamic failures through `Runtime_Access_Result`. + +### Rule + +Do not defer a statically knowable property error to runtime. + +## 18. Synchronization policy remains replaceable + +`Shared_Mutex_Policy` provides real shared/exclusive locking. `No_Lock_Policy` removes real mutex storage. + +NoLock should not allocate fake mutex arrays or construct meaningless lock objects. + +### Rule + +A policy that removes a capability should remove its storage and hot-path cost where possible. + +## 19. Per-instance synchronization overrides preserve object-local policy + +The default resolved topology is shared per type. An object may explicitly receive a `Property_Synchronization` override. + +Only such an object stores an override layout. Copy/move construction preserves that layout; assignment preserves the target object's synchronization policy. + +### Rule + +Do not make the default path pay the storage cost of a feature that only some instances use. + +## 20. Extensions decide domain behavior + +A persistence extension may choose fields based on its own metadata. A GUI may decide editability. An RPC layer may implement authorization. + +Those systems may inspect Structive metadata such as `readable`, `writable`, `sensitive` or custom extension Attributes, but Structive does not decide their policy for them. + +### Rule + +Core describes structure. Consumers decide behavior at their boundary. + +## 21. Non-goals + +Property Core is not intended to become: + +- an authorization engine; - an ORM; - a JSON library; -- a GUI framework; - an RPC framework; -- a signal/slot system; -- a transaction engine; -- a general-purpose reflection replacement for every language feature. +- a GUI binding framework; +- a transaction manager; +- an event bus; +- a scripting engine. -Structive should provide a strong structural contract that those systems can consume. +Such systems can consume Structive but should remain separate. -## 20. Evolution rules +## 22. Evolution rules -When changing the library, apply these questions in order: +When extending Structive, review the change against these rules: -1. Does this belong to the structural model, instance management, or an extension? -2. Is this new concept really an Attribute category rather than a new metadata channel? -3. Can the error be detected at compile time? -4. Does the change preserve native C++ member semantics? -5. Does it preserve the distinction between raw and managed access? -6. Is synchronization being mixed with validation, transaction or events? -7. Does Core now know something that should belong to an extension? -8. Does the API still make member pointers the natural typed identity? -9. Does a new convenience API duplicate an existing semantic path? -10. Does the change preserve existing function-signature meaning rather than silently redefining it? +1. Does it enhance normal C++ rather than replace it? +2. Is the information type-level or instance-level? +3. Is this intrinsic structural capability or external policy? +4. Can compile-time information remove runtime work? +5. Does a read-only stored property remain outside the lock topology? +6. Is synchronization limited to mutable consistency? +7. Is validation still explicit? +8. Does the extension own its own category semantics? +9. Is runtime adaptation kept separate from typed business APIs? +10. Is a new Core concept genuinely universal? -If a feature fails these questions, reconsider its layer before implementing it. +## 23. Architectural summary -## 21. Architectural summary - -Structive should remain a small number of strong concepts rather than a large number of magical conveniences: +The intended architecture is: ```text -ordinary C++ type - + explicit schema - + one Attribute model - + explicit capabilities - + explicit validation - + explicit synchronization - + optional managed instance behavior - + extension-owned interpretation +Native C++ struct + │ + ├── raw C++ access + │ + └── Structive schema + │ + ├── intrinsic readable/writable capability + ├── Attributes + ├── Constraints + ├── synchronization description + │ └── only mutable consistency domains create locks + └── managed object + ├── typed read/write + ├── guards + ├── traversal + └── intrinsic runtime access + +External systems + ├── GUI policy + ├── RPC policy + ├── persistence policy + └── other domain policy + +External policies consume Structive facts; they are not Structive Core access modes. ``` -The library is strongest when generic systems can understand a type without taking ownership of that type. +The compact statement of the design is: + +> **Structive describes what a property is and what it intrinsically supports. It does not decide who may use it.** diff --git a/docs/DESIGN.zh-CN.md b/docs/DESIGN.zh-CN.md index e0a81bd..2aa423e 100644 --- a/docs/DESIGN.zh-CN.md +++ b/docs/DESIGN.zh-CN.md @@ -4,434 +4,393 @@ ## 1. 设计目标 -Structive 的目标是给普通 C++ 对象增加一层机器可理解的结构语义,同时不强迫业务对象改用框架自己的存储模型。 +Structive 的目标是在保留 C++ 原生对象模型的前提下,为普通 struct 增加显式结构语义。 -它应该能够回答: +它不是第二套语言、反射运行时、安全边界、ORM 或对象框架。它是一层尽可能贴近普通 C++ 的结构元数据和 managed property 能力。 -- 哪些成员属于公开结构模型? -- 某个属性稳定的 runtime key 是什么? -- 哪些属性允许外部读取或写入? -- 哪些属性参与持久化? -- 哪些元数据属于 Presentation、Validation 或其他扩展? -- 哪些属性位于同一个同步域? -- 动态适配器如何在遵守 capability 的前提下读写属性? +目标模型: -但完成这些事情以后,底层类型仍然应该看起来像正常的 C++。 +```text +普通 C++ 数据 + + 显式 Schema + + Property 固有能力 + + Metadata + + 可选 Managed Synchronization + + 可选 Runtime Adapter +``` ## 2. 第一原则:增强,而不是替代 -Structive 不是第二套对象模型,而是附加在已有 C++ 类型旁边的结构层。 +第一原则: -推荐形态: +> **增强 struct,而不是替代 struct。** -```cpp -struct Device : structive::Property_Object { - double temperature; - std::string name; -}; -``` +注册字段仍是真实 C++ 成员。未注册状态仍然是普通 C++。只要 C++ 类型本身允许,raw access 永远存在。 -而不是: +Structive 不要求每个字段变成 `Property`,也不应该迫使业务对象进入第二套对象模型。 -```cpp -struct Device { - Framework_Property temperature; - Framework_Property name; -}; -``` +### 原则 -这个原则保护了原生 C++ 的几个关键性质: +如果一个能力可以通过 metadata 或很薄的 managed layer 实现,就不要改变原生 member model。 -1. 成员仍然是真实成员。 -2. 成员指针仍然可以作为可靠身份。 -3. 所有权规则允许时仍然可以 raw access。 -4. 未注册成员可以继续作为实现细节存在。 -5. Structive 元数据可以和存储形式独立演进。 +## 3. 类型信息与实例行为必须分层 -这个原则也有明确代价:Structive 不可能拦截直接成员访问。只有通过 managed path 的访问才会获得 Structive 管理行为。 - -## 3. 必须保持两层,而不是揉成一层 - -Structive 把类型级描述和实例级管理分开。 +Structive 有两层架构。 ### 3.1 类型层 -`Type_Descriptor` 产生 `Object_Schema`,描述: +`Type_Descriptor` 与 `Object_Schema` 保存结构事实: -- 注册属性; -- 属性 key; -- accessor; +- 注册 Property; +- key; +- intrinsic readable/writable; - Attribute; -- constraint; -- object 级可继承默认值; -- 默认 synchronization plan。 +- Constraint; +- 默认 synchronization 描述。 -这是类型的结构定义。 +这些属于类型。 ### 3.2 实例层 -`Property_Object` 提供: +`Property_Object` 提供实例行为: -- 同一类型共享的默认解析 lock slot 拓扑; -- 只有真实锁策略才需要的每实例 mutex 存储; -- 只有显式同步覆盖实例才持有的紧凑 topology; -- managed typed read/write; -- capability view; -- 静态与运行时多属性 guard; -- managed value traversal; -- `Property_Object_Base` type-erased runtime access。 +- managed read/write; +- 可写同步域所需的锁存储; +- 多属性 Guard; +- traversal; +- type-erased runtime access; +- 可选的每实例 synchronization override。 -这是实例行为,不是 Schema 身份。 +### 原则 -### 3.3 设计约束 - -不要把可变的实例同步状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。由类型级默认同步计划推导出的不可变 topology 可以由同类型所有实例共享。 - -以后完全可能有用户只想描述大量普通对象,却不愿意为每个对象承担同步状态成本。当前架构应该持续保留这种可能性。 +除非信息真的随实例变化,否则不要把类型级事实复制到每个对象里。 ## 4. 注册必须显式 -Structive 不认为所有 C++ 成员都天然属于结构模型。 +Structive 不假设所有成员都是 Property。 ```cpp -struct Device : structive::Property_Object { - int id; - double temperature; - mutable int internal_cache; +struct Device : Property_Object { + int temperature; + int internal_cache; }; ``` -如果 Schema 只注册 `id` 和 `temperature`,那么 `internal_cache` 对 Structive 完全不可见。 - -这是故意的。结构暴露本身就是 API 设计,不应该根据物理布局自动推断。 +只注册 `temperature` 时,`internal_cache` 完全不属于 Structive Schema。 ### 原则 -**Schema 才是公开结构契约;struct 的物理成员集合不是自动 Schema。** +注册决定参与;不在 Schema 中就不属于 Structive。 -## 5. C++ 内部优先使用编译期身份 +## 5. Intrinsic Capability 属于 Property 自己 -业务 C++ 代码通常应该通过成员指针定位 member-backed property: +Structive **没有内建访问控制系统**。 -```cpp -device.read<&Device::temperature>(); -device.write<&Device::temperature>(30.0); -device.schema().property<&Device::temperature>(); -``` - -这样编译器能够建立最强的“对象类型—字段”关系。 - -数字 index 适合编译期泛型遍历;字符串 key 适合运行时边界。 - -### 身份层级 +Property 只描述自己固有支持什么: ```text -业务 C++ 代码 → member pointer -编译期泛型代码 → property index -runtime / adapter → declared string key +none +read +write +read_write ``` -不要因为存在 key,就把本来可以静态确定的 C++ 业务代码全部字符串化。 - -## 6. Key 是结构协议标识,不是显示名称 - -每个 `Property_Descriptor` 都必须拥有非空 `key`,同一 Schema 内 key 必须唯一。 - -Key 被 Schema lookup、动态锁选择、runtime read/write 使用。因此它不只是 UI label。 - -即使 C++ 成员名不变,修改 key 也可能改变外部协议或持久化契约。 - -### 原则 - -**把 property key 当成协议级名字,重命名必须有意识。** - -## 7. 只有一套 Attribute 系统 - -Structive 不应该再长出 `hint`、`annotation`、`ui metadata`、`serializer metadata` 等彼此独立的元数据容器。 - -一个 Attribute 可以声明: - -- `attribute_category`:用于按 category 查询; -- `single_valued`:同一个声明中该 category 是否只能有一个值; -- `inheritable`:是否允许进入 object defaults; -- category 自己需要的 payload。 - -Core 和 Extension 使用完全相同的协议。 - -### 原则 - -**增加语义时优先增加 Attribute category + interpreter,而不是再造一套 metadata framework。** - -## 8. Category 必须有明确所有者 - -Core 只解释属于 Core 的 category。 - -例如 External Access 和 Persistence Access 会直接影响 Core capability view,所以 Core 理解它们是合理的;但 Core 不需要理解 `presentation::label`。 - -Presentation 解释 Presentation;未来 JSON 扩展解释 JSON;RPC 扩展解释 RPC。 - -Core 只负责统一保存、遍历和提供查询机制。 - -### 依赖约束 - -```text -Extension implementation - ↓ -Structive Property Core -``` - -不能为了某个扩展写起来方便,就反过来让 Core 依赖 Extension。 - -## 9. Defaults 是继承,不是隐藏行为 - -`defaults(...)` 用于支持可继承 Attribute。Property 级声明仍然可以覆盖同 category 的默认值。 - -当前 Core 中可继承的 category 包括 External Access、Persistence Access 和 Sensitive。 - -Defaults 应该用于稳定的对象级政策默认值,而不是用来制造大量隐式行为。 - -### 原则 - -**Defaults 用来减少重复,但不能让一个属性最终生效的政策变得无法从 Schema 规则中推导。** - -## 10. Managed Access 与 Raw Access 是两种契约 - -Raw access: +通常由 Accessor 自动推导,也可以通过 metadata 显式收窄。 ```cpp -device.temperature = 30.0; +field<&Device::serial_number>(key<"serial_number">, read_only) ``` -Managed access: +它表达: -```cpp -device.write<&Device::temperature>(30.0); -``` +> 在 Structive managed model 中,这个属性可读、不可写。 -Raw path 就是普通 C++,不会自动获得 Structive 锁和 capability 管理。 - -Managed path 会经过 descriptor 和该实例的同步拓扑。 - -两条路径同时存在是设计结果,不是漏洞。 +它不表达哪个用户、服务、GUI 或进程有权限看到它。 ### 原则 -不要假装继承 `Property_Object` 以后 public member 就自动变成强封装属性。如果某个子系统要求受管理同步,那么该子系统自己的编码约束必须要求使用 managed path。 +Property capability 描述结构,不描述授权。 -## 11. 固有能力先于边界投影 +## 6. Core 不拥有外部访问策略 -每个 Property 首先拥有 accessor 自身决定的能力: +Structive 不定义下面这些 managed access mode: ```text -intrinsic -├── read -└── write +internal +external +persistence +role +context +permission ``` -应用内部的普通 managed code 通过 `read()`、`write()`、lock 和 traversal 直接使用这套固有能力,因此 `internal` 不再是单独的 capability mode。 +GUI 自己决定哪些属性可编辑,RPC 自己决定暴露哪些字段,持久化系统自己决定保存哪些字段。 -External 和 Persistence 是同一份 Property definition 之上的边界投影: - -```text -Property -├── intrinsic: read / write -├── external: read / write projection -└── persistence: load / store projection -``` - -Core 会把投影 Attribute 与 accessor 的实际能力结合起来。投影可以收窄固有能力,但绝不能凭空创造 accessor 本身不具备的能力。 - -External view 不应该发展成第二份 Schema;Persistence view 也不应该成为第二份 Schema。它们都只是同一 Schema 的能力投影。 +Core 只提供结构事实,Consumer 自己定义策略。 ### 原则 -**一份 property definition,固有能力明确,边界投影显式。** +不要因为某个 Adapter 需要策略,就把这个策略塞进 Core。 -## 12. Validation = 元数据 + 显式操作 +## 7. Raw Access 与 Managed Access 是两种契约 -Constraint 描述候选值是否合法,但 `write()` 不会自动执行它。 - -必须保持分开的概念包括: - -```text -单字段 constraint -跨字段 invariant -locking -transaction boundary -rollback strategy -error reporting -side effects -``` - -一个通用 `write()` 不可能替所有业务正确猜出这些规则。 - -### 示例 +下面两句故意具有不同语义: ```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<&Device::max_speed>(); -guard.set<&Device::min_speed>(candidate_min); -guard.set<&Device::max_speed>(candidate_max); -if (candidate_min > candidate_max) { - guard.set<&Device::min_speed>(old_min); - guard.set<&Device::max_speed>(old_max); -} +device.temperature = 30; +device.write<&Device::temperature>(30); ``` -业务操作自己拥有 invariant 和 rollback 语义。 +Raw path 遵守普通 C++。Managed path 遵守 Structive Schema 与同步模型。 + +如果代码直接写一个 `read_only` public member,它就是主动绕过 Structive contract。 ### 原则 -**不要把 `write()` 变成隐藏事务引擎。** +Structive 服务于遵守 managed contract 的代码,不假装能阻止故意绕过系统的 raw C++。 -## 13. Synchronization 就只负责 Synchronization +## 8. Read-Only Metadata 必须产生真实优化 -Synchronization plan 的本质是把 property 映射到 lock slot,支持 independent、shared、unsynchronized、grouped 等配置。 +如果一个存储属性无法通过 Structive 被写入,那么不存在另一个 Structive managed writer 与它竞争。 -Group 表示一致性域:同一个 group 的 property 使用同一个 mutex slot。 +因此 intrinsic read-only 的存储属性: -多属性锁会对 slot 去重,并按稳定 slot 顺序获取锁。 +- 不分配 lock slot; +- 不贡献 mutex; +- 不受宽泛默认同步模式影响; +- managed read 不查询 lock slot; +- managed read 不构造 `shared_lock`。 -### Synchronization 不等于 +这是 Schema 信息直接产生的结构优化。 -- validation; -- transaction; -- rollback; -- event emission; -- dirty tracking; -- persistence commit。 +### 原则 -这些能力可以构建在 Structive 上层,但不能偷偷和锁耦合。 +如果 Schema 已经证明某项运行时状态没有必要,就不要分配,也不要执行。 -## 14. Computed Property 必须明确一致性边界 +## 9. 编译期信息必须消除运行时工作 -`computed_property` 通过 synchronized view 读取依赖项。这个 view 只允许读取和 computed property 本身处于同一个 resolved lock slot 的属性。 - -所以依赖 `min_speed`、`max_speed` 的 `speed_span` 应该和它们进入同一个同步组: +Typed API 在编译期知道目标 Property: ```cpp -synchronization( - sync_all_independent, - sync_group("speed", "min_speed", "max_speed", "speed_span") -) +device.read<&Device::serial_number>(); ``` -这样一致性关系由同步计划明确表达,而不是让 getter 随意读取任意字段。 +对于 stored read-only property,编译期路径直接绕过同步。 -### Trusted Accessor +同样,对只读属性的 typed write 应该通过 `requires` 从接口中消失,而不是运行后再拒绝。 -`trusted_computed_property` 和 `trusted_accessor_property` 会直接调用对象上的受信任成员函数,设计上绕开 synchronized view 的依赖检查。 +动态 key API 因为 key 只能运行期确定,所以才做 runtime check。 ### 原则 -**默认优先 synchronized computed property。只有 accessor 自身明确拥有或保证同步语义时才使用 trusted accessor。** +静态事实优先变成 `constexpr`、`requires`、`if constexpr`,不要变成 runtime branch。 -## 15. Runtime Access 是边界能力 +## 10. Synchronization 描述的是可变一致性域 -`Property_Object_Base` 提供 type-erased runtime interface,核心输入包括: +Synchronization 的目的,是协调 mutable managed state。 -- string key; -- 不传 mode 时使用 intrinsic access; -- 只有选择 `external` 或 `persistence` 投影时才使用 `Managed_Access_Mode`; -- `std::type_info`; -- 显式 result code。 +默认模式: -它适用于编译期不知道具体类型的 adapter。 +```text +independent +shared +unsynchronized +``` -它不应该成为把所有正常 typed C++ 代码动态化的理由。 +Group 让多个可变 Property 共享一个锁域。 + +Read-only stored property 即使被宽泛规则包含,也会从最终 resolved topology 中移除。 ### 原则 -**能在编译期确定的代码就留在编译期;只有真正跨动态边界时才进入 runtime path。** +Synchronization 解决 mutable consistency,不解决可见性。 -## 16. Compile-time Error 和 Runtime Error 分工明确 +## 11. Computed Read-Only 是特殊情况 -Structive 对静态可知问题尽量在编译期拒绝: +Computed Property 自身可以只读,但它可能依赖可写字段。 -- member 未注册; -- key 重复; -- 同一成员存储被重复注册; -- single-valued category 重复; -- 非 inheritable Attribute 被放入 `defaults(...)`; -- accessor 实际能力与声明 capability 冲突。 +Computed value 本身没有可写存储,但读取它时可能需要 mutable dependency 的一致快照。 -Runtime failure 留给 runtime 输入和 runtime 配置: +`Synchronized_Computed_Accessor` 因此使用 synchronized read view。需要原子一致性的 writable dependency 应和 computed property 处于同一个同步域。 -- 动态 key 不存在; -- 动态属性对该 view 不可访问; -- synchronization plan 引用了未知 key; -- runtime 同步规则重复配置同一个 property; -- runtime type mismatch。 +Read-only stored dependency 没有 managed writer,所以可以直接读取而无需锁。 ### 原则 -**静态可知的 Schema 错误不要拖到运行时;真正动态的 adapter 输入也不要硬伪装成编译期问题。** +不要给 read-only stored field 创建 mutex。Computed read 的同步只服务于需要一致性的 mutable dependency。 -## 17. 同步策略必须保持可替换 +## 12. 只有一套 Attribute 协议 -`Property_Object` 支持同步策略,默认是 `Shared_Mutex_Policy`,同时提供 `No_Lock_Policy`。 +Core 与 Extension 共用同一套 Attribute mechanism。 -这是重要架构缝隙。Core 语义不应该和某一种 mutex 或某一个全局 scheduler 焊死。 +Attribute 拥有 category,并可以声明 single-valued 与 inheritable 语义。 -`No_Lock_Policy` 只是取消真实互斥,不代表并发访问自动安全。 +UI、serialization、诊断等新领域不应该另起第二套 metadata system。 -## 18. Extension 设计规则 +### 原则 -一个好的 Structive Extension 应该: +新的 metadata domain 应扩展 Attribute protocol,而不是创造另一套框架。 -1. 定义属于自己领域的 category。 -2. 复用 Core Attribute 协议。 -3. 只解释自己拥有或明确依赖的 category。 -4. 依赖 Core,而不是要求 Core 反向依赖自己。 -5. 把领域 fallback 行为留在扩展里。 -6. 优先消费已有 Schema,不复制第二棵 descriptor tree。 -7. 不因为 adapter 需要便利规则就修改 Core 基础语义。 +## 13. Category 必须有明确所有者 -## 19. Core 的非目标 +定义 Attribute category 的组件拥有它的语义。 -Structive Core 不应该同时变成: +Presentation 拥有 Presentation Category。Core 可以保存、遍历、泛型读取,但不解释它。 +依赖方向保持: + +```text +Extension → Core +Core -X→ Extension +``` + +### 原则 + +Core 可以存储未知 Extension metadata,但不能学习 Extension 语义。 + +## 14. Defaults 只做 Metadata 继承 + +`defaults(...)` 用于 inheritable Attribute,不用于偷偷执行行为。 + +Property-level metadata 可以覆盖同一 single-valued category 的 object default。 + +Intrinsic capability 不允许继承,因为每个 Property 自己的读写能力必须单独成立。 + +### 原则 + +Defaults 只降低 metadata 重复,不得变成隐藏行为引擎。 + +## 15. Validation 必须显式 + +Constraint 描述有效值,但不自动塞进每次 `write()`。 + +```cpp +auto error = validate_property_value<&Device::temperature>(schema, candidate); +``` + +单字段 Validation、跨字段 invariant、transaction、rollback 是不同操作。 + +### 原则 + +不要让 `write()` 变成 validation + event + transaction + rollback 的黑盒工作流。 + +## 16. Runtime Access 是适配能力,不是主要编程模型 + +`Property_Object_Base` 提供动态 key 的 type-erased access。 + +Runtime access 只遵守 intrinsic capability,没有 runtime permission mode。 + +Adapter 自己决定是否暴露某个 Property、是否调用 runtime read/write。 + +### 原则 + +普通 C++ 业务代码优先 member-pointer typed API;runtime access 留给动态边界。 + +## 17. Compile-Time Error 与 Runtime Error 分工明确 + +静态 typed operation 应在编译期拒绝不可能的结构操作,例如: + +- 写 intrinsic read-only property; +- 对 read-only property 请求 typed unique guard; +- 读取 write-only property。 + +Runtime key API 才使用 `Runtime_Access_Result` 返回动态错误。 + +### 原则 + +静态可知的 Property 错误不要拖到运行期。 + +## 18. Synchronization Policy 必须保持可替换 + +`Shared_Mutex_Policy` 提供真实 shared/exclusive lock。`No_Lock_Policy` 不保存真实 mutex。 + +NoLock 不应该分配假的 mutex array,也不应该构造无意义的 lock object。 + +### 原则 + +一个 Policy 移除某项能力时,应尽可能同时移除它的存储和热路径成本。 + +## 19. 每实例 Synchronization Override 只让使用者付费 + +默认 resolved topology 每个类型共享。对象可以显式传入 `Property_Synchronization` 覆盖。 + +只有这种对象才保存 override layout。Copy/Move construction 保留该布局;Assignment 保留目标对象自身的 synchronization policy。 + +### 原则 + +默认路径不为少数实例才使用的功能承担存储成本。 + +## 20. Extension 自己决定领域行为 + +Persistence Extension 可以根据自己的 metadata 决定保存字段,GUI 可以决定 editability,RPC 可以实现 authorization。 + +这些系统可以读取 Structive 的 `readable`、`writable`、`sensitive` 或自定义 Attribute,但 Structive 不替它们做策略决定。 + +### 原则 + +Core 描述结构,Consumer 在自己的边界决定行为。 + +## 21. Core 的非目标 + +Property Core 不准备成为: + +- Authorization Engine; - ORM; -- JSON 库; -- GUI 框架; -- RPC 框架; -- signal/slot 系统; -- 事务引擎; -- 替代所有语言能力的“万能反射”。 +- JSON Library; +- RPC Framework; +- GUI Binding Framework; +- Transaction Manager; +- Event Bus; +- Scripting Engine。 -Structive 应该提供足够强的结构契约,让这些系统来消费它。 +这些系统可以消费 Structive,但应该独立存在。 -## 20. 演进原则 +## 22. 演进审核规则 -修改库时依次问: +以后扩展 Structive 时逐条检查: -1. 这是 structural model、instance management 还是 extension 的职责? -2. 这个新概念本质上是不是一个 Attribute category,而不是新的 metadata channel? -3. 这个错误能否在编译期发现? -4. 修改后是否仍然保留原生 C++ 成员语义? -5. Raw access 与 managed access 的边界是否仍然清晰? -6. 是否把 synchronization 和 validation、transaction、event 混在一起了? -7. Core 是否开始理解本应属于 Extension 的领域概念? -8. Member pointer 是否仍然是 typed API 最自然的身份? -9. 新的 convenience API 是否重复制造了另一条同语义路径? -10. 是否保持了已有函数签名和功能语义,而不是悄悄改义? +1. 它是在增强普通 C++,还是替代普通 C++? +2. 这个信息属于类型还是实例? +3. 这是 intrinsic structural capability,还是外部 policy? +4. 编译期信息能不能直接消掉运行时工作? +5. Read-only stored property 是否仍然完全不进入 lock topology? +6. Synchronization 是否只服务于 mutable consistency? +7. Validation 是否仍然显式? +8. Extension 是否拥有自己的 category 语义? +9. Runtime adaptation 是否与 typed business API 保持分离? +10. 新 Core 概念是否真的普适? -如果一个功能连续违反这些问题,应先重新考虑它属于哪一层,而不是立刻实现。 +## 23. 架构总结 -## 21. 架构总结 - -Structive 最强的形态应该是少量强概念,而不是大量魔法便利接口: +最终架构: ```text -普通 C++ 类型 - + 显式 Schema - + 一套 Attribute 模型 - + 显式 Capability - + 显式 Validation - + 显式 Synchronization - + 可选 Managed Instance 行为 - + Extension 自己解释自己的语义 +Native C++ struct + │ + ├── raw C++ access + │ + └── Structive Schema + │ + ├── intrinsic readable/writable + ├── Attributes + ├── Constraints + ├── synchronization description + │ └── 只有 mutable consistency domain 创建锁 + └── managed object + ├── typed read/write + ├── guards + ├── traversal + └── intrinsic runtime access + +External systems + ├── GUI policy + ├── RPC policy + ├── persistence policy + └── other domain policy + +外部 Policy 消费 Structive 的结构事实,但不是 Structive Core 的 access mode。 ``` -当泛型系统能够理解业务类型、却不需要接管业务类型时,Structive 的价值最大。 +一句话概括: + +> **Structive 描述 Property 是什么、它自身能做什么;Structive 不决定谁能使用它。** diff --git a/docs/EXTENSIONS.md b/docs/EXTENSIONS.md index 6121671..91dd4e1 100644 --- a/docs/EXTENSIONS.md +++ b/docs/EXTENSIONS.md @@ -4,65 +4,70 @@ ## 1. Extension role -Structive extensions add domain-specific interpretation without changing the Core structural model. +Extensions add domain metadata and domain interpretation without making Property Core depend on that domain. -The intended dependency is: +The dependency direction is: ```text -application / adapter - ↓ -Structive extension - ↓ -Structive Property Core +Extension → Property Core +Property Core -X→ Extension ``` -Core does not include or link extension code. +Core supplies: + +- schema and property descriptors; +- intrinsic readable/writable capability; +- the Attribute protocol; +- constraints; +- synchronization metadata and managed access; +- runtime structural access. + +Extensions add their own categories and interpretation. ## 2. Extension principle -An extension should normally add two things: +An extension should own only its domain semantics. -1. one or more Attribute categories that express domain metadata; -2. interpretation code that consumes those Attributes. +For example, Presentation owns: -It should not create a parallel property registry when the existing `Object_Schema` already contains the required structure. - -## 3. Current presentation extension - -The current extension module defines four Attribute categories: - -```cpp -presentation::Label_Category -presentation::Description_Category -presentation::Group_Category -presentation::Order_Category +```text +label +description +group +order ``` -Convenience Attribute values: +It does not need Property Core to understand presentation. -```cpp -presentation::label<"Temperature"> -presentation::description<"Current device temperature"> -presentation::group<"Environment"> -presentation::order<10> +Likewise, a future persistence extension may own persistence-specific metadata, and an RPC extension may own protocol metadata. + +## 3. External policy stays external + +Property Core has no built-in access-control mode. + +An extension or application layer is responsible for its own policy: + +```text +Should this property be visible? +Should this endpoint permit a write? +Should this property be persisted? +Does the current user have permission? ``` -They can be attached directly to a normal Core property: +An extension may inspect structural facts such as: -```cpp -field<&Device::temperature>( - key<"temperature">, - unit<"C">, - presentation::label<"Temperature">, - presentation::description<"Current device temperature">, - presentation::group<"Environment">, - presentation::order<10> -) +```text +Property::readable +Property::writable +sensitive metadata +extension-owned metadata ``` -No wrapper such as `hint(...)` is required. +but the final policy belongs to the consumer. -## 4. Presentation interpretation +A `read_only` property means the Structive managed object itself has no write operation for that property. It does not mean every external system must expose it. + +## 4. Current presentation extension Include: @@ -76,192 +81,192 @@ Link: target_link_libraries(my_target PRIVATE structive::property_extensions) ``` -Describe a property by member pointer: +Available Attributes: + +```cpp +presentation::label<"Temperature"> +presentation::description<"Current device temperature"> +presentation::group<"Environment"> +presentation::order<2> +``` + +Example: + +```cpp +field<&Device::temperature>( + key<"temperature">, + unit<"C">, + presentation::label<"Temperature">, + presentation::description<"Current device temperature">, + presentation::group<"Environment">, + presentation::order<2> +) +``` + +## 5. Presentation interpretation ```cpp auto info = presentation::describe<&Device::temperature>(device.schema()); ``` -The result contains: +`Presentation_Info` contains: -```cpp -struct Presentation_Info { - std::string_view key; - std::string_view label; - std::string_view description; - std::string_view group; - std::size_t order; - bool has_order; -}; +```text +key +label +description +group +order +has_order ``` -If no presentation label is declared, the extension uses the property key as the display label. This fallback is presentation policy and intentionally lives outside Core. +If no label is supplied, the presentation extension falls back to the property key. That fallback is presentation behavior, not Core behavior. -## 5. Inheritable extension Attributes +## 6. Inheritable extension Attributes -The presentation `group` Attribute is inheritable, so it may be placed in `defaults(...)`: +An extension category may choose to be inheritable. ```cpp -return object( - defaults( - external_access, - presentation::group<"Environment"> - ), - field<&Device::temperature>( - key<"temperature">, - presentation::label<"Temperature"> - ), - field<&Device::pressure>( - key<"pressure">, - presentation::label<"Pressure"> - ) -); +defaults( + presentation::group<"Environment"> +) ``` -The extension resolves an effective declared value through the same Core default mechanism. +Property-level metadata overrides an inheritable single-valued default from the same category. -`label`, `description` and `order` are not inheritable and therefore cannot be placed in `defaults(...)`. +Use inheritance only for metadata whose semantics genuinely support inheritance. -## 6. Designing a new extension Attribute +## 7. Designing a new extension Attribute -Example: +A simple extension Attribute can be defined as: ```cpp -namespace my_adapter { -struct Json_Name_Category {}; +namespace my_extension { +struct Format_Category {}; template -struct Json_Name_Attribute { - using attribute_category = Json_Name_Category; +struct Format_Attribute { + using attribute_category = Format_Category; static constexpr bool single_valued = true; static constexpr bool inheritable = false; static constexpr auto value = Value; }; template -inline constexpr Json_Name_Attribute json_name{}; +inline constexpr Format_Attribute format{}; } ``` -Use it in a schema: +Then attach it normally: ```cpp field<&Device::temperature>( key<"temperature">, - my_adapter::json_name<"temp"> + my_extension::format<"0.0 C"> ) ``` -The Core stores it without needing to know what JSON means. +Core does not require modification. -## 7. Reading extension Attributes +## 8. Reading extension Attributes -For a property descriptor: +Descriptor-local metadata: ```cpp -using Property = std::remove_cvref_t; -if constexpr (Property::has_attribute) { - const auto& value = property.attribute(); -} +using Property = std::remove_cvref_t())>; +static_assert(Property::template has_attribute); ``` -For inheritable categories, use effective declared lookup: +Effective inheritable metadata can use Core's generic effective-Attribute utilities. + +The extension owns fallback and interpretation rules. + +## 9. Multi-valued metadata + +If a category is not single-valued, an extension may allow multiple Attributes from that category. + +Do not mark a category `single_valued` unless duplicate declarations are structurally invalid. + +## 10. Recommended extension boundaries + +Good extension domains include: + +- presentation and editor hints; +- serialization naming and format hints; +- persistence mapping metadata; +- RPC/protocol naming metadata; +- diagnostics and telemetry metadata; +- documentation metadata. + +The domain implementation should remain outside Property Core. + +## 11. Using intrinsic capability + +A consumer may use intrinsic capability as a structural fact. + +For example, an editor can decide that a property is only eligible for an editable widget when: ```cpp -if constexpr (has_declared_effective_attribute_v) { - const auto& value = declared_effective_attribute(schema); -} +Property::writable ``` -This checks the property declaration first and then object defaults. +That is only eligibility. The editor may still apply additional application policy. -## 8. Multi-valued metadata +A persistence adapter may choose not to store a property even when `Property::readable` is true. -Core uniqueness is category-driven. An Attribute with `single_valued = true` and a non-void `attribute_category` may only appear once in one declaration. +An RPC endpoint may choose not to expose a writable property at all. -When a domain needs repeated annotations, design that metadata so it does not claim single-valued category uniqueness, then consume it through `for_each_attribute(...)`. +This separation is intentional. -Do not force naturally repeated metadata into one large unrelated object merely to satisfy a single-valued design. +## 12. Read-only optimization is a Core fact -## 9. Recommended extension boundaries +Extensions do not need to implement special locking for `read_only` stored properties. -A future extension may reasonably own metadata and interpretation for areas such as: +Core guarantees that such properties do not contribute lock slots or mutexes to the managed object and managed reads use the no-lock path. -- serialization naming and omission rules; -- RPC exposure rules; -- UI labels, groups and editor hints; -- database column mapping; -- configuration-file mapping; -- domain documentation generation. +An extension that deliberately performs raw C++ writes bypasses that guarantee and owns synchronization itself. -These are examples of extension domains, not currently implemented features. +## 13. Fallback behavior belongs to the consumer -The important boundary is that Core should not gain a direct dependency on their libraries or domain types. - -## 10. Core metadata may still be consumed - -An extension is allowed to interpret Core-owned categories when those categories are part of the extension's input contract. - -For example, an external adapter may use: - -- property `key` as the default protocol name; -- `External_Access` to determine exposure; -- `sensitive` to decide whether its own logs should omit a value. - -The extension may choose a policy based on those attributes, but it should not change what Core itself means by them. - -## 11. Fallback behavior belongs to the consumer - -The presentation extension demonstrates the intended rule: +Core should not decide domain fallbacks such as: ```text -no label Attribute - ↓ -presentation extension chooses property key as label +missing label -> key +missing database column -> key +missing RPC name -> key ``` -Core does not invent that fallback because a different consumer may want a different behavior. +Those rules belong to the corresponding extension or adapter. -The same principle should apply to future adapters. Defaults that exist only to make one domain pleasant should remain in that domain. +## 14. Avoid semantic leakage into Core -## 12. Avoid extension-to-Core semantic leakage +Do not add a Core enum or special branch only because one extension needs it. -Bad direction: +Prefer: ```text -JSON adapter needs alias support - ↓ -Core adds JSON_Alias_Category and JSON naming logic +extension-owned Attribute category ++ extension-owned interpretation ``` -Preferred direction: +over: ```text -JSON extension defines Json_Name_Category - ↓ -JSON extension interprets it - ↓ -Core remains domain-neutral +Core learns the extension domain ``` -## 13. Linked code versus header-only metadata +## 15. Linked code versus header-only metadata -Attribute definitions can often be header-only. Interpretation may be header-only or linked depending on implementation needs. +Metadata types can usually remain header-only. Linked extension code is appropriate when a domain needs non-template runtime behavior. -The current presentation extension demonstrates a linked extension target. `presentation::describe()` performs compile-time Attribute selection and calls linked `make_presentation_info()` for the concrete result construction/fallback step. +The current presentation extension keeps metadata in headers and provides `make_presentation_info` in the linked target. -Do not move linked implementation into Core merely because the extension is small. +## 16. Extension review checklist -## 14. Extension review checklist +Before adding an extension feature, ask: -Before accepting a new extension feature, ask: - -1. Does this concept belong to Core or to one consumer domain? -2. Can it be represented through the existing Attribute protocol? -3. Does the extension own the Attribute category it interprets? -4. Is an inheritable Attribute truly an object-wide default policy? -5. Is fallback behavior kept inside the extension? -6. Is the existing schema reused rather than duplicated? -7. Does the dependency still point from extension to Core? -8. Has Core remained free of third-party domain types? -9. Does the extension preserve the existing meaning of Core access, validation and synchronization? - -If the answer to the dependency or semantic-boundary questions is no, the layering should be redesigned before code is added. +1. Does Core already provide the required generic structural mechanism? +2. Can the feature be expressed as an extension-owned Attribute? +3. Is its fallback rule owned by the extension? +4. Is access/exposure policy being kept outside Core? +5. Is intrinsic `readable/writable` being treated as structure rather than authorization? +6. Does the extension avoid raw writes that would bypass managed synchronization unless intentional? +7. Does Core remain independent of the extension? diff --git a/docs/EXTENSIONS.zh-CN.md b/docs/EXTENSIONS.zh-CN.md index 8affe25..f11dcb2 100644 --- a/docs/EXTENSIONS.zh-CN.md +++ b/docs/EXTENSIONS.zh-CN.md @@ -4,65 +4,70 @@ ## 1. Extension 的职责 -Structive Extension 的作用是在不修改 Core 结构模型的前提下增加领域语义解释。 +Extension 给 Structive 增加领域 metadata 与领域解释,但不能让 Property Core 反向依赖具体领域。 -推荐依赖方向: +依赖方向: ```text -application / adapter - ↓ -Structive extension - ↓ -Structive Property Core +Extension → Property Core +Property Core -X→ Extension ``` -Core 不 include、不 link Extension。 +Core 提供: -## 2. Extension 的基本模式 +- Schema 与 Property Descriptor; +- intrinsic readable/writable; +- Attribute 协议; +- Constraint; +- Synchronization metadata 与 managed access; +- Runtime structural access。 -一个 Extension 通常只需要增加两类东西: +Extension 定义自己的 category 与解释逻辑。 -1. 一个或多个属于自己领域的 Attribute category; -2. 解释这些 Attribute 的实现代码。 +## 2. Extension 基本原则 -如果已有 `Object_Schema` 已经包含需要的结构信息,就不应该再复制第二份 property registry。 +Extension 只拥有自己的领域语义。 -## 3. 当前 Presentation Extension +例如 Presentation 拥有: -当前扩展模块定义四个 Attribute category: - -```cpp -presentation::Label_Category -presentation::Description_Category -presentation::Group_Category -presentation::Order_Category +```text +label +description +group +order ``` -对应便利 Attribute: +Property Core 不需要理解 presentation。 -```cpp -presentation::label<"Temperature"> -presentation::description<"Current device temperature"> -presentation::group<"Environment"> -presentation::order<10> +未来 Persistence Extension 可以拥有自己的持久化 metadata,RPC Extension 可以拥有协议 metadata。 + +## 3. 外部 Policy 永远属于外部 + +Property Core 完全没有内建访问控制模式。 + +Extension 或应用层自己负责: + +```text +这个属性要不要显示? +这个 endpoint 要不要允许写? +这个 Property 要不要持久化? +当前用户是否有权限? ``` -它们直接挂到普通 Core property 上: +Extension 可以读取结构事实: -```cpp -field<&Device::temperature>( - key<"temperature">, - unit<"C">, - presentation::label<"Temperature">, - presentation::description<"Current device temperature">, - presentation::group<"Environment">, - presentation::order<10> -) +```text +Property::readable +Property::writable +sensitive metadata +Extension 自己的 metadata ``` -不需要 `hint(...)` 一类额外包装。 +但最终 policy 属于 Consumer。 -## 4. Presentation 解释 +`read_only` 只表示 Structive managed object 自身没有这个 Property 的写操作,不代表任何外部系统必须暴露它。 + +## 4. 当前 Presentation Extension Include: @@ -76,192 +81,190 @@ Link: target_link_libraries(my_target PRIVATE structive::property_extensions) ``` -通过成员指针描述 property: +当前 Attribute: + +```cpp +presentation::label<"Temperature"> +presentation::description<"Current device temperature"> +presentation::group<"Environment"> +presentation::order<2> +``` + +示例: + +```cpp +field<&Device::temperature>( + key<"temperature">, + unit<"C">, + presentation::label<"Temperature">, + presentation::description<"Current device temperature">, + presentation::group<"Environment">, + presentation::order<2> +) +``` + +## 5. Presentation 解释 ```cpp auto info = presentation::describe<&Device::temperature>(device.schema()); ``` -返回: +`Presentation_Info`: -```cpp -struct Presentation_Info { - std::string_view key; - std::string_view label; - std::string_view description; - std::string_view group; - std::size_t order; - bool has_order; -}; +```text +key +label +description +group +order +has_order ``` -没有声明 presentation label 时,Presentation Extension 使用 property key 作为显示 label。这个 fallback 是 Presentation 自己的政策,不属于 Core。 +没有 label 时,Presentation Extension fallback 到 Property key。这个 fallback 属于 Presentation,不属于 Core。 -## 5. 可继承 Extension Attribute +## 6. 可继承 Extension Attribute -`presentation::group` 是 inheritable,因此可以放入 `defaults(...)`: +Extension category 可以选择 inheritable: ```cpp -return object( - defaults( - external_access, - presentation::group<"Environment"> - ), - field<&Device::temperature>( - key<"temperature">, - presentation::label<"Temperature"> - ), - field<&Device::pressure>( - key<"pressure">, - presentation::label<"Pressure"> - ) -); +defaults( + presentation::group<"Environment"> +) ``` -Extension 通过 Core 同一套 default 机制解析有效声明。 +Property-level metadata 覆盖同 category 的 inheritable single-valued default。 -`label`、`description`、`order` 不是 inheritable,因此不能放进 `defaults(...)`。 +只有真正适合继承的 metadata 才应该这样设计。 -## 6. 设计一个新的 Extension Attribute - -例如: +## 7. 设计新的 Extension Attribute ```cpp -namespace my_adapter { -struct Json_Name_Category {}; +namespace my_extension { +struct Format_Category {}; template -struct Json_Name_Attribute { - using attribute_category = Json_Name_Category; +struct Format_Attribute { + using attribute_category = Format_Category; static constexpr bool single_valued = true; static constexpr bool inheritable = false; static constexpr auto value = Value; }; template -inline constexpr Json_Name_Attribute json_name{}; +inline constexpr Format_Attribute format{}; } ``` -在 Schema 中直接使用: +直接挂在 Property: ```cpp field<&Device::temperature>( key<"temperature">, - my_adapter::json_name<"temp"> + my_extension::format<"0.0 C"> ) ``` -Core 负责存储它,但完全不需要知道 JSON 是什么。 +不需要修改 Core。 -## 7. 读取 Extension Attribute +## 8. 读取 Extension Attribute -单个 property descriptor: +Descriptor-local metadata: ```cpp -using Property = std::remove_cvref_t; -if constexpr (Property::has_attribute) { - const auto& value = property.attribute(); -} +using Property = std::remove_cvref_t())>; +static_assert(Property::template has_attribute); ``` -可继承 category 使用 effective declared lookup: +可继承 metadata 可以使用 Core 的通用 effective-Attribute 工具。 + +Fallback 和解释规则由 Extension 自己拥有。 + +## 9. Multi-Valued Metadata + +如果 category 不是 single-valued,Extension 可以允许同 category 多个 Attribute。 + +只有重复声明本身结构非法时才应该设置 `single_valued = true`。 + +## 10. 推荐 Extension 边界 + +适合的领域包括: + +- Presentation / Editor Hint; +- Serialization Name / Format Hint; +- Persistence Mapping Metadata; +- RPC / Protocol Naming Metadata; +- Diagnostics / Telemetry Metadata; +- Documentation Metadata。 + +领域实现继续放在 Property Core 外部。 + +## 11. 使用 Intrinsic Capability + +Consumer 可以把 intrinsic capability 当作结构事实。 + +例如 Editor 可以先用: ```cpp -if constexpr (has_declared_effective_attribute_v) { - const auto& value = declared_effective_attribute(schema); -} +Property::writable ``` -它先检查 property declaration,再检查 object defaults。 +判断一个 Property 是否具备“可编辑的结构资格”,但 Editor 仍然可以叠加自己的应用 policy。 -## 8. Multi-Valued Metadata +Persistence Adapter 即使看到 `Property::readable == true`,也完全可以决定不保存它。 -Core 的唯一性由 category 协议决定。拥有非 void `attribute_category` 且 `single_valued = true` 的 Attribute,在同一个声明中只能出现一次。 +RPC Endpoint 即使看到一个 Property writable,也可以完全不暴露它。 -如果某个领域天然需要重复 annotation,就不要错误地宣称它是 single-valued category;可以通过 `for_each_attribute(...)` 遍历并消费多项数据。 +这种分离是刻意设计。 -不要为了满足 single-valued 设计,把本来可以重复的独立元数据硬塞进一个巨大对象。 +## 12. Read-Only 优化属于 Core 保证 -## 9. 合理的未来 Extension 边界 +Extension 不需要为 stored `read_only` Property 自己实现特殊锁逻辑。 -未来 Extension 可以合理拥有例如: +Core 保证这类 Property 不贡献 lock slot 和 mutex,managed read 走 no-lock path。 -- serialization name / omission policy; -- RPC exposure rule; -- UI label / group / editor hint; -- database column mapping; -- configuration-file mapping; -- domain documentation generation。 +如果 Extension 故意通过 raw C++ 写它,就已经绕过该保证,同步责任由 Extension 自己承担。 -这些只是合理领域示例,不代表当前已经实现。 +## 13. Fallback 属于 Consumer -关键边界是 Core 不能因此直接依赖这些领域库或类型。 - -## 10. Extension 可以消费 Core Metadata - -Extension 可以读取 Core-owned category,只要这些 category 本来就是它的输入契约。 - -例如 External Adapter 可以使用: - -- property `key` 作为默认协议名字; -- `External_Access` 判断是否暴露; -- `sensitive` 决定自己的日志是否隐藏值。 - -Extension 可以基于这些信息建立自己领域的政策,但不能改变 Core 对这些 Attribute 的基础含义。 - -## 11. Fallback 行为属于 Consumer - -Presentation Extension 当前就是示范: +Core 不应该决定: ```text -没有 label Attribute - ↓ -Presentation Extension 使用 property key 作为 label +没有 label -> key +没有 database column -> key +没有 RPC name -> key ``` -Core 不应该发明这个 fallback,因为另一个 consumer 完全可能需要另一种策略。 +这些 fallback 属于对应 Extension 或 Adapter。 -未来 Adapter 也应该遵守这个原则。只为了某个领域“好用”的默认逻辑应该留在那个领域内部。 +## 14. 防止 Extension 语义泄漏进 Core -## 12. 防止 Extension 语义泄漏进 Core +不要因为一个 Extension 需要某个概念,就给 Core 增加特殊 enum 或分支。 -错误方向: +优先: ```text -JSON Adapter 需要 alias - ↓ -Core 增加 JSON_Alias_Category 和 JSON 命名逻辑 +Extension-owned Attribute category ++ Extension-owned interpretation ``` -正确方向: +而不是: ```text -JSON Extension 定义 Json_Name_Category - ↓ -JSON Extension 自己解释 - ↓ -Core 保持领域中立 +Core 学会 Extension 领域语义 ``` -## 13. Header-Only Metadata 与 Linked Implementation +## 15. Header-Only Metadata 与 Linked Code -Attribute 定义通常可以 header-only。具体解释逻辑可以根据实现需要选择 header-only 或 linked。 +Metadata 类型通常可以保持 header-only。领域确实需要非模板 runtime behavior 时,再放 linked implementation。 -当前 Presentation Extension 使用 linked target:`presentation::describe()` 在模板层完成 Attribute 选择,然后调用链接实现 `make_presentation_info()` 生成最终结果并处理 fallback。 +当前 Presentation Extension 把 metadata 放头文件,把 `make_presentation_info` 放 linked target。 -不能因为扩展代码量小,就把它的 linked implementation 搬进 Core。 +## 16. Extension 审核清单 -## 14. Extension 审核清单 +新增 Extension feature 前检查: -新增 Extension 功能前检查: - -1. 这个概念属于 Core,还是只属于某个 consumer domain? -2. 能否直接使用现有 Attribute 协议表达? -3. Extension 是否拥有自己解释的 category? -4. Inheritable Attribute 是否真的属于对象级默认政策? -5. Fallback 是否保留在 Extension 内? -6. 是否复用已有 Schema,而不是复制结构树? -7. 依赖是否仍然从 Extension 指向 Core? -8. Core 是否仍然没有第三方领域类型? -9. Extension 是否保持了 Core access、validation、synchronization 的原有语义? - -如果依赖方向或语义边界不成立,应先重新设计分层,再写代码。 +1. Core 是否已经提供所需的通用结构机制? +2. 能否表达成 Extension-owned Attribute? +3. Fallback 是否由 Extension 自己拥有? +4. Access / Exposure Policy 是否仍在 Core 外? +5. 是否把 intrinsic `readable/writable` 当结构事实,而不是 authorization? +6. 是否避免无意 raw write 绕过 managed synchronization? +7. Core 是否仍完全独立于 Extension? diff --git a/extensions/tests/presentation_test.cpp b/extensions/tests/presentation_test.cpp index 3feae80..dfbeb80 100644 --- a/extensions/tests/presentation_test.cpp +++ b/extensions/tests/presentation_test.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include using namespace structive; #define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false) struct Device : Property_Object { @@ -12,9 +12,9 @@ template <> struct structive::Type_Descriptor { static auto get() { return object( - defaults(external_access, presentation::group < "Environment" >), - field < &Device::temperature > (key < "temperature" >, presentation::label < "Temperature" >, presentation::order < 2 >), - field < &Device::pressure > (key < "pressure" >) + defaults(presentation::group<"Environment">), + field<&Device::temperature>(key<"temperature">, presentation::label<"Temperature">, presentation::order<2>), + field<&Device::pressure>(key<"pressure">) ); } }; @@ -22,8 +22,8 @@ int main() { Device device; const auto& temperature = device.schema().property<&Device::temperature>(); static_assert(std::remove_cvref_t::template has_attribute); - auto temperature_info = presentation::describe < &Device::temperature > (device.schema()); - auto pressure_info = presentation::describe < &Device::pressure > (device.schema()); + auto temperature_info = presentation::describe<&Device::temperature>(device.schema()); + auto pressure_info = presentation::describe<&Device::pressure>(device.schema()); REQUIRE(temperature_info.label == "Temperature"); REQUIRE(temperature_info.order == 2); REQUIRE(temperature_info.has_order);