api更新
This commit is contained in:
+92
-48
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user