Strange asOBJ_POD behavior with copy constructor and opAdd

Started by
2 comments, last by WitchLord 10 years, 7 months ago

Hi,

I'm working on adding a scripting in my game engine. I registered btVector3 (the Vector3 from the bullet physics engine), but it is half-working : some methods are working (constructors, length, ...), but some are not (opAdd, opMul, opDiv, opSub, ...) : I get completely random values (garbage memory)

I created a small script to test this, registering a dummy Integer class:


#include <angelscript.h>
#include <cassert>
#include <cstdio>
#include <string>

class Integer {
    public:
        Integer() {
            _i = 0;
        }

        Integer(int i) {
            _i = i;
        }

        int getInt() const {
            return _i;
        }

        void setInt(int i) {
            _i = i;
        }
    private:
        int _i;
};

void IntegerConstruct(Integer* self) {
    new(self) Integer();
}

void IntegerConstructInt(int i, Integer* self) {
    new(self) Integer(i);
}

void IntegerConstructCopy(const Integer& other, Integer* self) {
    new(self) Integer(other);
}

void print(const Integer& i) {
    printf("%d\n", i.getInt());
}

Integer operator+(const Integer& i1, const Integer& i2) {
    printf("%d %d\n", i1.getInt(), i2.getInt()); //Debug
    return Integer(i1.getInt() + i2.getInt());
}

const std::string code =
    "void main() {\n"
    "  Integer i(42);\n"
    "  Integer j(1337);\n"
    "  print(i);\n"
    "  print(j);\n"
    "  print(i.opAdd(j));\n"
    "  print(i + j);\n"
    "}\n"
;

class ScriptEngine {
    private:
        void MessageCallback(const asSMessageInfo* msg) {
            const char* type = "ERR ";
            if( msg->type == asMSGTYPE_WARNING )
                type = "WARN";
            else if( msg->type == asMSGTYPE_INFORMATION )
                type = "INFO";
            printf("%s (%d, %d) : %s : %s\n", msg->section, msg->row, msg->col, type, msg->message);
        }
        asIScriptEngine* _engine;
        asIScriptModule* _mod;

    public:
        ScriptEngine() {
            int r;
            _engine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
            r = _engine->SetMessageCallback(asMETHOD(ScriptEngine, MessageCallback), this, asCALL_THISCALL); assert(r >= 0);

            r = _engine->RegisterObjectType("Integer", sizeof(Integer), asOBJ_VALUE | asOBJ_APP_CLASS_CK | asOBJ_POD); assert(r >= 0);
            r = _engine->RegisterObjectBehaviour("Integer", asBEHAVE_CONSTRUCT, "void f()", asFUNCTION(IntegerConstruct), asCALL_CDECL_OBJLAST); assert(r >= 0);
            r = _engine->RegisterObjectBehaviour("Integer", asBEHAVE_CONSTRUCT, "void f(int i)", asFUNCTION(IntegerConstructInt), asCALL_CDECL_OBJLAST); assert(r >= 0);
            r = _engine->RegisterObjectBehaviour("Integer", asBEHAVE_CONSTRUCT, "void f(const Integer& in)", asFUNCTION(IntegerConstructCopy), asCALL_CDECL_OBJLAST); assert(r >= 0);
            r = _engine->RegisterObjectMethod("Integer", "int get_i() const", asMETHOD(Integer, getInt), asCALL_THISCALL); assert(r >= 0);
            r = _engine->RegisterObjectMethod("Integer", "void set_i(int)", asMETHOD(Integer, setInt), asCALL_THISCALL); assert(r >= 0);
            r = _engine->RegisterObjectMethod("Integer", "Integer opAdd(const Integer& in) const", asFUNCTIONPR(operator+, (const Integer&, const Integer&), Integer), asCALL_CDECL_OBJFIRST); assert(r >= 0);
            r = _engine->RegisterGlobalFunction("void print(const Integer& in)", asFUNCTION(print), asCALL_CDECL); assert(r >= 0);

            _mod = _engine->GetModule("Test", asGM_ALWAYS_CREATE);
            r = _mod->AddScriptSection("script", code.c_str(), code.size()); assert(r >= 0);
            r = _mod->Build(); assert(r >= 0);
        }

        ~ScriptEngine() {
            _engine->Release();
        }

        void run() {
            int r;
            asIScriptContext* ctx = _engine->CreateContext();
            asIScriptFunction* func = _mod->GetFunctionByDecl("void main()");
            r = ctx->Prepare(func); assert(r >= 0);
            r = ctx->Execute(); assert(r >= 0);
            ctx->Release();
        }
};



