From 2f51c16a873b3ee955e94a023c22d6c031d39b1d Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 26 Oct 2026 09:25:50 +0800 Subject: [PATCH 1/4] refactor(consul)!: adopt typed plugin construction Replace Consul creation macros with typed bases, a private owning factory and explicit DLL entry points. Preserve interface registration, disabled updates and request behavior, and add construction/lifecycle checks with bilingual migration guidance. BREAKING CHANGE: Consul implementation and plugin class layouts change. Rebuild Consul and its consumers together; all existing public types and logical header paths remain available. --- docs/guides/plugins.md | 2 + docs/guides/plugins_CN.md | 2 + docs/project-layout.md | 14 ++++ docs/project-layout_CN.md | 14 ++++ docs/reference/build-options.md | 2 +- docs/reference/build-options_CN.md | 2 +- src/plugins/README.md | 4 +- src/plugins/README_CN.md | 4 +- src/plugins/consul/CMakeLists.txt | 18 +----- src/plugins/consul/consul_factory.hpp | 13 ++++ src/plugins/consul/consul_module_impl.cpp | 5 ++ src/plugins/consul/consul_module_impl.hpp | 5 +- src/plugins/consul/consul_plugin.cpp | 23 +++++-- src/plugins/consul/consul_plugin.hpp | 12 +++- tests/consul/CMakeLists.txt | 36 +++++++++++ tests/consul/construction.cpp | 53 +++++++++++++++ tests/consul/entry.cpp | 45 +++++++++++++ tests/consul/entry_factory.cpp | 79 +++++++++++++++++++++++ tests/consul/exports.cpp | 31 +++++++++ tests/core/CMakeLists.txt | 4 ++ 20 files changed, 337 insertions(+), 31 deletions(-) create mode 100644 src/plugins/consul/consul_factory.hpp create mode 100644 tests/consul/CMakeLists.txt create mode 100644 tests/consul/construction.cpp create mode 100644 tests/consul/entry.cpp create mode 100644 tests/consul/entry_factory.cpp create mode 100644 tests/consul/exports.cpp diff --git a/docs/guides/plugins.md b/docs/guides/plugins.md index 0403f25b5..9c9e8f5cf 100644 --- a/docs/guides/plugins.md +++ b/docs/guides/plugins.md @@ -211,3 +211,5 @@ ABI stability. Lifecycle phases, draining and library unload rules are unchanged Net constructs its HTTP client through a private factory returning `std::unique_ptr` and registers it with updates enabled. Applications consume `` and typed manager lookup; the old `http_client_module_impl.hpp` is removed from the public/install interfaces. Rebuild affected consumers together; see [Net migration](../reference/network.md#api-and-private-construction). Kernel also uses typed construction while retaining its public `*_impl` and `kernel_plugin` types and header paths. Its private factory header is not installed. Rebuild Kernel and consumers together because those public class layouts change; the [Kernel migration table](../reference/kernel.md#typed-kernel-construction) records unchanged interface keys, update selection and registration order. + +Consul follows the same construction pattern while retaining all three public headers and types. Its private factory is not installed; the module keeps its interface RTTI key and disabled updates. Rebuild Consul and consumers together because its public implementation/plugin layouts change. See [Consul construction migration](../project-layout.md#typed-consul-construction) for test entry points and dependency conditions. diff --git a/docs/guides/plugins_CN.md b/docs/guides/plugins_CN.md index 1458fc47d..39ba62f2a 100644 --- a/docs/guides/plugins_CN.md +++ b/docs/guides/plugins_CN.md @@ -198,3 +198,5 @@ module_update::enabled)` 启用 update;`disabled` 则不参与 update,即使 Net 通过私有工厂返回 `std::unique_ptr`,并启用该模块的 update 注册。应用使用 `` 和管理器的类型化查找;旧 `http_client_module_impl.hpp` 从公开/安装接口移除。受影响的消费者需要一起重建,详见 [Net 迁移](../reference/network_CN.md#api-与私有创建)。 Kernel 同样使用类型化构造,并保留公开 `*_impl`、`kernel_plugin` 类型与头路径;私有工厂头不安装。公开类布局发生变化,Kernel 与消费者需要一起重建。[Kernel 迁移表](../reference/kernel_CN.md#kernel-类型化构造)记录保持不变的接口键、update 选择与注册顺序。 + +Consul 使用相同构造方式,保留全部三个公开头与类型。私有工厂不安装;模块保留接口 RTTI 键和禁用 update 的选择。公开实现/插件布局发生变化,Consul 与消费者必须一起重建。测试入口与依赖条件见 [Consul 构造迁移](../project-layout_CN.md#consul-类型化构造)。 diff --git a/docs/project-layout.md b/docs/project-layout.md index 6ce9b31b3..79cff408b 100644 --- a/docs/project-layout.md +++ b/docs/project-layout.md @@ -134,3 +134,17 @@ cmake --build etcd-client-headers --target check-etcd-client-headers --parallel ``` Header mapping does not establish Consul, Kafka or OSS SDK runtime compatibility. Physic and `physics_scene` still require the unsupported legacy PhysX 3 integration and retain their explicit configuration failure; no passing PhysX build is claimed. + +## Typed Consul construction + +**Breaking ABI/layout change:** `consul_module_impl` now inherits `module_base` and `consul_plugin` inherits `plugin_base`. Rebuild Consul and consumers together. All three public headers, their `` paths, public types and request methods remain available through `ark::consul`; the interface itself is unchanged. The C++ plugin boundary does not promise compiler-independent ABI compatibility. + +The private `consul_factory.hpp` returns `std::unique_ptr` and is excluded from public mappings and installation. Explicit `DllEntryPlugin` and `DllExitPlugin` retain the existing platform exports and logger setup. Installation registers the single `consul_module` interface RTTI key with `module_update::disabled`; uninstallation removes and destroys that module. The implementation still inherits the default update method. Configuration storage, HTTP-client lookup, TOML loading, request serialization and endpoints are unchanged. + +With `ARK_BUILD_CORE_TESTS=ON`, `ARK_BUILD_PROTO=ON` and `ARK_BUILD_CONFIG=ON`, `check-consul-construction` runs the actual factory/assembly doctest cases and a controlled-factory DSO test of the actual entry/exit source. These cover public bases, factory metadata, interface registration, duplicate rejection, disabled updates, configuration handoff, ownership and repeated stop. They require Protobuf, toml++ and the normal core/arknet dependencies, but no runtime plugin or Consul service. For an existing build configured with those options: + +```sh +cmake --build build --target check-consul-construction --parallel 2 +``` + +`ARK_BUILD_PLUGIN_CONSUL=ON` also requires `ARK_BUILD_APPLICATIONS=ON` and remains off by default. It links the generated protocol target `ark::proto` directly. With the core tests enabled, `check-consul-plugin` additionally inspects the actual Consul DSO exports in a separate process; `check-core` includes the applicable Consul checks. The controlled factory does not exercise the production HTTP client or a Consul server. These construction checks do not establish live Consul interoperability, protocol acceptance or arbitrary hot-update safety. diff --git a/docs/project-layout_CN.md b/docs/project-layout_CN.md index 82a8ee230..a251e8d4e 100644 --- a/docs/project-layout_CN.md +++ b/docs/project-layout_CN.md @@ -124,3 +124,17 @@ cmake --build etcd-client-headers --target check-etcd-client-headers --parallel ``` 头文件映射检查不代表 Consul、Kafka、OSS SDK 的运行时兼容性已经验证。Physic 与 `physics_scene` 仍依赖未受支持的旧版 PhysX 3 集成,保留明确的配置失败;此处没有宣称 PhysX 构建通过。 + +## Consul 类型化构造 + +**ABI/布局不兼容变更:** `consul_module_impl` 现在继承 `module_base`,`consul_plugin` 继承 `plugin_base`。Consul 与消费者必须一起重建。原有三个公开头、`` 路径、公开类型与请求方法继续通过 `ark::consul` 提供,接口本身不变。C++ 插件边界不承诺跨编译器的 ABI 兼容性。 + +私有 `consul_factory.hpp` 返回 `std::unique_ptr`,不映射、不安装。显式 `DllEntryPlugin` 与 `DllExitPlugin` 保留原有平台导出和日志初始化。安装时按 `consul_module` 接口 RTTI 键注册唯一模块,显式使用 `module_update::disabled`;卸载时移除并销毁该模块。实现仍继承默认 update 方法。配置存储、HTTP 客户端查找、TOML 加载、请求序列化及端点保持不变。 + +开启 `ARK_BUILD_CORE_TESTS=ON`、`ARK_BUILD_PROTO=ON` 与 `ARK_BUILD_CONFIG=ON` 后,`check-consul-construction` 运行真实工厂/装配的 doctest 用例,以及使用受控工厂与真实入口/出口源码的 DSO 测试。检查覆盖公开基类、工厂元数据、接口注册、重复注册拒绝、禁用 update、配置传递、所有权与重复 stop。依赖 Protobuf、toml++ 及通常的 core/arknet 依赖,无需运行时插件或 Consul 服务。对于已按上述选项配置的构建: + +```sh +cmake --build build --target check-consul-construction --parallel 2 +``` + +`ARK_BUILD_PLUGIN_CONSUL=ON` 还要求 `ARK_BUILD_APPLICATIONS=ON`,默认保持关闭。插件直接链接生成协议目标 `ark::proto`。开启核心测试后,`check-consul-plugin` 还在独立进程中检查真实 Consul DSO 的导出符号;`check-core` 包含当前条件下可用的 Consul 检查。受控工厂不执行生产 HTTP 客户端或访问 Consul 服务。这些构造检查不代表真实 Consul 互通、协议验收或任意热更新安全。 diff --git a/docs/reference/build-options.md b/docs/reference/build-options.md index bf0875f98..52cd11ef9 100644 --- a/docs/reference/build-options.md +++ b/docs/reference/build-options.md @@ -39,7 +39,7 @@ cmake -S . -B build -DARK_BUILD_APPLICATIONS=ON \ cmake --build build --target ark_mysql_plugin ark_redis_plugin ``` -All official plugin public headers are copied from explicit lists at configuration time into each build tree's generated plugin header mapping. Each `ark::` API target carries that include root even with runtime plugins disabled. MySQL, Redis, Utility, Net, Bus and HTTP exclude private assembly and factories. Kernel retains all 38 existing headers, including `kernel_plugin.hpp`; Consul, Etcd Client, Kafka, OSS SDK and Physic retain all 22 existing public headers, including implementation and assembly headers. Registration keys, public type visibility and optional plugin policies are retained; [Kernel typed construction](kernel.md#typed-kernel-construction) changes public class layouts and requires rebuilding consumers. Its private factory header is excluded from mapping and installation; header mapping does not validate optional driver integrations. Each public header has one co-located source; its generated include mapping belongs to the selected build tree, rather than a second source layout. +All official plugin public headers are copied from explicit lists at configuration time into each build tree's generated plugin header mapping. Each `ark::` API target carries that include root even with runtime plugins disabled. MySQL, Redis, Utility, Net, Bus and HTTP exclude private assembly and factories. Kernel retains all 38 existing headers, including `kernel_plugin.hpp`; Consul, Etcd Client, Kafka, OSS SDK and Physic retain all 22 existing public headers, including implementation and assembly headers. Registration keys, public type visibility and optional plugin policies are retained; [Kernel typed construction](kernel.md#typed-kernel-construction) and [Consul typed construction](../project-layout.md#typed-consul-construction) change public class layouts and require rebuilding consumers. Their private factory headers are excluded from mapping and installation; header mapping does not validate optional driver integrations. Each public header has one co-located source; its generated include mapping belongs to the selected build tree, rather than a second source layout. Link a consumer to `ark::mysql` and/or `ark::redis` for the public headers and usage requirements. These public interface targets do not load a runtime library. Deploy the selected libraries with the launcher and add their stems to the application's manifest: diff --git a/docs/reference/build-options_CN.md b/docs/reference/build-options_CN.md index da133ef1b..447194a30 100644 --- a/docs/reference/build-options_CN.md +++ b/docs/reference/build-options_CN.md @@ -39,7 +39,7 @@ cmake -S . -B build -DARK_BUILD_APPLICATIONS=ON \ cmake --build build --target ark_mysql_plugin ark_redis_plugin ``` -全部官方插件的公开头通过显式清单在配置阶段复制到各构建树生成的插件公开头映射。各 `ark::` API target 传递该目录,插件关闭时仍可使用。MySQL、Redis、Utility、Net、Bus、HTTP 的私有装配头和工厂不进入映射;Kernel 保留包括 `kernel_plugin.hpp` 在内的原有 38 个头;Consul、Etcd Client、Kafka、OSS SDK、Physic 保留包括实现头与装配头在内的原有 22 个公开头。注册键、公开类型可见性和可选插件策略继续保留;[Kernel 类型化构造](kernel_CN.md#kernel-类型化构造)改变公开类布局,需要重建消费者。其私有工厂头不映射、不安装;头文件映射不代表可选驱动集成已经验证。每个公共头仅有一份共置源码;生成的头文件映射属于选定构建树,不构成第二棵源码目录。 +全部官方插件的公开头通过显式清单在配置阶段复制到各构建树生成的插件公开头映射。各 `ark::` API target 传递该目录,插件关闭时仍可使用。MySQL、Redis、Utility、Net、Bus、HTTP 的私有装配头和工厂不进入映射;Kernel 保留包括 `kernel_plugin.hpp` 在内的原有 38 个头;Consul、Etcd Client、Kafka、OSS SDK、Physic 保留包括实现头与装配头在内的原有 22 个公开头。注册键、公开类型可见性和可选插件策略继续保留;[Kernel 类型化构造](kernel_CN.md#kernel-类型化构造)与 [Consul 类型化构造](../project-layout_CN.md#consul-类型化构造)改变公开类布局,需要重建消费者。两者的私有工厂头均不映射、不安装;头文件映射不代表可选驱动集成已经验证。每个公共头仅有一份共置源码;生成的头文件映射属于选定构建树,不构成第二棵源码目录。 业务插件链接 `ark::mysql` 和/或 `ark::redis`,以获取公开头文件和编译要求;公共接口目标不会加载运行时动态库。将所需动态库与启动器一起部署,并在应用清单中填写库名称: diff --git a/src/plugins/README.md b/src/plugins/README.md index 07bbf6720..66e429e98 100755 --- a/src/plugins/README.md +++ b/src/plugins/README.md @@ -26,4 +26,6 @@ See [project layout and public plugin interfaces](../../docs/project-layout.md) Kernel co-locates its 38 existing headers with its cpp files in `src/plugins/kernel/`. `ark::kernel` preserves every `` path, including `data_impl.hpp`, `data_list_impl.hpp` and `kernel_plugin.hpp`. The retained public implementation/plugin types use typed construction with private owning factories and explicit entry points. `kernel_factory.hpp` is neither mapped nor installed. The public class layouts change, so rebuild Kernel and consumers together; registration keys, update selection and lifecycle order are preserved. See [Kernel construction migration](../../docs/reference/kernel.md#typed-kernel-construction) for compatibility boundaries, consumption checks and Protobuf requirements. -The five remaining plugin families retain 22 byte-identical headers. Mapping checks do not validate Consul, Kafka or OSS SDK runtime integrations. Physic retains the unsupported PhysX 3 configuration failure; see [optional legacy dependencies](../../docs/dependencies.md#optional-legacy-integrations). +Consul retains its three public headers and implementation/plugin types while using typed construction, a private owning factory and explicit entry points. Rebuild Consul and consumers together because the public class layouts change. The single interface registration key, disabled updates and request behavior are preserved. See [Consul construction migration](../../docs/project-layout.md#typed-consul-construction) for build conditions and service-free construction checks. + +The other four legacy plugin families retain 19 byte-identical headers. Mapping checks do not validate Consul, Kafka or OSS SDK runtime integrations. Physic retains the unsupported PhysX 3 configuration failure; see [optional legacy dependencies](../../docs/dependencies.md#optional-legacy-integrations). diff --git a/src/plugins/README_CN.md b/src/plugins/README_CN.md index 188679dc2..82c42abd5 100644 --- a/src/plugins/README_CN.md +++ b/src/plugins/README_CN.md @@ -26,4 +26,6 @@ CMake target 和构建命令见[项目布局与公共插件接口](../../docs/pr Kernel 的原有 38 个头与 cpp 共置于 `src/plugins/kernel/`,通过 `ark::kernel` 保留全部 `` 路径,包括 `data_impl.hpp`、`data_list_impl.hpp` 和 `kernel_plugin.hpp`。保留的公开实现类型与插件类型使用类型化构造、私有拥有型工厂和显式入口。`kernel_factory.hpp` 不映射、不安装。公开类布局发生变化,Kernel 与消费者必须一起重建;注册键、update 选择和生命周期顺序保持不变。兼容边界、消费检查及 Protobuf 条件见 [Kernel 构造迁移](../../docs/reference/kernel_CN.md#kernel-类型化构造)。 -其余五类插件保留原有 22 个头的全部字节。映射检查不代表 Consul、Kafka、OSS SDK 的运行时集成已经验证。Physic 保留未受支持的 PhysX 3 配置失败;参见[可选旧集成依赖](../../docs/dependencies_CN.md)。 +Consul 保留原有三个公开头及实现/插件类型,同时使用类型化构造、私有拥有型工厂和显式入口。公开类布局发生变化,Consul 与消费者必须一起重建。唯一接口注册键、禁用 update 与请求行为保持不变。构建条件及无需服务的构造检查见 [Consul 构造迁移](../../docs/project-layout_CN.md#consul-类型化构造)。 + +其他四类旧插件保留原有 19 个头的全部字节。映射检查不代表 Consul、Kafka、OSS SDK 的运行时集成已经验证。Physic 保留未受支持的 PhysX 3 配置失败;参见[可选旧集成依赖](../../docs/dependencies_CN.md)。 diff --git a/src/plugins/consul/CMakeLists.txt b/src/plugins/consul/CMakeLists.txt index df78a2209..20b422335 100755 --- a/src/plugins/consul/CMakeLists.txt +++ b/src/plugins/consul/CMakeLists.txt @@ -1,17 +1 @@ -if(WIN32) - BUILD_PLUGIN_MACRO(ark_consul_plugin - debug AFProto_d.lib - optimized AFProto.lib - ${PROTOBUF_LIBRARIES}) -elseif(APPLE) - BUILD_PLUGIN_MACRO(ark_consul_plugin - debug ${BIN_OUTPUT_DIR}/AFProto_d.a - optimized ${BIN_OUTPUT_DIR}/AFProto.a - ${PROTOBUF_LIBRARIES}) -else(UNIX) - BUILD_PLUGIN_MACRO(ark_consul_plugin - debug :AFProto_d.a - optimized :AFProto.a - ${PROTOBUF_LIBRARIES}) -endif() -set_property(TARGET ark_consul_plugin APPEND PROPERTY LINK_LIBRARIES ark::config tomlplusplus::tomlplusplus) +BUILD_PLUGIN_MACRO(ark_consul_plugin ark::proto ark::config tomlplusplus::tomlplusplus) diff --git a/src/plugins/consul/consul_factory.hpp b/src/plugins/consul/consul_factory.hpp new file mode 100644 index 000000000..d65b95c48 --- /dev/null +++ b/src/plugins/consul/consul_factory.hpp @@ -0,0 +1,13 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#pragma once + +#include + +#include "ark/consul/consul_module.hpp" + +// Plugin-private construction; deliberately absent from public header mappings. +namespace ark::detail { + +std::unique_ptr make_consul_module(); + +} // namespace ark::detail diff --git a/src/plugins/consul/consul_module_impl.cpp b/src/plugins/consul/consul_module_impl.cpp index df4cc4b1a..65f8f07fb 100644 --- a/src/plugins/consul/consul_module_impl.cpp +++ b/src/plugins/consul/consul_module_impl.cpp @@ -3,9 +3,14 @@ #include "../detail/plugin_configuration.hpp" #include "ark/consul/consul_module_impl.hpp" #include "ark/consul/consul_plugin.hpp" +#include "consul_factory.hpp" namespace ark { +std::unique_ptr detail::make_consul_module() { + return std::make_unique(); +} + namespace { std::future> failed_request() { diff --git a/src/plugins/consul/consul_module_impl.hpp b/src/plugins/consul/consul_module_impl.hpp index b2cb20c0b..be2adec8c 100644 --- a/src/plugins/consul/consul_module_impl.hpp +++ b/src/plugins/consul/consul_module_impl.hpp @@ -1,14 +1,13 @@ #pragma once #include "ark/consul/consul_module.hpp" +#include "ark/module_base.hpp" #include "ark/net/http_client_module.hpp" -#include "ark/plugin_manager.hpp" namespace ark { -class consul_module_impl final : public consul_module +class consul_module_impl final : public module_base { - ARK_DECLARE_MODULE_FUNCTIONS public: bool init() override; diff --git a/src/plugins/consul/consul_plugin.cpp b/src/plugins/consul/consul_plugin.cpp index 1327a4d31..c6feb77ce 100644 --- a/src/plugins/consul/consul_plugin.cpp +++ b/src/plugins/consul/consul_plugin.cpp @@ -1,16 +1,31 @@ -#include "ark/consul/consul_module_impl.hpp" +#include + #include "ark/consul/consul_plugin.hpp" +#include "consul_factory.hpp" namespace ark { -ARK_DECLARE_PLUGIN_DLL_FUNCTION(consul_plugin) +ARK_EXPORT_FUNC void DllEntryPlugin(plugin_manager* manager, const std::string& name, logger* log) { + logger::init(log); + manager->register_plugin(name); +} + +ARK_EXPORT_FUNC void DllExitPlugin(plugin_manager* manager) { + manager->unregister_plugin(); +} void consul_plugin::install() { - ARK_REGISTER_MODULE(consul_module, consul_module_impl); + if (!register_module(detail::make_consul_module(), module_update::disabled)) + { + throw std::runtime_error("Consul module registration rejected"); + } } void consul_plugin::uninstall() { - ARK_UNREGISTER_MODULE(consul_module, consul_module_impl); + if (!unregister_module()) + { + throw std::runtime_error("Consul module unregistration rejected"); + } } } // namespace ark diff --git a/src/plugins/consul/consul_plugin.hpp b/src/plugins/consul/consul_plugin.hpp index d8b93aa2b..2aadec022 100755 --- a/src/plugins/consul/consul_plugin.hpp +++ b/src/plugins/consul/consul_plugin.hpp @@ -1,8 +1,14 @@ -#include "ark/plugin.hpp" -#include "ark/plugin_manager.hpp" +#pragma once + +#include "ark/plugin_base.hpp" namespace ark { -ARK_DECLARE_PLUGIN(consul_plugin) +class consul_plugin final : public plugin_base +{ +public: + void install() override; + void uninstall() override; +}; } // namespace ark diff --git a/tests/consul/CMakeLists.txt b/tests/consul/CMakeLists.txt new file mode 100644 index 000000000..fc45c3e15 --- /dev/null +++ b/tests/consul/CMakeLists.txt @@ -0,0 +1,36 @@ +# Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +set(consul_source "${PROJECT_SOURCE_DIR}/src/plugins/consul") + +# Compile real assembly/factory locally too: private C++ symbols are not DLL exports. +add_executable(ark_consul_construction_tests ../core/main.cpp construction.cpp + "${consul_source}/consul_plugin.cpp" "${consul_source}/consul_module_impl.cpp") +set_property(TARGET ark_consul_construction_tests PROPERTY LINK_LIBRARIES + ark::consul ark::proto ark::config tomlplusplus::tomlplusplus) +target_include_directories(ark_consul_construction_tests PRIVATE "${consul_source}") + +# A controlled module exercises the actual entry/exit functions without HTTP or Consul services. +add_library(ark_consul_entry_probe SHARED entry_factory.cpp "${consul_source}/consul_plugin.cpp") +set_property(TARGET ark_consul_entry_probe PROPERTY LINK_LIBRARIES ark::consul ark::proto) +target_include_directories(ark_consul_entry_probe PRIVATE "${consul_source}") +set_target_properties(ark_consul_entry_probe PROPERTIES PREFIX "") +add_executable(ark_consul_entry_tests entry.cpp) +set_property(TARGET ark_consul_entry_tests PROPERTY LINK_LIBRARIES ark::consul ark::proto) + +add_custom_target(check-consul-construction + COMMAND $ + COMMAND $ "$/" "ark_consul_entry_probe" + DEPENDS ark_consul_construction_tests ark_consul_entry_tests ark_consul_entry_probe + USES_TERMINAL VERBATIM) + +if(TARGET ark_consul_plugin) + # Keep real DSO export inspection separate from the host linking static AFProto. + add_executable(ark_consul_exports_tests exports.cpp) + set_property(TARGET ark_consul_exports_tests PROPERTY LINK_LIBRARIES ark::core) + add_custom_target(check-consul-plugin + COMMAND $ "$/" "ark_consul_plugin" + DEPENDS ark_consul_exports_tests ark_consul_plugin check-consul-construction + USES_TERMINAL VERBATIM) + add_dependencies(check-core check-consul-plugin) +else() + add_dependencies(check-core check-consul-construction) +endif() diff --git a/tests/consul/construction.cpp b/tests/consul/construction.cpp new file mode 100644 index 000000000..c466d65d7 --- /dev/null +++ b/tests/consul/construction.cpp @@ -0,0 +1,53 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include + +#include "../network/doctest.h" + +#include +#include +#include "consul_factory.hpp" + +#if __has_include() +#error "Consul public mappings must exclude the private factory" +#endif + +static_assert(std::is_base_of_v, ark::consul_module_impl>); +static_assert(std::is_base_of_v, ark::consul_plugin>); +static_assert(std::is_same_v); + +TEST_CASE("Consul real factory retains its public implementation and unpublished metadata") { + auto candidate = ark::detail::make_consul_module(); + REQUIRE(candidate != nullptr); + CHECK(typeid(*candidate) == typeid(ark::consul_module_impl)); + CHECK(candidate->get_plugin_manager() == nullptr); + CHECK(candidate->get_name().empty()); +} + +TEST_CASE("Consul assembly retains the interface key, disabled updates and explicit ownership") { + ark::plugin_manager manager; + ark::consul_plugin owner; + owner.set_plugin_manager(&manager); + CHECK(owner.get_plugin_version() == 0); + CHECK(owner.get_plugin_name() == typeid(ark::consul_plugin).name()); + owner.set_plugin_conf("consul-construction.toml"); + CHECK(ark::consul_plugin::get_plugin_conf() == "consul-construction.toml"); + owner.install(); + auto* instance = manager.find_module(); + REQUIRE(instance != nullptr); + CHECK(typeid(*instance) == typeid(ark::consul_module_impl)); + CHECK(instance->get_plugin_manager() == &manager); + CHECK(instance->get_name() == typeid(ark::consul_module).name()); + // Insertion succeeds only when assembly left the module out of the update set. + CHECK(manager.add_update_module(instance)); + manager.remove_update_module(instance->get_name()); + CHECK_THROWS_AS(owner.install(), std::runtime_error); + CHECK(manager.find_module() == instance); + owner.uninstall(); + // Reinstallation proves the public registration key and ownership were released. + CHECK_NOTHROW(owner.install()); + CHECK_NOTHROW(owner.uninstall()); + CHECK(manager.update(1)); + CHECK_THROWS_AS(owner.uninstall(), std::runtime_error); +} diff --git a/tests/consul/entry.cpp b/tests/consul/entry.cpp new file mode 100644 index 000000000..597dd1244 --- /dev/null +++ b/tests/consul/entry.cpp @@ -0,0 +1,45 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include + +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) + { + std::cerr << message << std::endl; + std::_Exit(1); + } +} + +} // namespace + +int main(int argc, char* argv[]) { + require(argc == 3, "Expected Consul entry-probe directory and library stem"); + ark::logger log; + ark::logger::init(&log); + log.set_log_func([](spdlog::level::level_enum, const std::string&) {}); + ark::dynamic_library library(argv[2]); + require(library.load(argv[1]), "Cannot load Consul entry-probe library"); + auto events = reinterpret_cast(library.get_symbol("consul_entry_events")); + require(events && events(0) == 0 && events(1) == 0, "Entry-probe state is not fresh"); + require(library.get_symbol("DllEntryPlugin") && library.get_symbol("DllExitPlugin"), + "Consul explicit entry/exit exports are missing"); + ark::plugin_manager manager; + require(manager.start(0, argv[1], "conf/", {{argv[2], "entry-probe.toml"}}), "Consul explicit entry failed"); + auto* instance = manager.find_module(); + require(instance && instance->get_plugin_manager() == &manager && + instance->get_name() == typeid(ark::consul_module).name(), + "Consul explicit entry lost interface registration metadata"); + require(events(0) == 1 && events(1) == 0, "Consul entry did not transfer factory ownership"); + require(manager.update(1) && events(2) == 0, "Consul entry changed disabled update selection"); + require(manager.stop(2) && events(1) == 1, "Consul explicit exit did not destroy its module exactly once"); + require(manager.stop(3) && events(1) == 1, "Repeated stop invoked Consul exit twice"); + require(library.unload(), "Cannot release Consul observation handle"); + std::cout << "Consul real assembly entry/exit, configuration, interface metadata and ownership passed\n"; +} diff --git a/tests/consul/entry_factory.cpp b/tests/consul/entry_factory.cpp new file mode 100644 index 000000000..d9f92b51b --- /dev/null +++ b/tests/consul/entry_factory.cpp @@ -0,0 +1,79 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include + +#include +#include +#include "consul_factory.hpp" + +namespace { + +unsigned created{}; +unsigned destroyed{}; +unsigned updated{}; + +class entry_consul final : public ark::module_base +{ +public: + entry_consul() { + ++created; + } + + ~entry_consul() override { + ++destroyed; + } + + bool init() override { + return ark::consul_plugin::get_plugin_conf() == "conf/entry-probe.toml"; + } + + bool update() override { + ++updated; + return true; + } + + std::future> register_service(const ark::consul::service_data&) override { + return {}; + } + + std::future> deregister_service(const std::string&) override { + return {}; + } + + std::future> get_health_services(const std::string&, const std::string&) override { + return {}; + } + + std::future> get_key_value(const std::string&) override { + return {}; + } + + std::future> set_key_value(const std::string&, const std::string&) override { + return {}; + } + + std::future> del_key_value(const std::string&) override { + return {}; + } +}; + +} // namespace + +namespace ark::detail { + +std::unique_ptr make_consul_module() { + return std::make_unique(); +} + +} // namespace ark::detail + +ARK_EXPORT_FUNC unsigned consul_entry_events(unsigned event) { + switch (event) + { + case 0: + return created; + case 1: + return destroyed; + default: + return updated; + } +} diff --git a/tests/consul/exports.cpp b/tests/consul/exports.cpp new file mode 100644 index 000000000..a5cbd76dc --- /dev/null +++ b/tests/consul/exports.cpp @@ -0,0 +1,31 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include + +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) + { + std::cerr << message << std::endl; + std::_Exit(1); + } +} + +} // namespace + +int main(int argc, char* argv[]) { + require(argc == 3, "Expected plugin directory and library stem"); + ark::logger log; + ark::logger::init(&log); + log.set_log_func([](spdlog::level::level_enum, const std::string&) {}); + ark::dynamic_library library(argv[2]); + require(library.load(argv[1]), "Cannot load actual Consul shared library"); + require(library.get_symbol("DllEntryPlugin") != nullptr, "Missing explicit DllEntryPlugin export"); + require(library.get_symbol("DllExitPlugin") != nullptr, "Missing explicit DllExitPlugin export"); + require(library.unload(), "Cannot unload Consul shared library"); + std::cout << argv[2] << ": actual DSO load, explicit entry/exit exports and unload passed\n"; +} diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt index e027b5fe2..ae1f7a9e2 100644 --- a/tests/core/CMakeLists.txt +++ b/tests/core/CMakeLists.txt @@ -222,6 +222,10 @@ if(ARK_BUILD_PROTO) add_dependencies(check-core check-kernel-initialization) endif() +if(ARK_BUILD_PROTO AND ARK_BUILD_CONFIG) + add_subdirectory(../consul "${CMAKE_CURRENT_BINARY_DIR}/consul") +endif() + if(ARK_BUILD_PROTO AND TARGET ark_kernel_plugin AND TARGET ark_utility_plugin) add_executable(ark_kernel_plugin_tests ../app/kernel_plugin.cpp) set_property(TARGET ark_kernel_plugin_tests PROPERTY LINK_LIBRARIES ark::kernel) From b794cfcdfb3c8f565f143690e17bd6c06f2ce2b2 Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 26 Oct 2026 09:59:50 +0800 Subject: [PATCH 2/4] test(consul): exercise construction across application jobs --- .github/workflows/ci.yml | 1 + docs/reference/build-options.md | 2 ++ docs/reference/build-options_CN.md | 2 ++ tests/ci/package-dependencies.test.mjs | 10 ++++++++++ tests/consul/entry_factory.cpp | 2 +- 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36bb77872..de2ec5664 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -285,6 +285,7 @@ jobs: -DARK_BUILD_APPLICATIONS=ON -DARK_BUILD_BENCHMARKS=ON -DARK_BUILD_PLUGIN_HTTP=ON + -DARK_BUILD_PLUGIN_CONSUL=ON -DARK_BUILD_PROTO=ON -DARK_BUILD_TESTS=ON -DARK_BUILD_CORE_TESTS=ON diff --git a/docs/reference/build-options.md b/docs/reference/build-options.md index 52cd11ef9..5b2a5cb37 100644 --- a/docs/reference/build-options.md +++ b/docs/reference/build-options.md @@ -41,6 +41,8 @@ cmake --build build --target ark_mysql_plugin ark_redis_plugin All official plugin public headers are copied from explicit lists at configuration time into each build tree's generated plugin header mapping. Each `ark::` API target carries that include root even with runtime plugins disabled. MySQL, Redis, Utility, Net, Bus and HTTP exclude private assembly and factories. Kernel retains all 38 existing headers, including `kernel_plugin.hpp`; Consul, Etcd Client, Kafka, OSS SDK and Physic retain all 22 existing public headers, including implementation and assembly headers. Registration keys, public type visibility and optional plugin policies are retained; [Kernel typed construction](kernel.md#typed-kernel-construction) and [Consul typed construction](../project-layout.md#typed-consul-construction) change public class layouts and require rebuilding consumers. Their private factory headers are excluded from mapping and installation; header mapping does not validate optional driver integrations. Each public header has one co-located source; its generated include mapping belongs to the selected build tree, rather than a second source layout. +Applications CI enables the optional Consul plugin on Linux, macOS and Windows. Its `check-core` run includes construction, DLL lifecycle and production export checks; these checks require no Consul service and do not establish service interoperability. + Link a consumer to `ark::mysql` and/or `ark::redis` for the public headers and usage requirements. These public interface targets do not load a runtime library. Deploy the selected libraries with the launcher and add their stems to the application's manifest: ```toml diff --git a/docs/reference/build-options_CN.md b/docs/reference/build-options_CN.md index 447194a30..5782fb155 100644 --- a/docs/reference/build-options_CN.md +++ b/docs/reference/build-options_CN.md @@ -41,6 +41,8 @@ cmake --build build --target ark_mysql_plugin ark_redis_plugin 全部官方插件的公开头通过显式清单在配置阶段复制到各构建树生成的插件公开头映射。各 `ark::` API target 传递该目录,插件关闭时仍可使用。MySQL、Redis、Utility、Net、Bus、HTTP 的私有装配头和工厂不进入映射;Kernel 保留包括 `kernel_plugin.hpp` 在内的原有 38 个头;Consul、Etcd Client、Kafka、OSS SDK、Physic 保留包括实现头与装配头在内的原有 22 个公开头。注册键、公开类型可见性和可选插件策略继续保留;[Kernel 类型化构造](kernel_CN.md#kernel-类型化构造)与 [Consul 类型化构造](../project-layout_CN.md#consul-类型化构造)改变公开类布局,需要重建消费者。两者的私有工厂头均不映射、不安装;头文件映射不代表可选驱动集成已经验证。每个公共头仅有一份共置源码;生成的头文件映射属于选定构建树,不构成第二棵源码目录。 +应用 CI 在 Linux、macOS、Windows 启用可选 Consul 插件,`check-core` 包含构造、动态库生命周期与生产库导出检查。这些检查不需要 Consul 服务,也不代表服务互通验收。 + 业务插件链接 `ark::mysql` 和/或 `ark::redis`,以获取公开头文件和编译要求;公共接口目标不会加载运行时动态库。将所需动态库与启动器一起部署,并在应用清单中填写库名称: ```toml diff --git a/tests/ci/package-dependencies.test.mjs b/tests/ci/package-dependencies.test.mjs index c4d44d354..9fee41e8e 100644 --- a/tests/ci/package-dependencies.test.mjs +++ b/tests/ci/package-dependencies.test.mjs @@ -77,3 +77,13 @@ test('unsupported PhysX and simulate integrations fail explicitly instead of sea assert.doesNotMatch(source, /3rdparty|find_package\(PhysX/); } }); + +test('Applications CI enables Consul construction and production export checks without a service', () => { + const applications = read('.github/workflows/ci.yml').split(' applications:\n')[1].split('\n database:')[0]; + assert.match(applications, /-DARK_BUILD_PLUGIN_CONSUL=ON/); + assert.match(applications, /run: cmake --build ci-build --target check-core/); + const checks = read('tests/consul/CMakeLists.txt'); + assert.match(checks, /if\(TARGET ark_consul_plugin\)/); + assert.match(checks, /add_dependencies\(check-core check-consul-plugin\)/); + assert.match(checks, /add_dependencies\(check-core check-consul-construction\)/); +}); diff --git a/tests/consul/entry_factory.cpp b/tests/consul/entry_factory.cpp index d9f92b51b..5fbf51591 100644 --- a/tests/consul/entry_factory.cpp +++ b/tests/consul/entry_factory.cpp @@ -31,7 +31,7 @@ class entry_consul final : public ark::module_base return true; } - std::future> register_service(const ark::consul::service_data&) override { + std::future> register_service(const ::consul::service_data&) override { return {}; } From 15b09018e51e2caec7a90326920928b5ff57c7f6 Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 26 Oct 2026 10:12:32 +0800 Subject: [PATCH 3/4] docs(consul): publish construction guide in canonical pages --- docs/guides/plugins.md | 2 +- docs/guides/plugins_CN.md | 2 +- docs/project-layout.md | 14 -------------- docs/project-layout_CN.md | 14 -------------- docs/reference/build-options.md | 2 +- docs/reference/build-options_CN.md | 2 +- docs/reference/directory-structure.md | 14 ++++++++++++++ docs/reference/directory-structure_CN.md | 14 ++++++++++++++ src/plugins/README.md | 4 ++-- src/plugins/README_CN.md | 4 ++-- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/guides/plugins.md b/docs/guides/plugins.md index 9c9e8f5cf..cd3fbcd5d 100644 --- a/docs/guides/plugins.md +++ b/docs/guides/plugins.md @@ -212,4 +212,4 @@ Net constructs its HTTP client through a private factory returning `std::unique_ Kernel also uses typed construction while retaining its public `*_impl` and `kernel_plugin` types and header paths. Its private factory header is not installed. Rebuild Kernel and consumers together because those public class layouts change; the [Kernel migration table](../reference/kernel.md#typed-kernel-construction) records unchanged interface keys, update selection and registration order. -Consul follows the same construction pattern while retaining all three public headers and types. Its private factory is not installed; the module keeps its interface RTTI key and disabled updates. Rebuild Consul and consumers together because its public implementation/plugin layouts change. See [Consul construction migration](../project-layout.md#typed-consul-construction) for test entry points and dependency conditions. +Consul follows the same construction pattern while retaining all three public headers and types. Its private factory is not installed; the module keeps its interface RTTI key and disabled updates. Rebuild Consul and consumers together because its public implementation/plugin layouts change. See [Consul construction migration](../reference/directory-structure.md#typed-consul-construction) for test entry points and dependency conditions. diff --git a/docs/guides/plugins_CN.md b/docs/guides/plugins_CN.md index 39ba62f2a..b66b700d4 100644 --- a/docs/guides/plugins_CN.md +++ b/docs/guides/plugins_CN.md @@ -199,4 +199,4 @@ Net 通过私有工厂返回 `std::unique_ptr`,并启用 Kernel 同样使用类型化构造,并保留公开 `*_impl`、`kernel_plugin` 类型与头路径;私有工厂头不安装。公开类布局发生变化,Kernel 与消费者需要一起重建。[Kernel 迁移表](../reference/kernel_CN.md#kernel-类型化构造)记录保持不变的接口键、update 选择与注册顺序。 -Consul 使用相同构造方式,保留全部三个公开头与类型。私有工厂不安装;模块保留接口 RTTI 键和禁用 update 的选择。公开实现/插件布局发生变化,Consul 与消费者必须一起重建。测试入口与依赖条件见 [Consul 构造迁移](../project-layout_CN.md#consul-类型化构造)。 +Consul 使用相同构造方式,保留全部三个公开头与类型。私有工厂不安装;模块保留接口 RTTI 键和禁用 update 的选择。公开实现/插件布局发生变化,Consul 与消费者必须一起重建。测试入口与依赖条件见 [Consul 构造迁移](../reference/directory-structure_CN.md#consul-类型化构造)。 diff --git a/docs/project-layout.md b/docs/project-layout.md index 79cff408b..6ce9b31b3 100644 --- a/docs/project-layout.md +++ b/docs/project-layout.md @@ -134,17 +134,3 @@ cmake --build etcd-client-headers --target check-etcd-client-headers --parallel ``` Header mapping does not establish Consul, Kafka or OSS SDK runtime compatibility. Physic and `physics_scene` still require the unsupported legacy PhysX 3 integration and retain their explicit configuration failure; no passing PhysX build is claimed. - -## Typed Consul construction - -**Breaking ABI/layout change:** `consul_module_impl` now inherits `module_base` and `consul_plugin` inherits `plugin_base`. Rebuild Consul and consumers together. All three public headers, their `` paths, public types and request methods remain available through `ark::consul`; the interface itself is unchanged. The C++ plugin boundary does not promise compiler-independent ABI compatibility. - -The private `consul_factory.hpp` returns `std::unique_ptr` and is excluded from public mappings and installation. Explicit `DllEntryPlugin` and `DllExitPlugin` retain the existing platform exports and logger setup. Installation registers the single `consul_module` interface RTTI key with `module_update::disabled`; uninstallation removes and destroys that module. The implementation still inherits the default update method. Configuration storage, HTTP-client lookup, TOML loading, request serialization and endpoints are unchanged. - -With `ARK_BUILD_CORE_TESTS=ON`, `ARK_BUILD_PROTO=ON` and `ARK_BUILD_CONFIG=ON`, `check-consul-construction` runs the actual factory/assembly doctest cases and a controlled-factory DSO test of the actual entry/exit source. These cover public bases, factory metadata, interface registration, duplicate rejection, disabled updates, configuration handoff, ownership and repeated stop. They require Protobuf, toml++ and the normal core/arknet dependencies, but no runtime plugin or Consul service. For an existing build configured with those options: - -```sh -cmake --build build --target check-consul-construction --parallel 2 -``` - -`ARK_BUILD_PLUGIN_CONSUL=ON` also requires `ARK_BUILD_APPLICATIONS=ON` and remains off by default. It links the generated protocol target `ark::proto` directly. With the core tests enabled, `check-consul-plugin` additionally inspects the actual Consul DSO exports in a separate process; `check-core` includes the applicable Consul checks. The controlled factory does not exercise the production HTTP client or a Consul server. These construction checks do not establish live Consul interoperability, protocol acceptance or arbitrary hot-update safety. diff --git a/docs/project-layout_CN.md b/docs/project-layout_CN.md index a251e8d4e..82a8ee230 100644 --- a/docs/project-layout_CN.md +++ b/docs/project-layout_CN.md @@ -124,17 +124,3 @@ cmake --build etcd-client-headers --target check-etcd-client-headers --parallel ``` 头文件映射检查不代表 Consul、Kafka、OSS SDK 的运行时兼容性已经验证。Physic 与 `physics_scene` 仍依赖未受支持的旧版 PhysX 3 集成,保留明确的配置失败;此处没有宣称 PhysX 构建通过。 - -## Consul 类型化构造 - -**ABI/布局不兼容变更:** `consul_module_impl` 现在继承 `module_base`,`consul_plugin` 继承 `plugin_base`。Consul 与消费者必须一起重建。原有三个公开头、`` 路径、公开类型与请求方法继续通过 `ark::consul` 提供,接口本身不变。C++ 插件边界不承诺跨编译器的 ABI 兼容性。 - -私有 `consul_factory.hpp` 返回 `std::unique_ptr`,不映射、不安装。显式 `DllEntryPlugin` 与 `DllExitPlugin` 保留原有平台导出和日志初始化。安装时按 `consul_module` 接口 RTTI 键注册唯一模块,显式使用 `module_update::disabled`;卸载时移除并销毁该模块。实现仍继承默认 update 方法。配置存储、HTTP 客户端查找、TOML 加载、请求序列化及端点保持不变。 - -开启 `ARK_BUILD_CORE_TESTS=ON`、`ARK_BUILD_PROTO=ON` 与 `ARK_BUILD_CONFIG=ON` 后,`check-consul-construction` 运行真实工厂/装配的 doctest 用例,以及使用受控工厂与真实入口/出口源码的 DSO 测试。检查覆盖公开基类、工厂元数据、接口注册、重复注册拒绝、禁用 update、配置传递、所有权与重复 stop。依赖 Protobuf、toml++ 及通常的 core/arknet 依赖,无需运行时插件或 Consul 服务。对于已按上述选项配置的构建: - -```sh -cmake --build build --target check-consul-construction --parallel 2 -``` - -`ARK_BUILD_PLUGIN_CONSUL=ON` 还要求 `ARK_BUILD_APPLICATIONS=ON`,默认保持关闭。插件直接链接生成协议目标 `ark::proto`。开启核心测试后,`check-consul-plugin` 还在独立进程中检查真实 Consul DSO 的导出符号;`check-core` 包含当前条件下可用的 Consul 检查。受控工厂不执行生产 HTTP 客户端或访问 Consul 服务。这些构造检查不代表真实 Consul 互通、协议验收或任意热更新安全。 diff --git a/docs/reference/build-options.md b/docs/reference/build-options.md index 5b2a5cb37..f6be18c32 100644 --- a/docs/reference/build-options.md +++ b/docs/reference/build-options.md @@ -39,7 +39,7 @@ cmake -S . -B build -DARK_BUILD_APPLICATIONS=ON \ cmake --build build --target ark_mysql_plugin ark_redis_plugin ``` -All official plugin public headers are copied from explicit lists at configuration time into each build tree's generated plugin header mapping. Each `ark::` API target carries that include root even with runtime plugins disabled. MySQL, Redis, Utility, Net, Bus and HTTP exclude private assembly and factories. Kernel retains all 38 existing headers, including `kernel_plugin.hpp`; Consul, Etcd Client, Kafka, OSS SDK and Physic retain all 22 existing public headers, including implementation and assembly headers. Registration keys, public type visibility and optional plugin policies are retained; [Kernel typed construction](kernel.md#typed-kernel-construction) and [Consul typed construction](../project-layout.md#typed-consul-construction) change public class layouts and require rebuilding consumers. Their private factory headers are excluded from mapping and installation; header mapping does not validate optional driver integrations. Each public header has one co-located source; its generated include mapping belongs to the selected build tree, rather than a second source layout. +All official plugin public headers are copied from explicit lists at configuration time into each build tree's generated plugin header mapping. Each `ark::` API target carries that include root even with runtime plugins disabled. MySQL, Redis, Utility, Net, Bus and HTTP exclude private assembly and factories. Kernel retains all 38 existing headers, including `kernel_plugin.hpp`; Consul, Etcd Client, Kafka, OSS SDK and Physic retain all 22 existing public headers, including implementation and assembly headers. Registration keys, public type visibility and optional plugin policies are retained; [Kernel typed construction](kernel.md#typed-kernel-construction) and [Consul typed construction](directory-structure.md#typed-consul-construction) change public class layouts and require rebuilding consumers. Their private factory headers are excluded from mapping and installation; header mapping does not validate optional driver integrations. Each public header has one co-located source; its generated include mapping belongs to the selected build tree, rather than a second source layout. Applications CI enables the optional Consul plugin on Linux, macOS and Windows. Its `check-core` run includes construction, DLL lifecycle and production export checks; these checks require no Consul service and do not establish service interoperability. diff --git a/docs/reference/build-options_CN.md b/docs/reference/build-options_CN.md index 5782fb155..b57abfb08 100644 --- a/docs/reference/build-options_CN.md +++ b/docs/reference/build-options_CN.md @@ -39,7 +39,7 @@ cmake -S . -B build -DARK_BUILD_APPLICATIONS=ON \ cmake --build build --target ark_mysql_plugin ark_redis_plugin ``` -全部官方插件的公开头通过显式清单在配置阶段复制到各构建树生成的插件公开头映射。各 `ark::` API target 传递该目录,插件关闭时仍可使用。MySQL、Redis、Utility、Net、Bus、HTTP 的私有装配头和工厂不进入映射;Kernel 保留包括 `kernel_plugin.hpp` 在内的原有 38 个头;Consul、Etcd Client、Kafka、OSS SDK、Physic 保留包括实现头与装配头在内的原有 22 个公开头。注册键、公开类型可见性和可选插件策略继续保留;[Kernel 类型化构造](kernel_CN.md#kernel-类型化构造)与 [Consul 类型化构造](../project-layout_CN.md#consul-类型化构造)改变公开类布局,需要重建消费者。两者的私有工厂头均不映射、不安装;头文件映射不代表可选驱动集成已经验证。每个公共头仅有一份共置源码;生成的头文件映射属于选定构建树,不构成第二棵源码目录。 +全部官方插件的公开头通过显式清单在配置阶段复制到各构建树生成的插件公开头映射。各 `ark::` API target 传递该目录,插件关闭时仍可使用。MySQL、Redis、Utility、Net、Bus、HTTP 的私有装配头和工厂不进入映射;Kernel 保留包括 `kernel_plugin.hpp` 在内的原有 38 个头;Consul、Etcd Client、Kafka、OSS SDK、Physic 保留包括实现头与装配头在内的原有 22 个公开头。注册键、公开类型可见性和可选插件策略继续保留;[Kernel 类型化构造](kernel_CN.md#kernel-类型化构造)与 [Consul 类型化构造](directory-structure_CN.md#consul-类型化构造)改变公开类布局,需要重建消费者。两者的私有工厂头均不映射、不安装;头文件映射不代表可选驱动集成已经验证。每个公共头仅有一份共置源码;生成的头文件映射属于选定构建树,不构成第二棵源码目录。 应用 CI 在 Linux、macOS、Windows 启用可选 Consul 插件,`check-core` 包含构造、动态库生命周期与生产库导出检查。这些检查不需要 Consul 服务,也不代表服务互通验收。 diff --git a/docs/reference/directory-structure.md b/docs/reference/directory-structure.md index bbb33359d..890acc45d 100644 --- a/docs/reference/directory-structure.md +++ b/docs/reference/directory-structure.md @@ -166,3 +166,17 @@ With `ARK_BUILD_APPLICATIONS=ON`, CMake builds seven server libraries: Game, Log ## Installed layout [Installation](../get-started/installation.md#install-and-consume-the-cmake-package) preserves logical `` headers under `/include/ark/`. CMake package files use the GNUInstallDirs library directory under `cmake/ark/`; selected launcher and runtime plugins install under `bin/`. Configuration samples and licensing are under `share/ark/`. Consumers use `find_package(ark CONFIG REQUIRED)` and `ark::core` / `ark::` targets; source consumers use `add_subdirectory` with the same target names. Neither requires a generated mapping path. Source/build directories are absent from installed exports; arknet and other dependencies remain external. `tests/install/` contains independent installed consumers and the disposable-producer relocation test. + +## Typed Consul construction + +**Breaking ABI/layout change:** `consul_module_impl` now inherits `module_base` and `consul_plugin` inherits `plugin_base`. Rebuild Consul and consumers together. All three public headers, their `` paths, public types and request methods remain available through `ark::consul`; the interface itself is unchanged. The C++ plugin boundary does not promise compiler-independent ABI compatibility. + +The private `consul_factory.hpp` returns `std::unique_ptr` and is excluded from public mappings and installation. Explicit `DllEntryPlugin` and `DllExitPlugin` retain the existing platform exports and logger setup. Installation registers the single `consul_module` interface RTTI key with `module_update::disabled`; uninstallation removes and destroys that module. The implementation still inherits the default update method. Configuration storage, HTTP-client lookup, TOML loading, request serialization and endpoints are unchanged. + +With `ARK_BUILD_CORE_TESTS=ON`, `ARK_BUILD_PROTO=ON` and `ARK_BUILD_CONFIG=ON`, `check-consul-construction` runs the actual factory/assembly doctest cases and a controlled-factory DSO test of the actual entry/exit source. These cover public bases, factory metadata, interface registration, duplicate rejection, disabled updates, configuration handoff, ownership and repeated stop. They require Protobuf, toml++ and the normal core/arknet dependencies, but no runtime plugin or Consul service. For an existing build configured with those options: + +```sh +cmake --build build --target check-consul-construction --parallel 2 +``` + +`ARK_BUILD_PLUGIN_CONSUL=ON` also requires `ARK_BUILD_APPLICATIONS=ON` and remains off by default. It links the generated protocol target `ark::proto` directly. With the core tests enabled, `check-consul-plugin` additionally inspects the actual Consul DSO exports in a separate process; `check-core` includes the applicable Consul checks. The controlled factory does not exercise the production HTTP client or a Consul server. These construction checks do not establish live Consul interoperability, protocol acceptance or arbitrary hot-update safety. diff --git a/docs/reference/directory-structure_CN.md b/docs/reference/directory-structure_CN.md index d83bc7044..18665fdf1 100644 --- a/docs/reference/directory-structure_CN.md +++ b/docs/reference/directory-structure_CN.md @@ -156,3 +156,17 @@ cmake --build etcd-client-headers --target check-etcd-client-headers --parallel `login/interface/login_net_module.hpp`、`login/include/login_plugin.hpp` 等源码包含路径通过构建树中的 `src/server/compat-include/` 转发头保留,内部源码使用相邻头文件。转发清单保留原有全部 28 个头文件,包括实现与插件声明,但不包含新增的私有工厂头。该兼容映射独立于公开 `` 映射,不新增可安装的服务端 API。具体类的继承链变化后,C++ 消费方需要重新编译。 设置 `ARK_BUILD_APPLICATIONS=ON` 后,CMake 编译 Game、Login、Proxy、World、Master、Dir、Router 七个服务动态库。Simulate 源码采用相同创建方式,但仍不参与默认构建,并保留未受支持的 PhysX 3/MessagePack 集成配置失败。源码共置不代表该集成已经通过验证。定向创建检查及其边界见[服务器架构](../concepts/server-architecture_CN.md#验证服务端创建方式)。 + +## Consul 类型化构造 + +**ABI/布局不兼容变更:** `consul_module_impl` 现在继承 `module_base`,`consul_plugin` 继承 `plugin_base`。Consul 与消费者必须一起重建。原有三个公开头、`` 路径、公开类型与请求方法继续通过 `ark::consul` 提供,接口本身不变。C++ 插件边界不承诺跨编译器的 ABI 兼容性。 + +私有 `consul_factory.hpp` 返回 `std::unique_ptr`,不映射、不安装。显式 `DllEntryPlugin` 与 `DllExitPlugin` 保留原有平台导出和日志初始化。安装时按 `consul_module` 接口 RTTI 键注册唯一模块,显式使用 `module_update::disabled`;卸载时移除并销毁该模块。实现仍继承默认 update 方法。配置存储、HTTP 客户端查找、TOML 加载、请求序列化及端点保持不变。 + +开启 `ARK_BUILD_CORE_TESTS=ON`、`ARK_BUILD_PROTO=ON` 与 `ARK_BUILD_CONFIG=ON` 后,`check-consul-construction` 运行真实工厂/装配的 doctest 用例,以及使用受控工厂与真实入口/出口源码的 DSO 测试。检查覆盖公开基类、工厂元数据、接口注册、重复注册拒绝、禁用 update、配置传递、所有权与重复 stop。依赖 Protobuf、toml++ 及通常的 core/arknet 依赖,无需运行时插件或 Consul 服务。对于已按上述选项配置的构建: + +```sh +cmake --build build --target check-consul-construction --parallel 2 +``` + +`ARK_BUILD_PLUGIN_CONSUL=ON` 还要求 `ARK_BUILD_APPLICATIONS=ON`,默认保持关闭。插件直接链接生成协议目标 `ark::proto`。开启核心测试后,`check-consul-plugin` 还在独立进程中检查真实 Consul DSO 的导出符号;`check-core` 包含当前条件下可用的 Consul 检查。受控工厂不执行生产 HTTP 客户端或访问 Consul 服务。这些构造检查不代表真实 Consul 互通、协议验收或任意热更新安全。 diff --git a/src/plugins/README.md b/src/plugins/README.md index 66e429e98..bb41192b6 100755 --- a/src/plugins/README.md +++ b/src/plugins/README.md @@ -20,12 +20,12 @@ All official plugins co-locate their headers, implementation and assembly under Use `ARK_BUILD_PLUGIN_` to select a plugin. Optional integrations require their own dependencies and verification. [MySQL](../../docs/reference/mysql.md) provides queries, prepared statements and transactions; [Redis](../../docs/reference/redis.md) provides commands and ordered pipelines. Both use named configuration and manager-owned coroutine tasks. See [database plugin activation](../../docs/reference/build-options.md#native-database-plugins) for build and runtime selection. Lua and Actor integrations remain planned work. -See [project layout and public plugin interfaces](../../docs/project-layout.md) for targets and build commands. +See [project layout and public plugin interfaces](../../docs/reference/directory-structure.md) for targets and build commands. [Utility](../../docs/reference/utility.md) documents the four module interfaces, owner-driven timers and the implementation-header migration. Kernel co-locates its 38 existing headers with its cpp files in `src/plugins/kernel/`. `ark::kernel` preserves every `` path, including `data_impl.hpp`, `data_list_impl.hpp` and `kernel_plugin.hpp`. The retained public implementation/plugin types use typed construction with private owning factories and explicit entry points. `kernel_factory.hpp` is neither mapped nor installed. The public class layouts change, so rebuild Kernel and consumers together; registration keys, update selection and lifecycle order are preserved. See [Kernel construction migration](../../docs/reference/kernel.md#typed-kernel-construction) for compatibility boundaries, consumption checks and Protobuf requirements. -Consul retains its three public headers and implementation/plugin types while using typed construction, a private owning factory and explicit entry points. Rebuild Consul and consumers together because the public class layouts change. The single interface registration key, disabled updates and request behavior are preserved. See [Consul construction migration](../../docs/project-layout.md#typed-consul-construction) for build conditions and service-free construction checks. +Consul retains its three public headers and implementation/plugin types while using typed construction, a private owning factory and explicit entry points. Rebuild Consul and consumers together because the public class layouts change. The single interface registration key, disabled updates and request behavior are preserved. See [Consul construction migration](../../docs/reference/directory-structure.md#typed-consul-construction) for build conditions and service-free construction checks. The other four legacy plugin families retain 19 byte-identical headers. Mapping checks do not validate Consul, Kafka or OSS SDK runtime integrations. Physic retains the unsupported PhysX 3 configuration failure; see [optional legacy dependencies](../../docs/dependencies.md#optional-legacy-integrations). diff --git a/src/plugins/README_CN.md b/src/plugins/README_CN.md index 82c42abd5..bed2c6eb9 100644 --- a/src/plugins/README_CN.md +++ b/src/plugins/README_CN.md @@ -20,12 +20,12 @@ 使用 `ARK_BUILD_PLUGIN_<名称大写>` 选择插件。可选插件需要各自的依赖和验证。[MySQL](../../docs/reference/mysql_CN.md) 提供查询、预处理语句和事务;[Redis](../../docs/reference/redis_CN.md) 提供命令和有序流水线。两者均通过具名配置和管理器托管的协程任务使用。构建与运行时选择见[数据库插件启用方式](../../docs/reference/build-options_CN.md#原生数据库插件)。Lua、Actor 的整合仍在规划中。 -CMake target 和构建命令见[项目布局与公共插件接口](../../docs/project-layout_CN.md)。 +CMake target 和构建命令见[项目布局与公共插件接口](../../docs/reference/directory-structure_CN.md)。 [Utility 参考](../../docs/reference/utility_CN.md)说明四个模块接口、所属线程驱动的定时器与实现头迁移。 Kernel 的原有 38 个头与 cpp 共置于 `src/plugins/kernel/`,通过 `ark::kernel` 保留全部 `` 路径,包括 `data_impl.hpp`、`data_list_impl.hpp` 和 `kernel_plugin.hpp`。保留的公开实现类型与插件类型使用类型化构造、私有拥有型工厂和显式入口。`kernel_factory.hpp` 不映射、不安装。公开类布局发生变化,Kernel 与消费者必须一起重建;注册键、update 选择和生命周期顺序保持不变。兼容边界、消费检查及 Protobuf 条件见 [Kernel 构造迁移](../../docs/reference/kernel_CN.md#kernel-类型化构造)。 -Consul 保留原有三个公开头及实现/插件类型,同时使用类型化构造、私有拥有型工厂和显式入口。公开类布局发生变化,Consul 与消费者必须一起重建。唯一接口注册键、禁用 update 与请求行为保持不变。构建条件及无需服务的构造检查见 [Consul 构造迁移](../../docs/project-layout_CN.md#consul-类型化构造)。 +Consul 保留原有三个公开头及实现/插件类型,同时使用类型化构造、私有拥有型工厂和显式入口。公开类布局发生变化,Consul 与消费者必须一起重建。唯一接口注册键、禁用 update 与请求行为保持不变。构建条件及无需服务的构造检查见 [Consul 构造迁移](../../docs/reference/directory-structure_CN.md#consul-类型化构造)。 其他四类旧插件保留原有 19 个头的全部字节。映射检查不代表 Consul、Kafka、OSS SDK 的运行时集成已经验证。Physic 保留未受支持的 PhysX 3 配置失败;参见[可选旧集成依赖](../../docs/dependencies_CN.md)。 From 096da12e0b6c5b519f338c00fc2ae33ebda66e25 Mon Sep 17 00:00:00 2001 From: NickYang <3690738+nick2781@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:41:39 +0000 Subject: [PATCH 4/4] fix(hash)!: standardize portable MurmurHash results (#155) MurmurHash3 previously produced different full-block results on ordinary MSVC and little-endian GCC/Clang. Standardize its default block interpretation and digest bytes to little endian, replace manual rotations with C++20 `std::rotl`, and make input/output transfers safe for byte-aligned buffers. Three explicit `_legacy_msvc` entries reproduce the previous MSVC results for application migration. Existing signatures and static/member behavior remain. English/Chinese documentation covers old/new mappings, persisted-key dual reads/new writes and coordinated routing migration. The built-in `consistent_hash` uses MurmurHash2 and retains its little-endian results. Local macOS acceptance: all 62 core cases / 69,857 assertions pass normally and under AddressSanitizer + UndefinedBehaviorSanitizer (leak detection disabled). Tests cover upstream SMHasher verification constants, independently derived legacy fixtures, all seven methods, seeds, tails, high-bit bytes, exact-size and unaligned buffers. Documentation passes seven tests and checks 137 pages / 273 references; actual bilingual utility articles and Markdown exports were inspected. Real MSVC/Linux acceptance remains with fresh CI and the protected full merge candidate. BREAKING CHANGE: default MurmurHash3 results change for the former MSVC block policy. Rebuild consumers and explicitly migrate affected persisted keys/routing; use the legacy entries during migration. The library does not migrate application data automatically. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/reference/utility.md | 78 +++++++++ docs/reference/utility_CN.md | 59 +++++++ src/core/hash/murmur_hash.hpp | 168 ++++++++++++------- tests/core/CMakeLists.txt | 4 +- tests/core/README.md | 10 ++ tests/core/README_CN.md | 8 + tests/core/headers/murmur_hash.cpp | 18 +++ tests/core/murmur_hash.cpp | 248 +++++++++++++++++++++++++++++ 8 files changed, 534 insertions(+), 59 deletions(-) create mode 100644 tests/core/headers/murmur_hash.cpp create mode 100644 tests/core/murmur_hash.cpp diff --git a/docs/reference/utility.md b/docs/reference/utility.md index f468f5e16..6be8ae7a6 100644 --- a/docs/reference/utility.md +++ b/docs/reference/utility.md @@ -64,6 +64,84 @@ Public headers now have their single source in `src/plugins/utility/`. CMake cop Range selection retains the existing modulo mapping, seed sequence and draw count. Each call with different endpoints consumes one draw; reversing the endpoints produces the same sequence from the same seed. The unsigned overloads retain their existing behavior. +## Core Murmur hashes + +`` provides MurmurHash2 and the x86-32, x86-128 and +x64-128 MurmurHash3 variants through `ark::core`. Supply a nonnegative `int` +length, a readable input buffer (also for an empty input), and writable output +storage of 4 bytes for 32-bit variants or 16 bytes for 128-bit variants. Input +and output may have any byte alignment; word loads and stores use `std::memcpy`. +MurmurHash3 uses C++20 `std::rotl` with the original rotation counts. Seeds, +tail mixing, result word order and the existing public signatures are unchanged; +`murmur_hash3_x86_128` remains an instance method, and the other variants remain +static. MurmurHash2 retains its fixed seed zero. + +### Digest compatibility and legacy access + +**Breaking change:** default MurmurHash3 now reads full blocks as little endian +and writes canonical little-endian digest bytes on every supported platform. +This retains existing little-endian GCC/Clang results and changes the former +ordinary MSVC full-block policy. The former platform detection did not select +MSVC's `_M_IX86`, `_M_X64` or `_M_ARM64` macros +([Microsoft predefined macros](https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros)); +it fell through to an unconditional byte swap on a little-endian host. +The public `BYTESWAP32` and `BYTESWAP64` macros remain available, but no longer +control these hash methods. Callers must rebuild to use the new header behavior. + +| Method family | Default result | Previous ordinary MSVC result | +| --- | --- | --- | +| MurmurHash3 x86-32 | `murmur_hash3_x86_32` | `murmur_hash3_x86_32_legacy_msvc` | +| MurmurHash3 x86-128 | `murmur_hash3_x86_128` (instance method) | `murmur_hash3_x86_128_legacy_msvc` (instance method) | +| MurmurHash3 x64-128 | `murmur_hash3_x64_128` | `murmur_hash3_x64_128_legacy_msvc` | +| MurmurHash2 | `murmur_hash2`; unchanged on little-endian platforms | Same method; no separate legacy route is needed | + +The three `_legacy_msvc` methods reproduce the old MSVC result on any supported +platform: full blocks are read as big endian, tails are assembled as little +endian, and output words are serialized as little endian. Pass the original +variant, exact input bytes, length and seed. The policies agree for inputs shorter +than a full block (4 bytes for x86-32, 16 bytes for either 128-bit variant). +For bytes `00 01 02 03` and seed zero, x86-32 produces numeric word `0xf4c0ec39` +by default and `0x2cb313f3` through the legacy entry. A previous big-endian +consumer must also account for the change from native-endian output to canonical +little-endian bytes; `_legacy_msvc` specifically reproduces old MSVC behavior. + +### Migrate stored keys and routing + +The built-in `ark::consistent_hash` currently uses MurmurHash2, whose results remain unchanged on little-endian platforms. The routing guidance below applies to consumers that derive their ring or shard keys from the affected MurmurHash3 policy. + +Identify each dataset's hash variant, seed, input encoding and previous compiler +policy before upgrading. Keep that policy with the data or in an explicit schema +version. A digest alone cannot reconstruct the original key. IDs, shard mappings, +cache keys and persisted indexes derived from old MSVC hashes require an explicit +migration; replacing the function and silently recomputing routing is unsafe. + +For lookup keys, use a staged **dual-read / new-write** rollout: write new records +with the standard policy and a version tag, read that key first, and fall back to +the matching `_legacy_msvc` key for older records. Compare the original logical +key when resolving a hash hit or collision. Backfill or move old entries using +transactional/atomic application rules, define duplicate handling, and retain +legacy reads until inventory and rollback requirements permit their removal. +Old readers must understand new records before new-only writes begin. The library +does not migrate application records automatically. + +For consistent hashing or shard ownership, keep all routers on one explicit ring +version while planning the transition. Compute the new ring from authoritative +node identities, compare placement, copy/rebalance affected data, and coordinate +the routing epoch and ownership cutover across participants. Keep the legacy +ring and legacy hash entry until that transfer is accepted. Mixed old/new rings +during an ordinary rolling binary update can route the same key differently; +a dual lookup alone does not migrate ownership. + +`check-core` includes the [upstream SMHasher verification values](https://github.com/aappleby/smhasher/blob/master/src/main.cpp#L67-L70) +with its [prefix/seed procedure](https://github.com/aappleby/smhasher/blob/master/src/KeysetTest.cpp#L11-L38), +plus separate scalar-reference fixtures for the legacy MSVC policy. Cases cover +all seven public methods, every MurmurHash3 tail length, full-width seeds, +high-bit input bytes and rotation paths, MurmurHash2 boundary vectors, and +exact-size byte buffers at offsets 0 through 7. AddressSanitizer can detect +allocation-end overreads; UndefinedBehaviorSanitizer can detect invalid alignment. +The fixtures establish hash behavior; application data migration needs its own +acceptance evidence. + ## Verification `check-typed-examples` loads the actual Utility DSO with `timers`, `module_lifecycle` and `entity_operations`. It verifies all four interface RTTI keys and manager ownership, GUID generation/parsing, zlib/Zstandard binary round trips, a timer triggered by owner-thread time advancement, and normal/idempotent shutdown. `check-app-presets` exercises the shipped launcher preset; `check-core` includes both when applications, examples, Kernel and Utility are enabled. The independent `tests/consumer` project's `check-sdk` compiles public/helper paths with runtime plugins disabled and rejects visible implementation or assembly headers. These are correctness checks, not performance or arbitrary callback-lifetime acceptance. diff --git a/docs/reference/utility_CN.md b/docs/reference/utility_CN.md index 933c8f645..747cec544 100644 --- a/docs/reference/utility_CN.md +++ b/docs/reference/utility_CN.md @@ -58,6 +58,65 @@ zlib、LZ4 和 Zstandard 普通调用不再共享可写输出缓冲区。Zstanda 范围选择保留现有的取模映射、种子序列和生成次数。端点不同的每次调用消耗一个随机数;使用相同种子并交换端点,得到相同序列。无符号重载的行为保持不变。 +## Core Murmur 哈希 + +`` 通过 `ark::core` 提供 MurmurHash2,以及 MurmurHash3 +的 x86-32、x86-128 和 x64-128 变体。调用方须提供非负 `int` 长度、可读输入缓冲区 +(空输入也需要有效缓冲区),以及可写输出存储:32 位变体需要 4 字节,128 位变体需要 +16 字节。输入和输出允许任意字节对齐,字读取和写入使用 `std::memcpy`。 +MurmurHash3 使用 C++20 `std::rotl`,旋转次数保持不变。种子、尾部混合、结果字顺序和 +原有公开签名不变;`murmur_hash3_x86_128` 仍为实例方法,其余变体仍为静态方法。 +MurmurHash2 保留固定种子零。 + +### 摘要兼容性与旧结果入口 + +**不兼容变更:** 默认 MurmurHash3 现在在所有支持的平台上按小端方式读取完整块, +并输出规范的小端摘要字节。这保留原有小端 GCC/Clang 结果,改变普通 MSVC 的旧版完整块策略。 +旧版平台检测未选择 MSVC 的 `_M_IX86`、`_M_X64` 或 `_M_ARM64` 宏 +(见 [Microsoft 预定义宏](https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros)), +导致小端主机进入无条件字节交换回退分支。公开宏 `BYTESWAP32` 和 `BYTESWAP64` +仍然保留,但不再控制这些哈希方法。调用方须重新编译,才能使用新的头文件行为。 + +| 方法族 | 默认结果 | 普通 MSVC 的旧结果 | +| --- | --- | --- | +| MurmurHash3 x86-32 | `murmur_hash3_x86_32` | `murmur_hash3_x86_32_legacy_msvc` | +| MurmurHash3 x86-128 | `murmur_hash3_x86_128`(实例方法) | `murmur_hash3_x86_128_legacy_msvc`(实例方法) | +| MurmurHash3 x64-128 | `murmur_hash3_x64_128` | `murmur_hash3_x64_128_legacy_msvc` | +| MurmurHash2 | `murmur_hash2`;在小端平台上保持不变 | 使用同一方法,无需独立旧版入口 | + +三个 `_legacy_msvc` 方法在所有支持的平台上复现旧 MSVC 结果:完整块按大端方式读取, +尾部按小端方式组装,输出字按小端方式序列化。调用时须使用原有变体、准确输入字节、 +长度和种子。输入短于完整块时,两种策略结果相同:x86-32 为不足 4 字节, +两个 128 位变体为不足 16 字节。对于字节 `00 01 02 03` 和种子零,x86-32 默认产生 +数值字 `0xf4c0ec39`,旧版入口产生 `0x2cb313f3`。旧版大端使用方还须处理输出从 +原生字节序变为规范小端字节序的变化;`_legacy_msvc` 专用于复现旧 MSVC 行为。 + +### 迁移已存储键与路由 + +框架内置 `ark::consistent_hash` 当前使用 MurmurHash2,在小端平台上的结果保持不变。以下路由迁移说明适用于使用受影响 MurmurHash3 策略构建环或分片键的调用方。 + +升级前,先确认每个数据集的哈希变体、种子、输入编码和旧编译器策略,并在数据中或 +显式 schema 版本中记录该策略。单独的摘要无法还原原始键。由旧 MSVC 哈希派生的 ID、 +分片映射、缓存键和持久化索引需要显式迁移,不能替换函数后直接重算路由。 + +对于查找键,分阶段采用**双读、新写**:新记录使用标准策略并携带版本标记;读取时 +先查新键,未命中再查匹配的 `_legacy_msvc` 旧键。解析命中或哈希冲突时,核对原始逻辑键。 +按应用的事务或原子操作规则回填或移动旧记录,明确重复记录的处理方式;确认存量清理和 +回滚要求后,再移除旧读路径。开始只写新格式前,旧读程序须能理解新记录。 +库本身不会自动迁移应用记录。 + +对于一致性哈希或分片归属,规划迁移时保持所有路由器使用同一个显式环版本。 +从权威节点标识构建新环,比较分配结果,复制或重平衡受影响数据,再协调各参与方的 +路由 epoch 与归属切换。数据转移验收前,保留旧环和旧哈希入口。普通二进制滚动升级中 +混用新旧环,可能把同一键路由到不同位置;仅增加双查不会完成归属迁移。 + +`check-core` 纳入[上游 SMHasher 验证值](https://github.com/aappleby/smhasher/blob/master/src/main.cpp#L67-L70) +及其[前缀和种子验证过程](https://github.com/aappleby/smhasher/blob/master/src/KeysetTest.cpp#L11-L38), +并为旧 MSVC 策略保留独立的标量参考固定值。用例覆盖全部七个公开方法、MurmurHash3 的 +全部尾部长度、完整宽度种子、高位字节和旋转路径、MurmurHash2 块边界固定值,以及 +偏移 0 到 7 的精确分配字节缓冲区。AddressSanitizer 可检查分配末端越界读取, +UndefinedBehaviorSanitizer 可检查无效对齐。固定值验证哈希行为;应用数据迁移仍须独立验收。 + ## 验证 `check-typed-examples` 与 timers、module_lifecycle、entity_operations 一起加载真实 Utility DSO,验证四个接口的 RTTI 注册键与管理器归属、GUID 生成与解析、zlib/Zstandard 二进制往返、由所属线程时间推进触发的定时器,以及正常和重复停机。`check-app-presets` 使用仓库启动器预设;应用、示例、Kernel、Utility 开启时,`check-core` 包含这两组检查。独立 `tests/consumer` 项目的 `check-sdk` 在关闭运行时插件时编译公开接口与辅助头路径,并拒绝可见的实现或装配头。这些检查验证正确性,不代表性能或任意回调生命周期验收。 diff --git a/src/core/hash/murmur_hash.hpp b/src/core/hash/murmur_hash.hpp index 128e42bcf..20fbdc7fd 100644 --- a/src/core/hash/murmur_hash.hpp +++ b/src/core/hash/murmur_hash.hpp @@ -22,6 +22,12 @@ #pragma once +#include +#include +#include +#include +#include + #include "ark/macros.hpp" #include "ark/platform.hpp" @@ -31,6 +37,33 @@ class murmur_hash { public: static void murmur_hash3_x86_32(const void* key, int len, uint32_t seed, void* out) { + murmur_hash3_x86_32_impl(key, len, seed, out); + } + + void murmur_hash3_x86_128(const void* key, const int len, uint32_t seed, void* out) { + murmur_hash3_x86_128_impl(key, len, seed, out); + } + + static void murmur_hash3_x64_128(const void* key, const int len, const uint32_t seed, void* out) { + murmur_hash3_x64_128_impl(key, len, seed, out); + } + + // Migration-only access to hashes produced by the former MSVC block policy. + static void murmur_hash3_x86_32_legacy_msvc(const void* key, int len, uint32_t seed, void* out) { + murmur_hash3_x86_32_impl(key, len, seed, out); + } + + void murmur_hash3_x86_128_legacy_msvc(const void* key, const int len, uint32_t seed, void* out) { + murmur_hash3_x86_128_impl(key, len, seed, out); + } + + static void murmur_hash3_x64_128_legacy_msvc(const void* key, const int len, const uint32_t seed, void* out) { + murmur_hash3_x64_128_impl(key, len, seed, out); + } + +private: + template + static void murmur_hash3_x86_32_impl(const void* key, int len, uint32_t seed, void* out) { const uint8_t* data = (const uint8_t*)key; const int nblocks = len / 4; @@ -42,18 +75,18 @@ class murmur_hash //---------- // body - const uint32_t* blocks = (const uint32_t*)(data + nblocks * 4); + const uint8_t* blocks = data + nblocks * 4; for (int i = -nblocks; i; i++) { - uint32_t k1 = getblock(blocks, i); + uint32_t k1 = getblock(blocks, i); k1 *= c1; - k1 = rotl32(k1, 15); + k1 = std::rotl(k1, 15); k1 *= c2; h1 ^= k1; - h1 = rotl32(h1, 13); + h1 = std::rotl(h1, 13); h1 = h1 * 5 + 0xe6546b64; } @@ -73,7 +106,7 @@ class murmur_hash case 1: k1 ^= tail[0]; k1 *= c1; - k1 = rotl32(k1, 15); + k1 = std::rotl(k1, 15); k1 *= c2; h1 ^= k1; }; @@ -85,12 +118,13 @@ class murmur_hash h1 = fmix(h1); - *(uint32_t*)out = h1; + store_word(out, h1); } //----------------------------------------------------------------------------- - void murmur_hash3_x86_128(const void* key, const int len, uint32_t seed, void* out) { + template + static void murmur_hash3_x86_128_impl(const void* key, const int len, uint32_t seed, void* out) { const uint8_t* data = (const uint8_t*)key; const int nblocks = len / 16; @@ -107,48 +141,48 @@ class murmur_hash //---------- // body - const uint32_t* blocks = (const uint32_t*)(data + nblocks * 16); + const uint8_t* blocks = data + nblocks * 16; for (int i = -nblocks; i; i++) { - uint32_t k1 = getblock(blocks, i * 4 + 0); - uint32_t k2 = getblock(blocks, i * 4 + 1); - uint32_t k3 = getblock(blocks, i * 4 + 2); - uint32_t k4 = getblock(blocks, i * 4 + 3); + uint32_t k1 = getblock(blocks, i * 4 + 0); + uint32_t k2 = getblock(blocks, i * 4 + 1); + uint32_t k3 = getblock(blocks, i * 4 + 2); + uint32_t k4 = getblock(blocks, i * 4 + 3); k1 *= c1; - k1 = rotl32(k1, 15); + k1 = std::rotl(k1, 15); k1 *= c2; h1 ^= k1; - h1 = rotl32(h1, 19); + h1 = std::rotl(h1, 19); h1 += h2; h1 = h1 * 5 + 0x561ccd1b; k2 *= c2; - k2 = rotl32(k2, 16); + k2 = std::rotl(k2, 16); k2 *= c3; h2 ^= k2; - h2 = rotl32(h2, 17); + h2 = std::rotl(h2, 17); h2 += h3; h2 = h2 * 5 + 0x0bcaa747; k3 *= c3; - k3 = rotl32(k3, 17); + k3 = std::rotl(k3, 17); k3 *= c4; h3 ^= k3; - h3 = rotl32(h3, 15); + h3 = std::rotl(h3, 15); h3 += h4; h3 = h3 * 5 + 0x96cd1c35; k4 *= c4; - k4 = rotl32(k4, 18); + k4 = std::rotl(k4, 18); k4 *= c1; h4 ^= k4; - h4 = rotl32(h4, 13); + h4 = std::rotl(h4, 13); h4 += h1; h4 = h4 * 5 + 0x32ac3b17; } @@ -172,7 +206,7 @@ class murmur_hash case 13: k4 ^= tail[12] << 0; k4 *= c4; - k4 = rotl32(k4, 18); + k4 = std::rotl(k4, 18); k4 *= c1; h4 ^= k4; @@ -185,7 +219,7 @@ class murmur_hash case 9: k3 ^= tail[8] << 0; k3 *= c3; - k3 = rotl32(k3, 17); + k3 = std::rotl(k3, 17); k3 *= c4; h3 ^= k3; @@ -198,7 +232,7 @@ class murmur_hash case 5: k2 ^= tail[4] << 0; k2 *= c2; - k2 = rotl32(k2, 16); + k2 = std::rotl(k2, 16); k2 *= c3; h2 ^= k2; @@ -211,7 +245,7 @@ class murmur_hash case 1: k1 ^= tail[0] << 0; k1 *= c1; - k1 = rotl32(k1, 15); + k1 = std::rotl(k1, 15); k1 *= c2; h1 ^= k1; }; @@ -243,13 +277,12 @@ class murmur_hash h3 += h1; h4 += h1; - ((uint32_t*)out)[0] = h1; - ((uint32_t*)out)[1] = h2; - ((uint32_t*)out)[2] = h3; - ((uint32_t*)out)[3] = h4; + const uint32_t result[]{h1, h2, h3, h4}; + store_words(out, result); } - static void murmur_hash3_x64_128(const void* key, const int len, const uint32_t seed, void* out) { + template + static void murmur_hash3_x64_128_impl(const void* key, const int len, const uint32_t seed, void* out) { const uint8_t* data = (const uint8_t*)key; const int nblocks = len / 16; @@ -262,28 +295,28 @@ class murmur_hash //---------- // body - const uint64_t* blocks = (const uint64_t*)(data); + const uint8_t* blocks = data; for (int i = 0; i < nblocks; i++) { - uint64_t k1 = getblock(blocks, i * 2 + 0); - uint64_t k2 = getblock(blocks, i * 2 + 1); + uint64_t k1 = getblock(blocks, i * 2 + 0); + uint64_t k2 = getblock(blocks, i * 2 + 1); k1 *= c1; - k1 = rotl64(k1, 31); + k1 = std::rotl(k1, 31); k1 *= c2; h1 ^= k1; - h1 = rotl64(h1, 27); + h1 = std::rotl(h1, 27); h1 += h2; h1 = h1 * 5 + 0x52dce729; k2 *= c2; - k2 = rotl64(k2, 33); + k2 = std::rotl(k2, 33); k2 *= c1; h2 ^= k2; - h2 = rotl64(h2, 31); + h2 = std::rotl(h2, 31); h2 += h1; h2 = h2 * 5 + 0x38495ab5; } @@ -313,7 +346,7 @@ class murmur_hash case 9: k2 ^= uint64_t(tail[8]) << 0; k2 *= c2; - k2 = rotl64(k2, 33); + k2 = std::rotl(k2, 33); k2 *= c1; h2 ^= k2; @@ -334,7 +367,7 @@ class murmur_hash case 1: k1 ^= uint64_t(tail[0]) << 0; k1 *= c1; - k1 = rotl64(k1, 31); + k1 = std::rotl(k1, 31); k1 *= c2; h1 ^= k1; }; @@ -354,10 +387,11 @@ class murmur_hash h1 += h2; h2 += h1; - ((uint64_t*)out)[0] = h1; - ((uint64_t*)out)[1] = h2; + const uint64_t result[]{h1, h2}; + store_words(out, result); } +public: static void murmur_hash2(const void* key, int len, void* out) { uint32_t h, k; @@ -398,32 +432,52 @@ class murmur_hash h *= 0x5bd1e995; h ^= h >> 15; - *(uint32_t*)out = h; + store_word(out, h); } private: - inline static uint32_t rotl32(uint32_t x, int8_t r) { -#ifdef ARK_PLATFORM_WIN - return _rotl(x, r); -#else - return (x << r) | (x >> (32 - r)); -#endif + template + static word_type byte_swap(word_type value) { + static_assert(std::is_unsigned_v); + word_type result = 0; + for (std::size_t byte = 0; byte < sizeof(word_type); ++byte) + { + result = (result << 8) | (value & 0xff); + value >>= 8; + } + return result; } - inline static uint64_t rotl64(uint64_t x, int8_t r) { -#ifdef ARK_PLATFORM_WIN - return _rotl64(x, r); -#else - return (x << r) | (x >> (64 - r)); -#endif + template + static word_type getblock(const uint8_t* p, int i) { + // Retain byte-addressed arithmetic even for the backwards x86 block walk. + word_type value; + std::memcpy(&value, p + i * static_cast(sizeof(word_type)), sizeof(value)); + // Standard blocks are little endian; old MSVC blocks were byte-swapped + // on a little-endian host. Legacy tails and output remain little endian. + if constexpr ((legacy_msvc && std::endian::native == std::endian::little) || + (!legacy_msvc && std::endian::native == std::endian::big)) + { + value = byte_swap(value); + } + return value; } - static uint32_t getblock(const uint32_t* p, int i) { - return BYTESWAP32(p[i]); + template + static void store_word(void* out, word_type value) { + if constexpr (std::endian::native == std::endian::big) + { + value = byte_swap(value); + } + std::memcpy(out, &value, sizeof(value)); } - static uint64_t getblock(const uint64_t* p, int i) { - return BYTESWAP64(p[i]); + template + static void store_words(void* out, const word_type (&words)[count]) { + for (std::size_t index = 0; index < count; ++index) + { + store_word(static_cast(out) + index * sizeof(word_type), words[index]); + } } static uint32_t fmix(uint32_t h) { diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt index e027b5fe2..a327d475d 100644 --- a/tests/core/CMakeLists.txt +++ b/tests/core/CMakeLists.txt @@ -1,12 +1,12 @@ add_library(ark_core_header_checks OBJECT headers/queue.cpp headers/guid128.cpp headers/rw_lock.cpp headers/array_map.cpp headers/bit_value.cpp headers/consistent_hash.cpp headers/list.cpp headers/lock_free_queue.cpp headers/lru_map.cpp headers/map.cpp headers/thread_safe_queue.cpp - headers/object_vector.cpp headers/error_code.cpp headers/logger.cpp) + headers/object_vector.cpp headers/error_code.cpp headers/logger.cpp headers/murmur_hash.cpp) target_link_libraries(ark_core_header_checks PRIVATE ark::core) add_executable(ark_core_tests main.cpp strings.cpp containers.cpp templates.cpp callbacks.cpp header_regressions.cpp cxx20_language.cpp concurrency.cpp plugin_manager_startup.cpp data_strings.cpp node_data_storage.cpp - object_vectors.cpp plugin_construction.cpp module_factory.cpp random_generator.cpp xxtea.cpp) + object_vectors.cpp plugin_construction.cpp module_factory.cpp random_generator.cpp xxtea.cpp murmur_hash.cpp) target_link_libraries(ark_core_tests PRIVATE ark::core ark::kernel ark::utility) add_dependencies(ark_core_tests ark_core_header_checks) add_library(ark_construction_header_checks OBJECT headers/module_base.cpp headers/plugin_base.cpp) diff --git a/tests/core/README.md b/tests/core/README.md index 63864e4cd..d57d4a947 100644 --- a/tests/core/README.md +++ b/tests/core/README.md @@ -20,6 +20,16 @@ The public header checks include `` as the first include and include it twice. Compile-time checks preserve its unsigned 32-bit underlying type and every declared error value. + +Murmur regressions include independent upstream SMHasher verification constants, +legacy swapped-block fixtures, all tail lengths, full-width seeds and the original +rotation paths. They also check MurmurHash2 golden words, exact-size input +allocations and input/output offsets 0 through 7 with output guard bytes. +Run the core executable with `--test-case="*Murmur*"` to select these cases. +See [Murmur compatibility](../../docs/reference/utility.md#core-murmur-hashes) +for canonical little-endian output and the explicit legacy MSVC migration path. AddressSanitizer and +UndefinedBehaviorSanitizer builds exercise overread and alignment checks. + With `ARK_BUILD_PROTO=ON`, additional cases verify Protobuf parse failures, actor identity, binary messages, legacy wire payloads and external JSON field names. They also exercise the new `ark::protobuf` descriptor and enum names. diff --git a/tests/core/README_CN.md b/tests/core/README_CN.md index c33ba20d4..12571eea9 100644 --- a/tests/core/README_CN.md +++ b/tests/core/README_CN.md @@ -19,6 +19,14 @@ C++20 语言回归验证六种精确整数类型分类、限定符与引用处 随机数回归将公开头文件放在首个包含位置,并重复包含两次。测试保留固定的原始、有符号及无符号随机序列、最终种子和生成次数,并验证 `INT_MIN`/`INT_MAX` 极值、异号、相邻、逆序及相等端点的有符号半开区间。端点相同时直接返回该值,不消耗随机数。这些用例属于 `ark_core_tests` 和 `check-core`;使用 `-fsanitize=undefined -fno-sanitize-recover=all` 构建时,还会通过 UndefinedBehaviorSanitizer 检查范围运算。参见[随机数工具行为](../../docs/reference/utility_CN.md#core-随机数工具)。 + +Murmur 回归包含独立的上游 SMHasher 验证常量、旧版完整块交换策略固定值、全部尾部长度、 +完整宽度种子和原有旋转路径。用例还验证 MurmurHash2 固定值、精确分配的输入缓冲区、 +输入和输出偏移 0 到 7,以及输出保护字节。运行核心测试程序时,通过 +`--test-case="*Murmur*"` 选择这些用例。规范小端输出和显式旧 MSVC 迁移入口见 +[Murmur 兼容性说明](../../docs/reference/utility_CN.md#core-murmur-哈希)。 +AddressSanitizer 和 UndefinedBehaviorSanitizer 构建可检查越界读取和无效对齐。 + 公共头检查将 `` 放在首个包含位置,并重复包含两次;编译期断言 验证其无符号 32 位底层类型及所有已声明的错误值。 diff --git a/tests/core/headers/murmur_hash.cpp b/tests/core/headers/murmur_hash.cpp new file mode 100644 index 000000000..87c28955a --- /dev/null +++ b/tests/core/headers/murmur_hash.cpp @@ -0,0 +1,18 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include + +// Keep the second inclusion separate to retain the include-guard check. +#include + +void murmur_hash_header_check() { + const uint8_t key[16]{}; + uint8_t output[16]{}; + ark::murmur_hash hash; + hash.murmur_hash3_x86_32(key, 16, 0, output); + hash.murmur_hash3_x86_128(key, 16, 0, output); + hash.murmur_hash3_x64_128(key, 16, 0, output); + hash.murmur_hash3_x86_32_legacy_msvc(key, 16, 0, output); + hash.murmur_hash3_x86_128_legacy_msvc(key, 16, 0, output); + hash.murmur_hash3_x64_128_legacy_msvc(key, 16, 0, output); + hash.murmur_hash2(key, 16, output); +} diff --git a/tests/core/murmur_hash.cpp b/tests/core/murmur_hash.cpp new file mode 100644 index 000000000..a0beb95c9 --- /dev/null +++ b/tests/core/murmur_hash.cpp @@ -0,0 +1,248 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include +#include +#include +#include +#include + +#include "../network/doctest.h" + +#include + +namespace { + +static_assert(std::is_member_function_pointer_v); +static_assert(std::is_member_function_pointer_v); +static_assert(std::is_pointer_v); +static_assert(std::is_pointer_v); + +void hash_x86_128(const void* key, int length, std::uint32_t seed, void* output) { + ark::murmur_hash{}.murmur_hash3_x86_128(key, length, seed, output); +} + +void hash_x86_128_legacy(const void* key, int length, std::uint32_t seed, void* output) { + ark::murmur_hash{}.murmur_hash3_x86_128_legacy_msvc(key, length, seed, output); +} + +void hash_murmur2(const void* key, int length, std::uint32_t, void* output) { + ark::murmur_hash::murmur_hash2(key, length, output); +} + +template +std::uint32_t verification_value(hash_function hash) { + // Austin Appleby's SMHasher VerificationTest: all prefix lengths 0..255, + // seeds 256-length, then hash their concatenated digests with seed zero. + std::array key{}; + std::array hashes{}; + std::array result{}; + for (int length = 0; length < 256; ++length) + { + key[length] = static_cast(length); + hash(key.data(), length, 256 - length, hashes.data() + length * output_size); + } + hash(hashes.data(), static_cast(hashes.size()), 0, result.data()); + return std::uint32_t{result[0]} | (std::uint32_t{result[1]} << 8) | (std::uint32_t{result[2]} << 16) | + (std::uint32_t{result[3]} << 24); +} + +template +std::array little_words(const std::array& bytes) { + std::array words{}; + for (std::size_t word = 0; word < word_count; ++word) + { + for (std::size_t byte = 0; byte < sizeof(word_type); ++byte) + { + words[word] |= static_cast(bytes[word * sizeof(word_type) + byte]) << (byte * 8); + } + } + return words; +} + +template +void check_byte_buffers(hash_function hash) { + alignas(8) std::array key{}; + for (std::size_t index = 0; index < key.size(); ++index) + { + key[index] = static_cast(index * 37 + 0x80); + } + // Cover every tail path and both sides of several full-block boundaries. + for (int length = 0; length <= 257; ++length) + { + if (length > 65 && length != 127 && length != 128 && length != 129 && length != 255 && length != 256 && + length != 257) + { + continue; + } + for (const auto seed : {0u, 1u, 0x80000000u, 0xffffffffu}) + { + alignas(8) std::array expected{}; + hash(key.data(), length, seed, expected.data()); + for (std::size_t offset = 0; offset < 8; ++offset) + { + CAPTURE(length); + CAPTURE(seed); + CAPTURE(offset); + // The readable range ends at the allocation boundary, so a sanitizer + // can detect full-block reads past the exact input length. + auto input = std::make_unique(std::max(1, length + offset)); + std::copy_n(key.begin(), length, input.get() + offset); + alignas(8) std::array output; + output.fill(0xa5); + const auto output_offset = (offset + 3) % 8; + hash(input.get() + offset, length, seed, output.data() + output_offset); + CHECK(std::equal(expected.begin(), expected.end(), output.begin() + output_offset)); + CHECK(std::all_of( + output.begin(), output.begin() + output_offset, [](auto value) { return value == 0xa5; })); + CHECK(std::all_of(output.begin() + output_offset + output_size, output.end(), + [](auto value) { return value == 0xa5; })); + } + } + } +} + +} // namespace + +TEST_CASE("Murmur3 retains upstream verification values and the legacy swapped-block policy") { + // Independent standard values and verification procedure: + // https://github.com/aappleby/smhasher/blob/master/src/main.cpp#L67-L70 + // https://github.com/aappleby/smhasher/blob/master/src/KeysetTest.cpp#L11-L38 + // Swapped-block values were derived from the same public-domain reference + // with only full-block byte order reversed; they are ARK compatibility fixtures. + CHECK(verification_value<4>(ark::murmur_hash::murmur_hash3_x86_32) == 0xb0f57ee3u); + CHECK(verification_value<16>(hash_x86_128) == 0xb3ece62au); + CHECK(verification_value<16>(ark::murmur_hash::murmur_hash3_x64_128) == 0x6384ba69u); + CHECK(verification_value<4>(ark::murmur_hash::murmur_hash3_x86_32_legacy_msvc) == 0xd1ab67cfu); + CHECK(verification_value<16>(hash_x86_128_legacy) == 0x2df8b022u); + CHECK(verification_value<16>(ark::murmur_hash::murmur_hash3_x64_128_legacy_msvc) == 0x501e703bu); +} + +TEST_CASE("Murmur3 rotations preserve full-width seeds and multi-block golden words") { + std::array key{}; + for (std::size_t index = 0; index < key.size(); ++index) + { + key[index] = static_cast(index); + } + + struct golden_case + { + int length; + std::uint32_t seed; + std::uint32_t x86_32; + std::array x86_128; + std::array x64_128; + }; + + // Fixed scalar-reference vectors exercise every production rotation count, + // high-bit seed preservation, exact blocks and a multi-block tail. + const std::array little{{ + {16, 0xffffffffu, 0x9fd6e060u, {0x4f872353u, 0x3ffde8eeu, 0x0c593cefu, 0xd276cbb5u}, + {0x75298e938ac005bfull, 0x26873cd6ed1b3b1eull}}, + {33, 0x12345678u, 0x2a4f25b0u, {0x0101769du, 0x6e0928d4u, 0x5f9361deu, 0xea784760u}, + {0xa94d180f895f3670ull, 0x9635b8a3feb6f62full}}, + {33, 0xffffffffu, 0xa5737017u, {0x53948860u, 0xb573ee1du, 0xb2e1371cu, 0x7d39669eu}, + {0xf14b6deed9373a70ull, 0xf83f262a61312136ull}}, + }}; + const std::array swapped{{ + {16, 0xffffffffu, 0x2d93b016u, {0x84c0695cu, 0x73902949u, 0x6bfdce30u, 0xefc92f7cu}, + {0x703b8fedb7306652ull, 0xb1ce86b7edcd8628ull}}, + {33, 0x12345678u, 0xb255cc88u, {0xaea33583u, 0x1fd73070u, 0x36c553ffu, 0x9f396b6du}, + {0xa34f5dea69959035ull, 0x734c22e1993ef38full}}, + {33, 0xffffffffu, 0x74b4bba8u, {0xc8d61452u, 0x878dd688u, 0xad51e4b6u, 0x5b85edf7u}, + {0xcdcdff4c5a639071ull, 0x8332c8a91ba2cf3dull}}, + }}; + for (std::size_t index = 0; index < little.size(); ++index) + { + CAPTURE(index); + if (index == 2) + { + for (std::size_t byte = 0; byte < key.size(); ++byte) + { + key[byte] = static_cast(byte * 37 + 0x80); + } + } + for (const bool legacy : {false, true}) + { + CAPTURE(legacy); + const auto& expected = legacy ? swapped[index] : little[index]; + std::array actual32{}; + std::array actual86{}; + std::array actual64{}; + const auto hash32 = + legacy ? ark::murmur_hash::murmur_hash3_x86_32_legacy_msvc : ark::murmur_hash::murmur_hash3_x86_32; + const auto hash86 = legacy ? hash_x86_128_legacy : hash_x86_128; + const auto hash64 = + legacy ? ark::murmur_hash::murmur_hash3_x64_128_legacy_msvc : ark::murmur_hash::murmur_hash3_x64_128; + hash32(key.data(), expected.length, expected.seed, actual32.data()); + hash86(key.data(), expected.length, expected.seed, actual86.data()); + hash64(key.data(), expected.length, expected.seed, actual64.data()); + CHECK((little_words(actual32)[0] == expected.x86_32)); + CHECK((little_words(actual86) == expected.x86_128)); + CHECK((little_words(actual64) == expected.x64_128)); + } + } +} + +TEST_CASE("Murmur2 retains seed-zero golden words around block boundaries") { + std::array key{}; + for (std::size_t index = 0; index < key.size(); ++index) + { + key[index] = static_cast(index); + } + const std::array, 10> cases{ + {{0, 0}, {1, 0xe94e6ebdu}, {3, 0xe2558031u}, {4, 0xa4985988u}, {15, 0x8184d621u}, {16, 0x5f3c0743u}, + {17, 0xb1aad377u}, {31, 0x1d7d6ee1u}, {32, 0xf1bc6c23u}, {33, 0x915a6941u}}}; + for (const auto& [length, expected] : cases) + { + CAPTURE(length); + std::array result{}; + ark::murmur_hash::murmur_hash2(key.data(), length, result.data()); + CHECK((little_words(result)[0] == expected)); + } +} + +TEST_CASE("Murmur3 default and legacy policies agree until a complete block is present") { + const std::array key{ + 0x80, 0xff, 0x00, 0x01, 0xa5, 0x5a, 0x81, 0xfe, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; + using hash_function = void (*)(const void*, int, std::uint32_t, void*); + + struct policy_case + { + hash_function standard; + hash_function legacy; + int block_size; + }; + + const std::array policies{{ + {ark::murmur_hash::murmur_hash3_x86_32, ark::murmur_hash::murmur_hash3_x86_32_legacy_msvc, 4}, + {hash_x86_128, hash_x86_128_legacy, 16}, + {ark::murmur_hash::murmur_hash3_x64_128, ark::murmur_hash::murmur_hash3_x64_128_legacy_msvc, 16}, + }}; + for (const auto& policy : policies) + { + for (int length = 0; length < policy.block_size; ++length) + { + for (const auto seed : {0u, 0x80000000u, 0xffffffffu}) + { + CAPTURE(length); + CAPTURE(seed); + std::array standard{}; + std::array legacy{}; + policy.standard(key.data(), length, seed, standard.data()); + policy.legacy(key.data(), length, seed, legacy.data()); + CHECK(standard == legacy); + } + } + } +} + +TEST_CASE("All Murmur variants accept byte-aligned inputs and outputs without changing digest bytes") { + check_byte_buffers<4>(ark::murmur_hash::murmur_hash3_x86_32); + check_byte_buffers<16>(hash_x86_128); + check_byte_buffers<16>(ark::murmur_hash::murmur_hash3_x64_128); + check_byte_buffers<4>(hash_murmur2); + check_byte_buffers<4>(ark::murmur_hash::murmur_hash3_x86_32_legacy_msvc); + check_byte_buffers<16>(hash_x86_128_legacy); + check_byte_buffers<16>(ark::murmur_hash::murmur_hash3_x64_128_legacy_msvc); +}