Working With the VM

From ZDoom Wiki
Jump to navigation Jump to search

GZDoom's VM is incredibly powerful in the flexibility it offers to front-end scripting. Unlike DECORATE and ACS, it can allow almost the entire functionality of the internal engine to be exposed including direct access to many of its variables and functions. Thanks to the VM's JIT compiler, the speed of it is almost identical to it being written in the engine itself as well. This guide will cover how to export all types of variables and functions to be used in ZScript and the many potential pitfalls that can happen when attempting to do so.

Before writing any engine logic, it's best to consider whether or not the engine needs direct access to it. For instance, some things can be done purely from ZScript which prevents having to recompile translation units. It also better exposes implementation details to modders as opposed to having to access the engine's source code. Some reasons to define logic within the engine itself:

  • Implementation details need to be hidden from ZScript.
  • Certain datatypes cannot be easily exported to ZScript.
  • A function needs access to engine info that ZScript cannot or should not be able to access.
  • Writing it in ZScript would be significantly more challenging.

For simple logic it may be best to stick to ZScript.

Variables

Accessing ZScript Variables

Normally working with variables isn't an issue since any exported variable must inherently exist within the engine first. For instance, the radius of an Actor exists within AActor itself meaning it can be manipulated directly within the engine. However, sometimes you need access to a variable defined within a ZScript class. Thankfully GZDoom has a fairly simply way of handling this. Every DObject has access to a function called ScriptVar() which takes an FName for the field and a PType for what type the variable should be (in most cases nullptr can be passed for this since a specific PType isn't needed).

In order to access the variable, you'll first need to make sure it has a proper FName definition. To do this, go to the namedef_custom.h header and within it, use the xx() macro to define it. The name you give it should match the variable's name (names are case insensitive so that can be ignored). For instance, if you had a ZScript variable named MyVariable, your name definition would look like:

xx(MyVariable)

A new constant, NAME_MyVariable, should now exist that you can pass in as the name argument. ScriptVar() itself will return a void pointer to the field so it must be correctly casted to its real datatype. The nature of it being a pointer means any changes to it will be propagated back to ZScript, so care should be taken if only trying to read from it. DObject offers many wrapper functions to handle getting basic types and casting for you e.g. BoolVar(), IntVar(), etc. These only take a name argument since they handle the PType automatically.

When accessing a variable, the engine will throw an error if it doesn't exist. This means that type checking is still incredibly important which can be done through the IsKindOf() function. For instance, if we wanted to access an Inventory's owner, we would do the following:

void MyItemFunc(AActor* item)
{
    if (item == nullptr || !item->IsKindOf(NAME_Inventory))
        return;

    const AActor* owner = item->PointerVar<AActor>(NAME_Owner);
    // ...
}

Exporting Variables

Sometimes you want to give your scripts access to an engine value, either so it can be read directly or because a script needs it. Exporting basic datatypes is simple enough, but more complex types have a set of rules that must be followed. For any variable to be exported correctly, its datatype must be exposed to ZScript in some way. For instance, an engine-only struct could not be properly exported and the struct itself must also be exported. GZDoom will refuse to start if it detects that any datatype has not been exported correctly (this is especially important for structs). For instance, if a value is stored as a uint8_t internally, it must be exported as a uint8 within ZScript. All exported fields must also be denoted by the native keyword:

native int MyVar;

There are some basic considerations that must be taken into account when exporting:

  • What scope should these be?
  • What level of read and write access should modders have?
  • Would an API make more sense as opposed to direct access?

All these questions should be given thought as not everything should be directly exported. Sometimes it's best to hide the implementation details in case it needs to be reworked in the future. For instance, you might export an iterator for a list of objects instead of exporting the list directly, this way the list can be modified internally without impacting how it's used externally, preventing lots of ZScript code breakage. Scoping is important as it can impact future decisions, specifically if networking ever comes into play. When exporting something, consider if it's something the "server" should control (play scope), if it's something that should be unique to each client (ui scope), or if either can use it freely (data scope). The readonly and internal ZScript keywords can also be of use for controlling write access to something. Things marked this way can still be freely modified within the engine itself, only ZScript has these limitations imposed.

