637 lines
18 KiB
Markdown
637 lines
18 KiB
Markdown
# Structive Property Core 完整指南
|
||
|
||
[English](CORE_GUIDE.md)
|
||
|
||
## 1. Include 与 CMake Target
|
||
|
||
```cpp
|
||
#include <structive/property/property.hpp>
|
||
```
|
||
|
||
```cmake
|
||
target_link_libraries(my_target PRIVATE structive::property_core)
|
||
```
|
||
|
||
Property Core 使用 C++20。
|
||
|
||
## 2. 定义 Managed Object
|
||
|
||
```cpp
|
||
using namespace structive;
|
||
struct Device : Property_Object<Device> {
|
||
double temperature{25.0};
|
||
double pressure{101.3};
|
||
int serial_number{1001};
|
||
};
|
||
```
|
||
|
||
`Property_Object<Device>` 增加 managed operation,字段本身仍是普通成员。
|
||
|
||
## 3. 定义 Type Descriptor
|
||
|
||
```cpp
|
||
template <>
|
||
struct structive::Type_Descriptor<Device> {
|
||
static auto get() {
|
||
return object<Device>(
|
||
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::serial_number>(
|
||
key<"serial_number">,
|
||
read_only
|
||
)
|
||
);
|
||
}
|
||
};
|
||
```
|
||
|
||
Descriptor 就是 `Device` 的结构定义。
|
||
|
||
## 4. Schema 静态保证
|
||
|
||
合法 Schema 保证:
|
||
|
||
- 每个 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<Device>);
|
||
using Schema = type_descriptor_schema_t<Device>;
|
||
static_assert(Valid_Property_Schema<Schema>);
|
||
```
|
||
|
||
## 5. 获取 Schema
|
||
|
||
```cpp
|
||
const auto& schema = type_descriptor<Device>();
|
||
const auto& same_schema = device.schema();
|
||
```
|
||
|
||
按 index 或 member pointer 获取 Property:
|
||
|
||
```cpp
|
||
const auto& first = schema.property<0>();
|
||
const auto& temperature = schema.property<&Device::temperature>();
|
||
```
|
||
|
||
## 6. Property Descriptor
|
||
|
||
Descriptor 暴露编译期结构事实:
|
||
|
||
```cpp
|
||
using Property = std::remove_cvref_t<decltype(schema.property<&Device::temperature>())>;
|
||
static_assert(Property::readable);
|
||
static_assert(Property::writable);
|
||
static_assert(Property::runtime_copy_writable);
|
||
using Value = Property::value_type;
|
||
using Accessor = Property::accessor_type;
|
||
```
|
||
|
||
`read_only` 字段:
|
||
|
||
```cpp
|
||
using Serial = std::remove_cvref_t<decltype(schema.property<&Device::serial_number>())>;
|
||
static_assert(Serial::readable);
|
||
static_assert(!Serial::writable);
|
||
```
|
||
|
||
运行时可直接取得 key:
|
||
|
||
```cpp
|
||
auto key_value = schema.property<&Device::temperature>().key();
|
||
```
|
||
|
||
## 7. Intrinsic Capability
|
||
|
||
Core capability:
|
||
|
||
```cpp
|
||
Property_Capability::none
|
||
Property_Capability::read
|
||
Property_Capability::write
|
||
Property_Capability::read_write
|
||
```
|
||
|
||
便捷 Attribute:
|
||
|
||
```cpp
|
||
read_only
|
||
write_only
|
||
read_write
|
||
inaccessible
|
||
```
|
||
|
||
Capability 属于 Property 自身,不是 authorization rule。
|
||
|
||
不声明 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
|
||
```
|
||
|
||
Consumer 自己决定 policy。例如 GUI 可以只展示部分 Property,即使这些 Property 在结构上都 readable。
|
||
|
||
Core 只给出 intrinsic `readable` / `writable`。
|
||
|
||
## 9. Core Attribute
|
||
|
||
### 9.1 Key
|
||
|
||
每个 Property 必须有 key:
|
||
|
||
```cpp
|
||
key<"temperature">
|
||
```
|
||
|
||
Key 是 runtime lookup 与 adapter 使用的结构协议身份。
|
||
|
||
### 9.2 Capability
|
||
|
||
```cpp
|
||
read_only
|
||
write_only
|
||
read_write
|
||
inaccessible
|
||
```
|
||
|
||
Capability Attribute 是 single-valued、non-inheritable。
|
||
|
||
### 9.3 Unit
|
||
|
||
```cpp
|
||
unit<"C">
|
||
unit<"kPa">
|
||
```
|
||
|
||
Core 保存单位信息,但不负责换算。
|
||
|
||
### 9.4 Sensitive
|
||
|
||
```cpp
|
||
sensitive<>
|
||
sensitive<false>
|
||
```
|
||
|
||
`sensitive` 是可继承 metadata,不实现访问控制。Consumer 可以把它作为自己 policy 的一个输入。
|
||
|
||
## 10. 自定义 Attribute 与 Defaults
|
||
|
||
任何遵守 Attribute protocol 的类型都可以挂在 Property 上。
|
||
|
||
```cpp
|
||
struct Group_Category {};
|
||
template <Fixed_String Value>
|
||
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<Device>(
|
||
defaults(sensitive<>),
|
||
field<&Device::temperature>(key<"temperature">)
|
||
)
|
||
```
|
||
|
||
Capability 故意不可继承,因为每个 Property 的固有操作集合必须独立成立。
|
||
|
||
## 11. Constraint 与 Validation
|
||
|
||
内置 Constraint:
|
||
|
||
```cpp
|
||
min_value<0>
|
||
max_value<100>
|
||
finite
|
||
```
|
||
|
||
自定义 Constraint:
|
||
|
||
```cpp
|
||
constraint<"even">([](int value) {
|
||
return value % 2 == 0;
|
||
})
|
||
```
|
||
|
||
显式验证:
|
||
|
||
```cpp
|
||
auto result = validate_property_value<&Device::temperature>(device.schema(), candidate);
|
||
if (result) {
|
||
auto key_value = result->property_key;
|
||
auto code = result->code;
|
||
}
|
||
```
|
||
|
||
`write()` 不自动调用 Validation。
|
||
|
||
## 12. Typed Managed Read/Write
|
||
|
||
C++ 业务代码优先 member pointer:
|
||
|
||
```cpp
|
||
auto temperature = device.read<&Device::temperature>();
|
||
device.write<&Device::temperature>(30.0);
|
||
```
|
||
|
||
也支持编译期 key:
|
||
|
||
```cpp
|
||
auto temperature = device.read<"temperature">();
|
||
device.write<"temperature">(30.0);
|
||
```
|
||
|
||
Typed API 由 intrinsic capability 约束。对 `read_only` 调用 write 时,函数在 overload resolution 阶段就不可用。
|
||
|
||
```cpp
|
||
template <class Object>
|
||
concept Can_Write_Serial = requires(Object& object) {
|
||
object.template write<&Device::serial_number>(1);
|
||
};
|
||
static_assert(!Can_Write_Serial<Device>);
|
||
```
|
||
|
||
## 13. Read-Only Fast Path
|
||
|
||
Stored read-only property 在编译期和同步解析阶段都会被裁掉。
|
||
|
||
```cpp
|
||
field<&Device::serial_number>(key<"serial_number">, read_only)
|
||
```
|
||
|
||
最终:
|
||
|
||
```text
|
||
serial_number -> unsynchronized_slot
|
||
```
|
||
|
||
`read<&Device::serial_number>()` 不查询 lock slot,也不构造 `shared_lock`。
|
||
|
||
即使默认是 `sync_all_shared` 也一样。
|
||
|
||
如果一个对象只有 stored read-only property,这些 Property 对 `resolved_synchronization().lock_count` 的贡献为 0。
|
||
|
||
## 14. Synchronization Plan
|
||
|
||
Synchronization 是独立的 topology 层,不是访问权限系统。它只回答一个问题:managed mutable state 并发访问时,哪些 Property 属于同一个一致性域。
|
||
|
||
Stored intrinsic read-only property 会在 lock slot materialization 之前被移除。因此即使默认规则是 `sync_all_shared`,也不会仅仅因为一个 stored `read_only` Property 而创建 mutex。
|
||
|
||
### 14.1 默认 Topology
|
||
|
||
| 默认规则 | 对真正需要同步的 Property 的含义 |
|
||
| --- | --- |
|
||
| `sync_all_independent` | 每个 Property 独立一个 lock domain |
|
||
| `sync_all_shared` | 所有 Property 共用一个 lock domain |
|
||
| `sync_all_unsynchronized` | 不创建真实 lock domain |
|
||
|
||
```cpp
|
||
synchronization(sync_all_independent)
|
||
```
|
||
|
||
`sync_all_unsynchronized` 是显式放弃同步。Structive 仍提供 managed access,但并发读写造成的线程安全责任由调用方承担。
|
||
|
||
### 14.2 编译期 Member Rule
|
||
|
||
Schema 在 C++ 中已知时优先使用 member pointer:
|
||
|
||
```cpp
|
||
synchronization(
|
||
sync_all_shared,
|
||
sync_independent<&Device::temperature>(),
|
||
sync_unsynchronized<&Device::debug_counter>()
|
||
)
|
||
```
|
||
|
||
Specification 针对具体 Schema materialize 时会检查 Member 是否真的注册。
|
||
|
||
### 14.3 Runtime Key Rule
|
||
|
||
Adapter 或动态配置代码可以使用 key 构造 `Synchronization_Plan`:
|
||
|
||
```cpp
|
||
Synchronization_Plan plan;
|
||
plan.set_default(Synchronization_Default::independent);
|
||
plan.unsynchronized("debug_counter");
|
||
```
|
||
|
||
Key 只能在 resolve 时验证。未知 Property、同一 Property 重复配置、空 Group、重复 Group 名都会抛 `std::invalid_argument`。
|
||
|
||
### 14.4 Group 表示一致性域
|
||
|
||
```cpp
|
||
synchronization(
|
||
sync_all_independent,
|
||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||
)
|
||
```
|
||
|
||
同一 Group 中真正需要同步的 Property 会解析到同一个 lock slot。Stored read-only Property 即使被宽泛规则或 Group 包含,也仍然是 `unsynchronized_slot`,因为 managed path 下不存在 writer。
|
||
|
||
Group 应该表达真实业务 invariant 或 snapshot boundary,而不是单纯为了减少 mutex 数量。
|
||
|
||
### 14.5 为什么 Synchronization API 有多种入口
|
||
|
||
这些入口对应的是“信息在什么时候已知”,不是重复抽象:
|
||
|
||
- 类型级默认规则描述一个类型的正常 topology;
|
||
- compile-time member rule 让普通 C++ 代码获得 Schema 编译期检查;
|
||
- runtime-key rule 服务动态 Adapter;
|
||
- per-instance override 服务确实需要特殊 topology 的个别对象;
|
||
- Guard 表达一次临时的多 Property 一致性操作。
|
||
|
||
它们最终都落到同一个 resolved lock-slot topology。
|
||
|
||
## 15. 每实例 Synchronization Override
|
||
|
||
对象可以显式覆盖类型默认策略:
|
||
|
||
```cpp
|
||
Device device{
|
||
property_synchronization(
|
||
synchronization(sync_all_shared)
|
||
)
|
||
};
|
||
```
|
||
|
||
Typed member rule:
|
||
|
||
```cpp
|
||
Device device{
|
||
property_synchronization<Device>(
|
||
synchronization(
|
||
sync_all_independent,
|
||
sync_group<&Device::temperature, &Device::pressure>("environment")
|
||
)
|
||
)
|
||
};
|
||
```
|
||
|
||
默认 topology 每类型只 resolve 一次并共享。只有显式 override 的实例才保存紧凑 override layout。Copy/move construction 保留源对象的 override topology;assignment 保留目标对象已经选择的 topology,因为赋值修改的是对象状态,不应该偷偷改变这个实例的同步策略。
|
||
|
||
## 16. 查看 Resolved Synchronization
|
||
|
||
```cpp
|
||
auto view = device.resolved_synchronization();
|
||
auto count = view.lock_count;
|
||
```
|
||
|
||
成员 slot:
|
||
|
||
```cpp
|
||
auto slot = device.lock_slot<&Device::temperature>();
|
||
```
|
||
|
||
无锁 Property 使用:
|
||
|
||
```cpp
|
||
Resolved_Synchronization_View::unsynchronized_slot
|
||
```
|
||
|
||
Stored read-only Property 自动得到这个值。`resolved_synchronization()` 和 `lock_slot()` 更适合诊断和框架检查;普通业务代码通常应该通过 `read`、`write`、`lock_shared`、`lock_unique` 表达意图,而不是依赖数字 slot。
|
||
|
||
## 17. 静态多属性 Guard
|
||
|
||
Read Guard:
|
||
|
||
```cpp
|
||
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
|
||
auto by_member = guard.get<&Device::temperature>();
|
||
auto by_key = guard.get<"pressure">();
|
||
```
|
||
|
||
Write Guard:
|
||
|
||
```cpp
|
||
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
|
||
guard.set<&Device::min_speed>(20);
|
||
guard.set<"maximum_speed">(120);
|
||
```
|
||
|
||
Member 和 compile-time key 统一使用同名 `get`/`set`。Capability 直接进入 overload constraint:read-only Property 不能创建 typed unique guard,`requires` 表达式中也不会看到它的 `set`。
|
||
|
||
Static Guard 会先解析 slot、去重,然后按稳定的数字 slot 顺序获取锁。调用方传入 Member 的顺序不会成为 mutex 获取顺序,因此两个调用方即使按相反 Member 顺序请求同一批 domain,也不会因为 API 参数顺序造成 lock-order inversion。
|
||
|
||
Stored read-only Property 可以出现在 shared guard 的逻辑 held set 中,但不会因此创建 mutex,仍保持 read-only zero-lock fast path。
|
||
|
||
## 18. Dynamic-Key Guard
|
||
|
||
```cpp
|
||
auto read_guard = device.lock_shared({"temperature", "pressure"});
|
||
auto write_guard = device.lock_unique({"temperature", "pressure"});
|
||
auto value = read_guard.get<"temperature">();
|
||
```
|
||
|
||
Dynamic Guard 服务“目标 Property 集合只有运行期才知道”的场景,因此 typed guard 的编译期错误在这里变成运行时错误:
|
||
|
||
- 未知 key 抛 `std::invalid_argument`;
|
||
- `lock_shared` 遇到不可读 Property 时拒绝;
|
||
- `lock_unique` 遇到不可写 Property 时拒绝;
|
||
- 后续 `get`/`set` 请求不在 held set 中的 Property 时抛 `std::logic_error`。
|
||
|
||
Dynamic Guard 和 Static Guard 使用完全相同的 resolved topology 和稳定锁顺序。
|
||
|
||
这些行为由 `core/tests/synchronization_test.cpp` 单独覆盖,包括 independent/shared/unsynchronized 默认规则、Group、override、非法 Plan、runtime-key Guard、阻塞语义以及相反 Member 顺序获取锁。
|
||
|
||
## 19. Traversal
|
||
|
||
只遍历 Schema:
|
||
|
||
```cpp
|
||
schema.for_each_property([&](auto index, const auto& descriptor) {
|
||
});
|
||
```
|
||
|
||
遍历 readable value:
|
||
|
||
```cpp
|
||
device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) {
|
||
});
|
||
```
|
||
|
||
一次性锁定后遍历:
|
||
|
||
```cpp
|
||
device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) {
|
||
});
|
||
```
|
||
|
||
Locked traversal 只获取真实存在的同步 slot,read-only stored property 不会引入锁。
|
||
|
||
获取全部 writable lock domain:
|
||
|
||
```cpp
|
||
device.with_all_writable_locked([&](auto& guard) {
|
||
});
|
||
```
|
||
|
||
它不会自动执行 validation 或 rollback。
|
||
|
||
## 20. Computed Property
|
||
|
||
Synchronized computed property 接收 read view:
|
||
|
||
```cpp
|
||
computed_property<Device, int>(depends_on<&Device::min_speed, &Device::max_speed>, [](const auto& view) {
|
||
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
||
}, key<"speed_span">)
|
||
```
|
||
|
||
`depends_on<...>` 把 dependency 变成 Schema 的显式结构事实。也可以通过 `depends_on_keys<"a", "b">` 按 key 声明。Computed view 只能读取已声明的直接 dependency。
|
||
|
||
如果 writable dependency 需要同一快照,只需要让这些 writable dependency 共享 synchronization domain:
|
||
|
||
```cpp
|
||
synchronization(
|
||
sync_all_independent,
|
||
sync_group("speed", "min_speed", "max_speed")
|
||
)
|
||
```
|
||
|
||
Computed Property 自身不要加入 synchronization rule。它的读取 slot 根据 dependency graph 自动推导;如果需要同步的 dependency 不在同一个 domain,Schema/实例 synchronization topology 会被拒绝。Read-only stored dependency 不需要 lock slot。
|
||
|
||
Schema 可通过 `schema_property_dependency_indices<Schema, Index>()` 读取某个 Property 的直接 dependency index。
|
||
|
||
## 21. Trusted Accessor Property
|
||
|
||
Trusted getter:
|
||
|
||
```cpp
|
||
trusted_computed_property<&Device::value>(key<"value">)
|
||
```
|
||
|
||
Trusted getter/setter:
|
||
|
||
```cpp
|
||
trusted_accessor_property<&Device::get_value, &Device::set_value>(key<"value">)
|
||
```
|
||
|
||
这类入口直接访问对象,而不是通过 synchronized dependency view。只有调用方明确掌握成员函数同步语义时才应该使用。
|
||
|
||
## 22. Runtime Type-Erased Access
|
||
|
||
`Property_Object_Base` 是动态 Adapter 边界,面向 GUI inspector、serialization adapter、脚本绑定、RPC 层等“只有运行期才知道 Property key”的代码。普通 typed C++ 业务代码仍应优先使用 `read` / `write`。
|
||
|
||
```cpp
|
||
Property_Object_Base& erased = device;
|
||
```
|
||
|
||
类型信息:
|
||
|
||
```cpp
|
||
erased.runtime_object_type();
|
||
erased.runtime_property_count();
|
||
```
|
||
|
||
### 22.1 Runtime Read
|
||
|
||
```cpp
|
||
auto result = erased.runtime_read("temperature", context, callback);
|
||
```
|
||
|
||
成功时 callback 恰好调用一次,并收到 Schema index、key、精确 `type_info` 以及当前 value 的指针。这个 value pointer 是借用指针,只在 callback 执行期间有效;必须同步消费或复制,不能保存到 callback 之后继续使用。
|
||
|
||
对于需要同步的 writable Property,Structive 在 callback 执行期间仍然持有对应的 managed read lock,因此 callback 不应该重入同一 lock domain 的冲突写操作。Stored read-only Property 继续走 intrinsic zero-lock fast path,不会因为入口变成 runtime 就额外创建或获取 mutex。
|
||
|
||
### 22.2 Runtime Write
|
||
|
||
```cpp
|
||
auto result = erased.runtime_write("temperature", typeid(double), &value);
|
||
```
|
||
|
||
Runtime write 明确不做隐式转换。`typeid(double)` 必须与 Property 声明的 value type 完全一致,`value` 指针在调用期间必须指向这个精确类型的有效对象。这个边界从 `const` 输入复制;如果一个 intrinsically writable Accessor 无法接受 copy-input,它会暴露 `runtime_copy_writable == false`,runtime access 返回 `unsupported_runtime_write`,而 typed `write` 在 Accessor 支持时仍可正常接收 move-only 值。成功写入继续复用 typed `write` 的 managed synchronization 路径。
|
||
|
||
### 22.3 Result Contract
|
||
|
||
| Result | 含义 |
|
||
| --- | --- |
|
||
| `ok` | lookup 和访问完成 |
|
||
| `unknown_property` | Schema 中不存在该 runtime key |
|
||
| `not_readable` | Property 存在,但 intrinsic capability 不可读 |
|
||
| `not_writable` | Property 存在,但 intrinsic capability 不可写 |
|
||
| `unsupported_runtime_write` | Property intrinsically writable,但 Accessor 无法接收 runtime copy-input 边界 |
|
||
| `type_mismatch` | runtime write 提供的类型和 Property value type 不一致 |
|
||
|
||
这里没有 external/persistence mode,也没有访问控制 policy。Adapter 自己决定是否对外暴露、是否调用 runtime read/write;Structive 只报告 Property 自身的 intrinsic capability。
|
||
|
||
### 22.4 为什么 Runtime API 故意保持底层
|
||
|
||
动态边界使用 `type_info`、`void*` 和 callback,是因为调用方在编译期根本不知道值类型。Core 不强制引入统一 `variant`、堆分配 `any`、serialization 格式或者 conversion registry,因为这些方案都会把所有权和转换策略重新塞回 Core。
|
||
|
||
因此 Runtime API 是 Adapter API,而不是普通业务代码首选 API。上层 Extension 完全可以按自己的领域需求再包一层 value container,而不用改变 Structive Core。
|
||
|
||
这些契约由 `core/tests/runtime_api_test.cpp` 单独覆盖,包括全部 result code、callback metadata、精确类型写入、managed blocking 语义,以及 read-only zero-lock runtime fast path。
|
||
|
||
## 23. Lock Policy
|
||
|
||
默认:
|
||
|
||
```cpp
|
||
Property_Object<Device, Shared_Mutex_Policy>
|
||
```
|
||
|
||
NoLock:
|
||
|
||
```cpp
|
||
Property_Object<Device, No_Lock_Policy>
|
||
```
|
||
|
||
`No_Lock_Policy` 不保存真实 mutex array,并从 managed hot path 去除真实 lock object。
|
||
|
||
## 24. Raw Object Access
|
||
|
||
```cpp
|
||
Device& raw = device.unsafe_object();
|
||
```
|
||
|
||
或直接 public member:
|
||
|
||
```cpp
|
||
device.temperature = 30.0;
|
||
```
|
||
|
||
都绕过 Structive managed contract。
|
||
|
||
这是刻意设计。Structive 是协作式结构基础设施,不是强制封装。
|
||
|
||
## 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。
|