Legacy: Have I completely misunderstood exceptions?

Started by Adam Novagen, November 18, 2023, 02:33:11

Previous topic - Next topic

Adam Novagen

This thread applies specifically to legacy BMax, not NG.

Okay. I thought I had this licked, thought I understood everything I needed to about exceptions. Then this happened.

I wanted my program to be able to catch any unexpected errors during beta testing or after release, and perform a master debug dump to a file to help me track down the problem. But now, after everything, it seems that the built-in Blitz exceptions - null object, array out-of-bounds, etc - only work in Debug mode, not release mode???

Example. This works fine with debug mode on or off:

SuperStrict

Try
    Throw "Error detected"
Catch err:Object
    Print err.toString()
EndTry

As you'd expect, it prints the exception "Error detected" every time, debug on or off. But now, try this:

SuperStrict

Type thing
    Field name:String
EndType

Local jason:thing = New thing
    jason.name = "jason"

Local barney:thing

Try
    Print jason.name
    Print barney.name ' Not initialized, Null object
Catch err:Object
    Print err.toString()
EndTry

With debug mode enabled, this prints "Attempt to access field or method of Null object" much as you'd expect. But with debug mode off, the program just crashes with the generic EXCEPTION_ACCESS_VIOLATION popup.

Just for laughs, I tried a bad array index, like this:

SuperStrict

Local array:Int[] = [5,10,15,20] ' 4 elements

Try
    For Local i:Int = 0 To 7 ' Try to get 8 elements
        Print "[" + i + "] = " + array[i]
    Next
Catch err:Object
    Print err.ToString()
EndTry

Again, with debug on, it prints the expected "Attempt to index array element beyond array length" error. But with debug mode off? It goes right through all eight elements, spitting out random numbers that presumably represent whatever garbage data is at those points in memory.

