unModified()

The only way to know for sure is to try

Faking OOP in Arma 3 SQF

Limitation spawns creativity

October 21, 2023

Helicopter flying on towards a populated military camp the top of the hill kicking dust in front of sun glare

Arma 3 v2.14, which was released September 5, 2023, added a simple OOP implementation with the new createHashMapObject command. This means the approach implemented in this article is no longer necessary.

It wasn't long ago that I introduced myself to a new project team, and told everyone that writing software as a hobby was somewhat therapeutic for me, that I find "inner peace" when writing code. I'm at it again. This time, it's not JavaScript, Lua, or any of the mainstream programming langauges you know. This time, it's SQF from the video game Arma 3.

SQF and what it is missing

Arma 3 is a millitary simulation (milsim) video game. SQF is Arma 3's scenario scripting language. It's a language with very simple yet unusual syntax where nearly everything is a command (i.e. unary and binary operators) and entire scripts are nothing more than a clever chain of "commands" disguised as regular programming language syntax. If there's one thing that is certain, it's that SQF will mess with your C-like language muscle memory.

But Arma 3 is an old game, which also makes SQF an old language. Even older if you consider the fact that SQF had a predecessor called SQS, the scripting language for the older Arma titles. SQF does not have the bells and whistles, nor the rapid improvements that we are used to seeing in modern general-purpose programming languages. Among those missing features are the lack of object-creation syntax (i.e. class and new) and lexical closures.

Not to say SQF is not object-oriented, it does have objects. Many of the game entities like units, groups, weapons, and vehicles are all objects with corresponding object-specific commands that are essentially the object's "methods". It's just that you can't define and create objects at the language level, free from any game objects.

To give an example, here's a simple factory function in JavaScript.

const createBox = (width, height) => ({
  getWidth: () => width,
  getHeight: () => height,
  getArea: () => width * height
});

const box1 = createBox(4,2)
console.log(box1.getWidth())  // 4
console.log(box1.getHeight()) // 2
console.log(box1.getArea())   // 8

Due to the lack of an equivalent to class and new, there would be no analog in SQF and is obviously not doable. But the lack of lexical closures wasn't so obvious until I tried it for myself. Here's the equivalent of the above JavaScript in SQF. It will compile without error, but it won't run the way we expect it to run:

createBox = {
  params ["_width", "_height"];

  createHashMapFromArray [
    ["getWidth", {_width}],
    ["getHeight", {_height}],
    ["getArea", {_width * _height}]
  ];
};

_box1 = [4,2] call createBox;
diag_log (call (_box1 get "getWidth"));  // Prints nothing
diag_log (call (_box1 get "getHeight")); // Prints nothing
diag_log (call (_box1 get "getArea"));   // Prints nothing

The issue with the above SQF code snippet is that when the getWidth, getHeight, and getArea "methods" are called from the box1 "instance", the value of both _width and _height are both undefined. The earlier call of createBox to "instantiate" _box1 did not create a scope level that would close over the returned HashMap and contain the values of the given _width and _height. To add to this, some SQF commands will outright not run if supplied an undefined value and would just move along without error, like in the case of getArea.

Problem breakdown and baseline

Simply put, we have two problems to solve:

  1. Be able to maintain classical OOP semantics. That is, we define a "class", instantiate an "instance" from that class, store state in that instance, pass around the instance, call methods on the instance, mutate the state of that instance.
  2. Be able to do a basic form of code reuse. That is, if I define that a "class" that "extends" another "class", then the instance of that extending class should have aspects from the extending class as well as the class it extended from.

And as excited as I am in explaining how I went about solving this problem, it's best if we define a baseline in another language so that we have something to compare it with. And so, I've prepared the typical box example in JavaScript:

class Rectangle {
  constructor(width, height) {
    this._width = width
    this._height = height
  }

  getWidth() {
    return this._width
  }

  getHeight() {
    return this._height
  }

  getArea() {
    return this._width * this._height
  }
}

