Summary 1.

This article describes an approach to implement C++ reflection through C++ variadic templates. This approach is very useful and is widely used in the Nebula high performance networking framework, enabling very powerful dynamic load and dynamic create capabilities.

Variadic templates is one of the most powerful new features in C++11. It is highly generalized for parameters, representing zero to any number of parameters of any type. The principles and applications of variable parameter templates are not the focus of this article, but the examples in this article will give you a good idea of how variable parameter templates are used.

Those familiar with Java or C# will probably know about reflection. Many well-known frameworks use reflection, which simply means creating an instance of a class based on its name (string). C++ does not provide reflection directly from the language, but the omnipotent C++ can use some tricks to achieve reflection. Bwar also needed reflection while developing the Nebula framework, and implemented C++ reflection using some online references and my own understanding of C++11 variable-parameter templates.


2. Implementation of analog reflection mechanism before C++11

The Nebula Framework is a high-performance, event-driven, general-purpose networking framework that provides a powerful, unified interface for fast business implementations without any business logic of its own. The business logic is compiled from Nebula’s Actor interface into the SO dynamic library. Nebula loads the business logic dynamic library to implement the Server functionality. Developers focus on writing the business logic code, leaving the network communications, timers, serialization, deserialization, and communication protocols to the framework.

The business logic classes written by developers are derived from Nebula’s base classes, but the business logic derivatives are completely unknown to Nebula. Nebula needs to load these dynamic libraries and create instances of the classes in the dynamic library to use the reflection mechanism. The first version of Nebula and its predecessor, the Starship framework, was developed in the C++03 standard. With unknown class names, unknown parameter numbers, unknown parameter types, and even more unknown arguments, Bwar didn’t have an efficient way to load dynamic libraries and create class instances. To do this, the constructors of all business logic entry classes are designed as no-argument constructors.

Bwar didn’t find a good implementation in 2015 and came up with a clever way to load dynamic libraries and create class instances (this isn’t reflection yet, it just does what reflection can or needs to do). This approach is available in Nebula’s C++03 release and in Starship, an underlying IM framework that has been running steadily for more than two years. The code for this implementation is shown below:

CmdHello.hpp:

#ifdef __cplusplus
extern "C" {
#endif// @note plugin code is compiled into so and stored in plugin directory. After loading the dynamic library, the framework calls create() to create the plugin class instance. neb::Cmd* create();#ifdef __cplusplus
}
#endif

namespace im
{

class CmdHello: public neb::Cmd
{
public:
    CmdHello();
    virtual ~CmdHello();
    virtual bool AnyMessage();
};

} /* namespace im */
Copy the code

CmdHello.cpp:

#include "CmdHello.hpp"

#ifdef __cplusplus
extern "C" {
#endif
neb::Cmd* create()
{
    neb::Cmd* pCmd = new im::CmdHello();
    return(pCmd);
}
#ifdef __cplusplus
}
#endif

namespace im
{

CmdHello::CmdHello()
{
}

CmdHello::~CmdHello()
{
}

bool CmdHello::AnyMessage()
{
    std::cout << "CmdHello" << std::endl;
    return(true); }}Copy the code

The key to the implementation is the create() function. Although each dynamic library writes the create() function, each dynamic library loads at a different address. Functions called from different addresses are not the same, so different instances can be created. Here is a snippet of code that dynamically loads the library and calls the create() function to create an instance of the class:

void* pHandle = NULL;
    pHandle = dlopen(strSoPath.c_str(), RTLD_NOW);
    char* dlsym_error = dlerror();
    if (dlsym_error)
    {
        LOG4_FATAL("cannot load dynamic lib %s!" , dlsym_error);
        if(pHandle ! = NULL) { dlclose(pHandle); }return(pSo);
    }
    CreateCmd* pCreateCmd = (CreateCmd*)dlsym(pHandle, strSymbol.c_str());
    dlsym_error = dlerror();
    if (dlsym_error)
    {
        LOG4_FATAL("dlsym error %s!" , dlsym_error);
        dlclose(pHandle);
        return(pSo);
    }
    Cmd* pCmd = pCreateCmd();