So... Is there seriously no way to catch these kinds of exceptions if debug mode is off? Because my program already handles all the errors that can happen externally, the whole point of this was to provide me with a helpful log if a playtester actually found a bug in my actual code. I mean, if that's the way it is, that's the way it is, but... God damn, man, I spent over a month writing that debug dump tracker >:(
We all know the main problem with dictionaries is that they contain too many words, and not enough butterscotch sauce!

col

Hiya,

Usually the Type in the catch expression would relate to what kinds of exceptions it will deal with.

Have you tried using
...
Catch err:TNullObjectException
...

I can't remember if that's an NG only thing only though, probably not and is probably also valid for brls BlitzMax.
Also it's quite normal for a release build to have undefined behaviour if your debug build isn't working correctly when it comes to accessing memory.

In the case of the index out of range, those ranges checks don't exist in release builds. Reading a range of memory that is allocated to your process is allowed, even if it outside the range of the array. If you tried to read memory outside of your process allocated range then you get an 'Exception Access Violation' on Windows and a 'Segmentation Fault' on the 'nix variants.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

Adam Novagen

Quote from: col on November 18, 2023, 09:12:40Usually the Type in the catch expression would relate to what kinds of exceptions it will deal with.

Have you tried using
...
Catch err:TNullObjectException
Yep, thought of that, tried it, and also tried Catch TBlitzException since TNullObjectException extends that. With debug mode off, it still just crashes to EXCEPTION_OBJECT_VIOLATION, try it yourself and you'll see unless I'm going absolutely crazy.

SuperStrict

Type thing
    Field name:String
EndType

Local jason:thing = New thing
    jason.name = "jason"

Local barney:thing

Try
    Print jason.name
    Print barney.name ' Not initialized, Null object
Catch err:TNullObjectException
    Print err.toString()
EndTry

Basically, the program never even reaches the Catch statement, it just EAV crashes instantly on the Null object.
We all know the main problem with dictionaries is that they contain too many words, and not enough butterscotch sauce!

Derron

Just for your interest:

NG's C code for release:
#line 5 "/home/ronny/Arbeit/Tools/BlitzMaxNG/tmp/untitled1.bmx"
{
BBOBJECT ex;
bbExTry {
case 0: {
#line 6 "/home/ronny/Arbeit/Tools/BlitzMaxNG/tmp/untitled1.bmx"
{
BBINT bbt_i=0;
for(;(bbt_i<=7);bbt_i=(bbt_i+1)){
#line 7 "/home/ronny/Arbeit/Tools/BlitzMaxNG/tmp/untitled1.bmx"
brl_standardio_Print(bbStringConcat(bbStringConcat(bbStringConcat(((BBString*)&_s11),bbStringFromInt(bbt_i)),((BBString*)&_s12)),bbStringFromInt(((BBINT*)BBARRAYDATA(bbt_array,1))[((BBUINT)bbt_i)])));
}
}
bbExLeave();
}
break;
case 1: {
ex = bbExCatch();
if (bbObjectDowncast((BBOBJECT)ex,(BBClass*)&bbObjectClass) != &bbNullObject) {
BBOBJECT bbt_err=(BBOBJECT)ex;
#line 10 "/home/ronny/Arbeit/Tools/BlitzMaxNG/tmp/untitled1.bmx"
brl_standardio_Print((bbt_err)->clas->ToString((BBOBJECT)bbt_err));
} else {
goto _rethrow;
}
goto _endtry;
}
break;
_rethrow:;
bbExThrow(ex);
}
}
_endtry:;


And this one is for debug:
bbt_array=bbt_2;
struct BBDebugStm __stmt_1 = {0x558ac721d846ce0d, 5, 0};
bbOnDebugEnterStm(&__stmt_1);
#line 5 "/home/ronny/Arbeit/Tools/BlitzMaxNG/tmp/untitled1.bmx"
{
BBOBJECT ex;
bbExTry {
case 0: {
bbOnDebugPushExState();
struct BBDebugScope __scope = {
BBDEBUGSCOPE_LOCALBLOCK,
0,
{
{
BBDEBUGDECL_END
}
}
};
bbOnDebugEnterScope((BBDebugScope *)&__scope);
struct BBDebugStm __stmt_0 = {0x558ac721d846ce0d, 6, 0};
bbOnDebugEnterStm(&__stmt_0);
#line 6 "/home/ronny/Arbeit/Tools/BlitzMaxNG/tmp/untitled1.bmx"
{
BBINT bbt_i=0;
for(;(bbt_i<=7);bbt_i=(bbt_i+1)){
struct BBDebugScope_1 __scope = {
BBDEBUGSCOPE_LOCALBLOCK,
0,
{
{
BBDEBUGDECL_LOCAL,
"i",
"i",
.var_address=&bbt_i
},
{
BBDEBUGDECL_END
}
}
};
bbOnDebugEnterScope((BBDebugScope *)&__scope);
struct BBDebugStm __stmt_0 = {0x558ac721d846ce0d, 7, 0};
bbOnDebugEnterStm(&__stmt_0);
#line 7 "/home/ronny/Arbeit/Tools/BlitzMaxNG/tmp/untitled1.bmx"
brl_standardio_Print(bbStringConcat(bbStringConcat(bbStringConcat(((BBString*)&_s11),bbStringFromInt(bbt_i)),((BBString*)&_s12)),bbStringFromInt(((BBINT*)BBARRAYDATAINDEX((bbt_array),(bbt_array)->dims,((BBUINT)bbt_i)))[((BBUINT)bbt_i)])));
bbOnDebugLeaveScope();
}
}
bbOnDebugLeaveScope();
bbExLeave();
bbOnDebugPopExState();
}
break;
case 1: {
bbOnDebugPopExState();
ex = bbExCatch();
if (bbObjectDowncast((BBOBJECT)ex,(BBClass*)&bbObjectClass) != &bbNullObject) {
BBOBJECT bbt_err=(BBOBJECT)ex;
struct BBDebugScope_1 __scope = {
BBDEBUGSCOPE_LOCALBLOCK,
0,
{
{
BBDEBUGDECL_LOCAL,
"err",
":Object",
.var_address=&bbt_err
},
{
BBDEBUGDECL_END
}
}
};
bbOnDebugEnterScope((BBDebugScope *)&__scope);
struct BBDebugStm __stmt_0 = {0x558ac721d846ce0d, 10, 0};
bbOnDebugEnterStm(&__stmt_0);
#line 10 "/home/ronny/Arbeit/Tools/BlitzMaxNG/tmp/untitled1.bmx"
brl_standardio_Print(((BBOBJECT)bbNullObjectTest((BBObject*)bbt_err))->clas->ToString((BBOBJECT)bbt_err));
bbOnDebugLeaveScope();
} else {
goto _rethrow;
}
goto _endtry;
}
break;
_rethrow:;
bbExThrow(ex);
}
}
_endtry:;


-> Both contain the "bbtry"-stuff, but only the debug build actually catches it there.

Means the same "unexpected" behaviour as in vanilla/legacy.


bye
Ron

Adam Novagen

Okay, thanks Ron. It does make sense, I assume that BMax checking for Null objects and throwing exceptions is considered a "debug" feature, which has its own associated overhead costs... I just wish I'd figured that out before spending an entire month building the debug dump. Oh well, that's what I get for testing it with a manually-invoked crash, instead of performing a real-world test by intentionally creating a Null object.

Live and learn, I suppose. I might be able to salvage it with some Null object checking in a few key loops; I'll look into that later. Thanks for the help.
We all know the main problem with dictionaries is that they contain too many words, and not enough butterscotch sauce!

Derron

I raised an issue for this - and there are some responses explaining why null/out-of-bounds is not throwing an exception:
https://github.com/bmx-ng/bcc/issues/636

Maybe it clarifies things for you.


PS: Yes, a "crashlog"-generation would be a nice feature. What I did - is to wrap my execution around a "gdb" call (gnu debugger) and having gdb-info generation enabled. Then when the program abnormally exits I let gdb print out/log the backtrace it can spit out. Of course this only works in NG.


bye
Ron

Adam Novagen

That was very kind of you Ron, I appreciate it. Some good news in the end, I realized that my crash dump can still be helpful anyway. Since it was intended for the private playtesting phase in the first place, there's no reason why I can't just distribute the debug build; the game is generally super-lightweight, even tested on pitiful Celeron machines from 2018, so there's no reason to worry about the performance overhead in this case. I'll keep release mode for when I actually publish it post-playtest, and even then I can always include the debug build in case players have more issues.
We all know the main problem with dictionaries is that they contain too many words, and not enough butterscotch sauce!