Skip to content

Commit e329157

Browse files
committed
feat: add GenericClass implementation with tests
Implements runtime-parameterized generic classes for inter-DLL compatibility. **GenericClass Runtime Class:** - New reflection class type for runtime-parameterized types - Auto-detects type parameters from m_*Type fields - Supports CreateDefaultObject(span<const Class*>) for instantiation - Coexists with TemplateClass for compile-time templates **Code Generation:** - ClangParser: Auto-detection of m_*Type fields matching "const Class*" - CppGenerator: Generates InternalXxxGenericClass with type injection - HeaderGenerator: Generates interface matching regular classes - Includes GenericClass.hpp in generated files **Parser & Macros:** - CCT_GENERIC_CLASS() annotation macro - Detection of [[cct::generic_class]] attributes - Namespace registration of generic classes
1 parent 9d00e39 commit e329157

File tree

18 files changed

+955
-14
lines changed

18 files changed

+955
-14
lines changed

README.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,129 @@ The package generator:
306306
3. For each explicit instantiation, generates a specialization class with the concrete type information
307307
4. Registers all specializations in the reflection namespace for runtime discovery
308308

309+
---
310+
311+
### Generic class support (runtime-parameterized types)
312+
313+
Concerto Reflection also supports **generic classes** – classes whose member variables store runtime type parameters. Unlike C++ templates which are resolved at compile-time, generic classes allow you to work with polymorphic type parameters at runtime without requiring explicit specializations.
314+
315+
#### Key differences: Templates vs Generics
316+
317+
| Aspect | Template Class | Generic Class |
318+
|--------|---|---|
319+
| **Type Resolution** | Compile-time (instantiation-based) | Runtime (parameter-based) |
320+
| **Specializations** | Explicit instantiations required | Single definition works for all type arguments |
321+
| **Type Safety** | Full compile-time checking | Type safety via `const Class*` pointers |
322+
| **Cross-DLL Compatibility** | Each specialization needs code generation | Single definition works across DLLs |
323+
| **Memory Model** | Separate binary code per specialization | Shared implementation, parameterized data |
324+
| **Use Cases** | Known type combinations at build time | Dynamic, unknown type combinations at runtime |
325+
326+
#### Annotating generic classes
327+
328+
Mark a class as generic using `CCT_GENERIC_CLASS()` and annotate type parameter fields with `CCT_GENERIC_TYPE()`:
329+
330+
```cpp
331+
class CCT_CLASS() CCT_GENERIC_CLASS() GenericContainer : public cct::refl::Object
332+
{
333+
public:
334+
GenericContainer() : m_elementType(nullptr) {}
335+
336+
CCT_MEMBER()
337+
CCT_GENERIC_TYPE()
338+
const cct::refl::Class* m_elementType;
339+
340+
CCT_METHOD()
341+
const cct::refl::Class* GetElementType() const
342+
{
343+
return m_elementType;
344+
}
345+
346+
CCT_OBJECT(GenericContainer);
347+
};
348+
```
349+
350+
Key points:
351+
- Type parameter fields **must** be of type `const cct::refl::Class*`
352+
- Use `CCT_GENERIC_TYPE()` annotation to mark type parameters
353+
- The macro automatically includes `CCT_MEMBER()` annotation
354+
- Provide getter methods to access type parameters
355+
356+
#### Using generic classes at runtime
357+
358+
Generic classes are instantiated with type arguments passed to `CreateDefaultObject()`:
359+
360+
```cpp
361+
const auto* containerClass = cct::refl::GetClassByName("cct::sample::GenericContainer");
362+
if (containerClass && containerClass->IsGenericClass())
363+
{
364+
auto* genericClass = dynamic_cast<cct::refl::GenericClass*>(
365+
const_cast<cct::refl::Class*>(containerClass));
366+
367+
// Get a type to use as argument
368+
const auto* int32Class = cct::refl::GetClassByName("cct::refl::Int32");
369+
370+
// Create instance with type argument(s)
371+
std::vector<const cct::refl::Class*> typeArgs = { int32Class };
372+
auto obj = genericClass->CreateDefaultObject(
373+
std::span<const cct::refl::Class*>(typeArgs));
374+
375+
if (obj)
376+
{
377+
// Access the type parameter
378+
auto container = dynamic_cast<YourGenericContainer*>(obj.get());
379+
const auto* elementType = container->GetElementType();
380+
// Use the element type information...
381+
}
382+
}
383+
```
384+
385+
#### Multi-parameter generics
386+
387+
Generic classes can have multiple type parameters:
388+
389+
```cpp
390+
class CCT_CLASS() CCT_GENERIC_CLASS() GenericPair : public cct::refl::Object
391+
{
392+
public:
393+
GenericPair() : m_keyType(nullptr), m_valueType(nullptr) {}
394+
395+
CCT_MEMBER()
396+
CCT_GENERIC_TYPE()
397+
const cct::refl::Class* m_keyType;
398+
399+
CCT_MEMBER()
400+
CCT_GENERIC_TYPE()
401+
const cct::refl::Class* m_valueType;
402+
403+
CCT_METHOD()
404+
const cct::refl::Class* GetKeyType() const { return m_keyType; }
405+
406+
CCT_METHOD()
407+
const cct::refl::Class* GetValueType() const { return m_valueType; }
408+
409+
CCT_OBJECT(GenericPair);
410+
};
411+
```
412+
413+
Instantiate with multiple type arguments:
414+
415+
```cpp
416+
const auto* int32Class = cct::refl::GetClassByName("cct::refl::Int32");
417+
const auto* int64Class = cct::refl::GetClassByName("cct::refl::Int64");
418+
419+
std::vector<const cct::refl::Class*> typeArgs = { int32Class, int64Class };
420+
auto obj = genericClass->CreateDefaultObject(
421+
std::span<const cct::refl::Class*>(typeArgs));
422+
```
423+
424+
#### How it works
425+
426+
The package generator:
427+
1. Detects `CCT_GENERIC_CLASS()` annotations on class declarations
428+
2. Identifies type parameter fields via `CCT_GENERIC_TYPE()` annotations
429+
3. Generates code that stores type parameters in member variables during instantiation
430+
4. Provides `CreateDefaultObject(span<const Class*>)` to pass type arguments at runtime
431+
309432
---
310433
---
311434