Copy the code

The configuration file corresponding to this dynamic library loading snippet is as follows:

{"cmd": 10001,"so_path":"plugins/CmdHello.so"."entrance_symbol":"create"."load":false."version": 1}Copy the code

These code implementations accomplish the purpose of loading dynamic libraries and creating instances of classes unknown to the framework. However, it is not as flexible as reflection and is a bit more cumbersome to use. After writing the business logic class, the developer needs to implement a corresponding global create() function.

C++ reflection mechanism to realize thinking

Bwar used to encapsulate a generic thread class based on pthreads in C++ templates. The following is a representative function implementation of this thread template class, which has some inspirations for designing C++ reflection mechanism implementation:

template <typename T>
void CThread<T>::StartRoutine(void* para)
{
    T* pT;
    pT = (T*) para;
    pT->Run();
}
Copy the code

Similarly, creating an unknown class instance can be done with new T(), or new T(T1, T2) if it is a constructor with arguments. The reflection mechanism for constructing classes without arguments can be implemented by creating class instances by class names, mapping “ClassName” to T, or mapping “ClassName” to functions that contain new T() statements. Considering that the number and type of parameters of new T(T1, T2) are unknown, the variable parameter template of C++11 is used to solve the parameter problem, that is, to complete the reflection mechanism of the class with parameters.

Nebula networking framework for C++ reflection implementation

With Nebula’s introduction and adoption of reflection, the code volume in the Nebula network framework has been reduced by about 10 percent, and the ease of use has improved dramatically. Considering the benefits of using reflection for Nebula’s business logic development, The reflection mechanism is arguably the next major improvement to the Nebula framework rewritten in C++14.

Nebula’s actors are event handlers. All of the business logic is abstracted into events and event handling, and reflection is applied to the dynamic creation of actors. Actor is divided into Cmd, Module, Step, Session four different types. Business logic code is implemented by subclassing these four different types of time handlers to focus on business logic implementation and not on anything outside of business logic. Cmd and Module are message processing repositories. What Cmd and Module are defined by the business developers is unknown to the framework, so Both Cmd and Module are configured in the configuration file. Nebula creates their instances with the Cmd and Module names (strings) in the configuration file. The key code for dynamically creating actors via reflection is as follows:

The Actor categories:

class Actor: public std::enable_shared_from_this<Actor>
Copy the code

Actor create factory (note the code comment) :

