diff --git a/README.md b/README.md index 5fd4e6e..e070100 100644 --- a/README.md +++ b/README.md @@ -448,11 +448,13 @@ Hosted users who want standard-library interoperability can opt in: target_compile_definitions(my_target PRIVATE PROTOCYTE_ENABLE_STD_STRING_VIEW=1) ``` -When `PROTOCYTE_ENABLE_STD_STRING_VIEW` is defined, the runtime includes -`` and both `::protocyte::Span` / `Span` and -`::protocyte::String` are implicitly convertible to `std::string_view`. The -default accessor return type remains `Span`, so code that does not -enable the option keeps the smaller no-exception surface. +When `PROTOCYTE_ENABLE_STD_STRING_VIEW` is set to a nonzero value, the runtime +includes `` and both `::protocyte::Span` / `Span` +and `::protocyte::String` are implicitly convertible to `std::string_view`. +Generated immutable `string` field accessors also return `std::string_view` under +this opt-in, so hosted code can pass string fields directly to +standard-library APIs such as `std::format`. Code that does not enable the +option keeps the smaller no-exception `Span` accessor surface. In a Windows kernel driver, one technically possible MSVC/STL-specific escape hatch is to provide the STL's internal out-of-range throw helper yourself so diff --git a/smoke/generated/compat.protocyte.hpp b/smoke/generated/compat.protocyte.hpp index 39032c4..e5ffda6 100644 --- a/smoke/generated/compat.protocyte.hpp +++ b/smoke/generated/compat.protocyte.hpp @@ -65,7 +65,7 @@ namespace protocyte_smoke::test::compat { } constexpr void clear_value() noexcept { value_ = {}; } - ::protocyte::Span label() const noexcept { return label_.view(); } + ::protocyte::StringView label() const noexcept { return label_.view(); } typename Config::String &mutable_label() noexcept { return label_; } template::protocyte::Status set_label(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -652,7 +652,7 @@ namespace protocyte_smoke::test::compat { } constexpr void clear_f_double() noexcept { f_double_ = {}; } - ::protocyte::Span f_string() const noexcept { return f_string_.view(); } + ::protocyte::StringView f_string() const noexcept { return f_string_.view(); } typename Config::String &mutable_f_string() noexcept { return f_string_; } template::protocyte::Status set_f_string(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -741,8 +741,8 @@ namespace protocyte_smoke::test::compat { constexpr bool has_oneof_string() const noexcept { return special_oneof_case_ == Special_oneofCase::oneof_string; } - ::protocyte::Span oneof_string() const noexcept { - return has_oneof_string() ? special_oneof.oneof_string.view() : ::protocyte::Span {}; + ::protocyte::StringView oneof_string() const noexcept { + return has_oneof_string() ? special_oneof.oneof_string.view() : ::protocyte::StringView {}; } template::protocyte::Status set_oneof_string(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -852,7 +852,7 @@ namespace protocyte_smoke::test::compat { has_opt_int32_ = false; } - ::protocyte::Span opt_string() const noexcept { return opt_string_.view(); } + ::protocyte::StringView opt_string() const noexcept { return opt_string_.view(); } bool has_opt_string() const noexcept { return has_opt_string_; } typename Config::String &mutable_opt_string() noexcept { has_opt_string_ = true; diff --git a/smoke/generated/cross_package.protocyte.hpp b/smoke/generated/cross_package.protocyte.hpp index a4d9029..a4ca8fd 100644 --- a/smoke/generated/cross_package.protocyte.hpp +++ b/smoke/generated/cross_package.protocyte.hpp @@ -5,7 +5,9 @@ #include +#if !PROTOCYTE_ENABLE_STD_STRING_VIEW #include +#endif namespace test::crosspkg { diff --git a/smoke/generated/example.protocyte.hpp b/smoke/generated/example.protocyte.hpp index 21b3f4c..05e9b0c 100644 --- a/smoke/generated/example.protocyte.hpp +++ b/smoke/generated/example.protocyte.hpp @@ -5,7 +5,9 @@ #include +#if !PROTOCYTE_ENABLE_STD_STRING_VIEW #include +#endif namespace test::ultimate { @@ -96,7 +98,7 @@ namespace test::ultimate { return out; } - ::protocyte::Span description() const noexcept { return description_.view(); } + ::protocyte::StringView description() const noexcept { return description_.view(); } typename Config::String &mutable_description() noexcept { return description_; } template::protocyte::Status set_description(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -394,7 +396,7 @@ namespace test::ultimate { return out; } - ::protocyte::Span name() const noexcept { return name_.view(); } + ::protocyte::StringView name() const noexcept { return name_.view(); } typename Config::String &mutable_name() noexcept { return name_; } template::protocyte::Status set_name(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -1118,7 +1120,7 @@ namespace test::ultimate { deep_oneof_case_ = Deep_oneofCase::none; } - ::protocyte::Span extreme() const noexcept { return extreme_.view(); } + ::protocyte::StringView extreme() const noexcept { return extreme_.view(); } typename Config::String &mutable_extreme() noexcept { return extreme_; } template::protocyte::Status set_extreme(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -1168,8 +1170,8 @@ namespace test::ultimate { } constexpr bool has_text() const noexcept { return deep_oneof_case_ == Deep_oneofCase::text; } - ::protocyte::Span text() const noexcept { - return has_text() ? deep_oneof.text.view() : ::protocyte::Span {}; + ::protocyte::StringView text() const noexcept { + return has_text() ? deep_oneof.text.view() : ::protocyte::StringView {}; } template::protocyte::Status set_text(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -2297,7 +2299,7 @@ namespace test::ultimate { } constexpr void clear_f_bool() noexcept { f_bool_ = {}; } - ::protocyte::Span f_string() const noexcept { return f_string_.view(); } + ::protocyte::StringView f_string() const noexcept { return f_string_.view(); } typename Config::String &mutable_f_string() noexcept { return f_string_; } template::protocyte::Status set_f_string(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -2399,8 +2401,8 @@ namespace test::ultimate { constexpr bool has_oneof_string() const noexcept { return special_oneof_case_ == Special_oneofCase::oneof_string; } - ::protocyte::Span oneof_string() const noexcept { - return has_oneof_string() ? special_oneof.oneof_string.view() : ::protocyte::Span {}; + ::protocyte::StringView oneof_string() const noexcept { + return has_oneof_string() ? special_oneof.oneof_string.view() : ::protocyte::StringView {}; } template::protocyte::Status set_oneof_string(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) @@ -2749,7 +2751,7 @@ namespace test::ultimate { has_opt_int32_ = false; } - ::protocyte::Span opt_string() const noexcept { return opt_string_.view(); } + ::protocyte::StringView opt_string() const noexcept { return opt_string_.view(); } bool has_opt_string() const noexcept { return has_opt_string_; } typename Config::String &mutable_opt_string() noexcept { has_opt_string_ = true; @@ -6063,7 +6065,7 @@ namespace test::ultimate { return out; } - ::protocyte::Span tag() const noexcept { return tag_.view(); } + ::protocyte::StringView tag() const noexcept { return tag_.view(); } typename Config::String &mutable_tag() noexcept { return tag_; } template::protocyte::Status set_tag(const Value &value) noexcept requires(::protocyte::ByteSpanSource && !::protocyte::TextSource) diff --git a/smoke/generated/protocyte/runtime/runtime.hpp b/smoke/generated/protocyte/runtime/runtime.hpp index 6fb2585..187b041 100644 --- a/smoke/generated/protocyte/runtime/runtime.hpp +++ b/smoke/generated/protocyte/runtime/runtime.hpp @@ -11,7 +11,7 @@ #include #include -#ifdef PROTOCYTE_ENABLE_STD_STRING_VIEW +#if PROTOCYTE_ENABLE_STD_STRING_VIEW #include #endif @@ -1269,11 +1269,11 @@ namespace protocyte { constexpr usize size() const noexcept { return size_; } constexpr usize size_bytes() const noexcept { return size_ * sizeof(T); } constexpr bool empty() const noexcept { return size_ == 0u; } -#ifdef PROTOCYTE_ENABLE_STD_STRING_VIEW +#if PROTOCYTE_ENABLE_STD_STRING_VIEW constexpr operator ::std::string_view() const noexcept requires(::std::same_as<::std::remove_cv_t, char>) { - return data_ == nullptr ? ::std::string_view {} : ::std::string_view {data_, size_}; + return ::std::string_view {data_, size_}; } #endif template constexpr Span first() const noexcept @@ -1316,6 +1316,12 @@ namespace protocyte { requires(!DataSizeSpanSource && PointerSpanSource) Span(Range &) -> Span<::std::remove_pointer_t>>; +#if PROTOCYTE_ENABLE_STD_STRING_VIEW + using StringView = ::std::string_view; +#else + using StringView = Span; +#endif + template concept SpanSource = requires(T &value) { Span {value}; } || requires(const T &value) { Span {value}; }; @@ -2869,7 +2875,7 @@ namespace protocyte { usize size() const noexcept { return bytes_.size(); } usize length() const noexcept { return size(); } bool empty() const noexcept { return bytes_.empty(); } -#ifdef PROTOCYTE_ENABLE_STD_STRING_VIEW +#if PROTOCYTE_ENABLE_STD_STRING_VIEW operator ::std::string_view() const noexcept { return view(); } #endif void clear() noexcept { bytes_.clear(); } diff --git a/smoke/src/host_smoke.cpp b/smoke/src/host_smoke.cpp index bf8eef9..7fc0746 100644 --- a/smoke/src/host_smoke.cpp +++ b/smoke/src/host_smoke.cpp @@ -2,6 +2,9 @@ #include #include #include +#if __has_include() +#include +#endif #include #include #include @@ -214,6 +217,16 @@ namespace { return protocyte::bytes_equal(lhs, rhs); } + template + bool view_equal(std::string_view lhs, protocyte::Span rhs) noexcept { + return view_equal(protocyte::Span {lhs.data(), lhs.size()}, rhs); + } + + template + bool view_equal(protocyte::Span lhs, std::string_view rhs) noexcept { + return view_equal(lhs, protocyte::Span {rhs.data(), rhs.size()}); + } + template protocyte::Span view_of(const uint8_t (&data)[N]) noexcept { return protocyte::Span {data, N}; } @@ -2603,8 +2616,12 @@ TEST_CASE("byte setters accept contiguous byte containers", "[smoke][runtime][by const auto string_view = protocyte::byte_span_of(string_payload); REQUIRE(string_view); CHECK(view_equal(message.f_string(), *string_view)); + static_assert(std::is_same_v); const std::string_view converted_span = message.f_string(); CHECK(converted_span == std::string_view {"hello"}); +#if defined(__cpp_lib_format) + CHECK(std::format("{}", message.f_string()) == "hello"); +#endif const std::string_view converted_string = message.mutable_f_string(); CHECK(converted_string == converted_span); diff --git a/smoke/src/kernel_driver_smoke.cpp b/smoke/src/kernel_driver_smoke.cpp index 5e422de..db5523c 100644 --- a/smoke/src/kernel_driver_smoke.cpp +++ b/smoke/src/kernel_driver_smoke.cpp @@ -24,6 +24,34 @@ void __cdecl operator delete[](void *ptr) noexcept { ::operator delete(ptr); } void __cdecl operator delete[](void *ptr, size_t) noexcept { ::operator delete(ptr); } #endif +#if PROTOCYTE_ENABLE_STD_STRING_VIEW && defined(_DEBUG) +// MSVC's debug STL imports these CRT assertion hooks through __imp_* data symbols. +__declspec(noreturn) void protocyte_debug_crt_shim_bugcheck(const char *symbol_name) { + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, + "Protocyte kernel smoke: MSVC STL debug CRT fallback called for %s.\n", symbol_name); + KeBugCheckEx(MANUALLY_INITIATED_CRASH, 0x50535643u, 0u, 0u, 0u); + for (;;) {} +} + +extern "C" __declspec(noreturn) void __cdecl protocyte_invoke_watson_shim(const wchar_t *, const wchar_t *, + const wchar_t *, unsigned int, uintptr_t) { + protocyte_debug_crt_shim_bugcheck("_invoke_watson"); +} + +extern "C" int __cdecl protocyte_crt_dbg_report_shim(int, const char *, int, const char *, const char *, ...) { + protocyte_debug_crt_shim_bugcheck("_CrtDbgReport"); +} + +using protocyte_invoke_watson_fn = void(__cdecl *)(const wchar_t *, const wchar_t *, const wchar_t *, unsigned int, + uintptr_t); +using protocyte_crt_dbg_report_fn = int(__cdecl *)(int, const char *, int, const char *, const char *, ...); + +extern "C" { + protocyte_invoke_watson_fn __imp__invoke_watson = &protocyte_invoke_watson_shim; + protocyte_crt_dbg_report_fn __imp__CrtDbgReport = &protocyte_crt_dbg_report_shim; +} +#endif + namespace { constexpr ULONG protocyte_pool_tag = 'TyCP'; diff --git a/src/protocyte/cpp.py b/src/protocyte/cpp.py index 0f5e236..9c6e345 100644 --- a/src/protocyte/cpp.py +++ b/src/protocyte/cpp.py @@ -210,7 +210,7 @@ def generate_header(file_model: FileModel, options: GeneratorOptions) -> str: w.line(f"#include <{options.runtime_prefix}/runtime.hpp>") extra_includes: list[str] = [] if _file_uses_string_view(file_model): - extra_includes.append("#include ") + extra_includes.extend(["#if !PROTOCYTE_ENABLE_STD_STRING_VIEW", "#include ", "#endif"]) for dependency in sorted(file_model.dependencies): extra_includes.append(f'#include "{_include_path(dependency, options)}"') if extra_includes: @@ -813,11 +813,13 @@ def emit_setter_body() -> None: return if item.kind in {"string", "bytes"}: typ = _field_type(item, options) - view_type = "::protocyte::Span" if item.kind == "string" else "::protocyte::Span" - w.line(f"{view_type} {item.cpp_name}() const noexcept {{ return {_member(item)}.view(); }}") - if item.proto3_optional: - w.line(f"bool has_{item.cpp_name}() const noexcept {{ return has_{item.cpp_name}_; }}") - w.line(f"{typ}& mutable_{item.cpp_name}() noexcept {{") + if item.kind == "string": + _emit_string_view_accessor(w, item.cpp_name, f"{_member(item)}.view()") + else: + w.line(f"::protocyte::Span {item.cpp_name}() const noexcept {{ return {_member(item)}.view(); }}") + if item.proto3_optional: + w.line(f"bool has_{item.cpp_name}() const noexcept {{ return has_{item.cpp_name}_; }}") + w.line(f"{typ}& mutable_{item.cpp_name}() noexcept {{") w.push() if item.proto3_optional: w.line(f"has_{item.cpp_name}_ = true;") @@ -879,10 +881,17 @@ def _emit_oneof_accessors(w: CppWriter, item: FieldModel, options: GeneratorOpti f"constexpr bool has_{item.cpp_name}() const noexcept {{ return {case_member} == {case_type}::{item.cpp_name}; }}" ) if item.kind in {"string", "bytes"}: - view_type = "::protocyte::Span" if item.kind == "string" else "::protocyte::Span" - w.line( - f"{view_type} {item.cpp_name}() const noexcept {{ return has_{item.cpp_name}() ? {_member(item)}.view() : {view_type}{{}}; }}" - ) + if item.kind == "string": + _emit_string_view_accessor( + w, + item.cpp_name, + f"has_{item.cpp_name}() ? {_member(item)}.view() : ::protocyte::StringView{{}}", + ) + else: + view_type = "::protocyte::Span" + w.line( + f"{view_type} {item.cpp_name}() const noexcept {{ return has_{item.cpp_name}() ? {_member(item)}.view() : {view_type}{{}}; }}" + ) def emit_setter_body() -> None: if item.kind == "bytes" and item.array_enabled: w.line( @@ -1961,6 +1970,14 @@ def _runtime_scalar_type(cpp_type: str) -> str: return _RUNTIME_SCALAR_TYPES.get(cpp_type, cpp_type) +def _emit_string_view_accessor( + w: CppWriter, + name: str, + expr: str, +) -> None: + w.line(f"::protocyte::StringView {name}() const noexcept {{ return {expr}; }}") + + def _file_uses_string_view(file_model: FileModel) -> bool: if any(constant.kind == CONSTANT_KIND_STRING for constant in file_model.constants): return True diff --git a/src/protocyte/runtime/runtime.hpp b/src/protocyte/runtime/runtime.hpp index 6fb2585..187b041 100644 --- a/src/protocyte/runtime/runtime.hpp +++ b/src/protocyte/runtime/runtime.hpp @@ -11,7 +11,7 @@ #include #include -#ifdef PROTOCYTE_ENABLE_STD_STRING_VIEW +#if PROTOCYTE_ENABLE_STD_STRING_VIEW #include #endif @@ -1269,11 +1269,11 @@ namespace protocyte { constexpr usize size() const noexcept { return size_; } constexpr usize size_bytes() const noexcept { return size_ * sizeof(T); } constexpr bool empty() const noexcept { return size_ == 0u; } -#ifdef PROTOCYTE_ENABLE_STD_STRING_VIEW +#if PROTOCYTE_ENABLE_STD_STRING_VIEW constexpr operator ::std::string_view() const noexcept requires(::std::same_as<::std::remove_cv_t, char>) { - return data_ == nullptr ? ::std::string_view {} : ::std::string_view {data_, size_}; + return ::std::string_view {data_, size_}; } #endif template constexpr Span first() const noexcept @@ -1316,6 +1316,12 @@ namespace protocyte { requires(!DataSizeSpanSource && PointerSpanSource) Span(Range &) -> Span<::std::remove_pointer_t>>; +#if PROTOCYTE_ENABLE_STD_STRING_VIEW + using StringView = ::std::string_view; +#else + using StringView = Span; +#endif + template concept SpanSource = requires(T &value) { Span {value}; } || requires(const T &value) { Span {value}; }; @@ -2869,7 +2875,7 @@ namespace protocyte { usize size() const noexcept { return bytes_.size(); } usize length() const noexcept { return size(); } bool empty() const noexcept { return bytes_.empty(); } -#ifdef PROTOCYTE_ENABLE_STD_STRING_VIEW +#if PROTOCYTE_ENABLE_STD_STRING_VIEW operator ::std::string_view() const noexcept { return view(); } #endif void clear() noexcept { bytes_.clear(); } diff --git a/tests/test_plugin.py b/tests/test_plugin.py index f193a29..a55075b 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -53,6 +53,16 @@ def test_runtime_rejects_unmatched_end_group_in_skip_field() -> None: assert "case WireType::EGROUP: return {};" not in runtime_header +def test_kernel_smoke_provides_debug_string_view_crt_shims() -> None: + source = (Path(__file__).resolve().parents[1] / "smoke" / "src" / "kernel_driver_smoke.cpp").read_text() + + assert "#if PROTOCYTE_ENABLE_STD_STRING_VIEW && defined(_DEBUG)" in source + assert "__imp__invoke_watson" in source + assert "__imp__CrtDbgReport" in source + assert "DbgPrintEx" in source + assert "KeBugCheckEx" in source + + def test_runtime_container_growth_checks_capacity_limits() -> None: runtime_header = runtime_files()["protocyte/runtime/runtime.hpp"] @@ -173,7 +183,8 @@ def test_runtime_byte_containers_use_bulk_copy_helpers() -> None: )[0] assert "#include " in runtime_header - assert "#ifdef PROTOCYTE_ENABLE_STD_STRING_VIEW\n#include \n#endif" in runtime_header + assert "#if PROTOCYTE_ENABLE_STD_STRING_VIEW\n#include \n#endif" in runtime_header + assert "#ifdef PROTOCYTE_ENABLE_STD_STRING_VIEW" not in runtime_header assert "inline void copy_bytes(u8 *dst, const u8 *src, const usize count) noexcept" in runtime_header assert "if (!count || dst == src)" in runtime_header assert "::std::memmove(dst, src, count);" in runtime_header @@ -198,6 +209,9 @@ def test_runtime_byte_containers_use_bulk_copy_helpers() -> None: assert "copy_bytes(temp.data(), view.data(), view.size());" in bytes_body assert "constexpr operator ::std::string_view() const noexcept" in span_body assert "requires(::std::same_as<::std::remove_cv_t, char>)" in span_body + assert "return ::std::string_view {data_, size_};" in span_body + assert "data_ == nullptr ? ::std::string_view {}" not in span_body + assert "#if PROTOCYTE_ENABLE_STD_STRING_VIEW\n using StringView = ::std::string_view;\n#else\n using StringView = Span;\n#endif" in runtime_header assert "using value_type = const char;" in string_body assert "Span view() const noexcept" in string_body assert "Span byte_view() const noexcept" in string_body @@ -676,7 +690,10 @@ def test_generated_header_contains_expected_field_api() -> None: assert "namespace demo {" in header assert "bool has_opt_name() const noexcept" in header - assert "::protocyte::Span opt_name() const noexcept" in header + assert "#include " not in header + assert " ::protocyte::StringView opt_name() const noexcept { return opt_name_.view(); }" in header + assert "::std::string_view opt_name()" not in header + assert "::protocyte::Span opt_name()" not in header assert "struct Sample {" in header assert "typename Config::template Map items_;" in header assert "typename Config::template Box<::demo::Sample> self_;" in header @@ -1037,8 +1054,12 @@ def test_generated_header_emits_constants_and_array_storage() -> None: header = files["arrays.protocyte.hpp"] runtime_header = files["protocyte/runtime/runtime.hpp"] - assert '#include \n\n#include ' in header - assert "#include " in header + assert ( + "#include \n\n" + "#if !PROTOCYTE_ENABLE_STD_STRING_VIEW\n" + "#include \n" + "#endif" + ) in header assert "inline constexpr ::protocyte::u32 FILE_CAP {16u};" in header assert 'inline constexpr ::std::string_view FILE_LABEL {"ell", 3u};' in header assert "inline constexpr bool FILE_READY {true};" in header @@ -1372,8 +1393,12 @@ def test_generated_header_emits_utf8_string_constants() -> None: assert not response.error header = next(file.content for file in response.file if file.name == "unicode.protocyte.hpp") - assert '#include \n\n#include ' in header - assert '#include ' in header + assert ( + "#include \n\n" + "#if !PROTOCYTE_ENABLE_STD_STRING_VIEW\n" + "#include \n" + "#endif" + ) in header assert 'static constexpr ::std::string_view NAME {"\\xc4"' in header assert '"\\x80"' in header assert '"\\xc3"' in header @@ -1417,7 +1442,13 @@ def test_generated_header_emits_tagged_union_oneofs() -> None: assert "void clear_choice() noexcept {" in header assert "destroy_at_(&choice.text);" in header assert "destroy_at_(&choice.inner);" in header - assert "::protocyte::Span text() const noexcept" in header + assert "#include " not in header + assert ( + " ::protocyte::StringView text() const noexcept { " + "return has_text() ? choice.text.view() : ::protocyte::StringView{}; }" + ) in header + assert "::std::string_view text()" not in header + assert "::protocyte::Span text()" not in header assert "union ChoiceStorage {" in header assert "ChoiceStorage() noexcept {}" in header assert "~ChoiceStorage() noexcept {}" in header