Refureku C++17 Runtime Reflection Library

repository·master·Indexed 42 days ago

https://github.com/jsoysouvanh/refureku

A C++17 runtime reflection library designed for game engine development. It uses a code generator via Kodgen to enable dynamic inspection and manipulation of classes, methods, fields, namespaces, and enums. Features include support for multiple inheritance, class templates, custom property attributes, and runtime loading/unloading of dynamic libraries.

Tokens
3.5K
Snippets
10
Records
13
Agent score
54%

What's inside Refureku

  1. Overview of Refureku

    master

    Refureku is a C++17 runtime reflection dynamic library. It consists of two main components:

    1. Code Generator: A Kodgen generation module that handles the generation of reflection code.
    2. Dynamic Library: The actual reflection API that is linked to your target program.

    It allows you to reflect namespaces, classes, methods (static and non-static), fields, variables, functions, enums, and enum values. It supports class templates, multiple inheritance, and runtime loading/unloading of dynamic libraries.

  2. Create and use custom properties (Attributes)

    master

    Refureku allows you to attach custom metadata (similar to C# attributes) to any entity using a powerful property system.

    1. Define a Property: Inherit from rfk::Property. Use CLASS(rfk::PropertySettings(...)) to specify which entity kinds (e.g., Struct, Class) this property can be attached to.
    2. Attach the Property: Use the property name as a macro argument in the CLASS() macro of the target entity.
    3. Read the Property: Use classArchetype->getPropertyByName("PropertyName") or classArchetype->getProperty<PropertyType>() to retrieve the metadata at runtime.
    // 1. Define the property
    class CLASS(rfk::PropertySettings(rfk::EEntityKind::Struct | rfk::EEntityKind::Class))
        ExampleProperty : public rfk::Property
    {
        public:
        int someData;
        ExampleProperty(int data): someData{data} {}
        ExampleProperty_GENERATED
    };
    
    // 2. Attach it to a class
    class CLASS(ExampleProperty(42)) TestClass3
    {
        TestClass3_GENERATED
    };
    
    // 3. Read it
    rfk::Class const* c = rfk::getDatabase().getFileLevelClassByName("TestClass3");
    rfk::Property const* prop = c->getPropertyByName("ExampleProperty");
  3. How to reflect a class in Refureku

    master

    To reflect a class, you must use the generated header files and specific macros. The library is designed so that you write the reflection markers once, and the code generator handles the rest.

    1. Include the generated header (e.g., "Generated/ClassName.rfkh.h").
    2. Use the CLASS() macro on your class definition.
    3. Use FIELD() or METHOD() macros on members you wish to reflect.
    4. Include the generated source header (e.g., "Generated/ClassName.rfks.h") in one of your project's source files.
    5. Add the ClassName_GENERATED macro inside the class and the File_ClassName_GENERATED marker in a source file to trigger registration.
    //TestClass.h
    #pragma once
    
    #include "Generated/TestClass.rfkh.h"
    
    class CLASS() TestClass
    {
        FIELD()
        int _intField;
    
        METHOD()
        void testMethod();
    
        TestClass_GENERATED
    };
    
    File_TestClass_GENERATED
    //TestClass.cpp
    #include "Generated/TestClass.rfks.h"
  4. Understand the generation workflow

    master

    The generation process follows a specific pipeline orchestrated by parseAndGenerate:

    1. Initialization: Sets up a DefaultLogger, a FileParser, a CodeGenManager, and a MacroCodeGenUnit.
    2. Module Registration: A ReflectionCodeGenModule is added to the MacroCodeGenUnit. This module is responsible for the actual reflection logic.
    3. Settings Loading: Configuration is loaded from the provided file path into the respective settings objects.
    4. Execution: The codeGenMgr.run() method is called, taking the FileParser and MacroCodeGenUnit to perform the parsing and code generation.
    5. Reporting: The results (success/failure, files parsed, files up-to-date, and duration) are logged via printGenerationResult.
  5. Run the Refureku reflection code generation

    master

    The Refureku generator runs in two distinct phases to handle internal and public metadata:

    1. Internal Generation: Runs runForInternalDirectory, which generates metadata into the Include/Internal/Refureku/Generated directory. This includes both header (.rfkh.h) and source (.rfks.h) files.
    2. Public Generation: Runs runForPublicDirectory, which generates metadata into the Include/Public/Refureku/Generated directory. To keep the public API clean, it automatically deletes all generated source files (.rfks.h) from this directory after the process completes, leaving only the headers.

    The generator uses rfk::ReflectionCodeGenModule to drive the actual reflection logic during the kodgen execution.

    // Typical execution flow in main()
    rfk::FileParser fileParser;
    // ... setup logger and manager ...
    
    rfk::ReflectionCodeGenModule reflectionCodeGenModule;
    codeGenUnit.addModule(reflectionCodeGenModule);
    
    runForInternalDirectory(logger, codeGenMgr, fileParser, codeGenUnit, codeGenUnitSettings);
    runForPublicDirectory(logger, codeGenMgr, fileParser, codeGenUnit, codeGenUnitSettings);
  6. Configure the generator via settings file

    master

    The generator uses a centralized settings file to manage the reflection generation process. The loadSettings function populates several key configuration objects from a single file path:

    • kodgen::ParsingSettings: Manages project include directories and compiler selection.
    • kodgen::CodeGenManagerSettings: Manages directories to be processed (getToProcessDirectories) and directories to be ignored (getIgnoredDirectories).
    • kodgen::MacroCodeGenUnitSettings: Manages the output directory for generated code.

    When running in development mode (RFK_DEV), the generator automatically attempts to set the compiler executable (g++, clang++, or msvc) based on the detected environment.

  7. Instantiate reflected classes

    master

    Reflected classes can be instantiated even without direct access to their static type:

    • Default Constructor: Use classArchetype->makeSharedInstance<T>() to create a rfk::SharedPtr<T>.
    • Base Class Instantiation: You can instantiate a class and store it in a pointer to its base class.
    • Custom Instantiation: If you need a custom constructor or a custom deleter, you can define a static method marked with METHOD(rfk::Instantiator) in your class. This method will be called when using makeSharedInstance with arguments.
    // Instantiate from default ctor
    rfk::Class const* c = rfk::getDatabase().getFileLevelClassByName("TestClass");
    rfk::SharedPtr<TestClass> instance = c->makeSharedInstance<TestClass>();
    
    // If TestClass has a base class, we can do
    rfk::SharedPtr<BaseClass> instance2 = c->makeSharedInstance<BaseClass>();

    // Custom Instantiator Example class CLASS() TestClass2 : public BaseClass { METHOD(rfk::Instantiator) static rfk::SharedPtr<TestClass2> customInstantiator(int i) { return rfk::SharedPtr<TestClass2>(new TestClass2(), [](TestClass2* ptr) { delete ptr; }); }

    TestClass2_GENERATED

    };

    // Usage rfk::Class const* c2 = rfk::getDatabase().getFileLevelClassByName("TestClass2"); rfk::SharedPtr<BaseClass> instance3 = c2->makeSharedInstance<BaseClass>(42);

  8. Manipulate reflected fields

    master

    Once you have a rfk::Field pointer, you can read or write values on a specific class instance:

    1. Get Field: Use classArchetype.getFieldByName("fieldName").
    2. Read Value: Use field->get<T>(instance) where T is the type of the field.
    3. Write Value: Use field->set(instance, value).
    // Get field
    rfk::Field const* field = TestClass::staticGetArchetype().getFieldByName("_intField");
    
    TestClass instance;
    
    // Read field value
    int value = field->get<int>(instance);
    
    // Write field value
    field->set(instance, 3);
  9. Manipulate reflected methods

    master

    You can invoke reflected methods using the rfk::Method object:

    1. Get Method: Use classArchetype.getMethodByName("methodName").
    2. Invoke without arguments: Use method->invoke(instance).
    3. Invoke with arguments and return value: Use method->invoke<ReturnType>(instance, arg1, arg2, ...).
    // Get method
    rfk::Method const* method = TestClass::staticGetArchetype().getMethodByName("testMethod");
    
    TestClass instance;
    
    // Call method without arguments
    method->invoke(instance);
    
    // With arguments and returned value
    int returnedValue = method->invoke<int>(instance, 1, 2, 3);
  10. Retrieve class metadata

    master

    You can access reflection metadata using several methods depending on what information you have available:

    • By Name: Use rfk::getDatabase().getFileLevelClassByName("ClassName") to find a class by its string name.
    • From Static Type: If the class is reflected, you can call ClassName::staticGetArchetype() to get its metadata directly.
    • From Type/Archetype: Use rfk::getArchetype<T>() to get the archetype for type T. This returns nullptr if the type is not reflected.
    // From name
    rfk::Class const* c = rfk::getDatabase().getFileLevelClassByName("TestClass");
    
    // From static type
    rfk::Class const& c2 = TestClass::staticGetArchetype();
    
    // From reflected/unreflected type
    rfk::Archetype const* c3 = rfk::getArchetype<TestClass>(); // nullptr if not reflected
  11. Get class metadata from a base pointer

    master

    For classes that publicly derive from rfk::Object (requires #include <Refureku/Object.h>), you can retrieve the dynamic type of an object instance through a base pointer or reference using the .getArchetype() method.

    TestClass instance;
    rfk::Object const* instancePtr = &instance;
    
    rfk::Class const& archetype = instancePtr->getArchetype();
  12. Configure the Refureku Library Generator settings

    master

    The loadSettings function configures the kodgen components used to generate reflection metadata for the Refureku library. It sets up file extensions, directory paths, naming patterns for generated files, and macro names used for property parsing.

    Key configuration details include:

    • Supported Extensions: .h, .hpp
    • Generated File Patterns:
      • Header: ##FILENAME##.rfkh.h
      • Source: ##FILENAME##.rfks.h
    • Macro Patterns:
      • Class Footer: ##CLASSFULLNAME##_GENERATED
      • Header Footer: File_##FILENAME##_GENERATED
      • Export Symbol: REFUREKU_API
    • Property Parsing Macros:
      • RFKNamespace, RFKClass, RFKStruct, RFKVariable, RFKField, RFKFunction, RFKMethod, RFKEnum, RFKEnumVal
    • Compiler: Uses clang++ with C++17.
    bool loadSettings(kodgen::CodeGenManagerSettings& codeGenMgrSettings, kodgen::ParsingSettings& parsingSettings,
                     kodgen::MacroCodeGenUnitSettings& codeGenUnitSettings, fs::path outputDirectory)