[bb] Minimalist exceptions by Yasha [ 1+ years ago ]

Started by BlitzBot, June 29, 2017, 00:28:40

Previous topic - Next topic

BlitzBot

Title : Minimalist exceptions
Author : Yasha
Posted : 1+ years ago

Description : This code requires <a href="http://www.fastlibs.com/index.php" target="_blank">MikhailV's free FastPointer DLL</a>.
This pair of rather unpleasant C functions lets you clunkily imitate exception handling in Blitz3D. For those who haven't used it before, exception handling lets you define control points with "handlers", and if you encounter an error deep inside many nested function calls, jump straight up the stack to the control point without having to mess about with returning error codes and have each function report that it failed to its individual call site and so on and so forth. Very good for the lazy coder.

In BlitzMax, you'd define the control point and handler with a Try/Catch block. We don't have those in Blitz3D, and have no way to define new syntax, so instead what we do is define the body and handler as two separate functions, and pass their function pointers to WithExceptionHandler (this is why you need the FastPointer DLL: this can't be done without function pointers), along with an argument for the body code.

Here's a demonstration:

Function Foo(a)
Print "Opening Foo with argument " + a
Bar(42)
Print "Ending Foo (never reached!)"
End Function

Function Bar(a)
Print "In Bar with " + a

;Oh no! An 'exceptional situation' requires special abort procedure!
ThrowException(42)

Print "We never get here!"
End Function

Function Hdl(e)
Print "Handled exception code: " + e
Return 64
End Function

Local fptr = FunctionPointer() : Goto skipF : Foo(0)
.skipF

Local hptr = FunctionPointer() : Goto skipH : Hdl(0)
.skipH

Print "WithResult: " + WithExceptionHandler(fptr, hptr, 47)

WaitKey
End

In the event you hit an "exceptional situation", calling ThrowException will jump right out of whatever you were doing, all the way back to the level of WithExceptionHandler, and call the handler function with whatever you passed as the exception code, which can then take any necessary steps to rectify the situation using this code (use this to signal a particular flag or perhaps pass a data object). WithExceptionHandler will then return the result of the handler function instead of whatever would have been returned had no exception been thrown.

So in the above example:
-- WithExceptionHandler is called with Foo, Hdl and 47
-- WithExceptionHandler calls Foo with 47
-- Foo does some stuff and calls Bar with 42
-- Bar panics, as its mathematics breaks down around the non-value that is 42
-- Bar calls ThrowException with 42
-- ThrowException nukes the call stack all the way down to WithExceptionHandler again
-- WithExceptionHandler is informed that something went wrong, and calls Hdl to deal with it
-- Hdl deals with the problem, and returns a final value
It's not that difficult once you get used to it: it's basically just a way to immediately return past more than one function call at a time. Note that this doesn't catch or otherwise interact with system or hardware exceptions (division by zero, MAV, etc.) - it only works with your own "soft" errors.
Be warned however, that bypassing everything that happens in the rest of the function will also bypass B3D's own internal cleanup instructions! These are important for dealing with object reference counts, string memory, and other things that it manages to keep things safe. If you jump out of the middle of a function (or there's one in the middle of the nuked section of stack) that would have needed to cleanup strings or type objects, you will likely get memory leaks as the hidden cleanup code is also bypassed! Not to mention that of course any Free calls of your own will be skipped over too.

So, I suggest for safety:

-- preferably only use this code with functions that deal in integers and floats (you should be safe with entities, images, banks and other "int handle"-style objects)

-- if you have to use type objects, only pass them in parameter slots or via global variables. DO NOT use anything that would "return" an object (this includes the Object command itself) if there's a risk that the cleanup code will be skipped over. Simple parameters appear to be OK as B3D optimises out their refcounts anyway. If in doubt, do not use.

-- Do not use unboxed strings anywhere near this code. Literals should be OK, but I expect using string variables is asking for disaster. Pass them in globals or type objects if necessary.

