c++中用于Callback / RPC的动态函数参数

Dynamic Function Args for Callback / RPC in C++

本文关键字:动态 函数 参数 RPC 用于 Callback c++      更新时间:2023-10-16

我需要在带参数的函数列表中注册如下函数:

void func1( int a , char* b ) {}
void func2( vec3f a , std::vector<float> b , double c) {}
...

当我通过网络接收到带有适当参数的数据时回调它们。我想象va_list会解决,但它不起作用:

void func1(int  a, char* b)
{
    printf("%d %s",a,b);
}
void prepare(...)
{
    va_list argList; 
    int args = 2;
    va_start(argList, args);
    ((void (*)(va_list))func1)(argList);
    va_end(argList);
}
int main(int argc, char **argv)
{
    prepare(1, "huhu");
    return 0;
}

解决这个问题最优雅的方法是什么?我知道std::bind/std::函数具有类似的功能,但我认为内部数据隐藏在std的深处。我只需要一些基本的数据类型,不一定是任意类型。如果预处理器使用##VA_ARGS或使用模板可以解决问题,我也可以接受。优先级是最容易使用的。

Edit1:我发现汇编可以解决(当我从内联汇编调用它们时,我如何将参数传递给c++函数)-但我更喜欢一个更独立于平台的解决方案。

如果您的目标是创建您自己的、小型的、特别的"rpc"解决方案,那么可能做出决策的主要驱动因素之一应该是:最少的代码2。越简单越好。

记住这一点,思考一下以下两种情况之间的区别是值得的:

  1. "真正的"RPC:处理程序应该像你写的那样带有RPC方法特定的签名。

  2. "Message passing":处理程序接收的消息要么是"end - point-determined type",要么是统一的消息类型。

现在,要得到类型1的解需要做什么?

传入的字节流/网络数据包需要根据选定的协议解析为某种类型的消息。然后,使用一些元信息(契约),根据{serviceContract, serviceMethod},需要在数据包中确认一组特定的数据项,如果存在,则需要调用相应的注册处理程序函数。在这个基础设施的某个地方,你通常有一个(可能是代码生成的)函数,它做的事情像这样:

void CallHandlerForRpcXYCallFoo( const RpcMessage*message )
{
     uint32_t arg0 = message->getAsUint32(0);
     // ...
     float argN = message->getAsFloat(N);
     Foo( arg0, arg1, ... argN );
}
当然,所有这些也可以打包到类和虚拟方法中,这些类是从服务契约元数据生成的。也许,还有一种方法可以通过一些过度的模板巫术来避免生成代码并拥有更通用的元实现。但是,所有这些都是工作,真正的工作。只是为了好玩,工作量太大了。而不是这样做,使用几十种已经做到这一点的技术中的一种会更容易。 到目前为止值得注意的是:在这幅图的某个地方,可能有一个(代码生成的)函数,看起来像上面给出的那个。

现在,要得到类型2的解需要做什么?

小于情形1。为什么?因为您只需在调用那些处理程序方法时停止实现,这些处理程序方法都将RpcMessage作为它们的单个参数。因此,您可以避免在这些方法之上生成"使其看起来像一个函数调用"层。

它不仅工作量更少,而且在合同发生变化的某些情况下也更健壮。如果一个更多的数据项被添加到"rpc解决方案","rpc函数"的签名必须改变。重新生成代码,调整应用程序代码。无论应用程序是否需要新数据项。另一方面,在方法2中,代码中没有破坏性的更改。当然,根据你的选择和合同的变化,它仍然会破裂。

所以,最优雅的解决方案是:不要使用RPC,使用消息传递。最好以rest的方式。

此外,如果您更喜欢"统一"的rpc消息而不是许多rpc契约特定的消息类型,则可以消除代码膨胀的另一个原因。

以防万一,我说的似乎有点太抽象,这里有一些模拟虚拟代码,草图解决方案2:

#include <cstdio>
#include <cstdint>
#include <map>
#include <vector>
#include <deque>
#include <functional>
// "rpc" infrastructure (could be an API for a dll or a lib or so:
// Just one way to do it. Somehow, your various data types need 
// to be handled/represented.
class RpcVariant
{
public:
    enum class VariantType
    {
        RVT_EMPTY,
        RVT_UINT,
        RVT_SINT,
        RVT_FLOAT32,
        RVT_BYTES
    };
private:
    VariantType m_type;
    uint64_t m_uintValue;
    int64_t m_intValue;
    float m_floatValue;
    std::vector<uint8_t> m_bytesValue;
    explicit RpcVariant(VariantType type)
        : m_type(type)
    {
    }
public:
    static RpcVariant MakeEmpty()
    {
        RpcVariant result(VariantType::RVT_EMPTY);
        return result;
    }
    static RpcVariant MakeUint(uint64_t value)
    {
        RpcVariant result(VariantType::RVT_UINT);
        result.m_uintValue = value;
        return result;
    }
    // ... More make-functions
    uint64_t AsUint() const
    {
        // TODO: check if correct type...
        return m_uintValue;
    }
    // ... More AsXXX() functions
    // ... Some ToWire()/FromWire() functions...
};
typedef std::map<uint32_t, RpcVariant> RpcMessage_t;
typedef std::function<void(const RpcMessage_t *)> RpcHandler_t;
void RpcInit();
void RpcUninit();
// application writes handlers and registers them with the infrastructure. 
// rpc_context_id can be anything opportune - chose uint32_t, here.
// could as well be a string or a pair of values (service,method) or whatever.
void RpcRegisterHandler(uint32_t rpc_context_id, RpcHandler_t handler);
// Then according to taste/style preferences some receive function which uses the registered information and dispatches to the handlers...
void RpcReceive();
void RpcBeginReceive();
void RpcEndReceive();
// maybe some sending, too...
void RpcSend(uint32_t rpc_context_id, const RpcMessage_t * message);
int main(int argc, const char * argv[])
{
    RpcInit();
    RpcRegisterHandler(42, [](const RpcMessage_t *message) { puts("message type 42 received."); });
    RpcRegisterHandler(43, [](const RpcMessage_t *message) { puts("message type 43 received."); });
    while (true)
    {
        RpcReceive();
    }
    RpcUninit();
    return 0;
}

如果RpcMessage随后被交易,当封装在std::shared_ptr中时,您甚至可以有多个处理程序或对同一消息实例进行一些转发(到其他线程)。这是一件特别烦人的事情,它需要rpc方法中的另一种"序列化"。这里,您只需转发消息。