class Square extends Rectangle {
  constructor (side) {
    super(side, side)
  }
}

const box1 = new Rectangle(4,2)
console.log(box1.getWidth())  // 4
console.log(box1.getHeight()) // 2
console.log(box1.getArea())   // 8

const box2 = new Square(4)
console.log(box2.getWidth())  // 4
console.log(box2.getHeight()) // 4
console.log(box2.getArea())   // 16

Proposed syntax

Back when I was still active in the open-source community, a wise open-source developer once taught me that the best way to design a library is to first design the API. That is, put myself in the shoes of another developer, how would I design the API so that it's easy for that other developer to use? And by extension, it also meant resisting the urge of diving straight into writing code and do the architecture work first. This is one of the few principles I live by to this day, and will be using in this exercise.

SQF does not have equivalents to class, new, this, and many of the other special OOP syntax. But we don't have to look far for an inspiration. JavaScript prior to ECMAScript 6 used object literals (essentially used like hash maps) both to define and to represent objects. Programmers have also historically used prefixes like _, __, @, and $ to give special meaning to properties. For instance, Drupal (PHP) represents render elements as associative arrays with special properties prefixed with #. And languages like Lua and Python pass the instance to methods as the first parameter in place of a magical this variable.

Combining these concepts, I've come up with a simple syntax to match the box baseline:

Rectangle = createHashMapFromArray [
  ["#constructor", [
    params ["_context", "_width", "_height"];
    _context set ["_width", _width];
    _context set ["_height", _height];
  ]],
  ["getWidth", {
    params ["_context"];
    (_context get "_width");
  }],
  ["getHeight", {
    params ["_context"];
    (_context get "_height");
  }],
  ["getArea", {
    params ["_context"];
    (_context get "_width") * (_context get "_height");
  }]
];

Square = createHashMapFromArray [
  ["#extends", Rectangle],
  ["#constructor", [
    params ["_context", "_side"];
    _context set ["_width", _side];
    _context set ["_height", _side];
  ]]
];

const _box1 = [Rectangle, [4,2]] call OOP_fnc_new;
console.log(call (_box1 get "getWidth"));  // 4
console.log(call (_box1 get "getHeight")); // 2
console.log(call (_box1 get "getArea"));   // 8

const _box2 = [Square, [4]] call OOP_fnc_new;
console.log(call (_box2 get "getWidth"));  // 4
console.log(call (_box2 get "getHeight")); // 4
console.log(call (_box2 get "getArea"));   // 16

A little clunky, but it's workable. Since we cannot create or overload operators, we use a factory function OOP_fnc_new in place of a new operator to create the instance. The name is special following the prescribed naming convention of TAG_fnc_functionName, with TAG being your globally unique namespace. For both classes and instances, they are represented using HashMaps. And just like Python and Lua, we pass in the instance as the first argument of methods.

We'll also have a few key differences from the official v2.14 implementation:

  • The constructors in the inheritance chain won't be called in sequence. Instead, the most recent override takes precedence to be consistent with methods.
  • We won't have equivalents to #clone and #type yet to keep the implementation simple.
  • We won't have equivalents to #delete and #flags as those involve engine-level information (e.g. we won't know when the instance is going out of scope, lost all references, or is going to be garbage-collected).
  • We'll be renaming #base and #create to #extends and #constructor respectively, as well as conventionally name the instance _context instead of _self to avoid collision with the official implementation.

Now with all the assumptions made and constraints set, we can work on the implementation.

Instantiation

Now that we have a structure for our "class", we'll need to create the "instances" from that class. As any of you who've gone through a basic programming course, you've probably heard at some point that "classes are blueprints". We'll do just that by creating copies of the class HashMap as our instances. In addition to copying, we'll also make sure to omit any class "metaproperties" (properties we've prefixed with #) from the instances.

Here's our initial implementation consisting of only instantiation:

OOP_fnc_new = {
  params [["_class", createHashMap], ["_arguments", []]];

  private _constructor = _class getOrDefault ["#constructor", {}];
  private _instance = [_class] call OOP_fnc_createInstance;

  ([_instance] + _arguments) call _constructor;

  _instance;
};

OOP_fnc_createInstance = {
  params ["_class"];

  private _methods = (keys _class) select {
    private _isMeta = (_x select[0,1]) isEqualTo "#";
    private _isMethod = (typeName (_class get _x)) isEqualTo "CODE";
    !_isMeta && _isMethod;
  };

  createHashMapFromArray (_methods apply {[_x, _class get _x]});
};

Code reuse and inheritance

Code reuse is a broad topic and are a lot of ways to do this, each with its own pros and cons. We could do classical inheritance like Java, where code reuse follows a rigid tree of classes extending other classes. We could do prototypal inheritance like JavaScript, where code reuse is done by walking up the prototype chain of live objects. Or we could do traits like in PHP, where code reuse is achieved by composition with traits.

To keep it simple, we'll do a simple "merge inheritance" following v2.14's OOP implementation. That is, we'll create the instantiable HashMap by merging all the HashMaps in the inheritance line from ancestor to descendant (i.e. descendant takes precedence) into one "resolved" HashMap. Once we have this resolved HashMap, we'll instantiate the instance from it just like in the previous section.

Here's an updated implementation which incorporates the inheritance functionality:

OOP_fnc_new = {
  params [["_class", createHashMap], ["_arguments", []]];

  private _inheritedClasses = [_class] call OOP_fnc_getInheritedClasses;
  private _resolvedClass = _inheritedClasses call OOP_fnc_shallowMerge;
  private _constructor = _resolvedClass getOrDefault ["#constructor", {}];
  private _instance = [_resolvedClass] call OOP_fnc_createInstance;

  ([_instance] + _arguments) call _constructor;

  _instance;
};

OOP_fnc_createInstance = {
  params ["_class"];

  private _methods = (keys _class) select {
    private _isMeta = (_x select[0,1]) isEqualTo "#";
    private _isMethod = (typeName (_class get _x)) isEqualTo "CODE";
    !_isMeta && _isMethod;
  };

  createHashMapFromArray (_methods apply {[_x, _class get _x]});
};

OOP_fnc_getInheritedClasses = {
  params ["_class"];
  private _extendsKey = "#extends";
  private _hasParent = _extendsKey in _class;
  private _parents = if _hasParent then [{[_class get _extendsKey] call OOP_fnc_getInheritedClasses},{[]}];
  _parents + [_class];
};

OOP_fnc_shallowMerge = {
  private _target = createHashMap;
  {_target merge [_x, true]} forEach _this;
  _target;
};

Context and dynamic code compilation

Looking back at the proposed syntax, notice how we used call to call getWidth, getHeight, getArea, and passed no arguments. And yet somehow, these methods magicaly receive the correct _context (i.e. the instance) as the first argument. If we use the code we have so far as-is, the caller would have had to supply the correct instance on every call. But somehow the caller is not (and will not be) doing that. How does this work?

To reproduce this scenario in a more familiar programming language, let's do some JavaScript:

// Calling getWidth() in the context of box1 (this === box1).
const box1 = new Rectangle(4,2)
console.log(box1.getWidth()) // 4

// Calling getWidth() out of context (this === undefined)
const getWidthNoContext = box1.getWidth
console.log(getWidthNoContext()) // error

// Buuut! I can create a getWidth() with a fixed context!
const getWidthFixedContext = getWidth.bind(box1)
console.log(getWidthFixedContext()) // 4

Just like how we created a new function with a predetermined this using Function.prototype.bind(), we can create Code with custom parameters at runtime in SQF using compile. More correctly speaking, compile works more like JavaScript's Function() constructor, allowing us to dynamically compile Code from strings at runtime. Instead of the instances holding references to the resolved class' methods, we'll replace them with dynamically generated Code. It's kinda like method decorators but more primitive. With this, we can intercept method calls on the instance, do whatever additonal things we want like add additional parameters, and then forward these calls to the original method.

