Jump to content
noubernou

carma2 - a thin object oriented prototype parser for SQF

Recommended Posts

carma2

A thinline object parser/translator for RV SQF offering a prototyped object system with automatic, unintrusive garbage collecting memory management.

Github: https://github.com/NouberNou/carma2

The only dependency is CBA.

Concept

The concept of carma2 is to add a very low overhead, very simple object implementation in SQF. The focus is on maintaining as many paradigms with SQF as possible, so as to not create too much of a disassociation with base SQF.

Implementation of objects is done in a very simple fashion that ultimately is closer to syntactic sugar than it is a "proper" object implementation, but through the help of helpers provides a robust object system. Objects are created using the new keyword and members and methods are accessed using the . operator and assigned/defined using the standard SQF = operator. The only major difference is that method invocation is done using () following the method name, instead of the standard SQF arg call function format (though it is entirely possible to invoke methods this way, though with some caveats).

A simple carma2 example is below:

_testObject = new carma2_object(); // create a new object from the default carma2_base object
_testObject.myMethod = { player sideChat "hello world!"; };
_testObject.myMethod(); // calls myMethod and displays "hello world!"
Method parameters can be accessed via the normal _this variable.

Objects as such then are defined as they are created, similar to the Javascript prototype system. As such creating a new object of an existing type is as easy as follows:

 

// using the code from above
_anotherTestObject = new _testObject();
_anotherTestObject.myMethod(); // calls myMethod on this new object.
Objects will copy their members (but will not copy their values) to the new object, and copy their methods as well. This means that you can easily define and create new objects. Type checking can be done via the special __prototype member in every object, which will contain the object that new was called on. Introspection can be achieved via using the normal allVariables SQF command on an object (carma2 objects are implemented as location objects, which have no in-game overhead).

Method definitions can access their calling object via the _thisObj variable.

For example:

_testObject = new carma2_object(); // create a new object from the default carma2_base object
_testObject.myMethod = { player sideChat format["myVal: %1", _thisObj.myVal]; };
_testObject.myVal = 2;
_testObject.myMethod(); // calls myMethod and displays "myVal: 2"
Objects can call a constructor like function on creation by assigning the special method __init. There are no destructors in carma2 as the system uses a garbage collecting reference tracker. Implementation of a special method for when the object is garbage collected (or the del keyword is used) is forthcoming, though programmers using carma2 should make sure to smartly implement resources that need to be freed in a way that is not dependent on the lifespan of the object.

Usage

Using carma2 is very simple. Launch with the mod enabled/included, as well as CBA.

A simple usage example is here:

#include "\x\carma2\rv\addons\lib\carma.hpp"

CARMA_COMPILE("test.sqf");
That code will compile and load the file test.sqf. You can then access any globally defined objects compiled in there.

The CARMA_COMPILE macro is a macro to carma2_fnc_compile. If you just wish to compile your code with out executing it (not often the case), you can pass an optional false argument. The compilation function will return the compiled results either way.

Advanced Concepts

Calling Parent Functions

Calling the parent function of an overridden object method can be done via the __prototype member as seen below:

_testObject = new carma2_object(); // create a new object from the default carma2_base object
_testObject.myMethod = { player sideChat "hello world!"; };
_testObject.myMethod(); // calls myMethod and displays "hello world!"

_anotherTestObject = new _testObject();
_anotherTestObject.myMethod = { _thisObj.__prototype.myMethod(); player sideChat "good bye world!"; };
_anotherTestObject.myMethod(); // calls myMethod on this new object, which invokes myMethod from _testObject, printing "hello world!" and then "good bye world!"
Chaining

Chaining members is allowed if they are also objects (if they are not, undefined RPT errors may occur).

_var = _testObject.myMemberObject.anotherMember; // accessing a members member.
_testObject.myMemberObject.someMethod(1,2,3); // invoking a members method.
As of the time of writing, method chaining produces syntax bugs. It will be fixed before release.

Anonymous Objects

Passing an object to a SQF function or a carma2 object method can be done anonymously via the new keyword.

[new someObject()] call some_sqf_fnc;
_myObject.method(new subObject());
Performance

A often run into drawback with object oriented systems in SQF are the overhead that objects introduce, either through their programmatic implementation or through their in engine implementation. In carma2, the language strives to be as close as possible to the engine, to minimize overhead. To do this carma2 utilizes the native setVariable and getVariable SQF functions on native SQF objects, which in this case are locations. Locations in SQF add no apparent overhead to game performance, and are simply resident in the SQF engine's memory. As such, tens of thousands of them can be initiated with no performance impact. This is already being utilized in projects such as ACRE for implementing a fast, SQF native hash-map implementation.

Because of this member variable access is a simple call to getVariable. Assignments are a simple call to setVariable. Invoking a method simply calls a wrapper function that creates the _thisObj special variable and then calls the arguments on a getVariable call. Overhead on method invocation is as little as 0.0077ms, and default single member access is often a third of that. This provides almost native SQF level speeds.