Src/Concerto/PackageGenerator/ClangParser/ClangParser.cpp

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ namespace
9999
constexpr std::array knownAnnotations = {
100100
"cct::Package"sv,
101101
"cct::Class"sv,
102+
"cct::GenericClass"sv,
103+
"cct::GenericType"sv,
102104
"cct::Member"sv,
103105
"cct::NativeMember"sv,
104106
"cct::Method"sv,
@@ -108,7 +110,8 @@ namespace
108110

109111
std::string_view annotation(AnnotateAttribute->getAnnotation().begin(), AnnotateAttribute->getAnnotation().end());
110112
bool known = false;
111-
for (auto& annotationName : knownAnnotations)
113+
114+
for (const auto& annotationName : knownAnnotations)
112115
{
113116
if (annotationName == annotation)
114117
{
@@ -117,7 +120,7 @@ namespace
117120
}
118121
}
119122

120-
if (known == false)
123+
if (!known)
121124
return false;
122125

123126
outScope = annotation;
@@ -231,16 +234,18 @@ namespace cct
231234

232235
std::optional<std::pair<std::string, TomlAttributes>> classAttr;
233236
std::optional<std::pair<std::string, TomlAttributes>> packageAttr;
237+
std::optional<std::pair<std::string, TomlAttributes>> genericClassAttr;
234238
for (const auto* A : recordDeclaration->attrs())
235239
{
236240
std::string scope; TomlAttributes attrs;
237241
if (!ExtractCctAttribute(A, *m_astContext, *m_sourceManager, *m_langOptions, scope, attrs))
238242
continue;
239243
if (scope == "cct::Class") classAttr = std::make_pair(scope, attrs);
240244
else if (scope == "cct::Package") packageAttr = std::make_pair(scope, attrs);
245+
else if (scope == "cct::GenericClass") genericClassAttr = std::make_pair(scope, attrs);
241246
}
242247
auto txt = recordDeclaration->getNameAsString();
243-
if (!classAttr && !packageAttr)
248+
if (!classAttr && !packageAttr && !genericClassAttr)
244249
return; // Not a reflection-relevant class
245250

246251
if (packageAttr)
@@ -261,13 +266,53 @@ namespace cct
261266
return;
262267
}
263268

264-
if (!classAttr)
265-
return; // Only keep classes with cct::Class
269+
// Determine which type of class this is
270+
if (!classAttr && !genericClassAttr)
271+
return; // Only keep classes with cct::Class or cct::GenericClass
266272

