diff --git a/compiler/fory_compiler/tests/test_generated_code.py b/compiler/fory_compiler/tests/test_generated_code.py index a1b08bff59..d5f7b39602 100644 --- a/compiler/fory_compiler/tests/test_generated_code.py +++ b/compiler/fory_compiler/tests/test_generated_code.py @@ -1169,6 +1169,32 @@ def test_cpp_nested_container_ref_uses_correct_pointer_type(): ) +def test_cpp_temporal_map_keys_use_fory_owned_wrappers(): + schema = parse_fdl( + dedent( + """ + package demo; + + message Holder { + map durations = 1; + map timestamps = 2; + map dates = 3; + } + """ + ) + ) + + cpp_output = render_files(generate_files(schema, CppGenerator)) + assert ( + "std::unordered_map" in cpp_output + ) + assert ( + "std::unordered_map" in cpp_output + ) + assert "std::unordered_map" in cpp_output + assert "std::map<" not in cpp_output + + def test_java_enum_generation_uses_fory_enum_ids(): schema = parse_fdl( dedent( diff --git a/cpp/fory/serialization/serialization_test.cc b/cpp/fory/serialization/serialization_test.cc index 8bed168d63..cda99cfdc1 100644 --- a/cpp/fory/serialization/serialization_test.cc +++ b/cpp/fory/serialization/serialization_test.cc @@ -22,6 +22,7 @@ #include "fory/serialization/skip.h" #include "fory/thirdparty/MurmurHash3.h" #include "gtest/gtest.h" +#include #include #include #include @@ -31,6 +32,8 @@ #include #include #include +#include +#include #include #include @@ -193,6 +196,24 @@ void test_roundtrip(const T &original, bool should_equal = true) { } } +template void expect_any_roundtrip(Fory &fory, const T &value) { + std::any original = value; + auto serialize_result = fory.serialize(original); + ASSERT_TRUE(serialize_result.ok()) + << "Serialization failed: " << serialize_result.error().to_string(); + + auto deserialize_result = + fory.deserialize(serialize_result.value()); + ASSERT_TRUE(deserialize_result.ok()) + << "Deserialization failed: " << deserialize_result.error().to_string(); + + std::any decoded = std::move(deserialize_result).value(); + EXPECT_EQ(decoded.type(), typeid(T)); + const auto *decoded_value = std::any_cast(&decoded); + ASSERT_NE(decoded_value, nullptr); + EXPECT_EQ(*decoded_value, value); +} + // ============================================================================ // Primitive Type Tests // ============================================================================ @@ -370,10 +391,11 @@ TEST(SerializationTest, DurationRoundtrip) { auto fory = Fory::builder().xlang(true).compatible(false).track_ref(false).build(); std::vector values = { - Duration(0), - std::chrono::seconds(12) + Duration(345678901), - -std::chrono::seconds(7) - std::chrono::milliseconds(45) - Duration(67), - Duration(-1), + Duration(std::chrono::nanoseconds(0)), + Duration(std::chrono::seconds(12) + std::chrono::nanoseconds(345678901)), + Duration(-std::chrono::seconds(7) - std::chrono::milliseconds(45) - + std::chrono::nanoseconds(67)), + Duration(std::chrono::nanoseconds(-1)), }; for (const Duration &original : values) { @@ -550,9 +572,9 @@ TEST(SerializationTest, DurationUsesSecondsAndNanosecondsPayload) { auto fory = Fory::builder().xlang(true).compatible(false).track_ref(false).build(); std::vector cases = { - {Duration(1234567890), 1, 234567890}, - {Duration(-1234567890), -1, -234567890}, - {Duration(-1), 0, -1}, + {Duration(std::chrono::nanoseconds(1234567890)), 1, 234567890}, + {Duration(std::chrono::nanoseconds(-1234567890)), -1, -234567890}, + {Duration(std::chrono::nanoseconds(-1)), 0, -1}, }; for (const TestCase &test_case : cases) { @@ -579,7 +601,8 @@ TEST(SerializationTest, DurationSkipConsumesSecondsAndNanosecondsPayload) { auto fory = Fory::builder().xlang(true).compatible(false).track_ref(false).build(); WriteContext write_ctx(fory.config(), fory.type_resolver().clone()); - Serializer::write_data(Duration(-1), write_ctx); + Serializer::write_data(Duration(std::chrono::nanoseconds(-1)), + write_ctx); ASSERT_FALSE(write_ctx.has_error()) << write_ctx.error().to_string(); ReadContext read_ctx(fory.config(), fory.type_resolver().clone()); @@ -805,6 +828,13 @@ TEST(SerializationTest, MapStringIntRoundtrip) { std::map{{"one", 1}, {"two", 2}, {"three", 3}}); } +TEST(SerializationTest, UnorderedMapStringIntRoundtrip) { + test_roundtrip(std::unordered_map{}); + test_roundtrip(std::unordered_map{{"one", 1}}); + test_roundtrip(std::unordered_map{ + {"one", 1}, {"two", 2}, {"three", 3}}); +} + TEST(SerializationTest, NestedVectorRoundtrip) { test_roundtrip(std::vector>{}); test_roundtrip(std::vector>{{1, 2}, {3, 4}, {5}}); @@ -1489,6 +1519,113 @@ TEST(SerializationTest, ThreadSafeForyRejectsRegistrationAfterFirstSerialize) { std::string::npos); } +TEST(SerializationTest, TemporalCarriersAreHashable) { + std::unordered_map date_map; + date_map[Date(0)] = "epoch"; + date_map[Date(18954)] = "future"; + date_map[Date(-1)] = "past"; + EXPECT_EQ(date_map.size(), 3u); + EXPECT_EQ(date_map[Date(0)], "epoch"); + + std::unordered_map dur_map; + dur_map[Duration(std::chrono::nanoseconds(0))] = "zero"; + dur_map[Duration(std::chrono::seconds(1))] = "one_sec"; + dur_map[Duration(std::chrono::nanoseconds(-1))] = "neg"; + EXPECT_EQ(dur_map.size(), 3u); + EXPECT_EQ(dur_map[Duration(std::chrono::nanoseconds(0))], "zero"); + + std::unordered_map ts_map; + ts_map[Timestamp()] = "epoch"; + ts_map[Timestamp(std::chrono::nanoseconds(1000000000LL))] = "one_sec"; + EXPECT_EQ(ts_map.size(), 2u); + EXPECT_EQ(ts_map[Timestamp()], "epoch"); +} + +TEST(SerializationTest, DurationChronoConversion) { + auto ns = std::chrono::seconds(5) + std::chrono::nanoseconds(123); + Duration d(ns); + EXPECT_EQ(d.to_chrono(), ns); + EXPECT_EQ(d.count(), ns.count()); + + Duration zero; + EXPECT_EQ(zero.count(), 0); + EXPECT_EQ(zero.to_chrono(), std::chrono::nanoseconds(0)); +} + +TEST(SerializationTest, TimestampChronoConversion) { + auto ns = std::chrono::nanoseconds(1700000000000000000LL); + Timestamp ts(ns); + EXPECT_EQ(ts.time_since_epoch(), ns); + EXPECT_EQ(ts.to_chrono().time_since_epoch(), ns); + + Timestamp epoch; + EXPECT_EQ(epoch.time_since_epoch().count(), 0); +} + +TEST(SerializationTest, TemporalAnyUsesCanonicalCarriers) { + auto fory = + Fory::builder().xlang(true).compatible(false).track_ref(false).build(); + ASSERT_TRUE(register_any_type(fory.type_resolver()).ok()); + ASSERT_TRUE(register_any_type(fory.type_resolver()).ok()); + + expect_any_roundtrip( + fory, Duration(std::chrono::seconds(12) + std::chrono::nanoseconds(34))); + expect_any_roundtrip(fory, + Timestamp(std::chrono::nanoseconds(1234567890123LL))); +} + +TEST(SerializationTest, ChronoTemporalAnyRegistrationIsRejected) { + using ChronoTimestamp = std::chrono::time_point; + auto fory = + Fory::builder().xlang(true).compatible(false).track_ref(false).build(); + + auto chrono_duration_registration = + register_any_type(fory.type_resolver()); + ASSERT_FALSE(chrono_duration_registration.ok()); + EXPECT_NE(chrono_duration_registration.error().to_string().find( + "explicit static targets"), + std::string::npos); + + ASSERT_TRUE(register_any_type(fory.type_resolver()).ok()); + ASSERT_TRUE(register_any_type(fory.type_resolver()).ok()); + + auto chrono_timestamp_registration = + register_any_type(fory.type_resolver()); + ASSERT_FALSE(chrono_timestamp_registration.ok()); + EXPECT_NE(chrono_timestamp_registration.error().to_string().find( + "explicit static targets"), + std::string::npos); + + expect_any_roundtrip(fory, Duration(std::chrono::nanoseconds(-1))); + expect_any_roundtrip(fory, Timestamp(std::chrono::nanoseconds(1000000000LL))); +} + +TEST(SerializationTest, ForyOwnedAndChronoShareWireEncoding) { + auto fory = + Fory::builder().xlang(true).compatible(false).track_ref(false).build(); + + auto ns = std::chrono::seconds(7) + std::chrono::nanoseconds(654321); + Duration fory_dur(ns); + std::chrono::nanoseconds chrono_dur = ns; + + auto fory_bytes = fory.serialize(fory_dur); + auto chrono_bytes = fory.serialize(chrono_dur); + ASSERT_TRUE(fory_bytes.ok()); + ASSERT_TRUE(chrono_bytes.ok()); + EXPECT_EQ(fory_bytes.value(), chrono_bytes.value()); + + auto decoded_from_fory = fory.deserialize( + fory_bytes.value().data(), fory_bytes.value().size()); + ASSERT_TRUE(decoded_from_fory.ok()); + EXPECT_EQ(decoded_from_fory.value(), ns); + + auto decoded_from_chrono = fory.deserialize( + chrono_bytes.value().data(), chrono_bytes.value().size()); + ASSERT_TRUE(decoded_from_chrono.ok()); + EXPECT_EQ(decoded_from_chrono.value(), fory_dur); +} + } // namespace test } // namespace serialization } // namespace fory diff --git a/cpp/fory/serialization/temporal_serializers.h b/cpp/fory/serialization/temporal_serializers.h index 978809c6bf..22527ffb8f 100644 --- a/cpp/fory/serialization/temporal_serializers.h +++ b/cpp/fory/serialization/temporal_serializers.h @@ -20,50 +20,18 @@ #pragma once #include "fory/serialization/serializer.h" +#include "fory/type/temporal.h" #include #include namespace fory { namespace serialization { -// ============================================================================ -// Temporal Type Aliases -// ============================================================================ - -/// Duration: absolute length of time as nanoseconds -using Duration = std::chrono::nanoseconds; - -/// Timestamp: point in time as nanoseconds since Unix epoch (Jan 1, 1970 UTC) -using Timestamp = std::chrono::time_point; - -/// Date: naive date without timezone as days since Unix epoch -class Date { -public: - Date() : days_since_epoch_(0) {} - explicit Date(int32_t days) : days_since_epoch_(days) {} - - int32_t days_since_epoch() const { return days_since_epoch_; } - - bool operator==(const Date &other) const { - return days_since_epoch_ == other.days_since_epoch_; - } - - bool operator!=(const Date &other) const { return !(*this == other); } - - bool operator<(const Date &other) const { - return days_since_epoch_ < other.days_since_epoch_; - } - -private: - int32_t days_since_epoch_; // Days since Jan 1, 1970 UTC -}; - // ============================================================================ // Duration Serializer // ============================================================================ -/// Serializer for Duration (std::chrono::nanoseconds) +/// Serializer for Duration /// Per xlang spec: serialized as signed varint64 seconds + signed int32 /// nanoseconds template <> struct Serializer { @@ -95,8 +63,9 @@ template <> struct Serializer { } static inline void write_data(const Duration &duration, WriteContext &ctx) { - auto seconds = std::chrono::duration_cast(duration); - auto remainder = duration - seconds; + auto ns = duration.to_chrono(); + auto seconds = std::chrono::duration_cast(ns); + auto remainder = ns - seconds; ctx.write_var_int64(seconds.count()); ctx.buffer().write_int32(static_cast(remainder.count())); } @@ -110,17 +79,17 @@ template <> struct Serializer { bool read_type) { bool has_value = read_null_only_flag(ctx, ref_mode); if (ctx.has_error() || !has_value) { - return Duration(0); + return Duration(); } if (read_type) { uint32_t type_id_read = ctx.read_uint8(ctx.error()); if (FORY_PREDICT_FALSE(ctx.has_error())) { - return Duration(0); + return Duration(); } if (type_id_read != static_cast(type_id)) { ctx.set_error( Error::type_mismatch(type_id_read, static_cast(type_id))); - return Duration(0); + return Duration(); } } return read_data(ctx); @@ -129,11 +98,11 @@ template <> struct Serializer { static inline Duration read_data(ReadContext &ctx) { int64_t seconds = ctx.read_var_int64(ctx.error()); if (FORY_PREDICT_FALSE(ctx.has_error())) { - return Duration(0); + return Duration(); } int32_t nanos = ctx.read_int32(ctx.error()); - return std::chrono::duration_cast(std::chrono::seconds(seconds)) + - Duration(nanos); + return Duration(std::chrono::seconds(seconds) + + std::chrono::nanoseconds(nanos)); } static inline Duration read_with_type_info(ReadContext &ctx, RefMode ref_mode, @@ -200,17 +169,17 @@ template <> struct Serializer { bool read_type) { bool has_value = read_null_only_flag(ctx, ref_mode); if (ctx.has_error() || !has_value) { - return Timestamp(std::chrono::nanoseconds(0)); + return Timestamp(); } if (read_type) { uint32_t type_id_read = ctx.read_uint8(ctx.error()); if (FORY_PREDICT_FALSE(ctx.has_error())) { - return Timestamp(std::chrono::nanoseconds(0)); + return Timestamp(); } if (type_id_read != static_cast(type_id)) { ctx.set_error( Error::type_mismatch(type_id_read, static_cast(type_id))); - return Timestamp(std::chrono::nanoseconds(0)); + return Timestamp(); } } return read_data(ctx); @@ -219,7 +188,7 @@ template <> struct Serializer { static inline Timestamp read_data(ReadContext &ctx) { int64_t seconds = ctx.read_int64(ctx.error()); if (FORY_PREDICT_FALSE(ctx.has_error())) { - return Timestamp(std::chrono::nanoseconds(0)); + return Timestamp(); } uint32_t nanos = ctx.read_uint32(ctx.error()); return Timestamp(std::chrono::seconds(seconds) + @@ -315,5 +284,110 @@ template <> struct Serializer { } }; +// ============================================================================ +// Chrono serializers +// +// These allow users to explicitly request chrono types as the deserialization +// target (e.g., fory.deserialize(...)). They share +// the same wire encoding as the Fory-owned carrier serializers above and +// delegate to them via conversion +// ============================================================================ + +/// Serializer for std::chrono::nanoseconds +/// Per xlang spec: serialized as signed varint64 seconds + signed int32 +/// nanoseconds +template <> struct Serializer { + static constexpr TypeId type_id = TypeId::DURATION; + + static inline void write_type_info(WriteContext &ctx) { + ctx.write_uint8(static_cast(type_id)); + } + + static inline void read_type_info(ReadContext &ctx) { + Serializer::read_type_info(ctx); + } + + static inline void write(const std::chrono::nanoseconds &ns, + WriteContext &ctx, RefMode ref_mode, bool write_type, + bool has_generics = false) { + Serializer::write(Duration(ns), ctx, ref_mode, write_type, + has_generics); + } + + static inline void write_data(const std::chrono::nanoseconds &ns, + WriteContext &ctx) { + Serializer::write_data(Duration(ns), ctx); + } + + static inline void write_data_generic(const std::chrono::nanoseconds &ns, + WriteContext &ctx, bool has_generics) { + write_data(ns, ctx); + } + + static inline std::chrono::nanoseconds + read(ReadContext &ctx, RefMode ref_mode, bool read_type) { + return Serializer::read(ctx, ref_mode, read_type).to_chrono(); + } + + static inline std::chrono::nanoseconds read_data(ReadContext &ctx) { + return Serializer::read_data(ctx).to_chrono(); + } + + static inline std::chrono::nanoseconds + read_with_type_info(ReadContext &ctx, RefMode ref_mode, + const TypeInfo &type_info) { + return read(ctx, ref_mode, false); + } +}; + +/// Serializer for std::chrono::time_point +/// Per xlang spec: serialized as int64 seconds + uint32 nanoseconds since Unix +/// epoch +template <> +struct Serializer> { + using ChronoTs = std::chrono::time_point; + static constexpr TypeId type_id = TypeId::TIMESTAMP; + + static inline void write_type_info(WriteContext &ctx) { + ctx.write_uint8(static_cast(type_id)); + } + + static inline void read_type_info(ReadContext &ctx) { + Serializer::read_type_info(ctx); + } + + static inline void write(const ChronoTs &tp, WriteContext &ctx, + RefMode ref_mode, bool write_type, + bool has_generics = false) { + Serializer::write(Timestamp(tp), ctx, ref_mode, write_type, + has_generics); + } + + static inline void write_data(const ChronoTs &tp, WriteContext &ctx) { + Serializer::write_data(Timestamp(tp), ctx); + } + + static inline void write_data_generic(const ChronoTs &tp, WriteContext &ctx, + bool has_generics) { + write_data(tp, ctx); + } + + static inline ChronoTs read(ReadContext &ctx, RefMode ref_mode, + bool read_type) { + return Serializer::read(ctx, ref_mode, read_type).to_chrono(); + } + + static inline ChronoTs read_data(ReadContext &ctx) { + return Serializer::read_data(ctx).to_chrono(); + } + + static inline ChronoTs read_with_type_info(ReadContext &ctx, RefMode ref_mode, + const TypeInfo &type_info) { + return read(ctx, ref_mode, false); + } +}; + } // namespace serialization } // namespace fory diff --git a/cpp/fory/serialization/type_resolver.h b/cpp/fory/serialization/type_resolver.h index 1b8eea007a..1a14a8570f 100644 --- a/cpp/fory/serialization/type_resolver.h +++ b/cpp/fory/serialization/type_resolver.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -1631,27 +1632,39 @@ get_type_info_with_resolver(TypeResolver &resolver) { template Result TypeResolver::register_any_type() { check_registration_thread(); - constexpr uint32_t static_type_id = - static_cast(Serializer::type_id); - TypeInfo *type_info = nullptr; - if (is_internal_type(static_type_id)) { - type_info = type_info_by_id_.get_or_default(static_type_id, nullptr); - if (FORY_PREDICT_FALSE(type_info == nullptr)) { - return Unexpected(Error::type_error("TypeInfo not found for type_id: " + - std::to_string(static_type_id))); - } + using ChronoTimestamp = std::chrono::time_point; + if constexpr (std::is_same_v || + std::is_same_v) { + // Chrono temporal serializers are explicit static adapters; + // Any reads for shared DURATION/TIMESTAMP TypeInfo stay on the Fory carrier + // types + return Unexpected(Error::type_error( + "Chrono temporal types are explicit static targets and cannot be " + "registered for std::any")); } else { - constexpr uint64_t ctid = type_index(); - type_info = type_info_by_ctid_.get_or_default(ctid, nullptr); - if (FORY_PREDICT_FALSE(type_info == nullptr)) { - return Unexpected(Error::type_error("Type not registered")); + constexpr uint32_t static_type_id = + static_cast(Serializer::type_id); + TypeInfo *type_info = nullptr; + if (is_internal_type(static_type_id)) { + type_info = type_info_by_id_.get_or_default(static_type_id, nullptr); + if (FORY_PREDICT_FALSE(type_info == nullptr)) { + return Unexpected(Error::type_error("TypeInfo not found for type_id: " + + std::to_string(static_type_id))); + } + } else { + constexpr uint64_t ctid = type_index(); + type_info = type_info_by_ctid_.get_or_default(ctid, nullptr); + if (FORY_PREDICT_FALSE(type_info == nullptr)) { + return Unexpected(Error::type_error("Type not registered")); + } } - } - type_info->harness.any_write_fn = &detail::any_write_adapter; - type_info->harness.any_read_fn = &detail::any_read_adapter; - register_type_internal_runtime(std::type_index(typeid(T)), type_info); - return Result(); + type_info->harness.any_write_fn = &detail::any_write_adapter; + type_info->harness.any_read_fn = &detail::any_read_adapter; + register_type_internal_runtime(std::type_index(typeid(T)), type_info); + return Result(); + } } template diff --git a/cpp/fory/type/temporal.h b/cpp/fory/type/temporal.h new file mode 100644 index 0000000000..a599fe7e5e --- /dev/null +++ b/cpp/fory/type/temporal.h @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +#include +#include +#include +#include + +namespace fory { + +/// Duration: absolute length of time as nanoseconds +class Duration { +public: + Duration() : ns_(0) {} + + explicit Duration(std::chrono::nanoseconds ns) : ns_(ns) {} + + std::chrono::nanoseconds to_chrono() const { return ns_; } + + int64_t count() const { return ns_.count(); } + + bool operator==(const Duration &other) const { return ns_ == other.ns_; } + + bool operator!=(const Duration &other) const { return !(*this == other); } + + bool operator<(const Duration &other) const { return ns_ < other.ns_; } + +private: + std::chrono::nanoseconds ns_; +}; + +/// Timestamp: point in time as nanoseconds since Unix epoch (Jan 1, 1970 UTC) +class Timestamp { +public: + using ChronoType = std::chrono::time_point; + Timestamp() : tp_() {} + + explicit Timestamp(ChronoType tp) : tp_(tp) {} + + explicit Timestamp(std::chrono::nanoseconds ns) : tp_(ns) {} + + ChronoType to_chrono() const { return tp_; } + + std::chrono::nanoseconds time_since_epoch() const { + return tp_.time_since_epoch(); + } + + bool operator==(const Timestamp &other) const { return tp_ == other.tp_; } + + bool operator!=(const Timestamp &other) const { return !(*this == other); } + + bool operator<(const Timestamp &other) const { return tp_ < other.tp_; } + +private: + ChronoType tp_; +}; + +/// Date: naive date without timezone as days since Unix epoch +class Date { +public: + Date() : days_since_epoch_(0) {} + explicit Date(int32_t days) : days_since_epoch_(days) {} + + int32_t days_since_epoch() const { return days_since_epoch_; } + + bool operator==(const Date &other) const { + return days_since_epoch_ == other.days_since_epoch_; + } + + bool operator!=(const Date &other) const { return !(*this == other); } + + bool operator<(const Date &other) const { + return days_since_epoch_ < other.days_since_epoch_; + } + +private: + int32_t days_since_epoch_; // Days since Jan 1, 1970 UTC +}; + +namespace serialization { + +// Keep legacy serialization names available with the carrier types +using Duration = ::fory::Duration; +using Timestamp = ::fory::Timestamp; +using Date = ::fory::Date; + +} // namespace serialization +} // namespace fory + +// ============================================================================ +// std::hash specializations for Fory temporal carrier types +// so they can be used as key types in unordered_map +// +// Hash inputs use the canonical numeric representation: +// Date -> days since epoch (int32_t) +// Duration -> total nanoseconds (int64_t) +// Timestamp -> epoch nanoseconds (int64_t) +// ============================================================================ + +namespace std { + +template <> struct hash { + size_t operator()(const fory::Date &d) const noexcept { + return hash{}(d.days_since_epoch()); + } +}; + +template <> struct hash { + size_t operator()(const fory::Duration &d) const noexcept { + return hash{}(d.count()); + } +}; + +template <> struct hash { + size_t operator()(const fory::Timestamp &t) const noexcept { + return hash{}(t.time_since_epoch().count()); + } +}; + +} // namespace std diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md index 633b90f1e0..c121352030 100644 --- a/docs/compiler/schema-idl.md +++ b/docs/compiler/schema-idl.md @@ -1311,38 +1311,38 @@ Underscore spellings for integer encoding are not FDL type names. ##### Date -| Language | Type | Notes | -| --------------------- | --------------------------- | --------------------------------------------------------------------------- | -| Java | `java.time.LocalDate` | | -| Python | `datetime.date` | | -| Go | `time.Time` | Time portion ignored | -| Rust | `fory::Date` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::NaiveDate` | -| C++ | `fory::serialization::Date` | | -| JavaScript/TypeScript | `Date` | | -| Dart | `LocalDate` | Fory package type | +| Language | Type | Notes | +| --------------------- | --------------------- | --------------------------------------------------------------------------- | +| Java | `java.time.LocalDate` | | +| Python | `datetime.date` | | +| Go | `time.Time` | Time portion ignored | +| Rust | `fory::Date` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::NaiveDate` | +| C++ | `fory::Date` | | +| JavaScript/TypeScript | `Date` | | +| Dart | `LocalDate` | Fory package type | ##### Timestamp -| Language | Type | Notes | -| --------------------- | -------------------------------- | ------------------------------------------------------------------------------- | -| Java | `java.time.Instant` | UTC-based | -| Python | `datetime.datetime` | | -| Go | `time.Time` | | -| Rust | `fory::Timestamp` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::NaiveDateTime` | -| C++ | `fory::serialization::Timestamp` | | -| JavaScript/TypeScript | `Date` | | -| Dart | `Timestamp` | Fory package type | +| Language | Type | Notes | +| --------------------- | ------------------- | ------------------------------------------------------------------------------- | +| Java | `java.time.Instant` | UTC-based | +| Python | `datetime.datetime` | | +| Go | `time.Time` | | +| Rust | `fory::Timestamp` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::NaiveDateTime` | +| C++ | `fory::Timestamp` | | +| JavaScript/TypeScript | `Date` | | +| Dart | `Timestamp` | Fory package type | ##### Duration -| Language | Type | Notes | -| -------- | ------------------------------- | -------------------------------------------------------------------------- | -| Java | `java.time.Duration` | | -| Python | `datetime.timedelta` | | -| Go | `time.Duration` | | -| Rust | `fory::Duration` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::Duration` | -| C++ | `fory::serialization::Duration` | | -| Dart | `Duration` | | +| Language | Type | Notes | +| -------- | -------------------- | -------------------------------------------------------------------------- | +| Java | `java.time.Duration` | | +| Python | `datetime.timedelta` | | +| Go | `time.Duration` | | +| Rust | `fory::Duration` | Set `rust_use_chrono_temporal_types = true` to generate `chrono::Duration` | +| C++ | `fory::Duration` | | +| Dart | `Duration` | | #### Any diff --git a/docs/guide/cpp/supported-types.md b/docs/guide/cpp/supported-types.md index c4dd9f0040..a80a69130b 100644 --- a/docs/guide/cpp/supported-types.md +++ b/docs/guide/cpp/supported-types.md @@ -189,29 +189,48 @@ OptionalInt value = 42; ## Temporal Types +`fory::Duration`, `fory::Timestamp`, and `fory::Date` are Fory-owned carrier +types declared by `fory/type/temporal.h`. They support `std::hash` and can be +used as `std::unordered_map` keys. + +FDL/codegen fields and dynamic `std::any` values use these Fory carrier types by +default. C++ `std::chrono` temporal types are supported as explicit +serialization and deserialization targets when the caller asks for those types. + ### Duration -`std::chrono::nanoseconds`: +Signed duration stored as nanoseconds. Construct from any `std::chrono` +duration that converts to `std::chrono::nanoseconds`, and call `to_chrono()` +to get the underlying value back: ```cpp -using Duration = std::chrono::nanoseconds; - -Duration d = std::chrono::seconds(30); +fory::Duration d(std::chrono::seconds(30)); auto bytes = fory.serialize(d).value(); -auto decoded = fory.deserialize(bytes).value(); +auto decoded = fory.deserialize(bytes).value(); + +// Convert to/from std::chrono +std::chrono::nanoseconds ns = decoded.to_chrono(); +int64_t count = decoded.count(); // total nanoseconds ``` ### Timestamp Point in time since Unix epoch: +Construct from a `fory::Timestamp::ChronoType` time_point or from nanoseconds +since epoch, and call `to_chrono()` to get the value back: ```cpp -using Timestamp = std::chrono::time_point; +using ChronoTs = fory::Timestamp::ChronoType; +auto now = std::chrono::time_point_cast( + std::chrono::system_clock::now()); + +fory::Timestamp ts(now); +auto bytes = fory.serialize(ts).value(); +auto decoded = fory.deserialize(bytes).value(); -Timestamp now = std::chrono::system_clock::now(); -auto bytes = fory.serialize(now).value(); -auto decoded = fory.deserialize(bytes).value(); +// Convert to/from std::chrono +ChronoTs tp = decoded.to_chrono(); +std::chrono::nanoseconds since_epoch = decoded.time_since_epoch(); ``` ### Date @@ -219,10 +238,10 @@ auto decoded = fory.deserialize(bytes).value(); Days since Unix epoch: ```cpp -Date date{18628}; // Days since 1970-01-01 +fory::Date date{18628}; // Days since 1970-01-01 auto bytes = fory.serialize(date).value(); -auto decoded = fory.deserialize(bytes).value(); +auto decoded = fory.deserialize(bytes).value(); ``` ## User-Defined Structs diff --git a/docs/guide/cpp/xlang-serialization.md b/docs/guide/cpp/xlang-serialization.md index fc943a7d4f..bd439a8b7d 100644 --- a/docs/guide/cpp/xlang-serialization.md +++ b/docs/guide/cpp/xlang-serialization.md @@ -190,11 +190,11 @@ Use the field metadata DSL's array node when the schema is dense `array`. ### Temporal Types -| C++ Type | Java Type | Python Type | Go Type | -| ----------- | ----------- | --------------- | --------------- | -| `Timestamp` | `Instant` | `datetime` | `time.Time` | -| `Duration` | `Duration` | `timedelta` | `time.Duration` | -| `Date` | `LocalDate` | `datetime.date` | `time.Time` | +| C++ Type | Java Type | Python Type | Go Type | +| ----------------- | ----------- | --------------- | --------------- | +| `fory::Timestamp` | `Instant` | `datetime` | `time.Time` | +| `fory::Duration` | `Duration` | `timedelta` | `time.Duration` | +| `fory::Date` | `LocalDate` | `datetime.date` | `time.Time` | ## Field Order Requirements diff --git a/docs/specification/xlang_type_mapping.md b/docs/specification/xlang_type_mapping.md index 1e7976fa06..a3e877710a 100644 --- a/docs/specification/xlang_type_mapping.md +++ b/docs/specification/xlang_type_mapping.md @@ -90,9 +90,9 @@ FDL spells them as an encoding modifier plus a semantic integer type. | named_ext | 32 | pojo/record | data class | object | struct/class | struct | struct | `[ForyStruct]` class/struct | @ForyStruct struct/class | @ForyStruct class | case class/class | data class/class | | union | 33 | Union | typing.Union | / | `std::variant` | / | tagged union enum | `[ForyUnion]` ADT record | tagged enum | @ForyUnion class | ADT enum | sealed class | | none | 36 | null | None | null | `std::monostate` | nil | `()` | null | nil | null | null | null | -| duration | 37 | Duration | timedelta | Number | duration | Duration | Duration | TimeSpan | Duration | Duration | java.time.Duration | kotlin.time.Duration | -| timestamp | 38 | Instant | datetime | Number | std::chrono::nanoseconds | Time | Timestamp | DateTime/DateTimeOffset | Date | Timestamp | java.time.Instant | java.time.Instant | -| date | 39 | LocalDate | datetime.date | Date | fory::serialization::Date | fory.Date | Date | DateOnly | LocalDate | LocalDate | java.time.LocalDate | java.time.LocalDate | +| duration | 37 | Duration | timedelta | Number | fory::Duration | Duration | Duration | TimeSpan | Duration | Duration | java.time.Duration | kotlin.time.Duration | +| timestamp | 38 | Instant | datetime | Number | fory::Timestamp | Time | Timestamp | DateTime/DateTimeOffset | Date | Timestamp | java.time.Instant | java.time.Instant | +| date | 39 | LocalDate | datetime.date | Date | fory::Date | fory.Date | Date | DateOnly | LocalDate | LocalDate | java.time.LocalDate | java.time.LocalDate | | decimal | 40 | BigDecimal | Decimal | Decimal | fory::serialization::Decimal | fory.Decimal | fory::Decimal | decimal | Decimal | Decimal | java.math.BigDecimal | java.math.BigDecimal | | binary | 41 | byte[] | bytes | / | `uint8_t[n]/vector` | `[n]uint8/[]T` | `Vec` | byte[] | Data | Uint8List | Array[Byte] | ByteArray | | `array` (bool_array) | 43 | bool[] | BoolArray / ndarray(np.bool\_) | BoolArray / Type.boolArray() | `bool[n]` | `[n]bool/[]T` | `Vec` | bool[] | [Bool] + @ArrayField | BoolList | Array[Boolean] | BooleanArray | @@ -127,6 +127,9 @@ Notes: of the current xlang type-mapping surface. - Current xlang uses `*_ARRAY` for one-dimensional primitive arrays and nested `list` for multi-dimensional arrays. +- C++ xlang `date`, `timestamp`, and `duration` map to `fory::Date`, `fory::Timestamp`, and + `fory::Duration` for generated schemas and dynamic `std::any` values. `std::chrono` temporal + types are explicit C++ serialization and deserialization targets only. - Kotlin KSP xlang maps `UByte`, `UShort`, `UInt`, and `ULong` to `uint8`, `uint16`, `uint32`, and `uint64`. Kotlin primitive and unsigned array carriers map to dense arrays. `ByteArray` maps to `binary` by default and to diff --git a/integration_tests/idl_tests/cpp/main.cc b/integration_tests/idl_tests/cpp/main.cc index 8e41bd1d54..cb41a987a1 100644 --- a/integration_tests/idl_tests/cpp/main.cc +++ b/integration_tests/idl_tests/cpp/main.cc @@ -24,12 +24,12 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include @@ -471,61 +471,69 @@ struct ExampleMessageArrays { }; struct ExampleMessageMaps { - std::map string_values_by_bool; - std::map string_values_by_int8; - std::map string_values_by_int16; - std::map string_values_by_fixed_i32; - std::map string_values_by_varint_i32; - std::map string_values_by_fixed_i64; - std::map string_values_by_varint_i64; - std::map string_values_by_tagged_i64; - std::map string_values_by_uint8; - std::map string_values_by_uint16; - std::map string_values_by_fixed_u32; - std::map string_values_by_varint_u32; - std::map string_values_by_fixed_u64; - std::map string_values_by_varint_u64; - std::map string_values_by_tagged_u64; - std::map string_values_by_string; - std::map + std::unordered_map string_values_by_bool; + std::unordered_map string_values_by_int8; + std::unordered_map string_values_by_int16; + std::unordered_map string_values_by_fixed_i32; + std::unordered_map string_values_by_varint_i32; + std::unordered_map string_values_by_fixed_i64; + std::unordered_map string_values_by_varint_i64; + std::unordered_map string_values_by_tagged_i64; + std::unordered_map string_values_by_uint8; + std::unordered_map string_values_by_uint16; + std::unordered_map string_values_by_fixed_u32; + std::unordered_map string_values_by_varint_u32; + std::unordered_map string_values_by_fixed_u64; + std::unordered_map string_values_by_varint_u64; + std::unordered_map string_values_by_tagged_u64; + std::unordered_map string_values_by_string; + std::unordered_map string_values_by_timestamp; - std::map + std::unordered_map string_values_by_duration; - std::map string_values_by_enum; - std::map float16_values_by_name; - std::map maybe_float16_values_by_name; - std::map bfloat16_values_by_name; - std::map maybe_bfloat16_values_by_name; - std::map> bytes_values_by_name; - std::map date_values_by_name; - std::map decimal_values_by_name; - std::map message_values_by_name; - std::map union_values_by_name; - std::map> uint8_array_values_by_name; - std::map> float32_array_values_by_name; - std::map> int32_array_values_by_name; - std::map string_values_by_date; - std::map bool_values_by_name; - std::map int8_values_by_name; - std::map int16_values_by_name; - std::map fixed_i32_values_by_name; - std::map varint_i32_values_by_name; - std::map fixed_i64_values_by_name; - std::map varint_i64_values_by_name; - std::map tagged_i64_values_by_name; - std::map uint8_values_by_name; - std::map uint16_values_by_name; - std::map fixed_u32_values_by_name; - std::map varint_u32_values_by_name; - std::map fixed_u64_values_by_name; - std::map varint_u64_values_by_name; - std::map tagged_u64_values_by_name; - std::map float32_values_by_name; - std::map float64_values_by_name; - std::map + std::unordered_map string_values_by_enum; + std::unordered_map float16_values_by_name; + std::unordered_map maybe_float16_values_by_name; + std::unordered_map bfloat16_values_by_name; + std::unordered_map + maybe_bfloat16_values_by_name; + std::unordered_map> bytes_values_by_name; + std::unordered_map + date_values_by_name; + std::unordered_map + decimal_values_by_name; + std::unordered_map message_values_by_name; + std::unordered_map union_values_by_name; + std::unordered_map> + uint8_array_values_by_name; + std::unordered_map> + float32_array_values_by_name; + std::unordered_map> + int32_array_values_by_name; + std::unordered_map + string_values_by_date; + std::unordered_map bool_values_by_name; + std::unordered_map int8_values_by_name; + std::unordered_map int16_values_by_name; + std::unordered_map fixed_i32_values_by_name; + std::unordered_map varint_i32_values_by_name; + std::unordered_map fixed_i64_values_by_name; + std::unordered_map varint_i64_values_by_name; + std::unordered_map tagged_i64_values_by_name; + std::unordered_map uint8_values_by_name; + std::unordered_map uint16_values_by_name; + std::unordered_map fixed_u32_values_by_name; + std::unordered_map varint_u32_values_by_name; + std::unordered_map fixed_u64_values_by_name; + std::unordered_map varint_u64_values_by_name; + std::unordered_map tagged_u64_values_by_name; + std::unordered_map float32_values_by_name; + std::unordered_map float64_values_by_name; + std::unordered_map timestamp_values_by_name; - std::map duration_values_by_name; - std::map enum_values_by_name; + std::unordered_map + duration_values_by_name; + std::unordered_map enum_values_by_name; FORY_STRUCT( ExampleMessageMaps, @@ -831,7 +839,8 @@ example_peer::ExampleMessage BuildExampleMessage() { static_cast(3)}; message.date_value = Date(19756); message.timestamp_value = ts(1706933106); - message.duration_value = std::chrono::seconds(42) + Duration(7000); + message.duration_value = + Duration(std::chrono::seconds(42) + std::chrono::nanoseconds(7000)); message.decimal_value = Decimal::from_int64(12345, 2); message.enum_value = example_peer::ExampleState::READY; message.message_value = leaf; @@ -864,8 +873,8 @@ example_peer::ExampleMessage BuildExampleMessage() { {static_cast(6), static_cast(7)}}; message.date_list = {Date(19723), Date(19724)}; message.timestamp_list = {ts(1704067200), ts(1704153600)}; - message.duration_list = {std::chrono::milliseconds(1), - std::chrono::seconds(2)}; + message.duration_list = {Duration(std::chrono::milliseconds(1)), + Duration(std::chrono::seconds(2))}; message.decimal_list = {Decimal::from_int64(125, 2), Decimal::from_int64(250, 2)}; message.enum_list = {example_peer::ExampleState::UNKNOWN, @@ -909,7 +918,8 @@ example_peer::ExampleMessage BuildExampleMessage() { message.string_values_by_tagged_u64 = {{9876543212ULL, "tagged-u64"}}; message.string_values_by_string = {{"name", "value"}}; message.string_values_by_timestamp = {{ts(1709528767), "time"}}; - message.string_values_by_duration = {{std::chrono::seconds(9), "duration"}}; + message.string_values_by_duration = { + {Duration(std::chrono::seconds(9)), "duration"}}; message.string_values_by_enum = { {example_peer::ExampleState::READY, "ready"}}; message.float16_values_by_name = {{"f16", f16(1.25F)}}; @@ -946,7 +956,8 @@ example_peer::ExampleMessage BuildExampleMessage() { message.float32_values_by_name = {{"float32", 3.25F}}; message.float64_values_by_name = {{"float64", 6.5}}; message.timestamp_values_by_name = {{"timestamp", ts(1717747750)}}; - message.duration_values_by_name = {{"duration", std::chrono::seconds(10)}}; + message.duration_values_by_name = { + {"duration", Duration(std::chrono::seconds(10))}}; message.enum_values_by_name = {{"enum", example_peer::ExampleState::FAILED}}; return message; } @@ -1063,7 +1074,7 @@ fory::Result RunEvolvingRoundTrip() { return fory::Result(); } -using StringMap = std::map; +using StringMap = std::unordered_map; fory::Result RunRoundTrip(bool compatible) { auto fory = fory::serialization::Fory::builder()