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
+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();
}