267273
Class cls;
268274
cls.name = recordDeclaration->getNameAsString();
269-
cls.scope = "cct::Class";
270-
cls.tomlAttributes = GetAttributesOr(classAttr->second, {});
275+
276+
// Handle generic class
277+
if (genericClassAttr)
278+
{
279+
cls.scope = "cct::GenericClass";
280+
cls.isGenericClass = true;
281+
cls.tomlAttributes = GetAttributesOr(genericClassAttr->second, {});
282+
283+
// Detect explicit type parameters via CCT_GENERIC_TYPE() annotations
284+
for (const auto* F : recordDeclaration->fields())
285+
{
286+
auto fieldName = F->getNameAsString();
287+
auto fieldType = QualTypeToString(F->getType(), *m_astContext);
288+
289+
// Check if field has CCT_GENERIC_TYPE annotation
290+
for (const auto* attr : F->getAttrs())
291+
{
292+
if (const auto* annotationAttr = dyn_cast<clang::AnnotateAttr>(attr))
293+
{
294+
auto annotation = annotationAttr->getAnnotation().str();
295+
if (annotation == "cct::GenericType")
296+
{
297+
// Verify field type is "const Class*"
298+
if (fieldType == "const cct::refl::Class *" ||
299+
fieldType == "const Class *" ||
300+
fieldType == "const cct::refl::Class*" ||
301+
fieldType == "const Class*")
302+
{
303+
cls.genericTypeParameterFields.push_back(fieldName);
304+
}
305+
break;
306+
}
307+
}
308+
}
309+
}
310+
}
311+
else
312+
{
313+
cls.scope = "cct::Class";
314+
cls.tomlAttributes = GetAttributesOr(classAttr->second, {});
315+
}
271316

272317
// Base class (first base)
273318
if (recordDeclaration->getNumBases() > 0)

Src/Concerto/PackageGenerator/CppGenerator/CppGenerator.cpp

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace cct
1717
Write("#include <Concerto/Core/Assert.hpp>");
1818
Write("#include <Concerto/Core/TypeInfo/TypeInfo.hpp>");
1919
Write("#include \"Concerto/Reflection/GlobalNamespace/GlobalNamespace.hpp\"");
20+
Write("#include <Concerto/Reflection/GenericClass/GenericClass.hpp>");
2021
Write("#include \"{}Package.gen.hpp\"", package.name);
2122