-- finally, this code is not safe. If in doubt, do not use it at all! This is for advanced users who want to "play about" only! "Using setjmp for exception handling" is one of those things that almost everyone on the internet will tell you never to do, and with good reason.
Stern warnings aside... enjoy! The entry below contains both .decls and DLL (that's all! really! I can't believe it's that simple either); alternatively, <a href="https://sites.google.com/site/nangdongseng/downloads/B3D exceptions.zip?attredirects=0&d=1" target="_blank">download a prebuilt version</a>. [/i]

Code :
Code (blitzbasic) Select
;/* Exceptions.decls:

.lib "Exceptions.dll"

WithExceptionHandler%(proc%, handler%, arg%) : "WithExceptionHandler@12"
ThrowException(code%) : "ThrowException@4"

; */

#include <stdlib.h>
#include <setjmp.h>

#define BBCALL __attribute((stdcall))

static jmp_buf * topBuffer;
static int code;

BBCALL int WithExceptionHandler(int (* proc)(int) BBCALL,
                         int (* handler)(int) BBCALL,
                         int arg) {
jmp_buf env;
jmp_buf * prev;
prev = topBuffer;
topBuffer = &env;
int res;
if (!setjmp(env)) {
res = proc(arg);
topBuffer = prev;
} else {
topBuffer = prev; // Can't return here on further problems
res = handler(code);
}
return res;
}

BBCALL void ThrowException(int c) {
code = c;
longjmp(*topBuffer, 1);
}


Comments :


virtlands(Posted 1+ years ago)

 Fascinating. I shall look into this further.  As it seems to be very complex from a first glance, then first I shall not use it casually.You said, "doesn't catch division by zero". Is there anything else you're not telling us. Great job, by the way,...


Yasha(Posted 1+ years ago)

 The reason it can't catch "hard" errors like division by zero or an illegal memory instruction is that there's already one exception system in place, provided by the OS: when the processor or MMU or whatever encounters something it considers illegal, it invokes that exception system; since B3D doesn't include an exception system of its own, the only handler for these is the one that the OS wrapped around the whole program on program startup (this is overly simplified). So the only control point it can see is the one wrapping the entire program structure.Languages designed with exceptions from the start can be designed to make the compiler "aware" of OS exceptions (C++ can be designed to do this; BlitzMax does not as far as I can tell). But because this is a low-level system feature involving processor interrupts and the like, there's no easy way to write code that can integrate with it without the compiler having been designed to do it from the start.This code provides a second exception system internal to your program logic. It is not tied to the hardware, and that means it can only catch exceptions that you threw by hand using ThrowException. The OS doesn't know about it and can't hook it's own existing exception system into it.You could use it to deal with things like that, but only by writing wrapper code that detects the exceptional situation using the same kind of guards that you'd use to detect them in exceptionless B3D code:
Const DIV_BY_ZERO_CODE = ...

Function SafeDivide(l, r)
    If r = 0 Then ThrowException DIV_BY_ZERO_CODE
    Return l / r
End Function

Function MathStuff(a, b, c)
    ;Math
    ;Math
    Local d = SafeDivide(a, b)
    ;Math
    Return ...
End Function

Function Handler(code)
    Select code
        Case DIV_BY_ZERO_CODE
            Print "I'm sorry, I can't divide by zero Dave"
    End Select
    Return 0
End Function

Function DoAllTheMath(arg)
    ;...
    Return MathStuff(42, 0, 47)    ;This 0 is going to be a problem later
End Function

Local MathAllPtr = ..., HdlPtr = ...  ; ... get fn pointers

Local result = WithExceptionHandler(MathAllPtr, HdlPtr, something)
The "hard" error is checked the same way it normally would be, but it saves you having to do this:Global ErrorStatusMonitor

Function SafeDivide(l, r)
    If r = 0 Then ErrorStatusMonitor = True : Return 0
    Return l / r
End Function

Function MathStuff(a, b, c)
    ;Math
    If Not ErrorStatusMonitor
         Local d = SafeDivide(a, b)
        If Not ErrorStatusMonitor
            ;Math
            Return ...
        EndIf
    EndIf
    Return 0
End Function

Local res = MathStuff(42, 0, 47)
If ErrorStatusMonitor Then Print "res is 0, but not a valid result, because something went wrong deep in the code"
(Extreme and ugly example.)Anything you can do with exceptions, you can also do with error codes. Exceptions are just a shortcut to allow the code to more closely reflect your intent by removing the safety clutter that's necessary, but not related to the task at hand. Similar to garbage collection in that respect.


virtlands(Posted 1+ years ago)

 Here's a simple + very safe (experimental) program I made to detect some math overflows and underflows.   Download link: <a href="http://uploadingit.com/file/ygba9xfllnxtkthe/SafeMath32_By_Virt.zip" target="_blank">http://uploadingit.com/file/ygba9xfllnxtkthe/SafeMath32_By_Virt.zip</a> It involves melding PureBasic, Blitz3d, and some assembly language together.   As usual, B3D accesses some special functions through a DLL.  Only those DLL functions supplied can detect overflows & underflows, so it's not 'exception' code in the same sense as yours.An underflow occurs when a math calculation becomes so low that it can't be stored in a regular 32-bit variable, and it tends towards zero.A gradual underflow occurs when any calculation loses precision by losing decimal point digits.( Gradual underflows are too laborious to keep track off, so this program only tries with simple underflows. )<a href="http://en.wikipedia.org/wiki/Arithmetic_underflow" target="_blank">http://en.wikipedia.org/wiki/Arithmetic_underflow</a>It's too bad that we can't put assembly language directly into Blitz3D.   Or can we? { a multiplication that's too large can cause a 'carry' }