Basic Types

Most basic datatypes can be directly exported with little issue, only needing to make sure their sizes match (e.g. int16_t -> int16, int -> int). Structs and DObjects will need to be exported directly as well in some capacity if access is required. Not every field needs to be exported and it can be limited to only select fields you desire. For instance, if you have two variables, varA and varB, you can choose to only export varA. Below are the common macros for exporting fields:

  • DEFINE_FIELD(_EngineClassName, EngineVariable)
Exports the given engine variable to ZScript. The name must match in ZScript but is case insensitive e.g. if you export varA the ZScript variable must also be called varA and located in the given class. The first character of the class name is ignored when it's exported so if you were to export a variable from DThinker it would expect it to be in the ZScript class Thinker.
  • _EngineClassName
The class/struct the variable resides in. The exported class will be just EngineClassName.
  • EngineVariable
The name of the variable as it appears in the given class.
  • DEFINE_FIELD_NAMED(_EngineClassName, EngineVariable, ZScriptVariableName)
Works similar to DEFINE_FIELD except the name of the exported variable can be changed. For instance, if you have variable varA but want it to be called myVar within ZScript, this would allow you to do that.
  • ZScriptVariableName
The name of the variable as it should appear in ZScript. Your ZScript variable definition must match this name but is case insensitive.
  • DEFINE_FIELD_X(ZScriptClassName, EngineClassName, EngineVariable)
Works similar to DEFINE_FIELD except the name of the ZScript class can be chosen if they aren't synchronized e.g. side_t vs Side.
  • ZScriptClassName
The name of the class/struct as it appears in ZScript. Your ZScript definition must match this name but is case insensitive.
  • DEFINE_FIELD_NAMED_X(ZScriptClassName, EngineClassName, EngineVariable, ZScriptVariableName)
A combination of DEFINE_FIELD_NAMED and DEFINE_FIELD_X.
Note: TMap cannot be directly exported. ZSMap (and by extension ZSMapIterator) should be used as the datatype if exporting to ZScript is desired.


Sometimes you have a variable that acts as a set of bit flags and want to export specific bits as their own variable e.g. bSolid in ZScript is a bit flag in the internal flags variable. It's common for all bit variables like this to be prefixed with b (short for boolean). The following macros allow these to be exported like this:

  • DEFINE_FIELD_BIT(_EngineClassName, EngineBitFlagsVariable, ZScriptVariableName, BitValue)
This has similar rules to DEFINE_FIELD_NAMED when it comes to naming conventions. The only unique aspect is the bit value to use which is commonly passed as an enum value e.g. STF_FULLBRIGHT. A direct number can also be used but this is not advised since it's not clear what meaning it has.
  • BitValue
The bit the variable should represent in the flags variable.
  • DEFINE_FIELD_X_BIT(ZScriptClassName, EngineClassName, EngineBitFlagsVariable, BitValue)
Cannot be used since its syntax doesn't allow any possible correct exporting of bits.

Some variables are meant to be accessible from anywhere and not through any specific class or struct. Within ZScript's *base.zs files is a struct named _. This is where all definitions for global variables should be defined. The following macros export them to this struct:

  • DEFINE_GLOBAL(EngineGlobalVariable)
Exposes the engine variable at a global access level.
  • EngineGlobalVariable
The name of the variable to export. This needs to appear exactly the same in ZScript but is case insensitive.
  • DEFINE_GLOBAL_NAMED(EngineGlobalVariable, ZScriptVariableName)
Works similar to DEFINE_GLOBAL but allows for a custom ZScript variable name.
  • ZScriptVariableName
The name as it appears in ZScript. The definition must match this but is case insensitive.

In exceedingly rare cases, an incomplete datatype needs to be exported because of its complexity and an existing datatype works well enough to cover its use case. The only example of this in GZDoom is the TStaticPointedArray class that stores a sector's lines since it acts similar to a dynamic array. A few macros exist to silence the unmatched size warning that can occur in these instances. These are incredibly volatile and should be avoided at all costs in favor of properly exporting your datatypes. Only use these if your datatype is locked down and made readonly. Never allow direct modifications of these types of variables from ZScript.