2223
for (auto& header : args)
@@ -31,7 +32,9 @@ namespace cct
3132
GenerateEnum(enum_);
3233
for (auto& klass : package.classes)
3334
{
34-
if (klass.isTemplateClass)
35+
if (klass.isGenericClass)
36+
GenerateGenericClass({}, klass);
37+
else if (klass.isTemplateClass)
3538
GenerateTemplateClass({}, klass);
3639
else
3740
GenerateClass({}, klass);
@@ -53,7 +56,9 @@ namespace cct
5356
GenerateEnum(enum_);
5457
for (auto& klass : ns.classes)
5558
{
56-
if (klass.isTemplateClass)
59+
if (klass.isGenericClass)
60+
GenerateGenericClass(namespaceChain + "::"s + std::string(ns.name), klass);
61+
else if (klass.isTemplateClass)
5762
GenerateTemplateClass(namespaceChain + "::"s + std::string(ns.name), klass);
5863
else
5964
GenerateClass(namespaceChain + "::"s + std::string(ns.name), klass);
@@ -100,7 +105,9 @@ namespace cct
100105
LeaveScope();
101106
for (auto& klass : ns.classes)
102107
{
103-
if (klass.isTemplateClass)
108+
if (klass.isGenericClass)
109+
Write("AddClass(std::make_unique<{}::Internal{}GenericClass>());", ns.name, klass.name);
110+
else if (klass.isTemplateClass)
104111
{
105112
Write("AddClass(std::make_unique<{}::Internal{}TemplateClass>());", ns.name, klass.name);
106113
for (const auto& specialization : klass.templateSpecializations)
@@ -274,6 +281,110 @@ namespace cct
274281
NewLine();
275282
}
276283

284+
void CppGenerator::GenerateGenericClass(std::string_view ns, const Class& klass)
285+
{
286+
Write("class Internal{}GenericClass : public cct::refl::GenericClass", klass.name);
287+
EnterScope();
288+
{
289+
Write("public:");
290+
Write("Internal{}GenericClass() : cct::refl::GenericClass(nullptr, \"{}\"s, nullptr)",
291+
klass.name, klass.name);
292+
EnterScope();
293+
LeaveScope();
294+
295+
NewLine();
296+
Write("void Initialize() override");
297+
EnterScope();
298+
{
299+
if (!ns.empty())
300+
Write("SetNamespace(GlobalNamespace::Get().GetNamespaceByName(\"{}\"sv));", ns);
301+
302+
if (!klass.base.empty())
303+
{
304+
Write("const Class* baseClass = GetClassByName(\"{}\"sv);", klass.base);
305+
Write("CCT_ASSERT(baseClass != nullptr, \"Could not find class '{}'\");", klass.base);
306+
Write("SetBaseClass(baseClass);");
307+
}
308+
309+
NewLine();
310+
Write("SetTypeParameterCount({});", klass.genericTypeParameterFields.size());
311+
312+
for (const auto& fieldName : klass.genericTypeParameterFields)
313+
Write("AddTypeParameter(\"{}\");", fieldName);
314+
315+
NewLine();
316+
// Add members and methods as in regular class
317+
for (auto& member : klass.members)
318+
{
319+
if (member.isNative)
320+
Write(R"(AddNativeMemberVariable("{}", cct::TypeId<{}>());)", member.name, member.type);
321+
else
322+
Write(R"(AddMemberVariable("{}", cct::refl::GetClassByName("{}"sv));)", member.name, member.type);
323+
}
324+
325+
}
326+
LeaveScope();
327+
328+
NewLine();
329+
Write("std::unique_ptr<cct::refl::Object> CreateDefaultObject() const override");
330+
EnterScope();
331+
{
332+
Write("CCT_ASSERT_FALSE(\"Cannot instantiate generic class directly. Use CreateDefaultObject(std::span<const Class*>)\");");
333+
Write("return nullptr;");
334+
}
335+
LeaveScope();
336+
337+
NewLine();
338+
Write("std::unique_ptr<cct::refl::Object> CreateDefaultObject(");
339+
Write(" std::span<const cct::refl::Class*> typeArgs) const override");
340+
EnterScope();
341+
{
342+
Write("if (typeArgs.size() != {})", klass.genericTypeParameterFields.size());
343+
EnterScope();
344+
{
345+
Write("CCT_ASSERT_FALSE(\"Expected {} type arguments, got {{}}\", typeArgs.size());",
346+
klass.genericTypeParameterFields.size());
347+
Write("return nullptr;");
348+
}
349+
LeaveScope();
350+
351+
if (ns.empty())
352+
Write("auto obj = std::make_unique<{}>();", klass.name);
353+
else
354+
Write("auto obj = std::make_unique<{}::{}>();", ns, klass.name);
355+
356+
// Inject type parameters
357+
for (std::size_t i = 0; i < klass.genericTypeParameterFields.size(); ++i)
358+
Write("obj->{} = typeArgs[{}];", klass.genericTypeParameterFields[i], i);
359+
360+
Write("obj->SetDynamicClass(this);");
361+
Write("obj->InitializeMemberVariables();");
362+
Write("return obj;");
363+
}
364+
LeaveScope();
365+
366+
NewLine();
367+
Write("cct::refl::Object* GetMemberVariable(std::size_t, const cct::refl::Object&) const override");
368+
EnterScope();
369+
{
370+
Write("return nullptr;");
371+
}
372+
LeaveScope();
373+
374+
NewLine();
375+
Write("void* GetNativeMemberVariable(std::size_t, const cct::refl::Object&) const override");
376+
EnterScope();
377+
{
378+
Write("return nullptr;");
379+
}
380+
LeaveScope();
381+
}
382+
LeaveScope(";");
383+
384+
Write("const cct::refl::Class* {}::m_class = nullptr;", klass.name);
385+
NewLine();
386+
}
387+
277388
void CppGenerator::GenerateTemplateClass(std::string_view ns, const Class& klass)
278389
{
279390
Write("namespace");

Src/Concerto/PackageGenerator/CppGenerator/CppGenerator.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace cct
1717
private:
1818
void GenerateNamespace(const Namespace& ns, const std::string& namespaceChain = "");
1919
void GenerateClass(std::string_view ns, const Class& klass);
20+
void GenerateGenericClass(std::string_view ns, const Class& klass);
2021
void GenerateTemplateClass(std::string_view ns, const Class& klass);
2122
void GenerateClassMethod(std::string_view className, const Class::Method& method, std::string_view ns, std::size_t methodIndex);
2223
void GenerateEnum(const Enum& enum_);

0 commit comments

Comments
 (0)