Yasha(Posted 1+ years ago)

 <div class="quote"> It's too bad that we can't put assembly language directly into Blitz3D. Or can we? </div><a href="../Community/posts8b7f.html?topic=92323" target="_blank">TCC permits inline assembly within C functions</a>.Not quite the same as your PureBasic use case: the asm has to be in a string, and then put inside a C function to be compiled at runtime, but it will work if you're desperate.<div class="quote"> I don't know how to pass or return TYPES into a DLL, (I sure tried though). </div>You can pass objects to the DLL using a .decls declaration like this:SomeDLLFunction(obj*) : "_myFunc@4"This will pass the DLL function a pointer to the first field. You can treat it as a C struct pointer from here on (I don't know PureBasic and can't tell you what the equivalent there would be). You use the same star sigil for all types, meaning one DLL function can accept any type of objects (also banks: note that it can't tell the difference between a valid bank and an arbitrary integer though, except when the latter crashes).There is no facility for returning objects. While I can think of a few hacks, I don't think there is any safe way to do this actually. Consider storing and returning Handles instead.


virtlands(Posted 1+ years ago)

 Okay, I shall study DLL parameters some more. Shall try to get it right this time.What does the @4 mean?   ( as in func1@4 )  


Yasha(Posted 1+ years ago)

 The string in the quote marks is the function's name as exported by the DLL. You can apparently leave this off if it's identical to the name you're giving it in B3D on the left.The @X is commonly added by GCC and similar compilers to represent the fact that the function takes four bytes' worth of arguments on the stack. It's just a naming convention that takes advantage of the fact that DLLs can export names with characters that would be illegal in C code (so you can use to @ to separate off a part of the name with more information). It does not have any practical implications and is not "checked" at runtime or anything like that.Besides, in this case I just made the name up.


virtlands(Posted 1+ years ago)

 Aha!  I've figured out TYPE parameters with simple INT fields, however the one's with string fields don't work.This old topic by Oldefoxx may do the trick:  I shall study this one.String Pointers (Including NULL) For DLLs<a href="../Community/post93fe.html?topic=33007&post=354882" target="_blank">http://www.blitzbasic.com/Community/post.php?topic=33007&post=354882</a>;------------------------------------------------------------------------------I guess I should have started a new topic on this. Displayed here is a successful attempt at accessing B3D types in a PureBasic DLL: [/i]