Error.gif
Warning: The following macros are only here for documentation purposes and should be avoided in favor of correctly exporting datatypes.
  • DEFINE_FIELD_UNSIZED(ZScriptClassName, EngineClassName, EngineVariable)
Works similar to DEFINE_FIELD_X.
  • DEFINE_FIELD_NAMED_UNSIZED(_EngineClassName, EngineVariable, ZScriptVariableName)
Works similar to DEFINE_FIELD_NAMED.
  • DEFINE_GLOBAL_UNSIZED(EngineGlobalVariable)
Works similar to DEFINE_GLOBAL.

DObjects and Structs

See Creating Objects and Structs for actual creation of these datatypes.

Classes and structs have their own unique set of rules. It's important to note that everything defined as a class in ZScript is treated like a DObject. This means that something can only be defined as a class within it if it's a valid DObject type. Other complex types, both classes and structs, will need to be exported as a struct. Structs have their own special handling that needs to exist in order to determine their size and alignment. In general, DObjects are much more powerful as not only can they be used in data structures like dynamic arrays and associative maps, but they can also be created from within ZScript itself. Their cost is that they must be managed by the garbage collector which makes them more expensive overall. Structs are broken down into two different types: native and non-native. Native structs can be used in data structures similar to DObjects but cannot be created from within ZScript (only the engine can make new instances). Non-native structs can be created from within ZScript like a regular struct but have many limitations such as not being returnable as values and only being able to store them in static arrays. Whether DObject or struct, which one you choose should come down to what you ultimately think the common use case will be and how locked down you want the type to be.

Similar to variables, the default scope of the class/struct should be considered. For instance, if the ZScript class is defined as play scope by default, this means most functionality, including creation via new(), can only be done from within the play scope. This allows certain datatypes to be locked to specific use cases, something that can be nice if you don't want a world-modifying class to be creatable from the UI. If no scope is given, it's treated as data scoped. If it's stored as a field in another class/struct, it'll automatically assume the scope of that class/struct.

For DObjects, they must be defined as a class. These can be given any name desired, but usually it's common to prefix them internally with some value (e.g. D) and then give it the same name in ZScript minus the prefix.

// In engine.
class DMyObj : public DObject
{
    // ...
}
// In ZScript.
// Inheriting from Object is optional here since all classes inherit from it by default.
class MyObj native
{
    // ...
}

From here, all that needs to be done is using the variable and function export macros correctly.

Structs are more complicated. All structs, regardless if native or non-native, must define their size and alignment info so they can be manipulated in memory correctly. This is found within the thingdef_data.cpp file within the InitThingdef() function. The function NewStruct() creates a new struct datatype to be usable in ZScript and takes the name as it appears in ZScript, a PType (this should be nullptr in almost all cases), and whether or not it's native (false by default). It returns a pointer to the newly created PStruct and from here, its Size and Align properties must be set to match the internal struct it represents:

// Non-native struct.
auto myTypeStruct = NewStruct("MyType", nullptr);
myTypeStruct->Size = sizeof(FMyType); // FMyType is the engine class/struct that's being exported.
myTypeStruct->Align = alignof(FMyType);

// Native struct.
auto myNativeTypeStruct = NewStruct("MyNativeType", nullptr, true);
myNativeTypeStruct->Size = sizeof(FMyNativeType);
myNativeTypeStruct->Align = alignof(FMyNativeType);

Within ZScript, they would be defined as follows:

// Non-native struct.
// Note that this doesn't have the "native" keyword. All exported fields and
// functions within it will still need this, but the struct definition itself
// should not unless defined as native.
struct MyType
{
    // ...
}

// Native struct.
struct MyNativeType native
{
    // ...
}

An edge case exists where an internal field that stores a type defined as a native struct within ZScript gets exported. Normally native structs are handled by reference, but the field itself stores the actual value. In this case, the @ prefix before the datatype can be used to tell ZScript it should use the in-line struct value and not a pointer to that struct. This is needed so that the correct sizing information is used within the field. The field variable will still be passed around and treated as a native struct, however, meaning it can still be stored in data structures and returned.