But that's just half of the problem.

The other half is knowing which instance to pass on to the original method. Say I create two instances of Square called _box1 and _box2. If I call the getWidth from _box1, then _context should be _box1. But if I call getWidth from _box2, then _context should be _box2. Something somewhere must be aware which instance the method is being called against. We know that this something will be our decorated methods since we don't want to disturb our designed caller and class structures. But how do we track which instance?

One way of doing this is to cache both instance and the resolved class it was instantiated from in separate HashMaps, both keyed by an instance-specific UUID generated upon instantiation. Then we can write our decorated methods so that each one is "hardcoded" with the UUID of the instance it was generated for. When the decorated method is called, the method would look up the instance and the resolved class with the UUID given to it, create the arguments list that includes the looked-up instance, and call the original method from the looked-up resolved class using the extended arguments.

Phew! This one was a long one to explain, but probably better explained with our final iteration of the code:

OOP_instanceCache = createHashMap;
OOP_classCache = createHashMap;
OOP_uuidStream = "00000000000000000000000000000000" splitString "";
OOP_uuidCharacters = "0123456789ABCDEF" splitString "";

OOP_fnc_new = {
  params [["_class", createHashMap], ["_arguments", []]];

  private _instanceId = call OOP_fnc_uuid;
  private _inheritedClasses = [_class] call OOP_fnc_getInheritedClasses;
  private _resolvedClass = _inheritedClasses call OOP_fnc_shallowMerge;
  private _constructor = _resolvedClass getOrDefault ["#constructor", {}];
  private _instance = [_instanceId, _resolvedClass] call OOP_fnc_createInstance;

  OOP_classCache set [_instanceId, _resolvedClass];
  OOP_instanceCache set [_instanceId, _instance];

  ([_instance] + _arguments) call _constructor;

  _instance;
};

OOP_fnc_createInstance = {
  params ["_instanceId", "_class"];

  private _methods = (keys _class) select {
    private _isMeta = (_x select[0,1]) isEqualTo "#";
    private _isMethod = (typeName (_class get _x)) isEqualTo "CODE";
    !_isMeta && _isMethod;
  };

  createHashMapFromArray (_methods apply {[_x, [_instanceId, _x] call OOP_fnc_bind]});
};

OOP_fnc_getInheritedClasses = {
  params ["_class"];
  private _extendsKey = "#extends";
  private _hasParent = _extendsKey in _class;
  private _parents = if _hasParent then [{[_class get _extendsKey] call OOP_fnc_getInheritedClasses},{[]}];
  _parents + [_class];
};

OOP_fnc_shallowMerge = {
  private _target = createHashMap;
  {_target merge [_x, true]} forEach _this;
  _target;
};

OOP_fnc_uuid = {
  (OOP_uuidStream apply {selectRandom OOP_uuidCharacters}) joinString "";
};

OOP_fnc_bind = {
  params ["_instanceId", "_methodName"];

  private _context = "(if (isNil{_this}) then {[]} else {if(typeName _this isEqualTo 'ARRAY') then {_this} else {[_this]}})";
  private _args = "([OOP_instanceCache get '%2'] + %1)";
  private _function = "((OOP_classCache get '%2') get '%3')";
  private _call = format ["%1 call %2", _args, _function];

  compile format [_call, _context, _instanceId, _methodName];
};

Conclusion

It took about a couple weeks to come up with this approach, and then a month to finally write this down in blog form. I also wrote the above off the top of my head, so I'll be missing a few semi-colons and brackets here and there. Too lazy to verify if the syntax is correct. SQF is also a niche language, it doesn't have the expansive ecosystem of dev tools like in more mainstream languages. You actually have to run the script in-game to know if it's right or wrong.

In the end, it was a fun challenge, one that I have not done in quite a while.