Share this post


Link to post
Share on other sites

Hi,

I am not an expert in this case, but for my understanding it is a very smart work.

I admire those work, the concept and everyone's advantage.

 

Keep it up. :)

 

Greetings

McLupo

Share this post


Link to post
Share on other sites

An ingame OO SQF-compiler? Very nice indeed!

 

If I see it correctly, you use locations as container-objects, right?

Share this post


Link to post
Share on other sites

An ingame OO SQF-compiler? Very nice indeed!

 

If I see it correctly, you use locations as container-objects, right?

Correct, locations are awesome. In ACRE2s current internal/testing builds I am spawning a pool of 50,000 of them to handle hash maps. There is literally no overhead like you would get with logics or other data containers in the engine. When I found this out soooo many things became possible haha.

Share this post


Link to post
Share on other sites

Nice! At the moment I am using still logics for most of stuff like this. I will probably test out locations now^^

Share this post


Link to post
Share on other sites

Nice! At the moment I am using still logics for most of stuff like this. I will probably test out locations now^^

The only problem with locations is that nearestLocation(s) will run a lot slower. To be honest though I've never really seen anyone use locations for anything, they aren't really used in the engine at all, and in the BI SQF code it is called in only a few places.

I feel it's a worthy sacrifice for what is gained. Logics will slow the game down after a couple hundred.

Share this post


Link to post
Share on other sites

ALICE and some other A2 modules use it very intensive, maybe it will be problematic when someone tries to port that stuff over to A3.

I tried to make villages and bases in co10 Escape locations to, but ran into some problems and reverted to markers.

Share this post


Link to post
Share on other sites

Implemented a number of new features and functionality: https://github.com/NouberNou/carma2#advanced-concepts

 

Pseudo-static Members with :: Operator

Using the :: operator you can easily access a objects prototype members/methods, the same as above using the __prototype member.

 

Since all objects that descend from a common prototype share the same instance of that prototype you can use it to define static methods/members that will be shared across all classes.

 

 

test_base = new carma2_object(); // this will be our prototype.
 
test_base.staticMember = 123; // assign the prototype object a member var
 
test_instance1 = new test_base();
test_instance2 = new test_base();
 
player sideChat format["test_instance1: %1", test_instance1::staticMember]; // prints 123
 
test_instance1::staticMember = 321; // assign the static variable on test_instance1 to 321
 
player sideChat format["test_instance2: %1", test_instance2::staticMember]; // print the static variable on test_instance2, prints 321
Calling Overriden Methods

Calling the original overridden methods is done via accessing the objects prototype object definition, either through the :: operator, or the __prototype member, and then using the magic methods __call(context, arg1, arg2, ...) or __apply(context, arg_array). These methods execute the desired overriden/parent method in a supplied context, commonly passing _thisObj to the method, along with the methods arguments.

 

An example is given below demonstrating the usage of the __call and __apply functions.

 

test_base = new carma2_object();
test_base.testVal = "base instance";
test_base.parentMethod = {
    diag_log text format["Object %1: %2, %3", _thisObj.__id, _thisObj.testVal, _this];
};
 
test_instance = new test_base();
test_instance.testVal = "child instance";
 
test_instance.testMethod = {
    // call the prototype method, in the context of the prototype, essentially a static method.
    _thisObj::parentMethod(1);
    
    // call the prototype method, but in the context of this instance using __call(context, arg1, arg2, ...)
    _thisObj::parentMethod.__call(_thisObj, 2);
    
    // call the prototype method, but using __apply(context, arg_array)
    _args = [3];
    _thisObj::parentMethod.__apply(_thisObj, _args);
};
 
test_instance.testMethod();
 

This prints to the RPT:

 

Object 1: base instance, [1]
Object 2: child instance, [2]
Object 2: child instance, [3]
 

An example of overridden method calling it's parent method is below.

 

 

test_base = new carma2_object();
test_base.testVal = "base instance";
test_base.testVal = 0;
test_base.testMethod = {
    diag_log text format["parent: %1 testVal: %2", _this[0], _thisObj.testVal];
};
 
test_instance = new test_base();
test_instance.testVal = 999;
test_instance.testMethod = {
    _thisObj::testMethod(333); // call the parent method as a static method, the context is the __prototype object.
    _thisObj::testMethod.__call(_thisObj, _this[0]); // call takes the args to the function in-situ after the context object
    _thisObj::testMethod.__apply(_thisObj, _this); // apply takes the args as an array
    diag_log text format["child: %1 testVal: %2", _this[0], _thisObj.testVal];
};
 
test_instance.testMethod(123);
 

Results in:

 

parent: 333 testVal: 0
parent: 123 testVal: 999
parent: 123 testVal: 999
child: 123 testVal: 999

Share this post


Link to post
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now

×