// In engine.
class DMyObj : public DObject
{
    FMyNativeType MyField;
    // ...
}
// In ZScript.
class MyObj native
{
    native @MyNativeType MyField;
    // ...
}

Functions

Calling Functions

Offered in the engine are many macros for calling VM-defined functions, including ones defined directly in ZScript. This method is more cumbersome compared to using an internal function directly but is sometimes required, especially in the case of functions defined in ZScript-only classes or virtual functions overridable from ZScript. Care must be taken when calling the macros below as they define their own variables. As such, the safest way to handle them is to put them in their own code block via { }:

// Wrong. This will cause duplicate variable definitions within the current scope.
{
    IFVIRTUAL(AActor, MyVirtualFunction1)
    {
        // ...
    }

    IFVIRTUAL(AActor, MyVirtualFunction2)
    {
        // ...
    }
}

// Correct. This avoids possible conflicts.
{
    {
        IFVIRTUAL(AActor, MyVirtualFunction1)
        {
            // ...
        }
    }

    {
        IFVIRTUAL(AActor, MyVirtualFunction2)
        {
            // ...
        }
    }
}

Below are the macros meant for assisting in calling virtual VM functions. This is the most common case since these functions can be overridden from ZScript directly and the correct version needs to be called.

  • IFVIRTUALPTR(DObjectPointer, EngineClassName, FunctionName)
Verifies if a virtual function exists on the given DObject. The result is stored in a VMFunction pointer called func and will only enter the block below it if func isn't null. In order to speed up future calls, the index of the virtual function is stored in a static unsigned variable called VIndex. Another PClass pointer variable, clss, is also created which is where the virtual function is pulled from by index.
  • DObjectPointer
The DObject that the function should be checked within.
  • EngineClassName
The internal engine class that corresponds to it e.g. AActor. See IFVIRTUALPTRNAME for getting ZScript-defined classes.
  • FunctionName
The name of the virtual function e.g. SpecialMissileHit.
  • IFVIRTUAL(EngineClassName, FunctionName)
This is the same as IFVIRTUALPTR but it assumes this is the DObject being referred to.
  • IFVIRTUALPTRNAME(DObjectPointer, NamedClass, FunctionName)
Sometimes a virtual function from a class defined within ZScript itself is needed. In this case, there is no corresponding internal engine name, but an FName for it can be generated instead (e.g NAME_Inventory). Instead of passing the internal class name, you'd pass that FName constant in its place. All other functionality is the same as IFVIRTUALPTR.
  • IFOVERRIDENVIRTUALPTRNAME(DObjectPointer, NamedClass, FunctionName)
Similar to IFVIRTUALPTRNAME but the below block of code only gets called if the virtual function was overridden. This is useful for optimizing VM calls with virtual functions that are empty by default as it avoids calling it entirely. An additional static VMFunction pointer, orig_func, is created to track the base function definition.


Sometimes a function that isn't virtual but is still defined from within ZScript must be called. The following macro assists with this:

  • IFVM(ZScriptClassName, FunctionName)
Checks to see if a given function exists within the ZScript class type. Note that this doesn't take any specific pointer since these functions can't be overridden. If it does exist, it's stored within a static VMFunction pointer called func. The block of code below it only executes if func isn't null.
  • ZScriptClassName
The class name as it's defined in ZScript e.g. Actor.
  • FunctionName
The name of the function to look for e.g. ObtainInventory.

To actually call the functions, you'll need to set up the parameters and return values (see the sections below for more info). VMCall is the function that actually calls the function. It takes the function to call (func in the case of using the macros), the parameters, the number of parameters, the return values, and the number of return values. Not every return value needs to be included, but they must be gotten in the order they're returned e.g. if you need the second return value, you'll have to include the first one as well. If a function has no parameters or return values, nullptr can be passed in its place with a count of 0.

Examples

