前言 算法
這裏,咱們打算對虛幻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等),歡迎你們持續關注,若是你有想提早了解的章節,也歡迎在下面留言,我可能會根據你們的留言來作優先級調整。
參考文章