如果你想知道这里发生了什么,看看我之前对这个主题的思考:在游戏引擎中使用快速虚拟机。
这些文字不断地变得越来越复杂,所以我不会责怪任何人迷失在下面的文字海洋中。
如果我们要向来宾动态添加一组功能,则必须确保开销较低,并且不存在与其他功能冲突的空间。因此,如果您希望通过添加新的系统调用来扩展来宾环境,那么您选择的系统调用号每次都必须是唯一的,并且两端都必须以某种方式知道该编号。您还必须从现在到永远永久使用该号码才能使用此功能。除了,如果你可以选择任何免费的系统呼叫号码,而双方都会在不知情的情况下使用这个号码,那会怎么样?
虽然这是可能的,而且我确实实现了一个解决方案,将免费的系统调用号写入客户内存,为这个新功能形成蹦床函数,但事实证明,简单地使用一个有散列映射支持的系统调用会更快。哦,好吧。原因相当简单:当您将一个内存范围分配给某个特定用途,然后再次将其分组为组ID和其中的索引时,您必须进行查找。还不如直接查个哈希表。
那么,让我们来看看这个功能本身。我们需要能够按类型定义函数,并为其命名。让我们看一个最简单的例子:停止计时器。
我们定义了一个函数Stop_Timer,它执行名为“Timer_Stop”的动态调用。它具有由模板参数指定的函数类型,并且实例化创建了一个可调用对象。如果游戏引擎中有“TIMER_STOP”的处理程序,用整数调用它将停止给定的计时器。
在主机端,我们为“Timer_Stop”连接了一个处理程序,如下所示:
请注意通过引用捕获计时器。Sysargs模板调用获取每个模板参数,并根据C调用约定确定它必须来自哪个寄存器,然后将其放入返回值中正确的元组索引中。当然是零开销。
该系统使用字符串的CRC32校验和来避免字符串比较。我们可以在编译时使用C++在脚本中完成这些操作。当添加散列时,我们还会检查冲突,这样就消除了一个潜在的麻烦,尽管这不太可能。用于校验和的多项式是两侧的模板参数,因此如果实际发生冲突,只需更改它即可解决问题。
模板<;TypeName函数&>结构调用{const uint32_t hash;stexpr调用(const char*f):hash(crc32(F)){}stexpr调用(Uint32_T H):hash(H){}模板<;typeName...。参数&>自动操作员()(参数...。Args)const{Static_Assert(std::is_invocable_v<;Func,args...>;);Using Ret=TypeName std::Invoke_Result<;Func,args...>;::type;Using Fch=Ret(*)(uint32_t,args...);AUTO FCH=重新解释_CAST<;FCH>;(&;dyncall_helt;(&;dyncall_helg;)。
实际的实施相当像巫毒。我们使用函数类型作为模板参数创建一个可调用对象。Callable确保传递给它的参数与函数类型匹配,这用作静态类型检查。然后,我们创建一个C函数调用,其中包含我们的散列,并将其提供给蹦床函数。然后,在主机端,我们可以读出这些相同的参数,就好像它是一个C函数调用一样。实际上,相当整洁。
在CRC32散列值的支持下,对所有动态调用使用一个共享的系统调用编号可以得到不错的结果:在我的机器上,调用的中位数实际上是13 ns。使用std::Function和散列查找存在相关的性能开销,但是实现者总是可以选择直接使用系统调用,这具有固定的3 ns开销。
远程调用只是一个虚拟机或多或少直接调用另一个虚拟机。
当您远程(在另一台机器上)执行一个函数时,也不是没有注意事项。例如,如果不能通过共享内存区访问本地对象,则不能传递对该对象的引用。这需要协调和辅助工具。这也意味着递归远程调用将不起作用。这是可以忍受的,但如果有一种方法可以仅仅为了远程调用而将一台机器的堆栈挂载到另一台机器上,情况会怎样呢?
如果远程计算机运行的是与调用方相同的二进制文件,则还可以使用只读静态数据和代码:字符串、常量、函数等。
如果堆栈位于内存中的不同位置,则可以使来自一台计算机的堆栈在另一台计算机上可用。因此,当您进行远程调用时,您所要做的就是在调用期间将调用方机器的堆栈挂载到远程机器中。只是,你不需要把所有的东西都装上。堆栈指针告诉您当前的“顶部”在哪里,不仅如此,而且仅当传递到远程机器的一个整数寄存器包含位于该区域内的值时,才需要挂载堆栈。
使用这些想法,我们可以将来自一台机器的直接C++函数调用整合到远程机器中,并且开销非常低。当堆栈不使用时,无需复制,无需额外费用。使用我们从动态函数调用中学到的相同内容,我们也可以使FarCall成为类型安全的:
在上面的例子中,为了类型安全和调用时使用正确的ABI,我们必须指定函数的类型。当您运行相同的二进制文件时,有一个优化,使您不必同时指定函数类型:
不同之处在于我们直接指向该函数,从中我们可以看到返回值和参数以及地址。当我们已经拥有函数地址时,我们不必查找它。名称Gameplay2动态地指的是机器。另一个不同之处和潜在的好处是不必使用C调用约定。该调用将作为常规的C++函数调用来调用。
Constexpr Execute Remotely Somefunc(";Gameplay2&34;,Some_Function);SomeStruct{.string=";Hello 123!";,.value=42};int r=Somefunc(1234,Some);
Long Some_Function(int value,SomeStruct&;Some){Print(";Hello Remote World!)。Value=";,value,";!\n";);print(";,ome.string,";;\n";);print(";,ome struct value:";,ome.value,";\n";);返回值;}。
该数据将在远程计算机上按原样显示,如日志中所示:
>;>;>;[gameplay2]说:你好,远程世界!Value=1234!>;>;>;[gameplay2]表示:某个结构字符串:Hello 123!>;>;>;[gameplay2]表示:某个结构值:42。
因为该函数是在代码中直接引用的,所以不需要将其设置为公共函数,也不需要将其添加到包含公共符号的文件中。
直接引用函数的远程函数调用有一个巨大的缺陷:我们不会将蹦床函数强制转换为C函数调用。它仍然是一个C++函数调用。这意味着您不能远程调用C函数,除非您添加了类似bool cxx=true的模板参数。我不知道您是否真的能确定您引用的函数的调用约定。也就是说,当通过字符串名引用函数时,函数名不能被损坏,我们可以假定这是一个C函数调用。
就目前而言,这就是我的全部。我最近试着比较了一些现代解释语言和Web汇编仿真器,我注意到你不得不处理的在主机和来宾之间构建自己的API的API实在是太可怕了。希望这能为这些知名项目的实现者提供一些想法。