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
+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