前言 算法
这里,咱们打算对虚幻4 中蓝图虚拟机的实现作一个大概的讲解,若是对其它的脚本语言的实现有比较清楚的认识,理解起来会容易不少,咱们先会对相关术语进行一个简单的介绍,而后会对蓝图虚拟机的实现作一个讲解。express
术语 编程
编程语言通常分为编译语言和解释型语言。c#
编译型语言 数组
程序在执行以前须要一个专门的编译过程,把程序编译成 为机器语言的文件,运行时不须要从新翻译,直接使用编译的结果就好了。程序执行效率高,依赖编译器,跨平台性差些。如C、C++、Delphi等.数据结构
解释性语言 多线程
编写的程序不进行预先编译,以文本方式存储程序代码。在发布程序时,看起来省了道编译工序。可是,在运行程序的时候,解释性语言必须先解释再运行。架构
然而关于Java、C#等是否为解释型语言存在争议,由于它们主流的实现并非直接解释执行的,而是也编译成字节码,而后再运行在jvm等虚拟机上的。jvm
UE4中蓝图的实现更像是lua的实现方式,它并不能独立运行,而是做为一种嵌入宿主语言的一种扩展脚本,lua能够直接解释执行,也能够编译成字节码并保存到磁盘上,下次调用能够直接加载编译好的字节码执行。编程语言
什么是虚拟机
虚拟机最初由波佩克[a]与戈德堡定义为有效的、独立的真实机器的副本。当前包括跟任何真实机器无关的虚拟机。虚拟机根据它们的运用和与直接机器的相关性分为两大类。系统虚拟机(如VirtualBox)提供一个能够运行完整操做系统的完整系统平台。相反的,程序虚拟机(如Java JVM)为运行单个计算机程序设计,这意謂它支持单个进程。虚拟机的一个本质特色是运行在虚拟机上的软件被局限在虚拟机提供的资源里——它不能超出虚拟世界。
而这里咱们主要关心的是程序虚拟机,VM既然被称为"机器",通常认为输入是知足某种指令集架构(instruction set architecture,ISA)的指令序列,中间转换为目标ISA的指令序列并加以执行,输出为程序的执行结果的,就是VM。源与目标ISA能够是同一种,这是所谓same-ISA VM。
分类
虚拟机实现分为基于寄存器的虚拟机和基于栈的虚拟机。
三地址指令
a = b + c;
若是把它变成这种形式:
add a, b, c
那看起来就更像机器指令了,对吧?这种就是所谓"三地址指令"(3-address instruction),通常形式为:
op dest, src1, src2
许多操做都是二元运算+赋值。三地址指令正好能够指定两个源和一个目标,能很是灵活的支持二元操做与赋值的组合。ARM处理器的主要指令集就是三地址形式的。
二地址指令
a += b;
变成:
add a, b
这就是所谓"二地址指令",通常形式为:
op dest, src
它要支持二元操做,就只能把其中一个源同时也做为目标。上面的add a, b在执行事后,就会破坏a原有的值,而b的值保持不变。x86系列的处理器就是二地址形式的。
一地址指令
显然,指令集能够是任意"n地址"的,n属于天然数。那么一地址形式的指令集是怎样的呢?
想像一下这样一组指令序列:
add 5
sub 3
这只指定了操做的源,那目标是什么?通常来讲,这种运算的目标是被称为"累加器"(accumulator)的专用寄存器,全部运算都靠更新累加器的状态来完成。那么上面两条指令用C来写就相似:
C代码 收藏代码
acc += 5;
acc -= 3;
只不过acc是"隐藏"的目标。基于累加器的架构近来比较少见了,在很老的机器上繁荣过一段时间。
零地址指令
那"n地址"的n若是是0的话呢?
看这样一段Java字节码:
Java bytecode代码 收藏代码
iconst_1
iconst_2
iadd
istore_0
注意那个iadd(表示整型加法)指令并无任何参数。连源都没法指定了,零地址指令有什么用??
零地址意味着源与目标都是隐含参数,其实现依赖于一种常见的数据结构——没错,就是栈。上面的iconst_一、iconst_2两条指令,分别向一个叫作"求值栈"(evaluation stack,也叫作operand stack"操做数栈"或者expression stack"表达式栈")的地方压入整型常量一、2。iadd指令则从求值栈顶弹出2个值,将值相加,而后把结果压回到栈顶。istore_0指令从求值栈顶弹出一个值,并将值保存到局部变量区的第一个位置(slot 0)。
零地址形式的指令集通常就是经过"基于栈的架构"来实现的。请必定要注意,这个栈是指"求值栈",而不是与系统调用栈(system call stack,或者就叫system stack)。千万别弄混了。有些虚拟机把求值栈实如今系统调用栈上,但二者概念上不是一个东西。
因为指令的源与目标都是隐含的,零地址指令的"密度"能够很是高——能够用更少空间放下更多条指令。所以在空间紧缺的环境中,零地址指令是种可取的设计。但零地址指令要完成一件事情,通常会比二地址或者三地址指令许多更多条指令。上面Java字节码作的加法,若是用x86指令两条就能完成了:
mov eax, 1
add eax, 2
基于栈与基于寄存器结构的区别
基于栈中的"栈"指的是"求值栈",JVM中"求值栈"被称为"操做数栈"。
栈帧
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
蓝图虚拟机的实现
前面咱们已经简单得介绍了虚拟机的相关术语,接下来咱们来具体讲解下虚幻4中蓝图虚拟机的实现。
字节码
虚拟机的字节码在Script.h文件中,这里咱们把它所有列出来,由于是专用的脚本语言,因此它里面会有一些特殊的字节码,如代理相关的代码(EX_BindDelegate、EX_AddMulticastDelegate),固然经常使用的语句也是有的,好比赋值、无条件跳转指令、条件跳转指令、switch等。
1 // 2 3 // Evaluatable expression item types. 4 5 // 6 7 enum EExprToken 8 9 { 10 11 // Variable references. 12 13 EX_LocalVariable = 0x00, // A local variable. 14 15 EX_InstanceVariable = 0x01, // An object variable. 16 17 EX_DefaultVariable = 0x02, // Default variable for a class context. 18 19 // = 0x03, 20 21 EX_Return = 0x04, // Return from function. 22 23 // = 0x05, 24 25 EX_Jump = 0x06, // Goto a local address in code. 26 27 EX_JumpIfNot = 0x07, // Goto if not expression. 28 29 // = 0x08, 30 31 EX_Assert = 0x09, // Assertion. 32 33 // = 0x0A, 34 35 EX_Nothing = 0x0B, // No operation. 36 37 // = 0x0C, 38 39 // = 0x0D, 40 41 // = 0x0E, 42 43 EX_Let = 0x0F, // Assign an arbitrary size value to a variable. 44 45 // = 0x10, 46 47 // = 0x11, 48 49 EX_ClassContext = 0x12, // Class default object context. 50 51 EX_MetaCast = 0x13, // Metaclass cast. 52 53 EX_LetBool = 0x14, // Let boolean variable. 54 55 EX_EndParmValue = 0x15, // end of default value for optional function parameter 56 57 EX_EndFunctionParms = 0x16, // End of function call parameters. 58 59 EX_Self = 0x17, // Self object. 60 61 EX_Skip = 0x18, // Skippable expression. 62 63 EX_Context = 0x19, // Call a function through an object context. 64 65 EX_Context_FailSilent = 0x1A, // Call a function through an object context (can fail silently if the context is NULL; only generated for functions that don't have output or return values). 66 67 EX_VirtualFunction = 0x1B, // A function call with parameters. 68 69 EX_FinalFunction = 0x1C, // A prebound function call with parameters. 70 71 EX_IntConst = 0x1D, // Int constant. 72 73 EX_FloatConst = 0x1E, // Floating point constant. 74 75 EX_StringConst = 0x1F, // String constant. 76 77 EX_ObjectConst = 0x20, // An object constant. 78 79 EX_NameConst = 0x21, // A name constant. 80 81 EX_RotationConst = 0x22, // A rotation constant. 82 83 EX_VectorConst = 0x23, // A vector constant. 84 85 EX_ByteConst = 0x24, // A byte constant. 86 87 EX_IntZero = 0x25, // Zero. 88 89 EX_IntOne = 0x26, // One. 90 91 EX_True = 0x27, // Bool True. 92 93 EX_False = 0x28, // Bool False. 94 95 EX_TextConst = 0x29, // FText constant 96 97 EX_NoObject = 0x2A, // NoObject. 98 99 EX_TransformConst = 0x2B, // A transform constant 100 101 EX_IntConstByte = 0x2C, // Int constant that requires 1 byte. 102 103 EX_NoInterface = 0x2D, // A null interface (similar to EX_NoObject, but for interfaces) 104 105 EX_DynamicCast = 0x2E, // Safe dynamic class casting. 106 107 EX_StructConst = 0x2F, // An arbitrary UStruct constant 108 109 EX_EndStructConst = 0x30, // End of UStruct constant 110 111 EX_SetArray = 0x31, // Set the value of arbitrary array 112 113 EX_EndArray = 0x32, 114 115 // = 0x33, 116 117 EX_UnicodeStringConst = 0x34, // Unicode string constant. 118 119 EX_Int64Const = 0x35, // 64-bit integer constant. 120 121 EX_UInt64Const = 0x36, // 64-bit unsigned integer constant. 122 123 // = 0x37, 124 125 EX_PrimitiveCast = 0x38, // A casting operator for primitives which reads the type as the subsequent byte 126 127 // = 0x39, 128 129 // = 0x3A, 130 131 // = 0x3B, 132 133 // = 0x3C, 134 135 // = 0x3D, 136 137 // = 0x3E, 138 139 // = 0x3F, 140 141 // = 0x40, 142 143 // = 0x41, 144 145 EX_StructMemberContext = 0x42, // Context expression to address a property within a struct 146 147 EX_LetMulticastDelegate = 0x43, // Assignment to a multi-cast delegate 148 149 EX_LetDelegate = 0x44, // Assignment to a delegate 150 151 // = 0x45, 152 153 // = 0x46, // CST_ObjectToInterface 154 155 // = 0x47, // CST_ObjectToBool 156 157 EX_LocalOutVariable = 0x48, // local out (pass by reference) function parameter 158 159 // = 0x49, // CST_InterfaceToBool 160 161 EX_DeprecatedOp4A = 0x4A, 162 163 EX_InstanceDelegate = 0x4B, // const reference to a delegate or normal function object 164 165 EX_PushExecutionFlow = 0x4C, // push an address on to the execution flow stack for future execution when a EX_PopExecutionFlow is executed. Execution continues on normally and doesn't change to the pushed address. 166 167 EX_PopExecutionFlow = 0x4D, // continue execution at the last address previously pushed onto the execution flow stack. 168 169 EX_ComputedJump = 0x4E, // Goto a local address in code, specified by an integer value. 170 171 EX_PopExecutionFlowIfNot = 0x4F, // continue execution at the last address previously pushed onto the execution flow stack, if the condition is not true. 172 173 EX_Breakpoint = 0x50, // Breakpoint. Only observed in the editor, otherwise it behaves like EX_Nothing. 174 175 EX_InterfaceContext = 0x51, // Call a function through a native interface variable 176 177 EX_ObjToInterfaceCast = 0x52, // Converting an object reference to native interface variable 178 179 EX_EndOfScript = 0x53, // Last byte in script code 180 181 EX_CrossInterfaceCast = 0x54, // Converting an interface variable reference to native interface variable 182 183 EX_InterfaceToObjCast = 0x55, // Converting an interface variable reference to an object 184 185 // = 0x56, 186 187 // = 0x57, 188 189 // = 0x58, 190 191 // = 0x59, 192 193 EX_WireTracepoint = 0x5A, // Trace point. Only observed in the editor, otherwise it behaves like EX_Nothing. 194 195 EX_SkipOffsetConst = 0x5B, // A CodeSizeSkipOffset constant 196 197 EX_AddMulticastDelegate = 0x5C, // Adds a delegate to a multicast delegate's targets 198 199 EX_ClearMulticastDelegate = 0x5D, // Clears all delegates in a multicast target 200 201 EX_Tracepoint = 0x5E, // Trace point. Only observed in the editor, otherwise it behaves like EX_Nothing. 202 203 EX_LetObj = 0x5F, // assign to any object ref pointer 204 205 EX_LetWeakObjPtr = 0x60, // assign to a weak object pointer 206 207 EX_BindDelegate = 0x61, // bind object and name to delegate 208 209 EX_RemoveMulticastDelegate = 0x62, // Remove a delegate from a multicast delegate's targets 210 211 EX_CallMulticastDelegate = 0x63, // Call multicast delegate 212 213 EX_LetValueOnPersistentFrame = 0x64, 214 215 EX_ArrayConst = 0x65, 216 217 EX_EndArrayConst = 0x66, 218 219 EX_AssetConst = 0x67, 220 221 EX_CallMath = 0x68, // static pure function from on local call space 222 223 EX_SwitchValue = 0x69, 224 225 EX_InstrumentationEvent = 0x6A, // Instrumentation event 226 227 EX_ArrayGetByRef = 0x6B, 228 229 EX_Max = 0x100, 230 231 };
栈帧
在Stack.h中咱们能够找到FFrame的定义,虽然它定义的是一个结构体,可是执行当前代码的逻辑是封装在这里面的。下面让咱们看一下它的数据成员:
1 // Variables. 2 3 UFunction* Node; 4 5 UObject* Object; 6 7 uint8* Code; 8 9 uint8* Locals; 10 11 12 13 UProperty* MostRecentProperty; 14 15 uint8* MostRecentPropertyAddress; 16 17 18 19 /** The execution flow stack for compiled Kismet code */ 20 21 FlowStackType FlowStack; 22 23 24 25 /** Previous frame on the stack */ 26 27 FFrame* PreviousFrame; 28 29 30 31 /** contains information on any out parameters */ 32 33 FOutParmRec* OutParms; 34 35 36 37 /** If a class is compiled in then this is set to the property chain for compiled-in functions. In that case, we follow the links to setup the args instead of executing by code. */ 38 39 UField* PropertyChainForCompiledIn; 40 41 42 43 /** Currently executed native function */ 44 45 UFunction* CurrentNativeFunction; 46 47 48 49 bool bArrayContextFailed;
咱们能够看到,它里面保存了当前执行的脚本函数,执行该脚本的UObject,当前代码的执行位置,局部变量,上一个栈帧,调用返回的参数(不是返回值),当前执行的原生函数等。而调用函数的返回值是放在了函数调用以前保存,调用结束后再恢复。大体以下所示:
1 uint8 * SaveCode = Stack.Code; 2 3 // Call function 4 5 …. 6 7 Stack.Code = SaveCode
下面咱们列出FFrame中跟执行相关的重要函数:
1 // Functions. 2 3 COREUOBJECT_API void Step( UObject* Context, RESULT_DECL ); 4 5 6 7 /** Replacement for Step that uses an explicitly specified property to unpack arguments **/ 8 9 COREUOBJECT_API void StepExplicitProperty(void*const Result, UProperty* Property); 10 11 12 13 /** Replacement for Step that checks the for byte code, and if none exists, then PropertyChainForCompiledIn is used. Also, makes an effort to verify that the params are in the correct order and the types are compatible. **/ 14 15 template<class TProperty> 16 17 FORCEINLINE_DEBUGGABLE void StepCompiledIn(void*const Result); 18 19 20 21 /** Replacement for Step that checks the for byte code, and if none exists, then PropertyChainForCompiledIn is used. Also, makes an effort to verify that the params are in the correct order and the types are compatible. **/ 22 23 template<class TProperty, typename TNativeType> 24 25 FORCEINLINE_DEBUGGABLE TNativeType& StepCompiledInRef(void*const TemporaryBuffer); 26 27 28 29 COREUOBJECT_API virtual void Serialize( const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category ) override; 30 31 32 33 COREUOBJECT_API static void KismetExecutionMessage(const TCHAR* Message, ELogVerbosity::Type Verbosity, FName WarningId = FName()); 34 35 36 37 /** Returns the current script op code */ 38 39 const uint8 PeekCode() const { return *Code; } 40 41 42 43 /** Skips over the number of op codes specified by NumOps */ 44 45 void SkipCode(const int32 NumOps) { Code += NumOps; } 46 47 48 49 template<typename TNumericType> 50 51 TNumericType ReadInt(); 52 53 float ReadFloat(); 54 55 FName ReadName(); 56 57 UObject* ReadObject(); 58 59 int32 ReadWord(); 60 61 UProperty* ReadProperty(); 62 63 64 65 /** May return null */ 66 67 UProperty* ReadPropertyUnchecked(); 68 69 70 71 /** 72 73 * Reads a value from the bytestream, which represents the number of bytes to advance 74 75 * the code pointer for certain expressions. 76 77 * 78 79 * @param ExpressionField receives a pointer to the field representing the expression; used by various execs 80 81 * to drive VM logic 82 83 */ 84 85 CodeSkipSizeType ReadCodeSkipCount(); 86 87 88 89 /** 90 91 * Reads a value from the bytestream which represents the number of bytes that should be zero'd out if a NULL context 92 93 * is encountered 94 95 * 96 97 * @param ExpressionField receives a pointer to the field representing the expression; used by various execs 98 99 * to drive VM logic 100 101 */ 102 103 VariableSizeType ReadVariableSize(UProperty** ExpressionField);
像ReadInt()、ReadFloat()、ReadObject()等这些函数,咱们看到它的名字就知道它是作什么的,就是从代码中读取相应的int、float、UObject等。这里咱们主要说下Step()函数,它的代码以下所示:
1 void FFrame::Step(UObject *Context, RESULT_DECL) 2 3 { 4 5 int32 B = *Code++; 6 7 (Context->*GNatives[B])(*this,RESULT_PARAM); 8 9 }
能够看到,它的主要做用就是取出指令,而后在原生函数数组中找到对应的函数去执行。
字节码对应函数
前面咱们列出了全部的虚拟机的全部字节码,那么对应每一个字节码具体执行部分的代码在哪里呢,具体能够到ScriptCore.cpp中查找定义,咱们能够看到每一个字节码对应的原生函数都在GNatives和GCasts里面:
它们的声明以下:
1 /** The type of a native function callable by script */ 2 3 typedef void (UObject::*Native)( FFrame& TheStack, RESULT_DECL ); 4 5 Native GCasts[]; 6 7 Native GNatives[EX_Max];
这样它都会对每个原生函数调用一下注册方法,经过IMPLEMENT_VM_FUNCTION和IMPLEMENT_CAST_FUNCTION宏实现。
具体代码以下图所示:
1 #define IMPLEMENT_FUNCTION(cls,func) \ 2 3 static FNativeFunctionRegistrar cls##func##Registar(cls::StaticClass(),#func,(Native)&cls::func); 4 5 6 7 #define IMPLEMENT_CAST_FUNCTION(cls, CastIndex, func) \ 8 9 IMPLEMENT_FUNCTION(cls, func); \ 10 11 static uint8 cls##func##CastTemp = GRegisterCast( CastIndex, (Native)&cls::func ); 12 13 14 15 #define IMPLEMENT_VM_FUNCTION(BytecodeIndex, func) \ 16 17 IMPLEMENT_FUNCTION(UObject, func) \ 18 19 static uint8 UObject##func##BytecodeTemp = GRegisterNative( BytecodeIndex, (Native)&UObject::func );
能够看到,它是定义了一个全局静态对象,这样就会在程序的main函数执行前就已经把函数放在数组中对应的位置了,这样在虚拟机执行时就能够直接调用到对应的原生函数了。
执行流程
咱们前面讲蓝图的时候讲过蓝图如何跟C++交互,包括蓝图调用C++代码,以及从C++代码调用到蓝图里面去。
C++调用蓝图函数
1 UFUNCTION(BlueprintImplementableEvent, Category = "AReflectionStudyGameMode") 2 3 void ImplementableFuncTest(); 4 5 6 7 void AReflectionStudyGameMode::ImplementableFuncTest() 8 9 { 10 11 ProcessEvent(FindFunctionChecked(REFLECTIONSTUDY_ImplementableFuncTest),NULL); 12 13 }
由于咱们这个函数没有参数,全部ProcessEvent中传了一个NULL,若是是有参数和返回值等,那么UHT会自动生成一个结构体用于存储参数和返回值等,这样当在C++里面调用函数时,就会去找REFLECTIONSTUDY_ImplementableFuncTest这个名字对应的蓝图UFunction,若是找到那么就会调用ProcessEvent来作进一步的处理。
ProcessEvent流程
蓝图调用C++函数
1 UFUNCTION(BlueprintCallable, Category = "AReflectionStudyGameMode") 2 3 void CallableFuncTest(); 4 5 6 7 DECLARE_FUNCTION(execCallableFuncTest) \ 8 9 { \ 10 11 P_FINISH; \ 12 13 P_NATIVE_BEGIN; \ 14 15 this->CallableFuncTest(); \ 16 17 P_NATIVE_END; \ 18 19 }
若是是经过蓝图调用的C++函数,那么UHT会生成如上的代码,而且若是有参数的话,会调用P_GET_UBOOL等来获取对应的参数,若是有返回值的话也会将返回值赋值。
总结
至此,加上前面咱们对蓝图编译的剖析,加上蓝图虚拟机的讲解,咱们已经对蓝图的实现原理有一个比较深刻的了解,本文并无对蓝图的前身unrealscript进行详细的讲解。有了这个比较深刻的认识后(若是想要有深入的认识,必须本身去看代码),相信你们在设计蓝图时会更游刃有余。固然若是有错误的地方也请你们指正,欢迎你们踊跃讨论。接下来可能会把重心放到虚幻4渲染相关的模块上,包括渲染API跨平台相关,多线程渲染,渲染流程,以及渲染算法上面,可能中间也会穿插一些其余的模块(好比动画、AI等),欢迎你们持续关注,若是你有想提早了解的章节,也欢迎在下面留言,我可能会根据你们的留言来作优先级调整。
参考文章