Note: When calling a VM function, all of its parameters must be present but return values are optional.
// In ZScript.
virtual int SpecialMissileHit(Actor victim) { /*..*/ }
// In engine.
int res = -1;
IFVIRTUAL(AActor, SpecialMissileHit)
{
    // Notice how non-static functions must pass in the "self" pointer as their first argument.
    VMValue params[] = { this, victim };
    VMReturn ret[] = { &res };

    VMCall(func, params, 2, ret, 1);
}
// In ZScript.
void ObtainInventory(Actor other) { /*..*/ }
// In engine.
IFVM(Actor, ObtainInventory)
{
    VMValue params[] = { other, this };

    VMCall(func, params, 2, nullptr, 0);
}

Exporting Functions

Functions are much more centralized to export than variables, but come with their own unique set of challenges. In particular, VM parameters and return values must be kept within certain types to properly fit within its registers. Some datatypes also impose limitations that prevent using the JIT compiler, noticeably slowing down code execution. Most of these situations can be worked around, but must be kept in mind when designing any sort of function meant to be exported to ZScript.

Function Definitions

Functions that are exported are broken down into two different variants. The most basic case is as a VM function. This is the slow form of function handling but is kept because not all functions can be defined natively. This one is always guaranteed to work despite its speed, so it serves as a good fallback in those instances. The other case is native functions. When the JIT compiler is enabled (on by default), the native function is called instead of the VM function. This means your VM and native functions must have the exact same parameter order and datatypes. The native function offers a significant performance boost so any VM function you define should have an equivalent native function if possible. Below are the list of datatypes that native functions support:

  • Pointers
  • References
  • void
  • (unsigned) int
  • double
  • bool (not supported as a return value, only a parameter)
Note: For native functions, int can be used to return a boolean value.

If a native function uses an unsupported datatype (e.g. it returns a bool), the compiler will give an error alerting you that something went wrong. By default GZDoom will compile any native code on start, giving a warning for any functions that aren't native compatible but couldn't be caught during compile time of the engine. However, if this functionality is disabled, a function will only throw a warning when it's used at least once.

  • DEFINE_ACTION_FUNCTION_NATIVE(_ZScriptClassName, ZScriptFunctionName, NativeFunctionName)
The macro for defining native functions. Note that there's a unique behavior present here where the first letter of _ZScriptClassName is ignored e.g. if a function for DThinker is defined, the class it exports to will be Thinker in ZScript. If the engine name and ZScript name don't match at all (e.g. side_t vs Side), _ can prefix the class name to act as padding.
  • _ZScriptClassName
The name of the class as it appears in ZScript. The first character is ignored by the engine.
  • ZScriptFunctionName
The name of the class' function as it appears in ZScript e.g. A_ChangeModel.
  • NativeFunctionName
The name of the internal engine function meant to represent it natively e.g. ChangeModelNative.

The common way to define a native VM function is to create its native function above it's VM definition as a static function. In this case all the static modifier does is tell the compiler that the function is not available in other translation units in the engine i.e. if it's defined in myfile.cpp, only myfile.cpp will have access to it. This is done for organizational reasons as these functions have little reason to be called outside of those specific instances. It's also common to give the native function the same name as its ZScript counterpart, but this is not required.

static void A_MyFunc(/*..*/)
{
    // ...
}

DEFINE_ACTION_FUNCTION_NATIVE(AActor, A_MyFunc, A_MyFunc)
{
    // ...
}

You can also have the body of your VM function call the native function directly as well if you want to reuse your code instead of creating the same definition twice, but this is also not required.

  • DEFINE_ACTION_FUNCTION(_ZScriptClassName, ZScriptFunctionName)
  • DEFINE_ACTION_FUNCTION_NATIVE0(_ZScriptClassName, ZScriptFunctionName, Unused)
This is a VM-only function and works similar to its native counterpart only it has no native callback. In this case no internal native function needs to be created. This should only be used if a function cannot be supported natively correctly. The alternative macro presented above (_NATIVE0) is used to quickly convert a native function macro to a non-native one. This is nice if you need to only temporarily disable native functionality on a function, otherwise it's identical to DEFINE_ACTION_FUNCTION.

For any exported function, the ZScript function must appear in the class it was defined under with its exact name and have the native qualifier. Instead of having a body, it should end with a statement terminator:

native static void A_MyStaticFunc(/*..*/); // Must not have a self pointer. See PARAM_PROLOGUE for more info.
native void A_MyFunc(/*..*/); // Must have a valid self pointer. See PARAM_SELF(_STRUCT)_PROLOGUE for more info.
native vararg void A_MyVAFunc(/*..*/, ...); // See PARAM_VA_POINTER for more info.
native action void A_MyActionFunc(/*..*/); // See PARAM_ACTION_PROLOGUE for more info.

Parameters

Parameters are handled by the VMValue class. The VM supports a limited set of datatypes and must be kept to the following:

  • (unsigned) int
  • double
  • const FString pointer
  • void pointer

Note that types like DVector4 are not directly supported. These are instead passed in as their floating-point values. For instance, a Vector4 in ZScript would be passed in as four doubles in the order of x, y, z, and w.

// In ZScript.
native static void MyFunction(Vector4 val);
// In engine.
DEFINE_ACTION_FUNCTION_NATIVE(_MyClass, MyFunction, MyFunction)
{
    PARAM_PROLOGUE
    PARAM_FLOAT(x)
    PARAM_FLOAT(y)
    PARAM_FLOAT(z)
    PARAM_FLOAT(w)
   // ...
}

This rule applies to all vector types and quaternions (quaternions are passed the same as Vector4). Your native functions should also apply this e.g. double x, double y, ... instead of DVector_ val. For native functions specifically, FStrings are passed in as constant references (const FString& param).

Note: ZScript functions with default arguments are purely ZScript-side. They do not have to be defined internally and all arguments will always be passed for a given VM function.


Every VM function must start with a self definition. Functions that don't have self are considered static and must have the static keyword in their ZScript function definition.

  • PARAM_PROLOGUE
Defines a function with no self and is therefore static.
  • PARAM_SELF_PROLOGUE(DObjectClassName)
Sets the first parameter to self as a DObject pointer of the given type. This does not need to be an exact match to the class being passed e.g. if the function belongs to Inventory, using type AActor is fine. Exact types should only be used if an internal engine definition exists for the class.
  • DObjectClassName
The name of the DObject's class type as it appears in the engine e.g. AActor.
  • PARAM_SELF_STRUCT_PROLOGUE(EngineClassName)
This works similar to PARAM_SELF_PROLOGUE except instead of a DObject type it holds a pointer to any other given class/struct type. This is needed for any sort of class/struct that isn't a DObject type.
  • PARAM_ACTION_PROLOGUE(AActorClassName)
This is a unique macro meant for defining any functions that have the action qualifier in their ZScript definition. It always defines self as an AActor pointer and defines two other parameters: stateowner, an AActor pointer of the given type, and stateinfo, a pointer of type FStateParamInfo, which contains additional data about the context the function is being called from. This should not be used with any other type of function except action functions.
  • AActorClassName
The name of the AActor class as it appears in the engine. Since every child class of AActor has been moved to ZScript, this will always be AActor.

Below is a list of supported parameter types. Note that some of these are simple wrappers e.g. PARAM_NAME is the same as taking an integer value and casting it as ENamedName. The parameter order must have the exact same order as the ZScript function definition, both for the VM and native functions.

  • PARAM_INT(VariableName)
  • PARAM_UINT(VariableName)
  • PARAM_BOOL(VariableName)
  • PARAM_NAME(VariableName)
Variable becomes type FName.
  • PARAM_SOUND(VariableName)
Variable becomes type FSoundID.
  • PARAM_COLOR(VariableName)
Variable becomes type PalEntry.
  • PARAM_FLOAT(VariableName)
  • PARAM_ANGLE(VariableName)
Variable becomes type DAngle.
  • PARAM_FANGLE(VariableName)
Variable becomes type FAngle.
  • PARAM_STRING(VariableName)
Const reference to an FString. This should be used if the passed string is not going to be modified.
  • PARAM_STRING_VAL(VariableName)
Mutable copy of the passed FString. Use this if any modifications need to occur to the string (these will not be propagated back to the parameter).
  • PARAM_STATELABEL(VariableName)