template<typename ... Targs> class ActorFactory { public: static ActorFactory* Instance() { if (nullptr == m_pActorFactory) { m_pActorFactory = new ActorFactory(); } return(m_pActorFactory); } virtual ~ActorFactory(){}; // Register "instance creation method (CreateObject method of DynamicCreator)" with ActorFactory, and give the method a name of "class name". You can obtain the "instance creation method" of the class by using "class name". This instance creation method is essentially a function pointer. STD ::function is more readable than a function pointer in C++11, so STD ::function is used. bool Regist(const std::string& strTypeName, std::function<Actor*(Targs&&... args)> pFunc); // Create an instance by passing in the "class name" and the parameters. Create an instance by passing in the "class name" from m_mapCreateFunction. Actor* Create(const std::string& strTypeName, Targs&&... args); private: ActorFactory(){}; static ActorFactory<Targs... >* m_pActorFactory; std::unordered_map<std::string, std::function<Actor*(Targs&&...) > > m_mapCreateFunction; }; template<typename ... Targs> ActorFactory<Targs... >* ActorFactory<Targs... >::m_pActorFactory = nullptr; template<typename ... Targs> bool ActorFactory<Targs... >::Regist(const std::string& strTypeName, std::function<Actor*(Targs&&... args)> pFunc) { if (nullptr == pFunc) { return (false); } bool bReg = m_mapCreateFunction.insert( std::make_pair(strTypeName, pFunc)).second; return (bReg); } template<typename ... Targs> Actor* ActorFactory<Targs... >::Create(const std::string& strTypeName, Targs&&... args) { auto iter = m_mapCreateFunction.find(strTypeName); if (iter == m_mapCreateFunction.end()) { return (nullptr); } else { return (iter->second(std::forward<Targs>(args)...) ); }}Copy the code

Create class dynamically (note the code comment) :

template<typename T, typename. Targs>class DynamicCreator
{
public:
    struct Register
    {
        Register()
        {
            char* szDemangleName = nullptr;
            std: :string strTypeName;
#ifdef __GNUC__
            szDemangleName = abi::__cxa_demangle(typeid(T).name(), nullptr.nullptr.nullptr);
#else
            Typeid (T).name() : typeid(T).name()
            //in this format? : szDemangleName = typeid(T).name();
            szDemangleName = abi::__cxa_demangle(typeid(T).name(), nullptr.nullptr.nullptr);
#endif
            if (nullptr! = szDemangleName) { strTypeName = szDemangleName;free(szDemangleName); } ActorFactory<Targs... >::Instance()->Regist(strTypeName, CreateObject); }inline void do_nothing(a)const {}; }; DynamicCreator() { m_oRegister.do_nothing();// The function call here has no actual content, but it is the key to complete the creation of m_oRegister instance before calling the dynamically created function
    }
    virtual ~DynamicCreator(){};

    // The method to dynamically create instances, which is used to create all Actor instances. This is a template method, and in fact each Actor derived class has its own CreateObject method.
    static T* CreateObject(Targs&&... args)
    {
        T* pT = nullptr;
        try
        {
            pT = new T(std::forward<Targs>(args)...) ; }catch(std::bad_alloc& e)
        {
            return(nullptr);
        }
        return(pT);
    }

private:
    static Register m_oRegister;
};

template<typename T, typename. Targs>typenameDynamicCreator<T, Targs... >::Register DynamicCreator<T, Targs... >::m_oRegister;Copy the code

The above ActorFactory and DynamicCreator are all implementations of C++ reflection mechanism. To accomplish the dynamic creation of instances, the class definition must meet the requirements. Here is a CmdHello class definition that can create instances dynamically (note the code comments) :

// Class definitions need to use multiple inheritance.
Neb ::Cmd is the actual base class of CmdHello (neb::Cmd is a derivative of Actor, which is explained in the description at the beginning of this section);
// The second inheritance is the need to dynamically create instances by class name, with template
// If the parameter is a pointer or reference of a type, the type should be specified as the template parameter. For example, const STD ::string& simply fill in STD ::string in the template argument for neb::DynamicCreator.
class CmdHello: public neb::Cmd, public neb::DynamicCreator<CmdHello, int32>
{
public:
    CmdHello(int32 iCmd);
    virtual ~CmdHello();

    virtual bool Init(a);
    virtual bool AnyMessage(
                    std: :shared_ptr<neb::SocketChannel> pChannel,
                    const MsgHead& oMsgHead,
                    const MsgBody& oMsgBody);
};

Copy the code

Let’s see how the reflection mechanism is invoked above:

template <typename. Targs>std: :shared_ptr<Cmd> WorkerImpl::MakeSharedCmd(Actor* pCreator, const std: :string& strCmdName, Targs... args)
{
    LOG4_TRACE("%s(CmdName \"%s\")", __FUNCTION__, strCmdName.c_str());
    Cmd* pCmd = dynamic_cast<Cmd*>(ActorFactory<Targs... >::Instance()->Create(strCmdName,std::forward<Targs>(args)...) );if (nullptr == pCmd)
    {
        LOG4_ERROR("failed to make shared cmd \"%s\"", strCmdName.c_str());
        return(nullptr); }... }Copy the code

A call to the MakeSharedCmd() method:

MakeSharedCmd(nullptr, oCmdConf["cmd"][i]("class"), iCmd);
Copy the code

This is a summary of the C++ reflection mechanism implemented with Nebula’s variable-parameter templates. This is one of the core features of the Nebula framework, and there are several production-ready C++ reflection implementations based on Nebula’s practices.

The application of this C++ reflection mechanism is prone to error

  • Class CmdHello: Public neb::Cmd, public neb::DynamicCreator

    The template arguments must match exactly the types of the arguments in the constructor.
    ,>
  • The argument type passed in where the create method is called must exactly match the parameter type. There can be no implicit conversions, such as a class constructor that takes an unsigned int and calls ActorFactory

    ::Instance()->Create() with int or short, unsigned short or enum causes ActorFactory to fail to find the corresponding “Instance creation method”, resulting in failure to Create an Instance by class name.

Pay attention to the above two points, basically there will be no problem.

5. An executable example

The code given above to illustrate the C++ reflection mechanism is all from the Nebula framework. Finally, an executable example is provided to deepen the understanding.

DynamicCreate.cpp:

#include <string> #include <iostream> #include <typeinfo> #include <memory> #include <unordered_map> #include <cxxabi.h>  namespace neb { class Actor { public: Actor(){std::cout << "Actor construct" << std::endl; } virtual ~Actor(){}; virtual void Say() { std::cout << "Actor" << std::endl; }}; template<typename ... Targs> class ActorFactory { public: //typedef Actor* (*ActorCreateFunction)(); //std::function< Actor*(Targs... args) > pp; static ActorFactory* Instance() { std::cout << "static ActorFactory* Instance()" << std::endl; if (nullptr == m_pActorFactory) { m_pActorFactory = new ActorFactory(); } return(m_pActorFactory); } virtual ~ActorFactory(){}; //Lambda: static std::string ReadTypeName(const char * name) //bool Regist(const std::string& strTypeName, ActorCreateFunction pFunc) //bool Regist(const std::string& strTypeName, std::function<Actor*()> pFunc) bool Regist(const std::string& strTypeName, std::function<Actor*(Targs&&... args)> pFunc) { std::cout << "bool ActorFactory::Regist(const std::string& strTypeName, std::function<Actor*(Targs... args)> pFunc)" << std::endl; if (nullptr == pFunc) { return(false); } std::string strRealTypeName = strTypeName; //[&strTypeName, &strRealTypeName]{int iPos = strTypeName.rfind(' '); strRealTypeName = std::move(strTypeName.substr(iPos+1, strTypeName.length() - (iPos + 1))); }; bool bReg = m_mapCreateFunction.insert(std::make_pair(strRealTypeName, pFunc)).second; std::cout << "m_mapCreateFunction.size() =" << m_mapCreateFunction.size() << std::endl; return(bReg); } Actor* Create(const std::string& strTypeName, Targs&&... args) { std::cout << "Actor* ActorFactory::Create(const std::string& strTypeName, Targs... args)" << std::endl; auto iter = m_mapCreateFunction.find(strTypeName); if (iter == m_mapCreateFunction.end()) { return(nullptr); } else { //return(iter->second()); return(iter->second(std::forward<Targs>(args)...) ); } } private: ActorFactory(){std::cout << "ActorFactory construct" << std::endl; }; static ActorFactory<Targs... >* m_pActorFactory; std::unordered_map<std::string, std::function<Actor*(Targs&&...) > > m_mapCreateFunction; }; template<typename ... Targs> ActorFactory<Targs... >* ActorFactory<Targs... >::m_pActorFactory = nullptr; template<typename T, typename ... Targs> class DynamicCreator { public: struct Register { Register() { std::cout << "DynamicCreator.Register construct" << std::endl; char* szDemangleName = nullptr; std::string strTypeName; #ifdef __GNUC__ szDemangleName = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr); #else //in this format? : szDemangleName = typeid(T).name(); szDemangleName = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr); #endif if (nullptr ! = szDemangleName) { strTypeName = szDemangleName; free(szDemangleName); } ActorFactory<Targs... >::Instance()->Regist(strTypeName, CreateObject); } inline void do_nothing()const { }; }; DynamicCreator() { std::cout << "DynamicCreator construct" << std::endl; m_oRegister.do_nothing(); } virtual ~DynamicCreator(){m_oRegister.do_nothing(); }; static T* CreateObject(Targs&&... args) { std::cout << "static Actor* DynamicCreator::CreateObject(Targs... args)" << std::endl; return new T(std::forward<Targs>(args)...) ; } virtual void Say() { std::cout << "DynamicCreator say" << std::endl; } static Register m_oRegister; }; template<typename T, typename ... Targs> typename DynamicCreator<T, Targs... >::Register DynamicCreator<T, Targs... >::m_oRegister; class Cmd: public Actor, public DynamicCreator<Cmd> { public: Cmd(){std::cout << "Create Cmd " << std::endl; } virtual void Say() { std::cout << "I am Cmd" << std::endl; }}; class Step: public Actor, DynamicCreator<Step, std::string, int> { public: Step(const std::string& strType, int iSeq){std::cout << "Create Step " << strType << " with seq " << iSeq << std::endl; } virtual void Say() { std::cout << "I am Step" << std::endl; }}; class Worker { public: template<typename ... Targs> Actor* CreateActor(const std::string& strTypeName, Targs&&... args) { Actor* p = ActorFactory<Targs... >::Instance()->Create(strTypeName, std::forward<Targs>(args)...) ; return(p); }}; } int main() { //Actor* p1 = ActorFactory<std::string, int>::Instance()->Create(std::string("Cmd"), std::string("neb::Cmd"), 1001); //Actor* p3 = ActorFactory<>::Instance()->Create(std::string("Cmd")); neb::Worker W; neb::Actor* p1 = W.CreateActor(std::string("neb::Cmd")); p1->Say(); //std::cout << abi::__cxa_demangle(typeid(Worker).name(), nullptr, nullptr, nullptr) << std::endl; std::cout << "----------------------------------------------------------------------" << std::endl; neb::Actor* p2 = W.CreateActor(std::string("neb::Step"), std::string("neb::Step"), 1002); p2->Say(); return(0); }Copy the code

The Nebula framework is written in the C++14 standard and has a precompilation option in the Makefile that can be compiled with C++11, but compilers that do not fully support all C++11 standards may not compile successfully. Gnu ++ 4.8.5 does not support variable-parameter templates. A compiler later than GCC 5.0 is recommended, preferably GCC 6, with Nebula using GCC6.4.

The example dynamiccreate.cpp given here can be compiled like this:

 g++ -std=c++11 DynamicCreate.cpp -o DynamicCreate
Copy the code

The execution result of the program is as follows:

DynamicCreator.Register construct static ActorFactory* Instance() ActorFactory construct bool ActorFactory::Regist(const  std::string& strTypeName, std::function<Actor*(Targs... args)> pFunc)
m_mapCreateFunction.size() =1
DynamicCreator.Register construct
static ActorFactory* Instance()
ActorFactory construct
bool ActorFactory::Regist(const std::string& strTypeName, std::function<Actor*(Targs... args)> pFunc)
m_mapCreateFunction.size() =1
static ActorFactory* Instance()
Actor* ActorFactory::Create(const std::string& strTypeName, Targs... args)
static Actor* DynamicCreator::CreateObject(Targs... args)
Actor construct
DynamicCreator construct
Create Cmd
I am Cmd
----------------------------------------------------------------------
static ActorFactory* Instance()
Actor* ActorFactory::Create(const std::string& strTypeName, Targs... args)
static Actor* DynamicCreator::CreateObject(Targs... args)
Actor construct
DynamicCreator construct
Create Step neb::Step with seq 1002
I am Step
Copy the code

It took six hours over the weekend to write it and post it at the right time. If you find Nebula useful and you find this article useful, please give it to Star on GitHub. Thank you. Nebula is not only a framework, but also provides a series of applications built on it with the goal of creating a high-performance distributed service cluster solution.


References:

  • C++ implementation reflection (dynamically creating objects based on class names)
  • Beauty of generalization -C++11 variable template parameters
  • The C++ standard library typeid