diff --git a/CMakeLists.txt b/CMakeLists.txt index 496bcc5..8f60315 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,30 @@ cmake_minimum_required(VERSION 3.20) -project(Structive LANGUAGES CXX) -if (CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) +project(Structive VERSION 1.0.0 LANGUAGES CXX) +include(GNUInstallDirs) +if(PROJECT_IS_TOP_LEVEL) + include(CTest) +endif() +option(STRUCTIVE_BUILD_EXAMPLES "Build Structive examples" ${PROJECT_IS_TOP_LEVEL}) +option(STRUCTIVE_BUILD_TESTS "Build Structive tests" ${BUILD_TESTING}) +option(STRUCTIVE_INSTALL "Install Structive as a standalone CMake package" ${PROJECT_IS_TOP_LEVEL}) +if(PROJECT_IS_TOP_LEVEL) set_property(GLOBAL PROPERTY USE_FOLDERS ON) set(CMAKE_FOLDER "Structive") -endif () -include(CTest) -get_property(_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) -foreach (_target IN LISTS _targets) - get_target_property(_folder "${_target}" FOLDER) - if (_folder STREQUAL "CTestDashboardTargets") - set_property(TARGET "${_target}" PROPERTY FOLDER "Structive/CTest") - endif () -endforeach () -include(${CMAKE_CURRENT_LIST_DIR}/core/main.cmake) -include(${CMAKE_CURRENT_LIST_DIR}/extensions/main.cmake) \ No newline at end of file +endif() +include("${CMAKE_CURRENT_LIST_DIR}/core/main.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/extensions/main.cmake") +if(STRUCTIVE_INSTALL) + include(CMakePackageConfigHelpers) + install(TARGETS structive_property_core EXPORT StructiveTargets INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + install(TARGETS structive_property_extensions EXPORT StructiveTargets ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/core/include/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/extensions/include/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + install(EXPORT StructiveTargets FILE StructiveTargets.cmake NAMESPACE structive:: DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Structive") + configure_package_config_file("${CMAKE_CURRENT_LIST_DIR}/cmake/StructiveConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/StructiveConfig.cmake" INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Structive") + write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/StructiveConfigVersion.cmake" VERSION "${PROJECT_VERSION}" COMPATIBILITY SameMajorVersion) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/StructiveConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/StructiveConfigVersion.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Structive") +endif() +if(PROJECT_IS_TOP_LEVEL AND STRUCTIVE_BUILD_TESTS AND STRUCTIVE_INSTALL) + add_test(NAME structive_install_consumer_test COMMAND "${CMAKE_COMMAND}" "-DSTRUCTIVE_SOURCE_DIR=${CMAKE_CURRENT_LIST_DIR}" "-DSTRUCTIVE_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}" "-DSTRUCTIVE_CONFIG=$" "-DSTRUCTIVE_GENERATOR=${CMAKE_GENERATOR}" "-DSTRUCTIVE_GENERATOR_PLATFORM=${CMAKE_GENERATOR_PLATFORM}" "-DSTRUCTIVE_GENERATOR_TOOLSET=${CMAKE_GENERATOR_TOOLSET}" -P "${CMAKE_CURRENT_LIST_DIR}/cmake/RunInstallConsumer.cmake") + set_tests_properties(structive_install_consumer_test PROPERTIES LABELS "install;package") +endif() diff --git a/README.md b/README.md index f3002ba..4984c75 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ runtime_read(key, context, callback) runtime_write(key, type_info, value) ``` -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. +They return `ok`, `unknown_property`, `not_readable`, `not_writable`, `unsupported_runtime_write` or `type_mismatch`. Runtime write is an exact-type copy-input boundary and performs no implicit conversion. A property can remain intrinsically `writable` while exposing `runtime_copy_writable == false` when its accessor requires move-only input; typed `write` still supports that property. 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. 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`. @@ -354,6 +354,8 @@ cmake --build build ctest --test-dir build --output-on-failure ``` +Standalone configuration exposes `STRUCTIVE_BUILD_EXAMPLES`, `STRUCTIVE_BUILD_TESTS` and `STRUCTIVE_INSTALL`. Examples default on only when Structive is the top-level project; tests follow `BUILD_TESTING`; standalone install defaults on. Installation exports `structive::property_core` and `structive::property_extensions` through `find_package(Structive CONFIG)`, and the standalone CTest suite verifies an external install consumer. + ## Documentation - [Design Philosophy and Principles](docs/DESIGN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 23f8238..24d5528 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -306,7 +306,7 @@ 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 state,callback 执行期间 managed read lock 仍然持有。Stored read-only Property 继续走和 typed read 一样的 zero-lock fast path。 +返回 `ok`、`unknown_property`、`not_readable`、`not_writable`、`unsupported_runtime_write` 或 `type_mismatch`。Runtime write 是精确类型的 copy-input 边界,不做隐式转换。如果 Accessor 只能接收 move-only 输入,Property 仍然可以保持 intrinsic `writable`,但会暴露 `runtime_copy_writable == false`;typed `write` 仍然支持这种 Property。Read callback 收到的是借用指针,只在 callback 期间有效;对于需要同步的 writable state,callback 执行期间 managed read lock 仍然持有。Stored read-only Property 继续走和 typed read 一样的 zero-lock fast path。 Core 不在这个边界强制引入 `variant`、`any`、转换注册表或 serialization 所有权策略,上层 Adapter 可以按领域需要封装。这里没有 runtime access mode,也没有访问控制;外部系统自行决定暴露策略,Structive 只报告 Property intrinsic capability。详细契约见 [Core Guide: Runtime Access](docs/CORE_GUIDE.zh-CN.md#22-runtime-type-erased-access),测试见 `core/tests/runtime_api_test.cpp`。 @@ -360,6 +360,8 @@ cmake --build build ctest --test-dir build --output-on-failure ``` +独立构建提供 `STRUCTIVE_BUILD_EXAMPLES`、`STRUCTIVE_BUILD_TESTS`、`STRUCTIVE_INSTALL`。Example 只在 Structive 作为顶层工程时默认开启;测试跟随 `BUILD_TESTING`;独立安装默认开启。安装后可通过 `find_package(Structive CONFIG)` 使用 `structive::property_core` 与 `structive::property_extensions`,并且 standalone CTest 会真实验证外部 install consumer。 + ## 详细文档 - [设计理念与原则](docs/DESIGN.zh-CN.md) diff --git a/cmake/RunInstallConsumer.cmake b/cmake/RunInstallConsumer.cmake new file mode 100644 index 0000000..501280a --- /dev/null +++ b/cmake/RunInstallConsumer.cmake @@ -0,0 +1,52 @@ +if(NOT DEFINED STRUCTIVE_SOURCE_DIR OR NOT DEFINED STRUCTIVE_BINARY_DIR) + message(FATAL_ERROR "STRUCTIVE_SOURCE_DIR and STRUCTIVE_BINARY_DIR are required") +endif() +set(prefix "${STRUCTIVE_BINARY_DIR}/install_consumer_prefix") +set(consumer_binary "${STRUCTIVE_BINARY_DIR}/install_consumer_build") +file(REMOVE_RECURSE "${prefix}" "${consumer_binary}") +set(build_command "${CMAKE_COMMAND}" --build "${STRUCTIVE_BINARY_DIR}" --target structive_property_extensions) +if(DEFINED STRUCTIVE_CONFIG AND NOT STRUCTIVE_CONFIG STREQUAL "") + list(APPEND build_command --config "${STRUCTIVE_CONFIG}") +endif() +execute_process(COMMAND ${build_command} RESULT_VARIABLE build_result) +if(NOT build_result EQUAL 0) + message(FATAL_ERROR "Failed to build Structive before install consumer test") +endif() +set(install_command "${CMAKE_COMMAND}" --install "${STRUCTIVE_BINARY_DIR}" --prefix "${prefix}") +if(DEFINED STRUCTIVE_CONFIG AND NOT STRUCTIVE_CONFIG STREQUAL "") + list(APPEND install_command --config "${STRUCTIVE_CONFIG}") +endif() +execute_process(COMMAND ${install_command} RESULT_VARIABLE install_result) +if(NOT install_result EQUAL 0) + message(FATAL_ERROR "Failed to install Structive") +endif() +set(configure_command "${CMAKE_COMMAND}" -S "${STRUCTIVE_SOURCE_DIR}/install_consumer" -B "${consumer_binary}" "-DCMAKE_PREFIX_PATH=${prefix}") +if(DEFINED STRUCTIVE_GENERATOR AND NOT STRUCTIVE_GENERATOR STREQUAL "") + list(APPEND configure_command -G "${STRUCTIVE_GENERATOR}") +endif() +if(DEFINED STRUCTIVE_GENERATOR_PLATFORM AND NOT STRUCTIVE_GENERATOR_PLATFORM STREQUAL "") + list(APPEND configure_command -A "${STRUCTIVE_GENERATOR_PLATFORM}") +endif() +if(DEFINED STRUCTIVE_GENERATOR_TOOLSET AND NOT STRUCTIVE_GENERATOR_TOOLSET STREQUAL "") + list(APPEND configure_command -T "${STRUCTIVE_GENERATOR_TOOLSET}") +endif() +execute_process(COMMAND ${configure_command} RESULT_VARIABLE configure_result) +if(NOT configure_result EQUAL 0) + message(FATAL_ERROR "Failed to configure Structive install consumer") +endif() +set(consumer_build_command "${CMAKE_COMMAND}" --build "${consumer_binary}") +if(DEFINED STRUCTIVE_CONFIG AND NOT STRUCTIVE_CONFIG STREQUAL "") + list(APPEND consumer_build_command --config "${STRUCTIVE_CONFIG}") +endif() +execute_process(COMMAND ${consumer_build_command} RESULT_VARIABLE consumer_build_result) +if(NOT consumer_build_result EQUAL 0) + message(FATAL_ERROR "Failed to build Structive install consumer") +endif() +set(ctest_command "${CMAKE_CTEST_COMMAND}" --test-dir "${consumer_binary}" --output-on-failure) +if(DEFINED STRUCTIVE_CONFIG AND NOT STRUCTIVE_CONFIG STREQUAL "") + list(APPEND ctest_command -C "${STRUCTIVE_CONFIG}") +endif() +execute_process(COMMAND ${ctest_command} RESULT_VARIABLE test_result) +if(NOT test_result EQUAL 0) + message(FATAL_ERROR "Structive install consumer failed") +endif() diff --git a/cmake/StructiveConfig.cmake.in b/cmake/StructiveConfig.cmake.in new file mode 100644 index 0000000..da12087 --- /dev/null +++ b/cmake/StructiveConfig.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ +include("${CMAKE_CURRENT_LIST_DIR}/StructiveTargets.cmake") +set(Structive_Core_FOUND TRUE) +set(Structive_Extensions_FOUND TRUE) +check_required_components(Structive) diff --git a/core/include/structive/property/descriptor.hpp b/core/include/structive/property/descriptor.hpp index d5df3d3..d872148 100644 --- a/core/include/structive/property/descriptor.hpp +++ b/core/include/structive/property/descriptor.hpp @@ -43,6 +43,7 @@ struct Property_Descriptor { static constexpr bool writable = Accessor::writable && (intrinsic_capability == Property_Capability::write || intrinsic_capability == Property_Capability::read_write); static constexpr bool synchronized_view_read = Accessor::synchronized_view_read; static constexpr bool trusted_object_access = Accessor::trusted_object_access; + static constexpr bool runtime_copy_writable = writable && requires(const Accessor& accessor, object_type& object, const value_type& candidate) { accessor.write(object, candidate); }; static_assert(unique_single_value_categories()); static_assert(!((intrinsic_capability == Property_Capability::read || intrinsic_capability == Property_Capability::read_write) && !Accessor::readable), "property capability requests read from a non-readable accessor"); static_assert(!((intrinsic_capability == Property_Capability::write || intrinsic_capability == Property_Capability::read_write) && !Accessor::writable), "property capability requests write from a non-writable accessor"); @@ -55,9 +56,11 @@ struct Property_Descriptor { template using attribute_type = find_attribute_in_list_t; template - static constexpr bool has_attribute = !std::same_as, void>; + static constexpr std::size_t attribute_count = count_attribute_category(); template - constexpr decltype(auto) attribute() const requires has_attribute { + static constexpr bool has_attribute = attribute_count != 0; + template + constexpr decltype(auto) attribute() const requires (attribute_count == 1) { using target = attribute_type; return std::get(attributes); } @@ -70,6 +73,17 @@ struct Property_Descriptor { (function(values), ...); }, attributes); } + template + constexpr void for_each_attribute(Function&& function) const { + std::apply([&](const auto&... values) { + ([&] { + using type = std::remove_cvref_t; + if constexpr (std::same_as, Category>) { + function(values); + } + }(), ...); + }, attributes); + } template constexpr void for_each_constraint(Function&& function) const { std::apply([&](const auto&... values) { diff --git a/core/include/structive/property/property_object.hpp b/core/include/structive/property/property_object.hpp index 52c39c3..07cb385 100644 --- a/core/include/structive/property/property_object.hpp +++ b/core/include/structive/property/property_object.hpp @@ -85,6 +85,7 @@ enum class Runtime_Access_Result { unknown_property, not_readable, not_writable, + unsupported_runtime_write, type_mismatch }; /// Callback used by `runtime_read`. @@ -123,8 +124,8 @@ public: 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. + /// Looks up `key` at runtime and performs a copy-input managed 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. Intrinsically writable properties that require move-only input report `unsupported_runtime_write`; typed `write` remains available for them. 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); } @@ -656,7 +657,7 @@ private: return Runtime_Access_Result::not_readable; } Runtime_Access_Result result = Runtime_Access_Result::unknown_property; - visit_schema_property(type_descriptor(), key, [&](auto property_index_constant, const auto&) { + visit_schema_property_at(type_descriptor(), *index, [&](auto property_index_constant, const auto&) { constexpr std::size_t property_index = decltype(property_index_constant)::value; using Property = typename Schema::template property_type; using Value = typename Property::value_type; @@ -707,13 +708,12 @@ private: return Runtime_Access_Result::not_writable; } Runtime_Access_Result result = Runtime_Access_Result::unknown_property; - visit_schema_property(type_descriptor(), key, [&](auto property_index_constant, const auto&) { + visit_schema_property_at(type_descriptor(), *index, [&](auto property_index_constant, const auto&) { constexpr std::size_t property_index = decltype(property_index_constant)::value; using Property = typename Schema::template property_type; - using Accessor = typename Property::accessor_type; using Value = typename Property::value_type; - if constexpr (!Schema::template property_type::writable || !requires(const Accessor& accessor, Derived& object, const Value& candidate) { accessor.write(object, candidate); }) { - result = Runtime_Access_Result::not_writable; + if constexpr (!Property::runtime_copy_writable) { + result = Runtime_Access_Result::unsupported_runtime_write; } else if (value_type != typeid(Value)) { result = Runtime_Access_Result::type_mismatch; } else { diff --git a/core/include/structive/property/schema.hpp b/core/include/structive/property/schema.hpp index 87943ac..276e71c 100644 --- a/core/include/structive/property/schema.hpp +++ b/core/include/structive/property/schema.hpp @@ -24,7 +24,7 @@ consteval std::string_view declared_property_key() { } template consteval bool unique_property_keys() { - constexpr std::array keys{declared_property_key()...}; + constexpr std::array keys{declared_property_key()...}; for (std::size_t i = 0; i < keys.size(); ++i) { for (std::size_t j = i + 1; j < keys.size(); ++j) { if (keys[i] == keys[j]) { @@ -374,19 +374,24 @@ Synchronization_Plan materialize_synchronization_plan(Source&& source) { } } template -bool visit_schema_property(const Schema& schema, std::string_view key_value, Function&& function) { +bool visit_schema_property_at(const Schema& schema, std::size_t index_value, Function&& function) { bool found = false; schema.for_each_property([&](auto index, const auto& descriptor) { - if (!found) { - using property_type = std::remove_cvref_t; - if (declared_property_key() == key_value) { - std::invoke(function, index, descriptor); - found = true; - } + if (!found && decltype(index)::value == index_value) { + std::invoke(function, index, descriptor); + found = true; } }); return found; } +template +bool visit_schema_property(const Schema& schema, std::string_view key_value, Function&& function) { + auto index = schema_property_index(key_value); + if (!index) { + return false; + } + return visit_schema_property_at(schema, *index, std::forward(function)); +} template auto make_object_schema(Defaults&& object_defaults, Source&& synchronization_source, Properties&&... properties) requires Synchronization_Source_Type> { using schema_type = Object_Schema, std::decay_t...>; diff --git a/core/main.cmake b/core/main.cmake index a438f2f..5fb2eeb 100644 --- a/core/main.cmake +++ b/core/main.cmake @@ -1,13 +1,16 @@ add_library(structive_property_core INTERFACE) add_library(structive::property_core ALIAS structive_property_core) -target_include_directories(structive_property_core INTERFACE "${CMAKE_CURRENT_LIST_DIR}/include") +set_target_properties(structive_property_core PROPERTIES EXPORT_NAME property_core) +target_include_directories(structive_property_core INTERFACE "$" "$") target_compile_features(structive_property_core INTERFACE cxx_std_20) -add_executable(structive_property_core_example "${CMAKE_CURRENT_LIST_DIR}/example/main.cpp") -target_link_libraries(structive_property_core_example PRIVATE structive::property_core) -if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(structive_property_core_example PRIVATE -Wall -Wextra -Wpedantic) -endif () -if (BUILD_TESTING) +if(STRUCTIVE_BUILD_EXAMPLES) + add_executable(structive_property_core_example "${CMAKE_CURRENT_LIST_DIR}/example/main.cpp") + target_link_libraries(structive_property_core_example PRIVATE structive::property_core) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(structive_property_core_example PRIVATE -Wall -Wextra -Wpedantic) + endif() +endif() +if(STRUCTIVE_BUILD_TESTS) 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") @@ -15,12 +18,60 @@ if (BUILD_TESTING) 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") + 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 () + 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 () + set_tests_properties(structive_property_core_test structive_property_runtime_api_test structive_property_synchronization_test PROPERTIES LABELS "unit;core") + function(structive_add_core_header_test header) + string(REPLACE "/" "_" target_suffix "${header}") + string(REPLACE "." "_" target_suffix "${target_suffix}") + set(target "structive_header_${target_suffix}") + set(source "${CMAKE_CURRENT_BINARY_DIR}/structive_header_tests/${target}.cpp") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/structive_header_tests") + file(WRITE "${source}" "#include <${header}>\nint main() {\n return 0;\n}\n") + add_executable("${target}" "${source}") + target_link_libraries("${target}" PRIVATE structive::property_core) + add_test(NAME "${target}" COMMAND "${target}") + set_tests_properties("${target}" PROPERTIES LABELS "header;core") + endfunction() + set(structive_core_headers + structive/property/accessor.hpp + structive/property/attributes.hpp + structive/property/descriptor.hpp + structive/property/fixed_string.hpp + structive/property/meta.hpp + structive/property/property.hpp + structive/property/property_object.hpp + structive/property/schema.hpp + structive/property/synchronization.hpp + structive/property/type_descriptor.hpp + structive/property/validation.hpp + ) + foreach(header IN LISTS structive_core_headers) + structive_add_core_header_test("${header}") + endforeach() + include(CheckCXXSourceCompiles) + function(structive_expect_compile_failure name source) + file(READ "${source}" source_text) + set(CMAKE_REQUIRED_INCLUDES "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/include") + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(result_variable "STRUCTIVE_COMPILE_FAIL_${name}") + unset("${result_variable}" CACHE) + check_cxx_source_compiles("${source_text}" "${result_variable}") + if(${result_variable}) + message(FATAL_ERROR "Structive compile-fail contract unexpectedly compiled: ${name}") + endif() + endfunction() + structive_expect_compile_failure(duplicate_key "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/duplicate_key.cpp") + structive_expect_compile_failure(duplicate_storage "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/duplicate_storage.cpp") + structive_expect_compile_failure(missing_key "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/missing_key.cpp") + structive_expect_compile_failure(capability_mismatch "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/capability_mismatch.cpp") + structive_expect_compile_failure(incompatible_constraint "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/incompatible_constraint.cpp") + structive_expect_compile_failure(foreign_sync_member "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/foreign_sync_member.cpp") +endif() diff --git a/core/tests/compile_fail/capability_mismatch.cpp b/core/tests/compile_fail/capability_mismatch.cpp new file mode 100644 index 0000000..23dd904 --- /dev/null +++ b/core/tests/compile_fail/capability_mismatch.cpp @@ -0,0 +1,8 @@ +#include +struct Capability_Mismatch { + const int value{}; +}; +int main() { + auto schema = structive::object(structive::field<&Capability_Mismatch::value>(structive::key<"value">, structive::read_write)); + return static_cast(schema.property_count); +} diff --git a/core/tests/compile_fail/duplicate_key.cpp b/core/tests/compile_fail/duplicate_key.cpp new file mode 100644 index 0000000..8d9f106 --- /dev/null +++ b/core/tests/compile_fail/duplicate_key.cpp @@ -0,0 +1,9 @@ +#include +struct Duplicate_Key { + int left{}; + int right{}; +}; +int main() { + auto schema = structive::object(structive::field<&Duplicate_Key::left>(structive::key<"value">), structive::field<&Duplicate_Key::right>(structive::key<"value">)); + return static_cast(schema.property_count); +} diff --git a/core/tests/compile_fail/duplicate_storage.cpp b/core/tests/compile_fail/duplicate_storage.cpp new file mode 100644 index 0000000..bb3c5c2 --- /dev/null +++ b/core/tests/compile_fail/duplicate_storage.cpp @@ -0,0 +1,8 @@ +#include +struct Duplicate_Storage { + int value{}; +}; +int main() { + auto schema = structive::object(structive::field<&Duplicate_Storage::value>(structive::key<"first">), structive::field<&Duplicate_Storage::value>(structive::key<"second">)); + return static_cast(schema.property_count); +} diff --git a/core/tests/compile_fail/foreign_sync_member.cpp b/core/tests/compile_fail/foreign_sync_member.cpp new file mode 100644 index 0000000..e5dd952 --- /dev/null +++ b/core/tests/compile_fail/foreign_sync_member.cpp @@ -0,0 +1,11 @@ +#include +struct Foreign { + int value{}; +}; +struct Local { + int value{}; +}; +int main() { + auto schema = structive::object(structive::synchronization(structive::sync_independent<&Foreign::value>()), structive::field<&Local::value>(structive::key<"value">)); + return static_cast(schema.property_count); +} diff --git a/core/tests/compile_fail/incompatible_constraint.cpp b/core/tests/compile_fail/incompatible_constraint.cpp new file mode 100644 index 0000000..9b13d83 --- /dev/null +++ b/core/tests/compile_fail/incompatible_constraint.cpp @@ -0,0 +1,10 @@ +#include +#include +struct Incompatible_Constraint { + int value{}; +}; +int main() { + auto only_string = structive::constraint<"string_only">([](const std::string&) { return true; }); + auto schema = structive::object(structive::field<&Incompatible_Constraint::value>(structive::key<"value">, only_string)); + return static_cast(schema.property_count); +} diff --git a/core/tests/compile_fail/missing_key.cpp b/core/tests/compile_fail/missing_key.cpp new file mode 100644 index 0000000..bac0c8b --- /dev/null +++ b/core/tests/compile_fail/missing_key.cpp @@ -0,0 +1,8 @@ +#include +struct Missing_Key { + int value{}; +}; +int main() { + auto schema = structive::object(structive::field<&Missing_Key::value>()); + return static_cast(schema.property_count); +} diff --git a/core/tests/property_core_test.cpp b/core/tests/property_core_test.cpp index d74cef2..aa34ab8 100644 --- a/core/tests/property_core_test.cpp +++ b/core/tests/property_core_test.cpp @@ -20,6 +20,16 @@ struct Test_Tag_Attribute { }; template inline constexpr Test_Tag_Attribute test_tag{}; +struct Test_Multi_Category {}; +template +struct Test_Multi_Attribute { + using attribute_category = Test_Multi_Category; + static constexpr bool single_valued = false; + static constexpr bool inheritable = false; + static constexpr int value = Value; +}; +template +inline constexpr Test_Multi_Attribute test_multi{}; #define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false) struct Device : Property_Object { Device() = default; @@ -35,7 +45,7 @@ struct structive::Type_Descriptor { static auto get() { return object( synchronization(sync_all_independent, sync_group<&Device::min_speed, &Device::max_speed>("speed_range")), - field<&Device::temperature>(key<"temperature">, min_value<-50>, max_value<200>, unit<"C">, test_tag<7>), + field<&Device::temperature>(key<"temperature">, min_value<-50>, max_value<200>, unit<"C">, test_tag<7>, test_multi<3>, test_multi<5>), field<&Device::pressure>(key<"pressure">), field<&Device::min_speed>(key<"minimum_speed">), field<&Device::max_speed>(key<"maximum_speed">), @@ -106,6 +116,13 @@ struct structive::Type_Descriptor { ); } }; +struct Empty_Device : Property_Object {}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object(); + } +}; struct Pure_Read_Only_Device : Property_Object { int id{21}; int version{4}; @@ -120,6 +137,10 @@ struct structive::Type_Descriptor { ); } }; +template +concept Can_Read_Multi_Attribute_As_Single = requires(const Property& property_value) { + property_value.template attribute(); +}; template concept Can_Write_Immutable = requires(Object& object) { object.template write<&Device::immutable_id>(1); @@ -180,7 +201,24 @@ int main() { REQUIRE(schema.template property<&Device::max_speed>().key() == "maximum_speed"); using Temperature_Property = std::remove_cvref_t())>; static_assert(Temperature_Property::template has_attribute); + static_assert(Temperature_Property::template attribute_count == 1); + static_assert(Temperature_Property::template attribute_count == 2); + static_assert(!Can_Read_Multi_Attribute_As_Single); REQUIRE(Temperature_Property::template attribute_type::value == 7); + int multi_sum = 0; + schema.template property<&Device::temperature>().template for_each_attribute([&](const auto& attribute) { + multi_sum += std::remove_cvref_t::value; + }); + REQUIRE(multi_sum == 8); + const auto& empty_schema = type_descriptor(); + static_assert(type_descriptor_schema_t::property_count == 0); + REQUIRE(empty_schema.synchronization_plan().property_rules().empty()); + Empty_Device empty_device; + REQUIRE(empty_device.resolved_synchronization().lock_count == 0); + Property_Object_Base& empty_erased = empty_device; + REQUIRE(empty_erased.runtime_property_count() == 0); + int empty_value = 0; + REQUIRE(empty_erased.runtime_read("missing", &empty_value, &runtime_read_int) == Runtime_Access_Result::unknown_property); Device device; REQUIRE(device.resolved_synchronization().lock_count == 3); REQUIRE(device.lock_slot<&Device::immutable_id>() == Resolved_Synchronization_View::unsynchronized_slot); diff --git a/core/tests/runtime_api_test.cpp b/core/tests/runtime_api_test.cpp index 2d62171..4c0230f 100644 --- a/core/tests/runtime_api_test.cpp +++ b/core/tests/runtime_api_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,15 @@ struct structive::Type_Descriptor { ); } }; +struct Move_Only_Runtime_Device : Property_Object { + std::unique_ptr value{std::make_unique(1)}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object(field<&Move_Only_Runtime_Device::value>(key<"value">)); + } +}; struct Runtime_Read_Capture { std::size_t calls{}; std::size_t index{}; @@ -82,6 +92,20 @@ static void test_runtime_metadata_and_results() { 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_copy_write_boundary() { + using Schema = type_descriptor_schema_t; + using Property = typename Schema::template property_type<0>; + static_assert(Property::writable); + static_assert(!Property::runtime_copy_writable); + Move_Only_Runtime_Device device; + device.write<&Move_Only_Runtime_Device::value>(std::make_unique(9)); + REQUIRE(*device.value == 9); + Property_Object_Base& erased = device; + std::unique_ptr replacement = std::make_unique(11); + REQUIRE(erased.runtime_write("value", typeid(std::unique_ptr), &replacement) == Runtime_Access_Result::unsupported_runtime_write); + REQUIRE(*device.value == 9); + REQUIRE(*replacement == 11); +} static void test_runtime_access_uses_managed_synchronization() { Runtime_Device device; Property_Object_Base& erased = device; @@ -134,6 +158,7 @@ static void test_runtime_read_only_fast_path() { } int main() { test_runtime_metadata_and_results(); + test_runtime_copy_write_boundary(); test_runtime_access_uses_managed_synchronization(); test_runtime_read_only_fast_path(); } diff --git a/docs/CORE_GUIDE.md b/docs/CORE_GUIDE.md index 8eb6996..0a4d684 100644 --- a/docs/CORE_GUIDE.md +++ b/docs/CORE_GUIDE.md @@ -97,6 +97,7 @@ A property descriptor exposes compile-time structural facts: using Property = std::remove_cvref_t())>; static_assert(Property::readable); static_assert(Property::writable); +static_assert(Property::runtime_copy_writable); using Value = Property::value_type; using Accessor = Property::accessor_type; ``` @@ -570,7 +571,7 @@ For a synchronized writable property, Structive keeps the corresponding managed auto result = erased.runtime_write("temperature", typeid(double), &value); ``` -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`. +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. The boundary copies from a `const` input. If a writable accessor cannot accept that copy input, its descriptor exposes `runtime_copy_writable == false` and runtime access returns `unsupported_runtime_write`; typed `write` still accepts move-only values when the accessor supports them. A successful runtime write uses the same managed synchronization path as typed `write`. ### 22.3 Result contract @@ -580,6 +581,7 @@ Runtime write intentionally performs no implicit conversion. `typeid(double)` mu | `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 | +| `unsupported_runtime_write` | the property is intrinsically writable but its accessor cannot accept the runtime copy-input boundary | | `type_mismatch` | runtime write supplied a type different from the declared property value type | 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. diff --git a/docs/CORE_GUIDE.zh-CN.md b/docs/CORE_GUIDE.zh-CN.md index 51be977..f1acc37 100644 --- a/docs/CORE_GUIDE.zh-CN.md +++ b/docs/CORE_GUIDE.zh-CN.md @@ -95,6 +95,7 @@ Descriptor 暴露编译期结构事实: using Property = std::remove_cvref_t())>; static_assert(Property::readable); static_assert(Property::writable); +static_assert(Property::runtime_copy_writable); using Value = Property::value_type; using Accessor = Property::accessor_type; ``` @@ -562,7 +563,7 @@ auto result = erased.runtime_read("temperature", context, callback); auto result = erased.runtime_write("temperature", typeid(double), &value); ``` -Runtime write 明确不做隐式转换。`typeid(double)` 必须与 Property 声明的 value type 完全一致,`value` 指针在调用期间必须指向这个精确类型的有效对象。成功写入复用 typed `write` 的同一 managed write 和 synchronization 语义。 +Runtime write 明确不做隐式转换。`typeid(double)` 必须与 Property 声明的 value type 完全一致,`value` 指针在调用期间必须指向这个精确类型的有效对象。这个边界从 `const` 输入复制;如果一个 intrinsically writable Accessor 无法接受 copy-input,它会暴露 `runtime_copy_writable == false`,runtime access 返回 `unsupported_runtime_write`,而 typed `write` 在 Accessor 支持时仍可正常接收 move-only 值。成功写入继续复用 typed `write` 的 managed synchronization 路径。 ### 22.3 Result Contract @@ -572,6 +573,7 @@ Runtime write 明确不做隐式转换。`typeid(double)` 必须与 Property 声 | `unknown_property` | Schema 中不存在该 runtime key | | `not_readable` | Property 存在,但 intrinsic capability 不可读 | | `not_writable` | Property 存在,但 intrinsic capability 不可写 | +| `unsupported_runtime_write` | Property intrinsically writable,但 Accessor 无法接收 runtime copy-input 边界 | | `type_mismatch` | runtime write 提供的类型和 Property value type 不一致 | 这里没有 external/persistence mode,也没有访问控制 policy。Adapter 自己决定是否对外暴露、是否调用 runtime read/write;Structive 只报告 Property 自身的 intrinsic capability。 diff --git a/docs/DESIGN.md b/docs/DESIGN.md index a28fba4..c883e5a 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -298,7 +298,7 @@ Examples: - requesting a typed unique guard for a read-only property; - reading a write-only property. -Runtime key APIs report dynamic failures through `Runtime_Access_Result`. +Runtime key APIs report dynamic failures through `Runtime_Access_Result`. The runtime write boundary is copy-input by design: intrinsic `writable` remains a structural fact, while `runtime_copy_writable` states whether the accessor can participate in that type-erased copy boundary. Move-only typed writes therefore do not get mislabeled as structurally non-writable. ### Rule diff --git a/docs/DESIGN.zh-CN.md b/docs/DESIGN.zh-CN.md index 2aa423e..d704bc5 100644 --- a/docs/DESIGN.zh-CN.md +++ b/docs/DESIGN.zh-CN.md @@ -294,7 +294,7 @@ Adapter 自己决定是否暴露某个 Property、是否调用 runtime read/writ - 对 read-only property 请求 typed unique guard; - 读取 write-only property。 -Runtime key API 才使用 `Runtime_Access_Result` 返回动态错误。 +Runtime key API 才使用 `Runtime_Access_Result` 返回动态错误。Runtime write 明确是 copy-input 边界:intrinsic `writable` 继续表示结构事实,`runtime_copy_writable` 单独表示 Accessor 是否能参与 type-erased copy write。这样 move-only typed write 不会被错误描述成结构上不可写。 ### 原则 diff --git a/extensions/main.cmake b/extensions/main.cmake index 6bbbadd..55353ee 100644 --- a/extensions/main.cmake +++ b/extensions/main.cmake @@ -1,18 +1,31 @@ add_library(structive_property_extensions STATIC "${CMAKE_CURRENT_LIST_DIR}/src/presentation.cpp") add_library(structive::property_extensions ALIAS structive_property_extensions) -target_include_directories(structive_property_extensions PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include") +set_target_properties(structive_property_extensions PROPERTIES EXPORT_NAME property_extensions) +target_include_directories(structive_property_extensions PUBLIC "$" "$") target_link_libraries(structive_property_extensions PUBLIC structive::property_core) target_compile_features(structive_property_extensions PUBLIC cxx_std_20) -if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(structive_property_extensions PRIVATE -Wall -Wextra -Wpedantic) -endif () -add_executable(structive_property_extensions_example "${CMAKE_CURRENT_LIST_DIR}/example/main.cpp") -target_link_libraries(structive_property_extensions_example PRIVATE structive::property_extensions) -if (BUILD_TESTING) +endif() +if(STRUCTIVE_BUILD_EXAMPLES) + add_executable(structive_property_extensions_example "${CMAKE_CURRENT_LIST_DIR}/example/main.cpp") + target_link_libraries(structive_property_extensions_example PRIVATE structive::property_extensions) +endif() +if(STRUCTIVE_BUILD_TESTS) add_executable(structive_property_extensions_test "${CMAKE_CURRENT_LIST_DIR}/tests/presentation_test.cpp") target_link_libraries(structive_property_extensions_test PRIVATE structive::property_extensions) - if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(structive_property_extensions_test PRIVATE -Wall -Wextra -Wpedantic) - endif () + endif() add_test(NAME structive_property_extensions_test COMMAND structive_property_extensions_test) -endif () + set_tests_properties(structive_property_extensions_test PROPERTIES LABELS "unit;extensions") + set(header structive/property/extensions/presentation.hpp) + set(target structive_header_structive_property_extensions_presentation_hpp) + set(source "${CMAKE_CURRENT_BINARY_DIR}/structive_header_tests/${target}.cpp") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/structive_header_tests") + file(WRITE "${source}" "#include <${header}>\nint main() {\n return 0;\n}\n") + add_executable("${target}" "${source}") + target_link_libraries("${target}" PRIVATE structive::property_extensions) + add_test(NAME "${target}" COMMAND "${target}") + set_tests_properties("${target}" PROPERTIES LABELS "header;extensions") +endif() diff --git a/install_consumer/CMakeLists.txt b/install_consumer/CMakeLists.txt new file mode 100644 index 0000000..08b7a2c --- /dev/null +++ b/install_consumer/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.20) +project(StructiveInstallConsumer LANGUAGES CXX) +include(CTest) +find_package(Structive CONFIG REQUIRED COMPONENTS Core Extensions) +add_executable(structive_install_consumer main.cpp) +target_link_libraries(structive_install_consumer PRIVATE structive::property_core structive::property_extensions) +target_compile_features(structive_install_consumer PRIVATE cxx_std_20) +add_test(NAME structive_install_consumer COMMAND structive_install_consumer) diff --git a/install_consumer/main.cpp b/install_consumer/main.cpp new file mode 100644 index 0000000..4043855 --- /dev/null +++ b/install_consumer/main.cpp @@ -0,0 +1,16 @@ +#include +#include +struct Consumer_Device : structive::Property_Object { + int value{1}; +}; +template <> +struct structive::Type_Descriptor { + static auto get() { + return object(field<&Consumer_Device::value>(key<"value">, presentation::label<"Value">)); + } +}; +int main() { + Consumer_Device device; + device.write<&Consumer_Device::value>(2); + return device.read<&Consumer_Device::value>() == 2 ? 0 : 1; +}