Stored as the index of the state label.
  • PARAM_STATE(VariableName)
Only usable in non-static Actor functions. Variable becomes a pointer of type FState and is gotten from self.
  • PARAM_STATE_ACTION(VariableName)
Only usable in action functions. Same as PARAM_STATE but gets the state from stateowner.
  • PARAM_POINTER(VariableName, ClassType)
Creates a pointer of the given type. This is only valid for parameters normally passed by reference e.g. structs.
  • PARAM_OUTPOINTER(VariableName, ClassType)
Similar to PARAM_POINTER but will accept a pointer from any datatype. Useful for direct modification of things normally passed by value e.g. int. Note that these parameters must have the out qualifier within ZScript.
  • PARAM_POINTERTYPE(VariableName, ClassType)
The same as PARAM_POINTER except the type must be explicitly declared as a pointer type i.e. T* instead of just T.
  • PARAM_OBJECT(VariableName, DObjectType)
Creates a DObject pointer of the given type.
  • PARAM_CLASS(VariableName, DObjectType)
Creates a meta class pointer of the given type e.g. a class<Actor> parameter in ZScript.
  • PARAM_POINTER_NOT_NULL(VariableName, ClassType)
Same as PARAM_POINTER but throws an error if the pointer is null.
  • PARAM_OBJECT_NOT_NULL(VariableName, DObjectType)
Same as PARAM_OBJECT but throws an error if the DObject is null.
  • PARAM_CLASS_NOT_NULL(VariableName, DObjectType)
Same as PARAM_CLASS but throws an error if the passed class type is null.

Below are a couple macros designed to assist with special cases for parameter handling:

  • PARAM_NULLCHECK(Pointer, VariableName)
Checks if the given pointer is null. If it is, aborts the VM marking it via the passed variable name.
  • PARAM_VA_POINTER(VariableName)
Stores a byte stream to the list of parameters at the end of a variable argument function. This is only for ZScript functions that are declared as vararg. It can be used to build a VMVa_List struct that gives proper access to the arguments:
PARAM_VA_POINTER(va_reginfo)

VMVa_List args = { param + [parameter offset], 0, numparam - [parameter offset + 1], va_reginfo + [parameter offset] };
while (args.curindex < args.numargs)
{
    if (args.reginfo[args.curindex] == REGT_INT)
        self->DoThingWithInt(args.args[args.curindex++].i);
    // ...
}

The parameter offset is the position of the PARAM_VA_POINTER argument (which will always come at the end). For instance, if there are five parameters in total, the offset would be four. The indices in reginfo store the type of the register and the indices in args store the actual VMValues.

If PARAM_ACTION_PROLOGUE is used, the following macros can be used to check the context of the function:

  • ACTION_CALL_FROM_ACTOR()
Returns true if the function is being called directly from the Actor.
  • ACTION_CALL_FROM_PSPRITE()
Returns true if the function is being called from a player's PSprite (e.g. their weapon).
  • ACTION_CALL_FROM_INVENTORY()
Returns true if the function is being called from an inventory item like CustomInventory.

Certain parameter types can be accepted by native functions, but not directly. In this case they must be converted from one type to another (usually from an int). For instance, if a Name argument is passed from ZScript, its actual type that gets passed is int since it represents the index of the Name internally. Below is a table covering the alternative VM parameter types and how to convert to them:

Parameter Type Desired Type Conversion Notes
int FName FName name = ENamedName(x);
int FSoundID FSoundID sound = FSoundID::fromInt(x);
int PalEntry PalEntry color = x; Converts directly from int to color.
double DAngle DAngle ang = DAngle::fromDeg(x);
double FAngle FAngle ang = FAngle::fromDeg(x);
int FState* FState* state = StateLabels.GetState(x, self->GetClass()); Must be a non-static function from an Actor. For action functions, stateowner should be used instead of self.

For other datatypes like pointers, a pointer of the desired type can be used directly e.g. AActor* self. Classes like class<Actor> are passed as a PClass* type. StateLabels are passed as an int as they're the index of the FState within the state table. out parameters are special in that their values are passed as a pointer instead of directly. As you can tell, this is mainly only useful for primitives since everything else is already passed as a pointer, non-native structs included.

