Exceptions or longjmp() Across Client C/C++ Code

So there's a somewhat uncomfortable issue in using the API, of what to do if an abrupt failure happens.

void my_c_function(int x) {
    char* memory = malloc(1020);
    ...
    rebElide("append 1020", rebI(x));  // can't append to integers, FAILs
}

Currently what happens in this situation depends on whether you're in the body of an API native which is being invoked by the trampoline or not. If you're not, the program just terminates. If you are, then there will be a longjmp or C++ exception (based on which you built with) thrown up the stack which gets caught at the trampoline layer of the API native invocation, and then propagated by that.

Either way you'll get a memory leak. :roll_eyes:

Every API Could Return Failure Values

Then you would be forced to check it:

Value* abrupt;
Value* result = rebValue(&abrupt, "append 1020", rebI(x));
if (abrupt) {  // an ERROR! value, we'd presume
    free(memory);
    /* handle error */
}

Currently if you want to do something like this, there are tools like ENRESCUE, which will give you an ERROR! if there's a failure, or a ^META of whatever else:

Value* meta = rebValue("sys.util/enrescue [append 1020", rebI(x), "]");
if (rebUnboxLogic("error?", meta")) {
    free(memory);
    /* handle error */
}
Value* result = rebValue(meta);  // evaluating meta will unmeta it

So yes, we could make that easier...and should:

Value* value;
Value* error = rebRescue(&value, "append 1020", rebI(x));
if (error) {
    free(memory);
    /* handle error */
}
/* use value, corrupt if error was non-null */

The API is built programmatically, so there's no real reason it couldn't have a Rescue variation for every entry point:

int result;
Value* error = rebRescueUnboxInteger(&result, ...);

I... guess we could do that. :face_with_diagonal_mouth: It's a bit unfortunate that you have to make separate entry points in C for all of these things. JavaScript can have interface modifiers:

const [error, result] = reb.Rescue.UnboxInteger(...);

But Let's Say You Don't Want That...

What if you want plain old rebElide(), not rebRescueElide(), and you want something automatic to happen.

The API exports C functions that are declared with extern "C". This means they cannot throw C++ exceptions. So if you want C++ exceptions so your code is unwound properly, that would have to be done in an inline wrapper of some kind, kind of like:

inline void rebElide(...) {  // not extern "C"
    Value* error = rebRescueElide(...);  // extern "C", can't throw
    if (error)
        throw error;  // throw is legal in non-extern "C" (if C++)
}

But then, rebFunction() would have to do its own exception handling to intercept these throws so you didn't cross the interpreter stack. It's doable.

The other option is to use longjmp().

What Do Other Languages Do?

Well, one case would be Ruby, which has rb_rescue2(), rb_protect(), rb_ensure()...

http://silverhammermba.github.io/emberb/c/#rescue

These seem to be longjmp()-based, and nothing special. Won't work with C++.

That's actually where I adopted the term RESCUE from (in the sense used here, and SYS.UTIL/RESCUE).

Long ago I thought to mirror the Ruby API, but I don't think we need to. There doesn't need to be a special "Dangerous Function" type or routines to handle it. Instead we just say that your dangerous function is simply a rebFunction() which has a C function as its implementation. So we piggy-back on whatever exception handling protects that implementation.

Anyway, short term I've just made a new rebRescue() function implementing the better idea, and gotten rid of the very-very-old Ruby clone routines. More work is needed, but I do want an answer for being able to properly run destructors in C++ code.

Added Convenience: rebMalloc(), rebRealloc(), rebFree()...

For the sake of convenience, there is a memory allocator which lets you allocate memory that will be cleaned up automatically in case of a failure, and doesn't require destructors to do it. It's just taken care of when a failed frame is taken off the stack.

These actually back the memory with a BINARY! series, and are cleaned up by the same mechanics that clean up unmanaged series that are in flight when an internal error occurs.

Just an added thing, but better to use C++ constructs if you have a C++ codebase and if the API is tweaked to properly support destructors, which it should be able to do without needing to compile the interpreter itself as C++!