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]