// In ZScript.
native void Modify(out int x);
// In engine.
void Modify(DObject* self, int* x)
{
    // ...
    // This doesn't need to be null checked since it's an argument that's guaranteed to exist.
    *int = res;
}

vararg functions don't support native functionality as they rely on the VM to access the passed arguments. They must be defined as exclusively VM functions so they should be used with care due to performance reasons.

Return Values

All VM functions must have a return value with the return being the number of values returned by it. If a function has no return value, it should return 0. This does not apply to native functions and they can return void. The VMReturn class handles all return types. It supports the following:

  • int pointer
  • double pointer
  • DVector2/3/4 pointer
  • static arrays of size 2/3/4
  • DQuaternion pointer
  • FString pointer
  • (const) void pointer pointer

Note that not all of the above types have default constructors and the Set*() functions may have to be used. For VM functions a set of macros are included for returning single values:

  • ACTION_RETURN_INT(int)
  • ACTION_RETURN_BOOL(bool)
  • ACTION_RETURN_FLOAT(double)
  • ACTION_RETURN_STRING(FString)
  • ACTION_RETURN_OBJECT(DObject*)
  • ACTION_RETURN_POINTER(void*)
  • ACTION_RETURN_CONST_POINTER(const void*)
The above two should only be used for types that are normally passed by reference e.g. structs.
  • ACTION_RETURN_VEC2(DVector2)
  • ACTION_RETURN_VEC3(DVector3)
  • ACTION_RETURN_VEC4(DVector4)
  • ACTION_RETURN_QUAT(DQuaternion)
  • ACTION_RETURN_STATE(FState*)

For functions in ZScript with multiple arguments e.g. int, double MyFunc(), there are no macros to assist and argument management must be handled manually. This is fairly simple but requires some checks as not every call will always return every value. The numret variable denotes how many return values are expected and the ret pointer stores all the VMReturns. A function can have no more than eight return values, but at that point it's better to consider using a struct that stores the return data as a single value. Following the example of two returns, an int followed by a double, it would be handled this way:

DEFINE_ACTION_FUNCTION_NATIVE(/*..*/)
{
    // ...

    if (numret > 0)
        ret[0].SetInt(intRes);
    if (numret > 1)
        ret[1].SetFloat(floatRes);

    return numret;
}

Native functions have special handling both for return types and multi returns. Not every type from a native function can be returned directly. The valid types are int, double, and standard pointers passed by reference e.g. DObjects or structs. For other types like FString and the vector types, the return result is instead handled as a pointer after all other parameters from the function have been listed. For instance, if you had a function that returns a string, string MyStringFunc(int x), it would take the following form:

void MyStringFunc(DObject* self, int x, FString* result)
{
    // ...
    // This doesn't need to be null checked because it's the direct return value.
    *result = fstringRes;
}

Note how since this isn't a static function, the first argument is always self. The type should match whatever type the function expects e.g. AActor. Since FString cannot be passed back directly it instead gets passed as a pointer at the end. The same applies for DVector2/3/4 and DQuaternion.

Multiple returns are handled in a similar way where only the first argument is directly returned and the rest are passed as pointers at the end of the parameter list. If the first return value can't be returned directly, it shows up at the start of the list of return values.

int MyFunc(DObject* self, double* res2)
{
    // ...
    // These need to be null checked since they're optional.
    if (res2 != nullptr)
        *res2 = floatRes;

    return intRes;
}

Example

// In ZScript.
class Actor : Thinker native
{
    // ...
    native clearscope string GetName() const;
    // ...
}
// In engine.
static void GetName(AActor* self, FString* result)
{
    *result = self->player != nullptr ? self->player->userinfo->GetName() : self->GetTag("");
}

DEFINE_ACTION_FUNCTION_NATIVE(AActor, GetName, GetName)
{
    PARAM_SELF_PROLOGUE(AActor)

    FString result;
    GetName(self, &result);
    ACTION_RETURN_STRING(result);
}