385 lines
13 KiB
Markdown
385 lines
13 KiB
Markdown
# Structive
|
||
|
||
**Structive 在不替代 C++ 原生数据模型的前提下,为普通 struct 增加显式结构元数据和受管理属性行为。**
|
||
|
||
[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 结构属性系统,明确分成两层:
|
||
|
||
```text
|
||
C++ 对象模型
|
||
普通成员、成员函数和原生布局
|
||
│
|
||
├── Type_Descriptor<T> → Object_Schema
|
||
│ 类型级结构、key、属性固有能力、Attribute、Constraint、同步描述
|
||
│
|
||
└── Property_Object<T>
|
||
实例级 managed read/write、同步、遍历和 runtime access
|
||
```
|
||
|
||
类型本身仍然是普通 C++:
|
||
|
||
```cpp
|
||
#include <structive/property/property.hpp>
|
||
using namespace structive;
|
||
struct Device : Property_Object<Device> {
|
||
double temperature{25.0};
|
||
int serial_number{1001};
|
||
};
|
||
template <>
|
||
struct structive::Type_Descriptor<Device> {
|
||
static auto get() {
|
||
return object<Device>(
|
||
field<&Device::temperature>(key<"temperature">, unit<"C">),
|
||
field<&Device::serial_number>(key<"serial_number">, read_only)
|
||
);
|
||
}
|
||
};
|
||
```
|
||
|
||
成员仍是真实成员。Structive 只在它们旁边增加一层显式结构语义。
|
||
|
||
## 核心思想
|
||
|
||
Structive 的第一原则:
|
||
|
||
> **增强 struct,而不是替代 struct。**
|
||
|
||
因此:
|
||
|
||
- 注册字段仍然是普通 C++ 成员;
|
||
- 未注册成员完全不进入 Structive;
|
||
- C++ 业务代码优先使用 member pointer 作为编译期身份;
|
||
- string key 用于动态系统和 adapter 边界;
|
||
- 不要求把字段替换成 `Property<T>` 包装器;
|
||
- 保留 raw C++ access;
|
||
- Structive 不试图给 public member 建立安全边界;
|
||
- 外部系统是否暴露、允许读还是允许写,由外部系统自己决定;
|
||
- Core 只描述属性本身固有能做什么。
|
||
|
||
## Core 完全不做访问控制
|
||
|
||
Structive 不再内建 `internal`、`external`、`persistence`、role、context 或 policy 访问模式。
|
||
|
||
Core 不暴露领域专用访问 View、权限枚举或持久化专用访问模式。
|
||
|
||
GUI、RPC、序列化器、插件系统、持久化系统都自己决定:
|
||
|
||
```text
|
||
我要不要暴露这个属性?
|
||
我要不要允许用户修改?
|
||
我要不要保存它?
|
||
```
|
||
|
||
Structive Core 只回答结构事实:
|
||
|
||
```text
|
||
这个属性自身能不能读?
|
||
这个属性自身能不能写?
|
||
它的 key 是什么?
|
||
它有哪些 Attribute 和 Constraint?
|
||
managed access 是否需要同步?
|
||
```
|
||
|
||
## 属性自身的 Intrinsic Capability
|
||
|
||
每个 Property 只有一套固有能力:
|
||
|
||
```text
|
||
none
|
||
read
|
||
write
|
||
read_write
|
||
```
|
||
|
||
正常情况下由 Accessor 自动推导。普通可写成员天然是 `read_write`,getter-only computed property 天然是 `read`。
|
||
|
||
Schema 可以显式收窄能力:
|
||
|
||
```cpp
|
||
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<Device> {
|
||
int id{1};
|
||
int value{0};
|
||
};
|
||
template <>
|
||
struct structive::Type_Descriptor<Device> {
|
||
static auto get() {
|
||
return object<Device>(
|
||
synchronization(sync_all_shared),
|
||
field<&Device::id>(key<"id">, read_only),
|
||
field<&Device::value>(key<"value">)
|
||
);
|
||
}
|
||
};
|
||
```
|
||
|
||
即使默认是 `sync_all_shared`,`id` 仍然固定解析成 `unsynchronized_slot`。只有 `value` 会为对象贡献 mutex。
|
||
|
||
前提是调用方遵守 managed contract。如果另一个线程直接写 `device.id`,那么它已经绕过 Structive,相关 data race 由调用方负责。
|
||
|
||
## Computed Read-Only Property
|
||
|
||
Computed Property 通常自身不可写,但它可能依赖可写字段:
|
||
|
||
```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">)
|
||
```
|
||
|
||
Computed value 本身没有可写存储。它的 synchronized view 保护的是**可写依赖的一致性域**。
|
||
|
||
- dependency 是 Schema 的显式结构事实,computed view 只能读取已声明的直接依赖;
|
||
- read-only 存储依赖可以直接读取,不需要锁;
|
||
- writable 依赖如果需要同一快照,只需要彼此处于同一个 synchronization group,computed property 的读取 slot 会从 dependency 自动推导;
|
||
- computed property 不再直接加入 synchronization rule,它的同步语义由 dependency graph 决定;
|
||
- dependency graph 必须是 DAG,Schema 形成时会在编译期拒绝 cycle;
|
||
- `depends_on<>` 是合法的显式零依赖声明,此类 computed read 固定为 unsynchronized。
|
||
|
||
## Managed Access 与 Raw Access
|
||
|
||
下面两句语义不同:
|
||
|
||
```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<T>` 描述类型,`Property_Object<T>` 给实例增加 managed behavior。
|
||
|
||
默认同步拓扑每个类型只解析并共享一次。实例不再保存默认的 per-property `vector`。实例只保存真正需要的 mutex storage;只有显式传入 `Property_Synchronization` 时才保存紧凑的实例级覆盖布局。
|
||
|
||
`No_Lock_Policy` 完全不保存真实 mutex。
|
||
|
||
## 统一 Property Metadata 模型
|
||
|
||
Property Descriptor 只有一份 metadata storage,其中可以同时保存 Attribute 和 Constraint。Core 与 Extension 的描述性 metadata 共用 Attribute 协议,Constraint 保持自己的 validation 协议:
|
||
|
||
```cpp
|
||
struct Label_Category {};
|
||
template <Fixed_String Value>
|
||
struct Label_Attribute {
|
||
using attribute_category = Label_Category;
|
||
static constexpr bool single_valued = true;
|
||
static constexpr bool inheritable = false;
|
||
static constexpr auto value = Value;
|
||
};
|
||
```
|
||
|
||
Extension metadata 可以直接挂在 Property 上。`for_each_metadata()` 遍历全部 metadata,`for_each_attribute()` 只遍历 Attribute,`for_each_constraint()` 只遍历 Constraint:
|
||
|
||
```cpp
|
||
field<&Device::temperature>(
|
||
key<"temperature">,
|
||
presentation::label<"Temperature">
|
||
)
|
||
```
|
||
|
||
Core 负责保存和遍历,但不解释 Extension 自己拥有的 category。
|
||
|
||
## Validation 必须显式
|
||
|
||
Constraint 是元数据,`write()` 不自动执行 validation:
|
||
|
||
```cpp
|
||
auto error = validate_property_value<&Device::temperature>(device.schema(), 500.0);
|
||
```
|
||
|
||
Validation、transaction、rollback、synchronization 是不同问题,不隐藏在一个 setter 里。
|
||
|
||
## Synchronization
|
||
|
||
Synchronization 是 topology 层,不是访问控制。它只描述 intrinsically mutable Property 在 managed 并发访问时如何组成一致性域。Stored read-only Property 会在 lock slot 创建前被裁掉,因此属性元数据会直接转化为更低的运行时同步成本。
|
||
|
||
Structive 提供三种默认规则:
|
||
|
||
```cpp
|
||
sync_all_independent
|
||
sync_all_shared
|
||
sync_all_unsynchronized
|
||
```
|
||
|
||
以及 typed / runtime-key override 和 Group:
|
||
|
||
```cpp
|
||
synchronization(
|
||
sync_all_independent,
|
||
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
||
)
|
||
```
|
||
|
||
这些入口并不是重复设计:compile-time member rule 服务 typed C++,runtime-key Plan 服务动态 Adapter,per-instance override 服务少数确实需要特殊 topology 的对象,Guard 服务一次临时的多 Property 一致性操作。它们最终都解析为同一套紧凑 lock-slot 模型。
|
||
|
||
多 Property Guard 会对 lock domain 去重,并按稳定 slot 顺序获取锁:
|
||
|
||
```cpp
|
||
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
|
||
auto old_min = guard.get<&Device::min_speed>();
|
||
auto old_max = guard.get<"maximum_speed">();
|
||
guard.set<"minimum_speed">(20);
|
||
guard.set<&Device::max_speed>(120);
|
||
```
|
||
|
||
Typed Guard 的 capability 在编译期通过 constraint 控制;runtime-key Guard 在运行期验证 key 和 capability。详细契约见 [Core Guide: Synchronization](docs/CORE_GUIDE.zh-CN.md#14-synchronization-plan),对应边界、阻塞和锁顺序测试见 `core/tests/synchronization_test.cpp`。
|
||
|
||
## Runtime Access
|
||
|
||
`Property_Object_Base` 故意保持为底层 type-erased Adapter 边界,面向只有运行期才知道 key 的代码:
|
||
|
||
```cpp
|
||
Property_Object_Base& erased = device;
|
||
auto type = erased.runtime_object_type();
|
||
auto count = erased.runtime_property_count();
|
||
```
|
||
|
||
动态操作只有:
|
||
|
||
```text
|
||
runtime_read(key, context, callback)
|
||
runtime_write(key, type_info, value)
|
||
```
|
||
|
||
返回 `ok`、`unknown_property`、`not_readable`、`not_writable`、`unsupported_runtime_write` 或 `type_mismatch`。Runtime write 是精确类型的 copy-input 边界,不做隐式转换。如果 Accessor 只能接收 move-only 输入,Property 仍然可以保持 intrinsic `writable`,但会暴露 `runtime_copy_writable == false`;typed `write` 仍然支持这种 Property。Read callback 收到的是借用指针,只在 callback 期间有效;对于需要同步的 writable state,callback 执行期间 managed read lock 仍然持有。Stored read-only Property 继续走和 typed read 一样的 zero-lock fast path。
|
||
|
||
Core 不在这个边界强制引入 `variant`、`any`、转换注册表或 serialization 所有权策略,上层 Adapter 可以按领域需要封装。这里没有 runtime access mode,也没有访问控制;外部系统自行决定暴露策略,Structive 只报告 Property intrinsic capability。详细契约见 [Core Guide: Runtime Access](docs/CORE_GUIDE.zh-CN.md#22-runtime-type-erased-access),测试见 `core/tests/runtime_api_test.cpp`。
|
||
|
||
## Core 当前元数据
|
||
|
||
Core 当前定义:
|
||
|
||
- `key<"...">`
|
||
- `read_only`
|
||
- `write_only`
|
||
- `read_write`
|
||
- `inaccessible`
|
||
- `unit<"...">`
|
||
- `sensitive<>`
|
||
- `min_value<...>`
|
||
- `max_value<...>`
|
||
- `finite`
|
||
- `constraint<"code">(...)`
|
||
|
||
其中四种 capability 只描述 Property 自身,不承担访问控制职责。
|
||
|
||
## 当前 Extension 元数据
|
||
|
||
Presentation Extension 定义:
|
||
|
||
- `presentation::label<"...">`
|
||
- `presentation::description<"...">`
|
||
- `presentation::group<"...">`
|
||
- `presentation::order<N>`
|
||
|
||
Presentation consumer 是否显示、是否允许编辑,由 consumer 自己决定,不属于 Property Core。
|
||
|
||
## 构建
|
||
|
||
```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
|
||
cmake --build build
|
||
ctest --test-dir build --output-on-failure
|
||
```
|
||
|
||
独立构建提供 `STRUCTIVE_BUILD_EXAMPLES`、`STRUCTIVE_BUILD_TESTS`、`STRUCTIVE_INSTALL`。Example 只在 Structive 作为顶层工程时默认开启;测试跟随 `BUILD_TESTING`;独立安装默认开启。安装后可通过 `find_package(Structive CONFIG)` 使用 `structive::property_core` 与 `structive::property_extensions`,并且 standalone CTest 会真实验证外部 install consumer。
|
||
|
||
测试按契约分工,避免在一个大用例中重复覆盖:
|
||
|
||
- `property_core_test.cpp`:Schema、Attribute、显式 Validation、typed access、traversal、computed property 与对象复制语义;
|
||
- `runtime_api_test.cpp`:type-erased result code、callback metadata、copy-write 边界、managed blocking 与只读零锁;
|
||
- `synchronization_test.cpp`:topology、非法 Plan、Guard held-set、阻塞关系与稳定锁顺序;
|
||
- `compile_fail/`:重复 key/storage/单值 Attribute、缺失 key、capability/constraint/member 不匹配;
|
||
- 独立公共头测试与 install consumer:保护 include 自足性和导出包边界。
|
||
|
||
## 详细文档
|
||
|
||
- [设计理念与原则](docs/DESIGN.zh-CN.md)
|
||
- [Core 完整指南](docs/CORE_GUIDE.zh-CN.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)
|