This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 20f51a1d2 fix(compiler): alias C++ union case types in metadata macros 
(#3814)
20f51a1d2 is described below

commit 20f51a1d2992c6c877f28d360dcd95de43128a7e
Author: Peiyang He <[email protected]>
AuthorDate: Sat Jul 4 15:44:58 2026 +0800

    fix(compiler): alias C++ union case types in metadata macros (#3814)
    
    ## Why?
    
    The C++ compiler can emit union metadata macros that fail to compile
    when a union case payload type contains a C++ template comma, e.g.
    `std::unordered_map<K, V>`.
    
    There are two affected paths:
    
    - `FORY_UNION(...)` for unions with at most 16 cases.
    - `FORY_UNION_IDS(...)` plus `FORY_UNION_CASE(...)` for larger unions.
    
    Example IDL:
    
    ```
    package probe.cpp_macro_fory_union;
    
    union SmallChoice {
      map<string, any> by_name = 1;
      string name = 2;
    }
    ```
    
    Generated C++ code:
    
    ```c++
    FORY_UNION(probe::cpp_macro_fory_union::SmallChoice,
      (by_name, std::unordered_map<std::string, std::any>, 
fory::F(1).map(fory::T::string(), fory::FieldNodeSpec{})),
      (name, std::string, fory::F(2))
    );
    ```
    
    Observed compile error:
    
    ```text
    /home/hpy/fory/cpp/fory/serialization/union_serializer.h:1203:18: error: 
'FORY_UNION_CASE_META_fory' has not been declared
    
/tmp/fory_cpp_macro_pr_wql7zo9p/generated/probe_cpp_macro_fory_union.h:140:4: 
error: 'by_name' was not declared in this scope
    
/tmp/fory_cpp_macro_pr_wql7zo9p/generated/probe_cpp_macro_fory_union.h:140:54: 
error: expected primary-expression before ',' token
    /home/hpy/fory/cpp/fory/serialization/union_serializer.h:1192:18: error: 
'FORY_UNION_CASE_TYPE_fory' was not declared in this scope; did you mean 
'FORY_UNION_CASE_TYPE_2'?
    ```
    
    **Root cause**:
    
    The generator passed **raw** C++ type names into `FORY_UNION` case
    tuples.
    `FORY_UNION` rendered each case as `(case_name, case_type, meta)`:
    
    
    
https://github.com/apache/fory/blob/23aa3f98ee56687c555c681a488a69d8cfbb5832/compiler/fory_compiler/generators/cpp.py#L1203-L1221
    
    This is unsafe for template types such as
    `std::unordered_map<std::string, std::any>`.
    
    C++ macro preprocessing happens before the compiler understands that the
    comma inside `<K, V>` belongs to a template argument list. The
    preprocessor treats that comma as a macro argument separator.
    
    `FORY_UNION` expects each case tuple to have either two entries `(name,
    meta)` or three entries `(name, type, meta)`:
    
    
    
https://github.com/apache/fory/blob/23aa3f98ee56687c555c681a488a69d8cfbb5832/cpp/fory/serialization/union_serializer.h#L1179-L1207
    
    When the generated tuple contains:
    
    ```cpp
    (by_name, std::unordered_map<std::string, std::any>, meta)
    ```
    
    the preprocessor sees more entries than intended because of the comma in
    `std::unordered_map<std::string, std::any>`.
    That breaks tuple-size selection and causes invalid macro names such as
    `FORY_UNION_CASE_META_fory` or `FORY_UNION_CASE_TYPE_fory`.
    
    
    ## What does this PR do?
    
    Fix this by generating a union-scoped type alias for case payload types
    whose rendered C++ type
    contains a comma, and use that alias in `FORY_UNION` and
    `FORY_UNION_CASE` metadata.
    
    The generated alias name is derived from the union case name using the
    pattern:
    
    `ForyCase` + PascalCase(case_name) + `Type`.
    
    e.g.
    
    ```cpp
    class SmallChoice final {
     public:
      using ForyCaseByNameType = std::unordered_map<std::string, std::any>;
      ...
    };
    
    FORY_UNION(probe::cpp_macro_fory_union::SmallChoice,
      (by_name, probe::cpp_macro_fory_union::SmallChoice::ForyCaseByNameType, 
fory::F(1).map(...)),
      (name, std::string, fory::F(2))
    );
    ```
    
    The macro now receives `SmallChoice::ForyCaseByNameType`, which contains
    no template comma and can be parsed safely by the preprocessor.
    
    Note: I will handle naming collisions universally in a new PR.
    
    ## Related issues
    
    N/A.
    
    ## AI Contribution Checklist
    
    - [X] Substantial AI assistance was used in this PR: `no`
    
    ## Does this PR introduce any user-facing change?
    
    N/A.
    
    ## Benchmark
    
    N/A.
---
 compiler/fory_compiler/generators/cpp.py           | 63 ++++++++++++++-------
 .../fory_compiler/tests/test_generated_code.py     | 65 ++++++++++++++++++++++
 2 files changed, 108 insertions(+), 20 deletions(-)

diff --git a/compiler/fory_compiler/generators/cpp.py 
b/compiler/fory_compiler/generators/cpp.py
index f7302dd15..c5d4de1a3 100644
--- a/compiler/fory_compiler/generators/cpp.py
+++ b/compiler/fory_compiler/generators/cpp.py
@@ -1063,9 +1063,19 @@ class CppGenerator(BaseGenerator):
         body_indent = f"{indent}  "
 
         case_enum = f"{class_name}Case"
-        case_types = [
+        raw_case_types = [
             self.get_union_case_type(field, parent_stack) for field in 
union.fields
         ]
+        case_aliases = [
+            f"ForyCase{self.to_pascal_case(field.name)}Type"
+            if "," in case_type
+            else None
+            for field, case_type in zip(union.fields, raw_case_types)
+        ]
+        case_types = [
+            alias if alias is not None else case_type
+            for alias, case_type in zip(case_aliases, raw_case_types)
+        ]
         variant_type = f"std::variant<{', '.join(case_types)}>"
 
         comment = self.format_type_id_comment(union, f"{indent}//")
@@ -1080,6 +1090,12 @@ class CppGenerator(BaseGenerator):
         lines.append(f"{body_indent}  }};")
         lines.append("")
 
+        for alias, case_type in zip(case_aliases, raw_case_types):
+            if alias is not None:
+                lines.append(f"{body_indent}  using {alias} = {case_type};")
+        if any(alias is not None for alias in case_aliases):
+            lines.append("")
+
         lines.append(f"{body_indent}  {class_name}() = default;")
         lines.append("")
 
@@ -1204,15 +1220,8 @@ class CppGenerator(BaseGenerator):
             union_type = self.get_namespaced_type_name(union.name, 
parent_stack)
             lines.append(f"FORY_UNION({union_type},")
             for index, field in enumerate(union.fields):
-                case_type = self.generate_namespaced_type(
-                    field.field_type,
-                    False,
-                    field.ref,
-                    field.element_optional,
-                    field.element_ref,
-                    False,
-                    False,
-                    parent_stack,
+                case_type = self.get_union_case_macro_type(
+                    field, union_type, parent_stack
                 )
                 case_ctor = self.to_snake_case(field.name)
                 meta = self.get_union_field_meta(field)
@@ -1225,16 +1234,7 @@ class CppGenerator(BaseGenerator):
         case_ids = ", ".join(str(field.number) for field in union.fields)
         lines.append(f"FORY_UNION_IDS({union_type}, {case_ids});")
         for field in union.fields:
-            case_type = self.generate_namespaced_type(
-                field.field_type,
-                False,
-                field.ref,
-                field.element_optional,
-                field.element_ref,
-                False,
-                False,
-                parent_stack,
-            )
+            case_type = self.get_union_case_macro_type(field, union_type, 
parent_stack)
             case_ctor = self.to_snake_case(field.name)
             meta = self.get_union_field_meta(field)
             lines.append(
@@ -1243,6 +1243,29 @@ class CppGenerator(BaseGenerator):
 
         return lines
 
+    def get_union_case_macro_type(
+        self,
+        field: Field,
+        union_type: str,
+        parent_stack: List[Message],
+    ) -> str:
+        """Return the C++ type name used in FORY_UNION and FORY_UNION_CASE 
macros."""
+        case_type = self.generate_namespaced_type(
+            field.field_type,
+            False,
+            field.ref,
+            field.element_optional,
+            field.element_ref,
+            False,
+            False,
+            parent_stack,
+        )
+        # FORY_UNION and FORY_UNION_CASE split macro arguments on commas,
+        # so raw template types such as std::unordered_map<K, V> need an alias.
+        if "," in case_type:
+            return 
f"{union_type}::ForyCase{self.to_pascal_case(field.name)}Type"
+        return case_type
+
     def get_union_case_type(self, field: Field, parent_stack: List[Message]) 
-> str:
         """Return the C++ type for a union case."""
         return self.generate_type(
diff --git a/compiler/fory_compiler/tests/test_generated_code.py 
b/compiler/fory_compiler/tests/test_generated_code.py
index 16e8fca31..c61c9c2ce 100644
--- a/compiler/fory_compiler/tests/test_generated_code.py
+++ b/compiler/fory_compiler/tests/test_generated_code.py
@@ -1126,6 +1126,71 @@ def 
test_cpp_generator_supports_decimal_fields_and_unions():
     assert "(amount, fory::serialization::Decimal, fory::F(1))" in cpp_output
 
 
+def test_cpp_union_aliases_comma_payload_types():
+    schema = parse_fdl(
+        dedent(
+            """
+            package gen;
+
+            union MapChoice {
+                map<string, any> by_name = 1;
+                map<string, int32> counts = 2;
+                list<any> values = 3;
+                string name = 4;
+            }
+
+            union LargeChoice {
+                map<string, int32> counts = 1;
+                bool enabled = 2;
+                int8 i8 = 3;
+                int16 i16 = 4;
+                int32 i32 = 5;
+                int64 i64 = 6;
+                uint8 u8 = 7;
+                uint16 u16 = 8;
+                uint32 u32 = 9;
+                uint64 u64 = 10;
+                float32 f32 = 11;
+                float64 f64 = 12;
+                string name = 13;
+                bytes blob = 14;
+                decimal amount = 15;
+                date day = 16;
+                timestamp ts = 17;
+            }
+            """
+        )
+    )
+
+    cpp_output = render_files(generate_files(schema, CppGenerator))
+    assert (
+        "using ForyCaseByNameType = std::unordered_map<std::string, std::any>;"
+        in cpp_output
+    )
+    assert (
+        "using ForyCaseCountsType = std::unordered_map<std::string, int32_t>;"
+        in cpp_output
+    )
+    assert (
+        "(by_name, gen::MapChoice::ForyCaseByNameType, "
+        "fory::F(1).map(fory::T::string(), fory::FieldNodeSpec{}))" in 
cpp_output
+    )
+    assert (
+        "(counts, gen::MapChoice::ForyCaseCountsType, "
+        "fory::F(2).map(fory::T::string(), fory::T::int32().varint()))" in 
cpp_output
+    )
+    assert (
+        "(values, std::vector<std::any>, "
+        "fory::F(3).list(fory::FieldNodeSpec{}))" in cpp_output
+    )
+    assert "(name, std::string, fory::F(4))" in cpp_output
+    assert (
+        "FORY_UNION_CASE(gen::LargeChoice, 1, "
+        "gen::LargeChoice::ForyCaseCountsType, gen::LargeChoice::counts, "
+        "fory::F(1).map(fory::T::string(), fory::T::int32().varint()));" in 
cpp_output
+    )
+
+
 def test_cpp_omits_equality_for_any_types():
     schema = parse_fdl(
         dedent(


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to