api更新

This commit is contained in:
2026-08-07 20:20:46 +08:00
parent 3205dd84e8
commit caec91ae3f
13 changed files with 653 additions and 201 deletions
+14 -18
View File
@@ -250,7 +250,9 @@ Validation, transactions, rollback and synchronization are separate concerns.
## Synchronization
Structive provides:
Synchronization is a topology layer, not access control. It defines which intrinsically mutable properties share a managed consistency domain. Stored read-only properties are removed before lock slots are created, so metadata directly reduces runtime synchronization cost.
Structive provides three defaults:
```cpp
sync_all_independent
@@ -258,7 +260,7 @@ sync_all_shared
sync_all_unsynchronized
```
and per-property/group rules:
and typed or runtime-key overrides/groups:
```cpp
synchronization(
@@ -267,21 +269,23 @@ synchronization(
)
```
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.
The several entry forms are intentional: compile-time member rules serve typed C++ code, runtime-key plans serve adapters, per-instance overrides serve exceptional objects, and guards express temporary multi-property consistency. They all resolve to the same compact lock-slot model.
Multi-property guards deduplicate lock slots and acquire them in stable order:
Multi-property guards deduplicate lock domains and acquire them in stable slot order:
```cpp
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
auto old_min = guard.get<&Device::min_speed>();
guard.set<&Device::min_speed>(20);
auto old_max = guard.get<"maximum_speed">();
guard.set<"minimum_speed">(20);
guard.set<&Device::max_speed>(120);
```
A typed unique guard can only be requested for intrinsically writable properties.
Typed guard capability is constrained at compile time. Runtime-key guards validate keys and capability at runtime. See [Core Guide: Synchronization](docs/CORE_GUIDE.md#14-synchronization-plans) and `core/tests/synchronization_test.cpp` for topology, validation, blocking and lock-order coverage.
## Runtime access
`Property_Object_Base` exposes only intrinsic dynamic access:
`Property_Object_Base` is deliberately a low-level type-erased adapter boundary for code that discovers keys only at runtime:
```cpp
Property_Object_Base& erased = device;
@@ -292,21 +296,13 @@ auto count = erased.runtime_property_count();
Runtime operations are key based:
```text
runtime_read(key, ...)
runtime_read(key, context, callback)
runtime_write(key, type_info, value)
```
They return:
They return `ok`, `unknown_property`, `not_readable`, `not_writable` or `type_mismatch`. Runtime write requires an exact type match and performs no implicit conversion. The read callback receives a borrowed pointer that is valid only during the callback; synchronized writable state remains read-locked while the callback executes. Stored read-only properties retain the same zero-lock fast path as typed reads.
```text
ok
unknown_property
not_readable
not_writable
type_mismatch
```
There is no runtime access mode. An external adapter decides whether it should call `runtime_read` or `runtime_write` for a given property.
Core intentionally does not impose `variant`, `any`, conversion registries or serialization ownership on this boundary. Higher-level adapters may wrap it. There is no runtime access mode or access-control policy: an external system decides what it exposes, while Structive reports only intrinsic property capability. See [Core Guide: Runtime access](docs/CORE_GUIDE.md#22-runtime-type-erased-access) and `core/tests/runtime_api_test.cpp`.
## Current Core metadata
+15 -19
View File
@@ -256,7 +256,9 @@ Validation、transaction、rollback、synchronization 是不同问题,不隐
## Synchronization
Structive 提供:
Synchronization 是 topology 层,不是访问控制。它只描述 intrinsically mutable Property 在 managed 并发访问时如何组成一致性域。Stored read-only Property 会在 lock slot 创建前被裁掉,因此属性元数据会直接转化为更低的运行时同步成本。
Structive 提供三种默认规则:
```cpp
sync_all_independent
@@ -264,7 +266,7 @@ sync_all_shared
sync_all_unsynchronized
```
以及 Propertygroup 规则
以及 typed / runtime-key overrideGroup
```cpp
synchronization(
@@ -273,21 +275,23 @@ synchronization(
)
```
只有真正需要同步的 Property 才进入最终 lock topology。read-only 存储属性即使被宽泛默认规则覆盖,也会在 resolve 阶段被裁掉
这些入口并不是重复设计:compile-time member rule 服务 typed C++runtime-key Plan 服务动态 Adapterper-instance override 服务少数确实需要特殊 topology 的对象,Guard 服务一次临时的多 Property 一致性操作。它们最终都解析为同一套紧凑 lock-slot 模型
属性 Guard 对 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>();
guard.set<&Device::min_speed>(20);
auto old_max = guard.get<"maximum_speed">();
guard.set<"minimum_speed">(20);
guard.set<&Device::max_speed>(120);
```
typed `lock_unique` 只能用于 intrinsically writable property
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` 只提供属性自身的动态访问
`Property_Object_Base` 故意保持为底层 type-erased Adapter 边界,面向只有运行期才知道 key 的代码
```cpp
Property_Object_Base& erased = device;
@@ -295,24 +299,16 @@ auto type = erased.runtime_object_type();
auto count = erased.runtime_property_count();
```
动态接口只有:
动态操作只有:
```text
runtime_read(key, ...)
runtime_read(key, context, callback)
runtime_write(key, type_info, value)
```
返回
返回 `ok``unknown_property``not_readable``not_writable``type_mismatch`。Runtime write 要求类型完全一致,不做隐式转换。Read callback 收到的是借用指针,只在 callback 期间有效;对于需要同步的 writable statecallback 执行期间 managed read lock 仍然持有。Stored read-only Property 继续走和 typed read 一样的 zero-lock fast path。
```text
ok
unknown_property
not_readable
not_writable
type_mismatch
```
没有 runtime access mode。外部 adapter 自己决定是否应该对某个属性调用 `runtime_read``runtime_write`
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 当前元数据
+8
View File
@@ -10,9 +10,17 @@ endif ()
if (BUILD_TESTING)
find_package(Threads REQUIRED)
add_executable(structive_property_core_test "${CMAKE_CURRENT_LIST_DIR}/tests/property_core_test.cpp")
add_executable(structive_property_runtime_api_test "${CMAKE_CURRENT_LIST_DIR}/tests/runtime_api_test.cpp")
add_executable(structive_property_synchronization_test "${CMAKE_CURRENT_LIST_DIR}/tests/synchronization_test.cpp")
target_link_libraries(structive_property_core_test PRIVATE structive::property_core Threads::Threads)
target_link_libraries(structive_property_runtime_api_test PRIVATE structive::property_core Threads::Threads)
target_link_libraries(structive_property_synchronization_test PRIVATE structive::property_core Threads::Threads)
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(structive_property_core_test PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(structive_property_runtime_api_test PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(structive_property_synchronization_test PRIVATE -Wall -Wextra -Wpedantic)
endif ()
add_test(NAME structive_property_core_test COMMAND structive_property_core_test)
add_test(NAME structive_property_runtime_api_test COMMAND structive_property_runtime_api_test)
add_test(NAME structive_property_synchronization_test COMMAND structive_property_synchronization_test)
endif ()
+2
View File
@@ -6,4 +6,6 @@ Core has no access-control modes. A property only reports its own intrinsic `rea
Stored `read_only` properties do not receive lock slots or contribute mutexes, and managed reads use the no-lock fast path.
Focused runtime and synchronization contracts are regression-tested in `tests/runtime_api_test.cpp` and `tests/synchronization_test.cpp`.
See [Core Guide](../docs/CORE_GUIDE.md) and [Design Philosophy](../docs/DESIGN.md).
+2
View File
@@ -6,4 +6,6 @@ Core 完全没有访问控制 mode。Property 只报告自身 intrinsic `readabl
Stored `read_only` Property 不分配 lock slot、不贡献 mutexmanaged read 直接走 no-lock fast path。
Runtime 与 synchronization 的边界契约分别由 `tests/runtime_api_test.cpp``tests/synchronization_test.cpp` 做独立回归测试。
详见 [Core 完整指南](../docs/CORE_GUIDE.zh-CN.md) 与 [设计理念](../docs/DESIGN.zh-CN.md)。
@@ -79,6 +79,7 @@ public:
}
};
}
/// Result of type-erased runtime property access. Runtime lookup never performs implicit type conversion.
enum class Runtime_Access_Result {
ok,
unknown_property,
@@ -86,7 +87,11 @@ enum class Runtime_Access_Result {
not_writable,
type_mismatch
};
/// Callback used by `runtime_read`.
/// The value pointer is valid only for the duration of the callback and must be consumed or copied synchronously. For synchronized writable state, the corresponding managed read lock remains held while the callback executes, so the callback must not re-enter a conflicting write on the same lock domain.
using Runtime_Read_Callback = void (*)(void*, std::size_t, std::string_view, const std::type_info&, const void*);
/// Type-erased boundary intended for adapters that discover a property key only at runtime.
/// Normal C++ business code should prefer `Property_Object::read/write`, which preserve compile-time type and capability checking.
class Property_Object_Base {
struct Runtime_Interface {
const std::type_info& (*object_type)() noexcept;
@@ -105,19 +110,26 @@ protected:
template <class Object, Synchronization_Policy Policy>
friend class Property_Object;
public:
/// Returns the concrete managed object type represented by this erased base.
const std::type_info& runtime_object_type() const noexcept {
return runtime_interface_->object_type();
}
/// Returns the number of properties in the concrete object's schema.
std::size_t runtime_property_count() const noexcept {
return runtime_interface_->property_count();
}
/// Looks up `key` at runtime and invokes `callback` exactly once on success.
/// The operation follows the same intrinsic readable capability and synchronization rules as typed `read`; stored read-only properties therefore keep their zero-lock fast path. `callback` is a required non-null function pointer.
Runtime_Access_Result runtime_read(std::string_view key, void* context, Runtime_Read_Callback callback) const {
return runtime_interface_->read(*this, key, context, callback);
}
/// Looks up `key` at runtime and performs the same managed write used by typed `write`.
/// `value_type` must exactly match the property's declared value type and `value` must point to a live object of that exact type for the duration of the call. No numeric, string or user-defined conversion is attempted.
Runtime_Access_Result runtime_write(std::string_view key, const std::type_info& value_type, const void* value) {
return runtime_interface_->write(*this, key, value_type, value);
}
};
/// Explicit per-instance synchronization override. The contained plan changes only lock topology; it does not change property capability or validation.
struct Property_Synchronization {
Synchronization_Plan plan;
};
@@ -129,6 +141,8 @@ Property_Synchronization property_synchronization(Source&& source) requires Prop
using Schema = type_descriptor_schema_t<Object>;
return {materialize_synchronization_plan<Schema>(std::forward<Source>(source))};
}
/// Non-owning inspection view of the effective synchronization topology for one object instance.
/// `unsynchronized_slot` means that managed access for the property owns no mutex domain. For stored read-only properties this is automatic.
struct Resolved_Synchronization_View {
static constexpr std::size_t unsynchronized_slot = std::numeric_limits<std::size_t>::max();
std::span<const std::size_t> lock_slots;
@@ -323,12 +337,10 @@ private:
public:
Single_Read_View(const Property_Object& owner, std::size_t property_index, std::size_t lock_slot) : owner_(&owner), property_index_(property_index), lock_slot_(lock_slot) {}
template <auto Member>
decltype(auto) get() const {
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
using Property = typename Schema::template property_type<index>;
static_assert(Property::readable);
if constexpr (!Property::writable && !Property::accessor_type::synchronized_view_read) {
return owner_->template read_unlocked<index>(*this);
}
@@ -338,12 +350,10 @@ private:
return owner_->template read_unlocked<index>(*this);
}
template <Fixed_String Key>
decltype(auto) get_key() const {
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
using Property = typename Schema::template property_type<index>;
static_assert(Property::readable);
if constexpr (!Property::writable && !Property::accessor_type::synchronized_view_read) {
return owner_->template read_unlocked<index>(*this);
}
@@ -413,27 +423,22 @@ private:
}
public:
template <std::size_t Index>
decltype(auto) get_index() const {
using Schema = type_descriptor_schema_t<Derived>;
static_assert(Index < Schema::property_count);
static_assert(Schema::template property_type<Index>::readable);
decltype(auto) get_index() const requires Schema_Readable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
if (!holds(Index)) {
throw std::logic_error("Property is outside the held synchronization set");
}
return owner_->template read_unlocked<Index>(*this);
}
template <auto Member>
decltype(auto) get() const {
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
return get_index<index>();
}
template <Fixed_String Key>
decltype(auto) get_key() const {
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
return get_index<index>();
}
};
@@ -460,51 +465,41 @@ private:
}
public:
template <std::size_t Index>
decltype(auto) get_index() const {
using Schema = type_descriptor_schema_t<Derived>;
static_assert(Index < Schema::property_count);
static_assert(Schema::template property_type<Index>::readable);
decltype(auto) get_index() const requires Schema_Readable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
if (!holds(Index)) {
throw std::logic_error("Property is outside the held synchronization set");
}
return owner_->template read_unlocked<Index>(*this);
}
template <auto Member>
decltype(auto) get() const {
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
return get_index<index>();
}
template <Fixed_String Key>
decltype(auto) get_key() const {
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
return get_index<index>();
}
template <std::size_t Index, class Value>
void set_index(Value&& value) {
using Schema = type_descriptor_schema_t<Derived>;
static_assert(Index < Schema::property_count);
static_assert(Schema::template property_type<Index>::writable);
void set_index(Value&& value) requires Schema_Writable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
if (!holds(Index)) {
throw std::logic_error("Property is outside the held synchronization set");
}
owner_->template write_unlocked<Index>(std::forward<Value>(value));
}
template <auto Member, class Value>
void set(Value&& value) {
void set(Value&& value) requires Schema_Writable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
set_index<index>(std::forward<Value>(value));
}
template <Fixed_String Key, class Value>
void set_key(Value&& value) {
void set(Value&& value) requires Schema_Writable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
set_index<index>(std::forward<Value>(value));
}
};
@@ -530,27 +525,22 @@ private:
}
public:
template <std::size_t Index>
decltype(auto) get_index() const {
using Schema = type_descriptor_schema_t<Derived>;
static_assert(Index < Schema::property_count);
static_assert(Schema::template property_type<Index>::readable);
decltype(auto) get_index() const requires Schema_Readable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
if (!holds(Index)) {
throw std::logic_error("Property is outside the held synchronization set");
}
return owner_->template read_unlocked<Index>(*this);
}
template <auto Member>
decltype(auto) get() const {
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
return get_index<index>();
}
template <Fixed_String Key>
decltype(auto) get_key() const {
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
return get_index<index>();
}
};
@@ -576,51 +566,41 @@ private:
}
public:
template <std::size_t Index>
decltype(auto) get_index() const {
using Schema = type_descriptor_schema_t<Derived>;
static_assert(Index < Schema::property_count);
static_assert(Schema::template property_type<Index>::readable);
decltype(auto) get_index() const requires Schema_Readable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
if (!holds(Index)) {
throw std::logic_error("Property is outside the held synchronization set");
}
return owner_->template read_unlocked<Index>(*this);
}
template <auto Member>
decltype(auto) get() const {
decltype(auto) get() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
return get_index<index>();
}
template <Fixed_String Key>
decltype(auto) get_key() const {
decltype(auto) get() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
return get_index<index>();
}
template <std::size_t Index, class Value>
void set_index(Value&& value) {
using Schema = type_descriptor_schema_t<Derived>;
static_assert(Index < Schema::property_count);
static_assert(Schema::template property_type<Index>::writable);
void set_index(Value&& value) requires Schema_Writable_Property_Index<type_descriptor_schema_t<Derived>, Index> {
if (!holds(Index)) {
throw std::logic_error("Property is outside the held synchronization set");
}
owner_->template write_unlocked<Index>(std::forward<Value>(value));
}
template <auto Member, class Value>
void set(Value&& value) {
void set(Value&& value) requires Schema_Writable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
set_index<index>(std::forward<Value>(value));
}
template <Fixed_String Key, class Value>
void set_key(Value&& value) {
void set(Value&& value) requires Schema_Writable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
set_index<index>(std::forward<Value>(value));
}
};
@@ -778,6 +758,7 @@ public:
const auto& schema() const noexcept {
return type_descriptor<Derived>();
}
/// Returns the effective lock topology after intrinsic capability filtering and any per-instance override.
Resolved_Synchronization_View resolved_synchronization() const noexcept {
using Schema = type_descriptor_schema_t<Derived>;
if (custom_lock_layout_) {
@@ -796,30 +777,27 @@ public:
auto read() const requires Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
return read_one<index>();
}
template <Fixed_String Key>
auto read_key() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
auto read() const requires Schema_Readable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
return read_one<index>();
}
template <auto Member, class Value>
void write(Value&& value) requires Schema_Writable_Property_Member<type_descriptor_schema_t<Derived>, Member> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_member_property_index_v<Schema, Member>;
static_assert(index < Schema::property_count);
write_one<index>(std::forward<Value>(value));
}
template <Fixed_String Key, class Value>
void write_key(Value&& value) requires Schema_Writable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
void write(Value&& value) requires Schema_Writable_Property_Key<type_descriptor_schema_t<Derived>, Key> {
using Schema = type_descriptor_schema_t<Derived>;
constexpr auto index = schema_property_index_v<Schema, Key>;
static_assert(index < Schema::property_count);
write_one<index>(std::forward<Value>(value));
}
/// Returns the resolved lock slot for a registered member. `unsynchronized_slot` means no mutex is created for that property.
template <auto Member>
std::size_t lock_slot() const noexcept {
using Schema = type_descriptor_schema_t<Derived>;
@@ -855,22 +833,26 @@ private:
return Static_Write_Guard<sizeof...(Members)>{*this, std::move(targets)};
}
public:
/// Runtime-key shared guard. Keys are validated at runtime; unknown or non-readable properties throw `std::invalid_argument`. Lock domains are deduplicated and acquired in stable slot order.
Read_Guard lock_shared(std::span<const std::string_view> keys) const {
return lock_shared_impl(keys);
}
Read_Guard lock_shared(std::initializer_list<std::string_view> keys) const {
return lock_shared_impl(keys);
}
/// Runtime-key unique guard. Keys are validated at runtime; unknown or non-writable properties throw `std::invalid_argument`. Lock domains are deduplicated and acquired in stable slot order.
Write_Guard lock_unique(std::span<const std::string_view> keys) {
return lock_unique_impl(keys);
}
Write_Guard lock_unique(std::initializer_list<std::string_view> keys) {
return lock_unique_impl(keys);
}
/// Compile-time shared guard for registered readable members. Invalid or non-readable selections are removed by constraints before the function can be called.
template <auto... Members>
auto lock_shared() const requires (Schema_Readable_Property_Member<type_descriptor_schema_t<Derived>, Members> && ...) {
return lock_shared_impl<Members...>();
}
/// Compile-time unique guard for registered writable members. The implementation sorts and deduplicates resolved slots so callers may request members in any order without introducing lock-order inversion.
template <auto... Members>
auto lock_unique() requires (Schema_Writable_Property_Member<type_descriptor_schema_t<Derived>, Members> && ...) {
return lock_unique_impl<Members...>();
@@ -201,6 +201,8 @@ template <class Schema, auto Member>
inline constexpr std::size_t schema_member_property_index_v = schema_member_property_index<Schema, Member>();
template <class Schema, auto Member>
concept Schema_Property_Member = Property_Schema<Schema> && std::is_member_object_pointer_v<decltype(Member)> && schema_member_property_index_v<Schema, Member> < Schema::property_count;
template <class Schema, std::size_t Index>
concept Schema_Property_Index = Property_Schema<Schema> && Index < Schema::property_count;
template <class Schema, std::size_t Index, class Category, class Fallback>
struct Effective_Attribute {
private:
@@ -234,6 +236,10 @@ template <class Schema>
concept Valid_Property_Schema = Property_Schema<Schema>;
template <class Schema, std::size_t Index>
inline constexpr bool property_requires_synchronization_v = Schema::template property_type<Index>::writable || Schema::template property_type<Index>::synchronized_view_read;
template <class Schema, std::size_t Index>
concept Schema_Readable_Property_Index = Schema_Property_Index<Schema, Index> && Schema::template property_type<Index>::readable;
template <class Schema, std::size_t Index>
concept Schema_Writable_Property_Index = Schema_Property_Index<Schema, Index> && Schema::template property_type<Index>::writable;
template <class Schema, auto Member>
concept Schema_Readable_Property_Member = Schema_Property_Member<Schema, Member> && Schema::template property_type<schema_member_property_index_v<Schema, Member>>::readable;
template <class Schema, auto Member>
@@ -12,14 +12,19 @@
#include <utility>
#include <vector>
namespace structive {
/// Defines the default lock-domain topology for properties that actually require managed synchronization.
/// Stored intrinsic read-only properties are filtered out later and never receive a lock slot, regardless of this default.
enum class Synchronization_Default {
independent,
shared,
unsynchronized
};
/// Changes the default topology used before per-property and group overrides are applied.
struct Sync_Default_Rule {
Synchronization_Default value{Synchronization_Default::independent};
};
/// Runtime-key override for one property. The key is resolved against the schema when the plan is materialized.
/// `independent` forces a dedicated domain; `unsynchronized` deliberately removes the property from real locking.
struct Sync_Property_Rule {
enum class Mode {
independent,
@@ -28,15 +33,19 @@ struct Sync_Property_Rule {
Mode mode{Mode::independent};
std::string property;
};
/// Runtime-key declaration that places multiple properties in one consistency domain.
/// Group membership affects only properties that require synchronization; intrinsic read-only stored properties remain lock-free.
struct Sync_Group_Rule {
std::string name;
std::vector<std::string> properties;
};
/// Compile-time member-pointer form of a single-property synchronization override.
template <auto Member, Sync_Property_Rule::Mode Mode>
struct Sync_Member_Rule {
static constexpr auto member = Member;
static constexpr auto mode = Mode;
};
/// Compile-time member-pointer form of a synchronization group. Member validity is checked against the target schema during materialization.
template <auto... Members>
struct Sync_Member_Group_Rule {
static_assert(sizeof...(Members) > 0);
@@ -71,6 +80,10 @@ Sync_Group_Rule sync_group(std::string_view name, Properties&&... properties) re
(rule.properties.emplace_back(std::string_view(std::forward<Properties>(properties))), ...);
return rule;
}
/// Runtime-materialized synchronization description.
/// A plan describes topology, not mutex objects: resolution converts keys and groups into compact lock slots for one schema.
/// Invalid references, duplicate property configuration, empty groups and duplicate group names are rejected while resolving the plan.
/// The schema default plan is resolved once per object type; a `Property_Synchronization` override resolves a separate compact layout only for that instance.
class Synchronization_Plan {
Synchronization_Default default_mode_{Synchronization_Default::independent};
std::vector<Sync_Property_Rule> property_rules_;
@@ -143,6 +156,8 @@ concept Synchronization_Spec_Type = requires {
};
template <class Type>
concept Synchronization_Source_Type = Synchronization_Plan_Type<Type> || Synchronization_Spec_Type<Type>;
/// Builds synchronization configuration without forcing every caller onto runtime strings.
/// If every rule is already runtime-shaped, this returns `Synchronization_Plan` directly. If any rule carries compile-time member pointers, it returns a typed specification that is later materialized against the concrete schema.
template <class... Rules>
auto synchronization(Rules&&... rules) {
if constexpr ((Runtime_Synchronization_Rule<Rules> && ...)) {
@@ -153,6 +168,9 @@ auto synchronization(Rules&&... rules) {
return Synchronization_Spec<std::decay_t<Rules>...>{{std::forward<Rules>(rules)...}};
}
}
/// Compact result after applying defaults, overrides, groups and intrinsic property capabilities.
/// `lock_slots[i]` identifies the mutex domain for property `i`; `unsynchronized_slot` means no mutex exists for that property.
/// `lock_count` is the number of actual logical mutex domains after read-only filtering and group deduplication.
template <std::size_t Property_Count>
struct Resolved_Synchronization_Plan {
static constexpr std::size_t unsynchronized_slot = std::numeric_limits<std::size_t>::max();
+29 -8
View File
@@ -57,7 +57,7 @@ struct structive::Type_Descriptor<Computed_Device> {
field<&Computed_Device::max_speed>(key<"max_speed">),
field<&Computed_Device::fixed_offset>(key<"fixed_offset">, read_only),
computed_property<Computed_Device, int>([](const auto& view) {
return view.template get<&Computed_Device::max_speed>() - view.template get<&Computed_Device::min_speed>() + view.template get<&Computed_Device::fixed_offset>();
return view.template get<"max_speed">() - view.template get<"min_speed">() + view.template get<"fixed_offset">();
}, key<"speed_span">)
);
}
@@ -128,16 +128,29 @@ template <class Object>
concept Can_Lock_Immutable_Unique = requires(Object& object) {
object.template lock_unique<&Device::immutable_id>();
};
template <class Object>
concept Can_Write_Immutable_By_Key = requires(Object& object) {
object.template write<"immutable_id">(1);
};
using Device_Write_Guard = decltype(std::declval<Device&>().template lock_unique<&Device::temperature>());
template <class Guard>
concept Can_Guard_Set_Immutable = requires(Guard& guard) {
guard.template set<&Device::immutable_id>(1);
};
template <class Guard>
concept Can_Guard_Set_Immutable_By_Key = requires(Guard& guard) {
guard.template set<"immutable_id">(1);
};
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_min = guard.get<"minimum_speed">();
int old_max = guard.get<&Device::max_speed>();
guard.set<&Device::min_speed>(min_speed);
guard.set<"minimum_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<"minimum_speed">(old_min);
guard.set<&Device::max_speed>(old_max);
return false;
}
@@ -148,7 +161,12 @@ static void runtime_read_int(void* context, std::size_t, std::string_view, const
int main() {
static_assert(Property_Described_Object<Device>);
static_assert(!Can_Write_Immutable<Device>);
static_assert(!Can_Write_Immutable_By_Key<Device>);
static_assert(!Can_Lock_Immutable_Unique<Device>);
static_assert(!Can_Guard_Set_Immutable<Device_Write_Guard>);
static_assert(!Can_Guard_Set_Immutable_By_Key<Device_Write_Guard>);
static_assert(requires(const Device& value) { value.template read<"temperature">(); });
static_assert(requires(Device& value) { value.template write<"temperature">(1); });
const auto& schema = type_descriptor<Device>();
using Schema = type_descriptor_schema_t<Device>;
static_assert(Valid_Property_Schema<Schema>);
@@ -171,6 +189,9 @@ int main() {
REQUIRE(device.read<&Device::temperature>() == 21);
device.write<&Device::temperature>(22);
REQUIRE(device.temperature == 22);
REQUIRE(device.read<"temperature">() == 22);
device.write<"temperature">(23);
REQUIRE(device.temperature == 23);
REQUIRE(device.read<&Device::immutable_id>() == 7);
REQUIRE(&device.unsafe_object() == &device);
REQUIRE(&device.schema() == &schema);
@@ -212,13 +233,13 @@ int main() {
REQUIRE(value_visits == 5);
Computed_Device computed;
REQUIRE(computed.lock_slot<&Computed_Device::fixed_offset>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(computed.read_key<"speed_span">() == 95);
REQUIRE(computed.read<"speed_span">() == 95);
computed.write<&Computed_Device::min_speed>(20);
REQUIRE(computed.read_key<"speed_span">() == 85);
REQUIRE(computed.read<"speed_span">() == 85);
Lockless_Device lockless;
REQUIRE(lockless.read<&Lockless_Device::left>() == 1);
lockless.write<&Lockless_Device::right>(4);
REQUIRE(lockless.read_key<"sum">() == 5);
REQUIRE(lockless.read<"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);
@@ -234,7 +255,7 @@ 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 == 22);
REQUIRE(runtime_value == 23);
REQUIRE(erased.runtime_read("immutable_id", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok);
REQUIRE(runtime_value == 7);
int runtime_write_value = 35;
+139
View File
@@ -0,0 +1,139 @@
#include <structive/property/property.hpp>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <semaphore>
#include <string>
#include <thread>
#include <typeinfo>
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 Runtime_Device : Property_Object<Runtime_Device> {
int value{10};
int serial{7};
int command{0};
};
template <>
struct structive::Type_Descriptor<Runtime_Device> {
static auto get() {
return object<Runtime_Device>(
synchronization(sync_all_independent),
field<&Runtime_Device::value>(key<"value">),
field<&Runtime_Device::serial>(key<"serial">, read_only),
field<&Runtime_Device::command>(key<"command">, write_only)
);
}
};
struct Runtime_Read_Capture {
std::size_t calls{};
std::size_t index{};
std::string key;
const std::type_info* type{};
int value{};
};
static void capture_int(void* context, std::size_t index, std::string_view key, const std::type_info& type, const void* value) {
auto& capture = *static_cast<Runtime_Read_Capture*>(context);
++capture.calls;
capture.index = index;
capture.key = key;
capture.type = &type;
capture.value = *static_cast<const int*>(value);
}
template <class Object>
concept Can_Read_Command = requires(const Object& object) {
object.template read<"command">();
};
template <class Object>
concept Can_Write_Serial = requires(Object& object) {
object.template write<"serial">(1);
};
static void test_runtime_metadata_and_results() {
Runtime_Device device;
Property_Object_Base& erased = device;
static_assert(!Can_Read_Command<Runtime_Device>);
static_assert(!Can_Write_Serial<Runtime_Device>);
REQUIRE(erased.runtime_object_type() == typeid(Runtime_Device));
REQUIRE(erased.runtime_property_count() == 3);
Runtime_Read_Capture capture;
REQUIRE(erased.runtime_read("value", &capture, &capture_int) == Runtime_Access_Result::ok);
REQUIRE(capture.calls == 1);
REQUIRE(capture.index == 0);
REQUIRE(capture.key == "value");
REQUIRE(capture.type != nullptr && *capture.type == typeid(int));
REQUIRE(capture.value == 10);
capture = {};
REQUIRE(erased.runtime_read("serial", &capture, &capture_int) == Runtime_Access_Result::ok);
REQUIRE(capture.calls == 1);
REQUIRE(capture.index == 1);
REQUIRE(capture.value == 7);
capture = {};
REQUIRE(erased.runtime_read("command", &capture, &capture_int) == Runtime_Access_Result::not_readable);
REQUIRE(capture.calls == 0);
REQUIRE(erased.runtime_read("missing", &capture, &capture_int) == Runtime_Access_Result::unknown_property);
REQUIRE(capture.calls == 0);
int value = 20;
REQUIRE(erased.runtime_write("value", typeid(int), &value) == Runtime_Access_Result::ok);
REQUIRE(device.value == 20);
int command = 5;
REQUIRE(erased.runtime_write("command", typeid(int), &command) == Runtime_Access_Result::ok);
REQUIRE(device.command == 5);
REQUIRE(erased.runtime_write("serial", typeid(int), &value) == Runtime_Access_Result::not_writable);
double wrong_type = 1.0;
REQUIRE(erased.runtime_write("value", typeid(double), &wrong_type) == Runtime_Access_Result::type_mismatch);
REQUIRE(erased.runtime_write("missing", typeid(int), &value) == Runtime_Access_Result::unknown_property);
}
static void test_runtime_access_uses_managed_synchronization() {
Runtime_Device device;
Property_Object_Base& erased = device;
std::binary_semaphore read_done{0};
std::jthread reader;
{
auto guard = device.lock_unique<&Runtime_Device::value>();
reader = std::jthread([&] {
Runtime_Read_Capture capture;
REQUIRE(erased.runtime_read("value", &capture, &capture_int) == Runtime_Access_Result::ok);
REQUIRE(capture.value == 10);
read_done.release();
});
REQUIRE(!read_done.try_acquire_for(std::chrono::milliseconds(20)));
}
REQUIRE(read_done.try_acquire_for(std::chrono::seconds(2)));
reader.join();
std::binary_semaphore write_done{0};
std::jthread writer;
{
auto guard = device.lock_shared<&Runtime_Device::value>();
writer = std::jthread([&] {
int value = 30;
REQUIRE(erased.runtime_write("value", typeid(int), &value) == Runtime_Access_Result::ok);
write_done.release();
});
REQUIRE(!write_done.try_acquire_for(std::chrono::milliseconds(20)));
}
REQUIRE(write_done.try_acquire_for(std::chrono::seconds(2)));
writer.join();
REQUIRE(device.value == 30);
}
static void test_runtime_read_only_fast_path() {
Runtime_Device device;
Property_Object_Base& erased = device;
REQUIRE(device.lock_slot<&Runtime_Device::serial>() == Resolved_Synchronization_View::unsynchronized_slot);
std::binary_semaphore done{0};
std::jthread reader;
{
auto guard = device.lock_unique<&Runtime_Device::value>();
reader = std::jthread([&] {
Runtime_Read_Capture capture;
REQUIRE(erased.runtime_read("serial", &capture, &capture_int) == Runtime_Access_Result::ok);
REQUIRE(capture.value == 7);
done.release();
});
REQUIRE(done.try_acquire_for(std::chrono::milliseconds(200)));
}
reader.join();
}
int main() {
test_runtime_metadata_and_results();
test_runtime_access_uses_managed_synchronization();
test_runtime_read_only_fast_path();
}
+194
View File
@@ -0,0 +1,194 @@
#include <structive/property/property.hpp>
#include <barrier>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <semaphore>
#include <stdexcept>
#include <string_view>
#include <thread>
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 Sync_Device : Property_Object<Sync_Device> {
Sync_Device() = default;
explicit Sync_Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {}
int id{1};
int a{10};
int b{20};
int c{30};
};
template <>
struct structive::Type_Descriptor<Sync_Device> {
static auto get() {
return object<Sync_Device>(
synchronization(sync_all_independent),
field<&Sync_Device::id>(key<"id">, read_only),
field<&Sync_Device::a>(key<"a">),
field<&Sync_Device::b>(key<"b">),
field<&Sync_Device::c>(key<"c">)
);
}
};
static Sync_Device with_plan(Synchronization_Plan plan) {
return Sync_Device{property_synchronization(std::move(plan))};
}
static void test_topology_resolution() {
Sync_Device independent;
auto ro = independent.lock_slot<&Sync_Device::id>();
auto a = independent.lock_slot<&Sync_Device::a>();
auto b = independent.lock_slot<&Sync_Device::b>();
auto c = independent.lock_slot<&Sync_Device::c>();
REQUIRE(ro == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(independent.resolved_synchronization().lock_count == 3);
REQUIRE(a != b && a != c && b != c);
auto shared = with_plan(synchronization(sync_all_shared));
REQUIRE(shared.resolved_synchronization().lock_count == 1);
REQUIRE(shared.lock_slot<&Sync_Device::a>() == shared.lock_slot<&Sync_Device::b>());
REQUIRE(shared.lock_slot<&Sync_Device::b>() == shared.lock_slot<&Sync_Device::c>());
REQUIRE(shared.lock_slot<&Sync_Device::id>() == Resolved_Synchronization_View::unsynchronized_slot);
auto unsynchronized = with_plan(synchronization(sync_all_unsynchronized));
REQUIRE(unsynchronized.resolved_synchronization().lock_count == 0);
REQUIRE(unsynchronized.lock_slot<&Sync_Device::a>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(unsynchronized.lock_slot<&Sync_Device::b>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(unsynchronized.lock_slot<&Sync_Device::c>() == Resolved_Synchronization_View::unsynchronized_slot);
auto grouped = Sync_Device{property_synchronization<Sync_Device>(synchronization(sync_all_independent, sync_group<&Sync_Device::a, &Sync_Device::b>("ab")))};
REQUIRE(grouped.resolved_synchronization().lock_count == 2);
REQUIRE(grouped.lock_slot<&Sync_Device::a>() == grouped.lock_slot<&Sync_Device::b>());
REQUIRE(grouped.lock_slot<&Sync_Device::a>() != grouped.lock_slot<&Sync_Device::c>());
auto one_unsynchronized = Sync_Device{property_synchronization<Sync_Device>(synchronization(sync_all_independent, sync_unsynchronized<&Sync_Device::b>()))};
REQUIRE(one_unsynchronized.resolved_synchronization().lock_count == 2);
REQUIRE(one_unsynchronized.lock_slot<&Sync_Device::b>() == Resolved_Synchronization_View::unsynchronized_slot);
auto one_independent = Sync_Device{property_synchronization<Sync_Device>(synchronization(sync_all_shared, sync_independent<&Sync_Device::c>()))};
REQUIRE(one_independent.resolved_synchronization().lock_count == 2);
REQUIRE(one_independent.lock_slot<&Sync_Device::a>() == one_independent.lock_slot<&Sync_Device::b>());
REQUIRE(one_independent.lock_slot<&Sync_Device::c>() != one_independent.lock_slot<&Sync_Device::a>());
auto read_only_grouped = Sync_Device{property_synchronization<Sync_Device>(synchronization(sync_all_independent, sync_group<&Sync_Device::id, &Sync_Device::a>("mixed")))};
REQUIRE(read_only_grouped.lock_slot<&Sync_Device::id>() == Resolved_Synchronization_View::unsynchronized_slot);
REQUIRE(read_only_grouped.resolved_synchronization().lock_count == 3);
}
static void test_plan_validation() {
bool unknown_thrown = false;
try {
Synchronization_Plan plan;
plan.independent("missing");
auto device = with_plan(std::move(plan));
(void)device;
} catch (const std::invalid_argument&) {
unknown_thrown = true;
}
REQUIRE(unknown_thrown);
bool duplicate_property_thrown = false;
try {
Synchronization_Plan plan;
plan.independent("a").unsynchronized("a");
auto device = with_plan(std::move(plan));
(void)device;
} catch (const std::invalid_argument&) {
duplicate_property_thrown = true;
}
REQUIRE(duplicate_property_thrown);
bool empty_group_thrown = false;
try {
Synchronization_Plan plan;
plan.group("empty", std::span<const std::string_view>{});
auto device = with_plan(std::move(plan));
(void)device;
} catch (const std::invalid_argument&) {
empty_group_thrown = true;
}
REQUIRE(empty_group_thrown);
}
static void test_dynamic_guard_validation() {
Sync_Device device;
bool unknown_thrown = false;
try {
auto guard = device.lock_shared({"missing"});
(void)guard;
} catch (const std::invalid_argument&) {
unknown_thrown = true;
}
REQUIRE(unknown_thrown);
bool read_only_unique_thrown = false;
try {
auto guard = device.lock_unique({"id"});
(void)guard;
} catch (const std::invalid_argument&) {
read_only_unique_thrown = true;
}
REQUIRE(read_only_unique_thrown);
auto read_guard = device.lock_shared({"id"});
REQUIRE(read_guard.get<"id">() == 1);
}
static void test_independent_and_shared_blocking() {
Sync_Device independent;
std::binary_semaphore independent_other_done{0};
{
auto guard = independent.lock_unique<&Sync_Device::a>();
std::jthread writer([&] {
independent.write<&Sync_Device::b>(21);
independent_other_done.release();
});
REQUIRE(independent_other_done.try_acquire_for(std::chrono::milliseconds(200)));
}
REQUIRE(independent.read<&Sync_Device::b>() == 21);
std::binary_semaphore same_done{0};
std::jthread same_writer;
{
auto guard = independent.lock_unique<&Sync_Device::a>();
same_writer = std::jthread([&] {
independent.write<&Sync_Device::a>(11);
same_done.release();
});
REQUIRE(!same_done.try_acquire_for(std::chrono::milliseconds(20)));
}
REQUIRE(same_done.try_acquire_for(std::chrono::seconds(2)));
same_writer.join();
auto shared = with_plan(synchronization(sync_all_shared));
std::binary_semaphore shared_done{0};
std::jthread shared_writer;
{
auto guard = shared.lock_unique<&Sync_Device::a>();
shared_writer = std::jthread([&] {
shared.write<&Sync_Device::b>(22);
shared_done.release();
});
REQUIRE(!shared_done.try_acquire_for(std::chrono::milliseconds(20)));
}
REQUIRE(shared_done.try_acquire_for(std::chrono::seconds(2)));
shared_writer.join();
}
static void test_static_guard_order_is_stable() {
Sync_Device device;
std::barrier start{2};
std::jthread first([&] {
start.arrive_and_wait();
for (int index = 0; index < 100; ++index) {
auto guard = device.lock_unique<&Sync_Device::a, &Sync_Device::b>();
auto a = guard.get<"a">();
auto b = guard.get<&Sync_Device::b>();
guard.set<"a">(a + 1);
guard.set<&Sync_Device::b>(b + 1);
}
});
std::jthread second([&] {
start.arrive_and_wait();
for (int index = 0; index < 100; ++index) {
auto guard = device.lock_unique<&Sync_Device::b, &Sync_Device::a>();
auto b = guard.get<"b">();
auto a = guard.get<&Sync_Device::a>();
guard.set<"b">(b + 1);
guard.set<&Sync_Device::a>(a + 1);
}
});
first.join();
second.join();
REQUIRE(device.read<&Sync_Device::a>() == 210);
REQUIRE(device.read<&Sync_Device::b>() == 220);
}
int main() {
test_topology_resolution();
test_plan_validation();
test_dynamic_guard_validation();
test_independent_and_shared_blocking();
test_static_guard_order_is_stable();
}
+92 -48
View File
@@ -270,8 +270,8 @@ device.write<&Device::temperature>(30.0);
Compile-time key access is also available:
```cpp
auto temperature = device.read_key<"temperature">();
device.write_key<"temperature">(30.0);
auto temperature = device.read<"temperature">();
device.write<"temperature">(30.0);
```
The typed interfaces are constrained by intrinsic capability. A write to `read_only` does not participate in overload resolution.
@@ -308,44 +308,51 @@ If an object has only stored read-only properties, those properties contribute z
## 14. Synchronization plans
### 14.1 Independent
Synchronization is intentionally a topology layer rather than a property permission system. It answers only one question: when managed mutable state is accessed concurrently, which properties share a consistency domain?
Stored intrinsic read-only properties are removed before lock slots are materialized. A broad rule such as `sync_all_shared` therefore never creates a mutex merely for a stored `read_only` property.
### 14.1 Default topologies
| Default | Meaning for properties that require synchronization |
| --- | --- |
| `sync_all_independent` | each property receives its own lock domain |
| `sync_all_shared` | all properties share one lock domain |
| `sync_all_unsynchronized` | no real lock domain is created |
```cpp
synchronization(sync_all_independent)
```
Each property that actually requires synchronization gets its own lock domain.
`sync_all_unsynchronized` is an explicit opt-out. Structive still provides managed access, but the caller owns the thread-safety consequences of concurrent reads and writes.
### 14.2 Shared
### 14.2 Compile-time member rules
```cpp
synchronization(sync_all_shared)
```
All properties that require synchronization share one lock domain.
Read-only stored properties are excluded before lock slots are materialized.
### 14.3 Unsynchronized
```cpp
synchronization(sync_all_unsynchronized)
```
Managed access performs no real locking even for writable properties.
### 14.4 Per-property override
Use member pointers when the schema is known in C++ code:
```cpp
synchronization(
sync_all_independent,
sync_unsynchronized<&Device::temperature>()
sync_all_shared,
sync_independent<&Device::temperature>(),
sync_unsynchronized<&Device::debug_counter>()
)
```
Dynamic-key rule forms are also available.
Member validity is checked while the synchronization specification is materialized for the schema.
### 14.5 Groups
### 14.3 Runtime-key rules
Adapters or configuration code may build a `Synchronization_Plan` from keys:
```cpp
Synchronization_Plan plan;
plan.set_default(Synchronization_Default::independent);
plan.unsynchronized("debug_counter");
```
Key validity cannot be known until the plan is resolved. Unknown properties, duplicate property configuration, empty groups and duplicate group names are rejected with `std::invalid_argument`.
### 14.4 Groups are consistency domains
```cpp
synchronization(
@@ -354,7 +361,21 @@ synchronization(
)
```
Members in one group resolve to the same lock slot if they require synchronization.
Members in one group resolve to the same lock slot if they actually require synchronization. A read-only stored member may appear in a broad rule or group, but it still resolves to `unsynchronized_slot` because there is no managed writer to protect.
A group should express a real invariant or snapshot boundary. It should not be used merely to reduce the mutex count.
### 14.5 Why the synchronization API has several forms
The forms represent different information availability, not duplicate concepts:
- type-level defaults describe the normal topology once per object type;
- compile-time member rules give typed C++ code compile-time schema checking;
- runtime-key rules support adapters that discover property names dynamically;
- per-instance overrides support objects whose synchronization topology genuinely differs from the type default;
- guards express a temporary multi-property consistency operation.
The common semantic model is always the same resolved lock-slot topology.
## 15. Per-instance synchronization override
@@ -381,7 +402,7 @@ Device device{
};
```
The default topology is shared per type. Only an object with an explicit override stores its compact override layout.
The default topology is resolved once and shared per type. Only an object with an explicit override stores a compact override layout. Copy and move construction preserve an object's override topology; assignment preserves the destination object's existing topology because assignment changes object state, not the synchronization policy chosen for that instance.
## 16. Inspect resolved synchronization
@@ -402,7 +423,7 @@ No-lock properties use:
Resolved_Synchronization_View::unsynchronized_slot
```
For a read-only stored property this is automatic.
For a stored read-only property this is automatic. `resolved_synchronization()` and `lock_slot()` are primarily diagnostics and framework-level inspection APIs; normal business code should usually express its intent through `read`, `write`, `lock_shared` and `lock_unique` instead of reasoning about numeric slots.
## 17. Static multi-property guards
@@ -410,7 +431,8 @@ Read guard:
```cpp
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
auto temperature = guard.get<&Device::temperature>();
auto by_member = guard.get<&Device::temperature>();
auto by_key = guard.get<"pressure">();
```
Write guard:
@@ -418,23 +440,33 @@ Write guard:
```cpp
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
guard.set<&Device::min_speed>(20);
guard.set<&Device::max_speed>(120);
guard.set<"maximum_speed">(120);
```
Static guards deduplicate slots and acquire them in stable slot order.
Member and compile-time-key access intentionally use the same `get`/`set` names. Capability constraints are part of the overload itself: a typed unique guard cannot be requested for a read-only property, and `set` does not exist for a non-writable property in a `requires` expression.
A typed unique guard requires all selected properties to be intrinsically writable.
Static guards resolve member slots, deduplicate repeated domains and acquire locks in stable numeric slot order. Request order therefore does not become mutex acquisition order, avoiding lock-order inversion when two callers request the same domains in different member order.
A read-only stored property may participate in a shared guard's logical held set without introducing a mutex. This allows a guard API to read it consistently with its intrinsic contract while preserving the read-only zero-lock fast path.
## 18. Dynamic-key guards
```cpp
auto read_guard = device.lock_shared({"temperature", "pressure"});
auto write_guard = device.lock_unique({"temperature", "pressure"});
auto value = read_guard.get<"temperature">();
```
Unknown keys throw `std::invalid_argument`.
Dynamic guards exist for callers whose selected property set is known only at runtime. Because the keys are dynamic, errors that typed guards reject through constraints become runtime errors:
A dynamic unique guard rejects a read-only property because the key is only known at runtime.
- an unknown key throws `std::invalid_argument`;
- `lock_shared` rejects a non-readable property;
- `lock_unique` rejects a non-writable property;
- a later `get`/`set` throws `std::logic_error` if the requested property is outside the guard's held set.
Dynamic and static guards use the same resolved lock topology and the same stable lock ordering.
The behavior above is covered directly by `core/tests/synchronization_test.cpp`, including independent/shared/unsynchronized defaults, groups, overrides, invalid plans, runtime-key guards, blocking behavior and reversed member-order acquisition.
## 19. Traversal
@@ -509,7 +541,7 @@ These forms access the object directly rather than using synchronized dependency
## 22. Runtime type-erased access
Use `Property_Object_Base` for dynamic adapters:
`Property_Object_Base` is the dynamic adapter boundary. It is intended for GUI inspectors, serialization adapters, scripting bridges, RPC layers and other code that learns a property key only at runtime. Normal typed C++ code should prefer `read` and `write`.
```cpp
Property_Object_Base& erased = device;
@@ -522,31 +554,43 @@ erased.runtime_object_type();
erased.runtime_property_count();
```
Read:
### 22.1 Runtime read
```cpp
auto result = erased.runtime_read("temperature", context, callback);
```
Write:
On success the callback is invoked exactly once with the schema index, key, exact `type_info` and a pointer to the current value. The value pointer is borrowed and is valid only during the callback; copy or consume it synchronously and never retain it.
For a synchronized writable property, Structive keeps the corresponding managed read lock held while the callback executes. The callback should therefore not re-enter a conflicting managed write on the same lock domain. A stored read-only property follows the intrinsic zero-lock fast path and does not acquire a mutex merely because the access is dynamic.
### 22.2 Runtime write
```cpp
auto result = erased.runtime_write("temperature", typeid(double), &value);
```
Possible results:
Runtime write intentionally performs no implicit conversion. `typeid(double)` must exactly match the property's declared value type, and the pointer must address a live value of that exact type for the duration of the call. A successful runtime write uses the same managed write path and synchronization semantics as typed `write`.
```text
ok
unknown_property
not_readable
not_writable
type_mismatch
```
### 22.3 Result contract
Runtime access has no external/persistence mode. An adapter owns its own policy and chooses whether it calls the intrinsic read/write operation.
| Result | Meaning |
| --- | --- |
| `ok` | lookup and access completed |
| `unknown_property` | no schema property has that runtime key |
| `not_readable` | the property exists but its intrinsic capability is not readable |
| `not_writable` | the property exists but its intrinsic capability is not writable |
| `type_mismatch` | runtime write supplied a type different from the declared property value type |
Runtime read of a stored read-only property follows the same no-lock fast path as typed read.
There is no external/persistence access mode and no access-control policy in this API. An adapter decides whether it wants to expose or call runtime read/write; Structive reports only the property's intrinsic capability.
### 22.4 Why this API is deliberately low-level
The runtime boundary uses `type_info`, `void*` and a callback because the value type is unknown to the caller at compile time. Core does not impose a universal `variant`, heap-owned `any`, serialization format or conversion registry, because any of those would add ownership and conversion policy that belongs to a higher-level adapter.
This is therefore an adapter API rather than the preferred business-code API. A higher-level extension may wrap it in domain-specific value containers without changing Structive Core.
The contract is covered directly by `core/tests/runtime_api_test.cpp`, including every result code, callback metadata, exact-type writes, managed blocking behavior and the read-only zero-lock runtime fast path.
## 23. Lock policies
+92 -48
View File
@@ -264,8 +264,8 @@ device.write<&Device::temperature>(30.0);
也支持编译期 key
```cpp
auto temperature = device.read_key<"temperature">();
device.write_key<"temperature">(30.0);
auto temperature = device.read<"temperature">();
device.write<"temperature">(30.0);
```
Typed API 由 intrinsic capability 约束。对 `read_only` 调用 write 时,函数在 overload resolution 阶段就不可用。
@@ -300,44 +300,51 @@ serial_number -> unsynchronized_slot
## 14. Synchronization Plan
### 14.1 Independent
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)
```
每个真正需要同步的 Property 各自一个 lock domain
`sync_all_unsynchronized` 是显式放弃同步。Structive 仍提供 managed access,但并发读写造成的线程安全责任由调用方承担
### 14.2 Shared
### 14.2 编译期 Member Rule
```cpp
synchronization(sync_all_shared)
```
所有真正需要同步的 Property 共用一个 lock domain。
Read-only stored property 在 slot materialization 前就被排除。
### 14.3 Unsynchronized
```cpp
synchronization(sync_all_unsynchronized)
```
即使 writable property 也不执行真实锁。
### 14.4 单 Property Override
Schema 在 C++ 中已知时优先使用 member pointer
```cpp
synchronization(
sync_all_independent,
sync_unsynchronized<&Device::temperature>()
sync_all_shared,
sync_independent<&Device::temperature>(),
sync_unsynchronized<&Device::debug_counter>()
)
```
也支持动态 key 规则
Specification 针对具体 Schema materialize 时会检查 Member 是否真的注册
### 14.5 Group
### 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(
@@ -346,7 +353,21 @@ synchronization(
)
```
组、且真正需要同步的 Property 解析到同一个 lock slot。
一 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
@@ -373,7 +394,7 @@ Device device{
};
```
默认 topology 每类型共享。只有显式 override 的实例才保存紧凑 override layout。
默认 topology 每类型只 resolve 一次并共享。只有显式 override 的实例才保存紧凑 override layout。Copy/move construction 保留源对象的 override topologyassignment 保留目标对象已经选择的 topology,因为赋值修改的是对象状态,不应该偷偷改变这个实例的同步策略。
## 16. 查看 Resolved Synchronization
@@ -394,7 +415,7 @@ auto slot = device.lock_slot<&Device::temperature>();
Resolved_Synchronization_View::unsynchronized_slot
```
Stored read-only property 自动得到这个值。
Stored read-only Property 自动得到这个值。`resolved_synchronization()``lock_slot()` 更适合诊断和框架检查;普通业务代码通常应该通过 `read``write``lock_shared``lock_unique` 表达意图,而不是依赖数字 slot。
## 17. 静态多属性 Guard
@@ -402,7 +423,8 @@ Read Guard
```cpp
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
auto temperature = guard.get<&Device::temperature>();
auto by_member = guard.get<&Device::temperature>();
auto by_key = guard.get<"pressure">();
```
Write Guard
@@ -410,23 +432,33 @@ Write Guard
```cpp
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
guard.set<&Device::min_speed>(20);
guard.set<&Device::max_speed>(120);
guard.set<"maximum_speed">(120);
```
Static Guard 会对 slot 去重,并使用稳定 slot 顺序获取锁
Member 和 compile-time key 统一使用同名 `get`/`set`。Capability 直接进入 overload constraintread-only Property 不能创建 typed unique guard`requires` 表达式中也不会看到它的 `set`
Typed unique guard 要求所有目标 Property intrinsically writable
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">();
```
未知 key 抛 `std::invalid_argument`
Dynamic Guard 服务“目标 Property 集合只有运行期才知道”的场景,因此 typed guard 的编译期错误在这里变成运行时错误:
Dynamic unique guard 如果遇到 read-only property,也会因为 key 只能运行期确定而运行时拒绝。
- 未知 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
@@ -501,7 +533,7 @@ trusted_accessor_property<&Device::get_value, &Device::set_value>(key<"value">)
## 22. Runtime Type-Erased Access
动态 Adapter 使用:
`Property_Object_Base` 是动态 Adapter 边界,面向 GUI inspector、serialization adapter、脚本绑定、RPC 层等“只有运行期才知道 Property key”的代码。普通 typed C++ 业务代码仍应优先使用 `read` / `write`
```cpp
Property_Object_Base& erased = device;
@@ -514,31 +546,43 @@ 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 PropertyStructive 在 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` 指针在调用期间必须指向这个精确类型的有效对象。成功写入复用 typed `write` 的同一 managed write 和 synchronization 语义。
```text
ok
unknown_property
not_readable
not_writable
type_mismatch
```
### 22.3 Result Contract
Runtime access 没有 external/persistence mode。Adapter 自己拥有 policy,然后决定是否调用 intrinsic runtime read/write。
| Result | 含义 |
| --- | --- |
| `ok` | lookup 和访问完成 |
| `unknown_property` | Schema 中不存在该 runtime key |
| `not_readable` | Property 存在,但 intrinsic capability 不可读 |
| `not_writable` | Property 存在,但 intrinsic capability 不可写 |
| `type_mismatch` | runtime write 提供的类型和 Property value type 不一致 |
Stored read-only property 的 runtime read 同样走 no-lock fast path
这里没有 external/persistence mode,也没有访问控制 policy。Adapter 自己决定是否对外暴露、是否调用 runtime read/writeStructive 只报告 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