-
-
Notifications
You must be signed in to change notification settings - Fork 17
StackData Structure
The StackData structure is a key part of the dotnow interpreter and is used throughout to represent a primitive or object value along with type identifier information. dotnow supports the following primitive types which can all be stored or retrieved via a StackData structure:
Int8 (sbyte), UInt8 (byte), Int16 (short), UInt16 (ushort), Int32 (int), UInt32 (uint), Int64 (long), UInt64 (ulong), Single (float), Double (double), Ref (System.Object)
The following C# code shows how you can store each of the supported types into a given StackData struct:
StackData data = default;
// sbyte example
sbyte val8 = 45;
data.Primitive.Int8 = val8; // Store the value
data.type = ObjectType.Int8; // The type must also be provided
// byte example
byte valU8 = 255;
data.Primitive.Int8 = (sbyte)valU8; // Store the value - must cast to signed
data.type = ObjectType.UInt8; // The type must also be provided
// short example
short val16 = 1234;
data.Primitive.Int16 = val16; // Store the value
data.type = ObjectType.Int16; // The type must also be provided
// ushort example
short valU16 = 12345;
data.Primitive.Int16 = (short)valU16;// Store the value - must cast to signed
data.type = ObjectType.UInt16; // The type must also be provided
// int example
int val32 = 66666;
data.Primitive.Int32 = val32; // Store the value
data.type = ObjectType.Int32; // The type must also be provided
// uint example
uint valU32 = 99999;
data.Primitive.Int32 = (int)valU32; // Store the value - must cast to signed
data.type = ObjectType.UInt32; // The type must also be provided
// long example
long val64 = 66666;
data.Primitive.Int64 = val64; // Store the value
data.type = ObjectType.Int64; // The type must also be provided
// ulong example
ulong valU64 = 99999;
data.Primitive.Int64 = (long)valU64; // Store the value - must cast to signed
data.type = ObjectType.UInt64; // The type must also be provided
// float example
float valF = 123.456f;
data.Primitive.Single = valF; // Store the value
data.type = ObjectType.Single; // The type must also be provided
// double example
double valD = 200.4009d;
data.Primitive.Double= valD; // Store the value - must cast to signed
data.type = ObjectType.Double; // The type must also be provided
// object example
object obj = "Hello World;
data.refValue = obj; // Assign to the reference value rather than primitive
data.type = ObjectType.Ref; // Set type to reference
Note that there are also additional ObjectTypes of RefBoxed
and ByRef
which should not be used directly and are only intended for internal use.
There are also additional helper methods for for reading and writing values to and from a StackData struct, but in most cases it will be faster to write the data and type direct is possible. An example of such helper methods:
StackData data = default;
// Load an int value into `data`
StackData.AllocTypedSlow(ref data, typeof(int), 123); // Load values an a 32bit in, as the name suggests this is not most performant.
StackData.AllocTyped(ref data, TypeCode.Int32, 123); // A much quicker alternative if the TypeCode is known
Reading a value from a StackData struct is much the same as writing so I will not list all examples, but here are some examples of extracting certain values. Ideally the type stored within the StackData struct would be known at compile time, which will be the case if you are implementing direct call bindings (you will know the method declaring type and argument types ahead of time), otherwise you can check the value contained via the type
property:
StackData data = ...
// Int32 example
Debug.Log(data.Primitive.Int32);
// ULong example
Debug.Log((ulong)data.Primitive.Int64); // We must cast back to unsigned values
// Object example (UnityEngine.Transform)
Transform t = (Transform)data.refValue;
// Unknown type example
switch(data.type)
{
case ObjectType.Int16: Debug.Log("short"); break;
case ObjectType.Single: Debug.Log("float"); break;
case ObjectType.Ref: Debug.Log(data.refValue != null ? data.refValue.GetType() : "null"); break;
// etc.
}
Again for best performance it is recommended to read the values directly as shown above, but there are also helper methods for extracting data as a specified type, for example:
StackData data = ...
// Int example
int myInt = (int)data.UnboxAsTypeSlow(typeof(int)); // Get value as integer - not most performant
myInt = (int)data.UnboxAsType(TypeCode.Int32); // Much more performant if type code is known ahead of time
Direct call bindings are special methods that will be called by the interpreter and that should invoke a specific interop method. Direct call bindings are not required but should be implemented where performance is a concern, as they can avoid an expensive relfection invoke call which would otherwise be required by making use of information known at compile time.
A direct call binding should be implemented for each specific interop method, where an interop method is simply any method that may be called from interpreted code, but is running on the native backed (mono/IL2CPP). For example, Debug.Log
would be an example of an interop call. Note that a direct call binding will need to be implemented for each overload of a given method.
Here is an example of a direct call binding for the static Component.FindObjectOfType
method. This is a good method to use as an example because it accepts multiple parameters and must return a vaue, so it will show how the StackData struct can be used to achieve that. We will also show an instance method example for clarity:
[Preserve]
[CLRMethodDirectCallBinding(typeof(Component), "FindObjectOfType", typeof(Type), typeof(bool))]
public static void UnityEngine_Component_FindObjectOfType(StackData[] stack, int offset)
{
}
The above code is what the direct call binding would look like. We can see that it takes in a StackData[] stack
and int offset
. The stack value is just a large array of StackData representing the entire stack memory for the current dotnow thread. The offset is important because it lets us know at what index into the stack our data is located, the data in this case being the 2 parameters.
So first we would want to extract the arguments from the stack as the correct type:
// Get the type parameter
Type arg0 = (Type)stack[index].refValue; // For static methods arg0 is at offset
// Get the bool parameter
bool arg1 = stack[index + 1].Primitive.Int32 != 0 ? true : false; // For static methods arg1 is at offset + 1
As you can see we can simply read the parameters in order and for methods with extra parameters you would just continue with + 2, + 3, etc.
All that is left after that is to call the method directly which is very simple indeed and then return the value. Return the value in this case simply means to load the value back onto the stack at the base offset, for example: stack[offset] = return value
.
UnityEngine.Object result = Component.FindObjectOfType(arg0, arg1);
// Return value in this case is a reference type, so as previously shown in the Store Data section, we can load the value with a type:
stack[offset].refValue = result;
stack[offset].type = ObjectType.Ref;
That is it for a static method, it is not too difficult once you understand how the arguments are provided and how return values must be loaded.
Lets move on to an example of an instance method which is very slightly different but still uses the same principals:
For this example we will use the GameObject.GetComponentInChildren
because it is an instance method with 2 arguments and must return a value, so we can show how that would look as a direct call binding. Here is the direct call binding definition just for clarity:
[Preserve]
[CLRMethodDirectCallBinding(typeof(GameObject), "GetComponentInChildren", typeof(Type), typeof(bool))]
public static void UnityEngine_Component_GetComponentInChildren(StackData[] stack, int offset)
{
}
For instance methods the offset always points to the object instance that the method was called for, which in this case would be a GameObject instance. We can load that quite simply:
GameObject instance = (GameObject)stack[offset].refValue;
Arguments for instance methods work exactly the same as static methods, only they are stored at the next slot in the array, so argument 0 is + 1, argument 1 is + 2, etc:
// Get the type parameter
Type arg0 = (Type)stack[index + 1].refValue; // For instance methods arg0 is at offset + 1
// Get the bool parameter
bool arg1 = stack[index + 2].Primitive.Int32 != 0 ? true : false; // For instance methods arg1 is at offset + 2
Next we can call the method and return the value just the same as the static direct call binding:
Component result = instance.GetComponentInChildren(arg0, arg1);
// Return value in this case is a reference type, so as previously shown in the Store Data section, we can load the value with a type:
stack[offset].refValue = result;
stack[offset].type = ObjectType.Ref;