int main() {
    ScriptEngine script;
    script.run();
    return 0;
}

When I run it, I get an output like this:


42
1337
36575664 42
36575664
36575664 42
36575664

What I notice:

- Well, operator+ get 36575664 (it changes at every execution) instead of 1337

- 42 is the second argument of operator+ whereas it should be the first

- The result of 36575664 + 42 = 36575664 (n + 42 = n)

Maybe the implicit constructor isn't working? Let's try:


//...
int main() {
    Integer i(42);
    Integer j(i);
    print(i);
    print(j);
    return 0;
}

I get this output:


42
42

So the implicit constructor works. But let's try to add an explicit one:


//...
        Integer(const Integer& other) {
            _i = other._i;
        }

//...

int main() { //The implicit constructor test is removed
    ScriptEngine script;
    script.run();
    return 0;
}

And now I get this output (which is the output expected):


42
1337
42 1337
1379
42 1337
1379

But now the things start to be very strange. Let's put a debug printf in the copy constructor, and in the function that is used to register it to angelscript:


//...

        Integer(const Integer& other) {
            _i = other._i;
            printf("foo\n");
        }

//...

void IntegerConstructCopy(const Integer& other, Integer* self) {
    printf("bar\n");
    new(self) Integer(other);
}

I get this output:


42
1337
42 1337
1379
42 1337
1379

So the copy constructor is never called, but is needed! Isn't this strange?

To go back to my original problem, here is my current script engine.

I looked in the btVector3's source file, and there is an explicit copy constructor, so I really don't know why my code isn't working...

Advertisement

It is all related to the native calling convention. In your first test you hadn't registered the Integer class correctly, which is why you were getting the wrong result. The original declaration should be registered with the flags asOBJ_APP_CLASS_C, not asOBJ_APP_CLASS_CK as it didn't have an explicit copy constructor.

When you explicitly declared the copy constructor in the Integer class the result was corrected as now the flag asOBJ_APP_CLASS_CK was correct.

If you're using a compiler with support for C++11 features (e.g. gnuc 4.7+, or MSVC 2012+), you can use the helper function GetTypeTraits<T>() to automatically determine the correct flags. For example:

#include <add_on/scripthelper/scripthelper.h>
r = _engine->RegisterObjectType("Integer", sizeof(Integer), asOBJ_VALUE | asOBJ_POD | GetTypeTraits<Integer>()); assert(r >= 0);

As for AngelScript calling or not calling the copy constructor, that is an optimization thing. There are still some places in the library where AngelScript will first create the object with the default constructor and then assign the value with the opAssign method. These are already on my to-do list to improve so that the copy constructor is called directly.

The btVector3 type has another trait that is not yet supported by AngelScript, and that is that the type is 16byte aligned (to optimize it for SIMD instructions). As such you cannot register this type as a value type. If you really want to use btVector3 directly in the scripts you need to register it as a scoped reference type, which will behave as a value type in the script, but is really allocated on the heap where it can be properly aligned to 16byte boundaries. Combine that with a memory pool for the allocation and deallocation of the vectors and you should not have any noticeable performance impacts.

Another option is that you avoid exposing the btVector3 type directly to the script, and instead use an ordinary unaligned structure for the vector3 type that the script will use. Of course, you will need to create wrappers for the functions that expect the btVector3 type, but hopefully you don't have too many of those exposed to the scripts.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Thanks, now my code works well. I've choosen the second option.

But I have a question: What can I do if I want to use the implicit copy constructor of a class, because I have a function with this signature: "Integer get_int()"? If I use asOBJ_APP_CLASS_C, angelscript tells I cannot make a copy, and if I use asOBJ_APP_CLASS_CK, it does not works because of the native calling convention.

Whatever you do, do not try to trick to AngelScript. It will just cause difficult to find bugs as things don't quite work as it should in all cases. ph34r.png

Seriously, the asOBJ_APP_ flags must be registered correctly, as these are what AngelScript uses to know exactly how the host application expects to handle the type in the native calling convention. If you can, I recommend that you use the GetTypeTraits<T>() helper function. It will make sure you don't make a mistake in chosing the wrong flags.

A few flags cannot be determined automatically though, and these are asOBJ_APP_CLASS_ALLINTS, asOBJ_APP_CLASS_ALLFLOATS, and asOBJ_APP_CLASS_ALIGN8. On platforms that split value types in different CPU registers (e.g. 64bit Linux) these flags may be needed to tell AngelScript about the content of the type, so that it will be able to place the data in the correct CPU registers.

The manual explains what each flags means and how to use them.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

This topic is closed to new replies.

Advertisement