Cpp2 exceptions

The three flags are mutually exclusive. Combining them results in whatever is tested first. As PL_Q_NORMAL is defined as 0, this doesn’t matter. PL_Q_NORMAL is called that way as it was what happened originally when SWI-Prolog didn’t even have exceptions: the recursive call would trap the debugger in case of an exception. PL_Q_CATCH_EXCEPTION causes the recursive call to fail silently and make the exception available as PL_exception(qid) in the recursive query before it is closed. It is intended for C code that wants to handle an exception in C after a call to Prolog raised one. PL_Q_PASS_EXCEOPTION came last and transfers the exception to the “parent” environment, so it can transparently “bubble up” from mutual recursive Prolog → C → Prolog … calls. I.e., you can close the query and the exception is (still) around as PL_exception(0).

For my reference:

https://swi-prolog.discourse.group/t/exceptions-in-foreign-code-more-questions/3527

https://swi-prolog.discourse.group/t/foreign-language-interface-handling-prolog-exceptions-in-c/1807

https://swi-prolog.discourse.group/t/exception-in-foreign-c-code/4504

prolog_exception_hook/4

(I didn’t find anything in https://groups.google.com/g/swi-prolog)

1 Like

I will update foreign.doc with this information about the PL_Q_* flags.

There’s a footnote in foreign.doc that says:

Do not pass the integer 0 for normal operation, as this is interpreted as PL_Q_NODEBUG for backward compatibility reasons.

I grep-ed the source code and it appears that PL_Q_NORMAL isn’t explicitly check but instead there are checks for PL_Q_NODEBUG, PL_Q_CATCH_EXCEPTION, PL_Q_PASS_EXCEPTION. There’s some code that I don’t understand in PL_close_query() that checks for PL_Q_PASS_EXCEPTION and it’s checked in isCaughtInOuterQuery()

By experiment, it seems that PL_Q_CATCH_EXCEPTION does not make the exception available with PL_exception(0) but it is available with PL_exception(qid) … is this intended behavior?

Also, if PL_Q_PASS_EXCEPTION is specified, the traceback is printed and a “did not clear exception” message happens if I reply a (abort) to the traceback prompt (which also gets the term $aborted returned from PL_exception(qid)).

I’ve put some testing code in https://github.com/kamahen/packages-cpp/tree/exceptions … you can see what happens with queries such as plunit_wchar:ffi_call(bad_pred(foo), [catch_exception]) and changing the options to [pass_exception] or [normal].

Originally, this was a bool parameter. I think the meaning was to allow debugging or not inside the callback, where nodebug was mostly intended for callbacks from the debugger. Seems 0 is no longer a meaningful value.

PL_Q_PASS_EXCEPTION means the exception is intended to be propagated further up. isCaughtInOuterQuery() is used by the debugger to decide whether the current exception is caught or not. If it is caught, we just unwind and make the catch/3 do its job. If not caught we try to support debugging with as much as possible context still in place. If the exception happens inside a nested call to Prolog and the nested call has PL_Q_PASS_EXCEPTION we continue the search in the outer environment.

Yes. PL_Q_CATCH_EXCEPTION means that the C code calling Prolog will handle the exception after a call to PL_next_solution() by calling PL_exception(qid). This is the initial support for handling exceptions inside calls from C to Prolog. As indicated PL_Q_PASS_EXCEPTION is supposed to propagate the exception.

If you have an exception and you are called from Prolog, you must return (with FALSE) to Prolog or you may handle the exception in C(++), but then you must call PL_clear_exception() to remove it from the environment. This doesn’t just apply to exceptions from PL_Q_PASS_EXCEPTION, but for exceptions from all the API calls.

Yes. abort/0 is the same as throw('$aborted'). Should probably not have been a $-term :frowning: That was introduced when threads were added and we could no longer simply destroy the stacks and create new ones.

I have some trouble finding what you are referring to. Hopefully the above descriptions make clear how it is supposed to work. Note that if neither PL_Q_PASS_EXCEPTION nor PL_Q_CATCH_EXCEPTION is specified the exception is handled, possibly using the debugger, inside the nested Prolog call and the return value will be FALSE with no sign of an exception. There are few cases where that makes sense since we have proper exception handling.

My code for trying out the various permutations of PL_open_query() is foreign predicate ffi_call_(Goal, Flags), where Flags is an integer. (plunit_call:ffi_call/2 translate human-readable flags to an integer and then calls the foreign predicate ffi_call/2). I will update this from time to time as I figure out more combinations of flags and how to handle them; and eventually I’ll incorporate that knowledge into the C++ interface (PlQuery).

The documentation for PL_open_query() says:

PL_open_query() can return the query identifier 0 if there is not enough space on the environment stack. This function succeeds, even if the referenced predicate is not defined. In this case, running the query using PL_next_solution() will return an existence_error. See PL_exception().

This situation is a bit difficult to reproduce … if PL_open_query() returns 0, is there something in PL_exception(0)? The reason I ask is that I’m trying to turn all error conditions into C++ exceptions (the current code often uses PlFail() and then converts that in PREDICATE to a Prolog failure, which is why C++ try-catch doesn’t work the way that @mgondan1 expects).

Yes. It calls the code below. ensureLocalSpace() leaves a resource error for the local stack on failure.

  if ( !ensureLocalSpace(lneeded) )
    return (qid_t)0;

Any API call that does not fail for logical reasons should leave an exception. The grey area are the PL_get_*() functions that fail silently if the input is invalid. For that we have the PL_get_*_ex() versions.

It appears that PL_Q_PASS_EXCEPTION requires also calling PL_cut_query() or PL_close_query() to avoid triggering assert(onStack(local, fr2)) in discardChoicesAfter() … but PL_Q_NORMAL doesn’t trigger the assertion.

Is it correct that PL_Q_NORMAL is deprecated and either PL_Q_PASS_EXCEPTION or PL_Q_CATCH_EXCEPTION should be used in all new code? (For example, it seems that calling PL_cut_query() with PL_Q_NORMAL returning FALSE and an exception results in exiting the top-level Prolog if the user responds with “leap” in the debugger.)

I’ve modified the call_ffi_/2 foreign function and added some call tests in packages-cpp/test_ffi.pl at 9a2670ed2c3019a38f6931d75bede07b8a7109d2 · kamahen/packages-cpp · GitHub@jan please take a look at them to see if they’re behaving the way you expect. I’ll then add some more documentation to foreign.doc and make appropriate changes to SWI-cpp2.h and its tests (which aren’t working right now).

[This is my fork, exceptions branch]

1 Like

Thanks. Checked it out. All behaves as one should expect. The only dubious one is ffi_call(non_existant_pred(foo), [normal]). exiting Prolog on leap.

Not terminating the query using PL_cut_query() or PL_close_query() fails to restore the local stack after the query and further behavior is undefined (read, it will typically crash one way or another).

Note that if you use PL_Q_CATCH_EXCEPTION you should process the exception using PL_exception(qid) before closing the query. Closing the query discards the exception. On the other hand, using PL_Q_PASS_EXECEPTION requires you to close/cut the query before continuing.

A useful test is to use PL_Q_PASS_EXECEPTION, close using PL_close_query() and validate the exception is still as expected, i.e., that the data backtracing performed by PL_close_query() does not destroy the exception.

It is good to see this cleaned. It has become a bit of a mess as a result of adding features and trying to maintain compatibility of the API.

1 Like

It’s not clear to me how to do this – there’s already a catch/3 in the test, so I don’t see how I could do this. I already have this test:

test(ffi_call, error(existence_error(procedure,test_ffi:non_existant_pred/1),_)) :-
    ffi_call(non_existant_pred(foo), [pass_exception]).

Given its history, exception handling is pretty clean. Is it correct to say that new code should either use PL_Q_PASS_EXCEPTION or PL_Q_CATCH_EXCEPTION and not use PL_Q_NORMAL (or flags=0)?

A quick inspection of ffi_call/2 tells me it either uses PL_cut_query(qid) or nothing (which is invalid). Alternatively you may use PL_close_query(qid), which destroys all (stack) data created by the query. If you do (many) repeated calls from C(++) on Prolog (without returning control to Prolog), using PL_close_query(qid) avoids garbage collections. The downside is side effects such as setting global variables are lost as well. Quite often that is fine. The number of scenarios where it provides a significant is probably also limited.

For all practical purposes, yes. I can’t see a sensible use case for normal applications. When used in a foreign predicate, PL_Q_PASS_EXCEPTION is almost always what needs to be used. If the main control is in C(++), PL_Q_CATCH_EXCEPTION could make sense. On the other hand, you have to deal with exceptions from the other API calls as well and using PL_Q_PASS_EXCEPTION makes handling all exceptions uniform. So, I think the C++ interface can (should) always use PL_Q_PASS_EXCEPTION.

My plan is to make PL_Q_PASS_EXCEPTION the default; but if the user specifies PL_Q_CATCH_EXCEPTION, the C++ API will do the extra work to turn the Prolog exception into a C++ exception, for uniform handling (e.g., the same as the “unify” methods).

Globals are also mentioned in the documentation:

Note that it is allowed to store this handle as global data and reuse it for future queries.

and I didn’t understand it. By “global”, do you mean C/C++ globals or Prolog globals? (My programming style is to avoid globals and even singletons as much as possible, so I don’t know how other people do “globals”)

It refers to the sentence before:

The p argument specifies the predicate, and should be the result of a call to PL_pred() or PL_predicate().

You see it at work in the example at the bottom of the page:

  static predicate_t p;

  if ( !p )
    p = PL_predicate("is_a", 2, "database");

This avoids 5 hash lookups (two to get the atoms, one to get the module, one to get the functor and the final one to get the predicate associated with the functor in the module).

1 Like

Does this mean that the following code won’t work as expected?

static foreign_t
my_pred(term_t t)
{ static predicate_t p = 0;
  qid_t qid = PL_open_query(...);
  
  if ( !p )
    p = PL_predicate("is_a", 2, "database");
  }

  ...

  PL_close_query(qid);
  return TRUE;
}

But the code will work if the p = PL_predicate(...) is moved to before the PL_open_query(), or if PL_cut_query() is used?

The only claim made is that PL_predicate() is a function that will return the same value when called multiple times in the same process. Also the predicate_t reference is never invalidated. Although abolish/1 destroys a predicate, it does not destroy the (now) undefined predicate reference. This implies you can do the lookup only once and cache the result. Whether you cache the return of PL_predicate() nor when you call it has any semantic implications (as long as you call it before using :slight_smile: ).

Prolog global variables as set with b_setval/2 are one of the few things that may be affected by calling PL_close_query(qid) rather than PL_cut_query(qid). This typically has no visible effect, but there are exceptions. For example, posting CHR constraints internally uses b_setval/2. This also explains the addition of PL_cut_query(): before the existence of Prolog global variables there was no way to access changes to the stacks created by the callback to Prolog, so discarding them made perfect sense. In particular so because early versions did not have a garbage collector. This resulted in the natural pair PL_open_query() … PL_close_query() with the current semantics.

I’m starting to think that PL_Q_CATCH_EXCEPTION is the best default for C++ and not PL_Q_PASS_EXCEPTION.

The general philosophy of SWI-cpp2.h is that Prolog exceptions are transformed into C++ exceptions (currently, this isn’t fully implemented, but I’m working on it). For example, if PlTerm::unify_term() fails because of a resource error, it will throw an appropriate PlException object. Similarly, if PlTerm_string() gets a resource error, it will also throw a PlException object (this is part of the “no half-constructed objects” philosophy).

By default, PREDICATE catches such exceptions and turns them into Prolog exceptions. However, the user can intercept them using C++ trycatch.

In the same way, it makes sense for the user to be able to use trycatch on a call to PlQuery::next_solution() – that is a false return means simply that the call failed and trycatch is used for error handling. If there’s no trycatch, PREDICATE will catch the exception and turn it into a Prolog exception – this is very similar to how PL_Q_PASS_EXCEPTION works except that for the order in which the debugger is entered (see attached output) assuming that the outer layer uses the debugger and doesn’t use catch/3 – if it does use catch/3, then it should look essentially the same for both PL_Q_PASS_EXCEPTION and PL_Q_CATCH_EXCEPTION.

In short: PL_Q_PASS_EXCEPTION makes things a bit simpler for the C programmer but doesn’t seem to have any advantage for the C++ programmer, so the default for C++ should be PL_Q_CATCH_EXCEPTION.

I’m thinking of disallowing PL_Q_NORMAL or PL_Q_PASS_EXCEPTION – this would simplify my code and test cases. If anyone really wants to use these, they can always use the C API.

As to the sequence of entering the debugger with PL_Q_PASS_EXCEPTION … The “Unknown predicate” message is output first, then the debugger waits for the response (“creep” in the example below) before returning from PL_next_solution(). I suppose this makes sense (the user might want to abort), but it surprised me when I first encountered it. Also, in the case of PL_Q_PASS_EXCEPTION, the “context” is filled in the exception term; with PL_Q_CATCH_EXCEPTION, it isn’t (I suppose the debugger filled in the context).

(The code is in packages-cpp/test_ffi.c at 930b367f50dcc13ad1276263d5bfb80a844784d7 · kamahen/packages-cpp · GitHub )
The messages such as (1) ... ffi_call [pass_exception]: non_existant_pred(foo) are output by the foreign function ffi_call_/2:

[debug]  ?- plunit_call:ffi_call(non_existant_pred(foo),[pass_exception]).
(1) ... ffi_call [pass_exception]: non_existant_pred(foo)
ERROR: Unknown procedure: test_ffi:non_existant_pred/1
ERROR: In:
ERROR:   [14] test_ffi:non_existant_pred(foo)
ERROR:   [13] call(test_ffi:non_existant_pred(foo)) at /home/peter/src/swipl-devel/build/home/boot/init.pl:502
ERROR:   [12] <meta call>
ERROR:   [11] test_ffi:ffi_call_(non_existant_pred(foo),16) <foreign>
ERROR:   [10] plunit_call:ffi_call(non_existant_pred(foo),[pass_exception]) at /home/peter/src/swipl-devel/packages/cpp/test_ffi.pl:229
ERROR:    [9] toplevel_call(user:plunit_call: ...) at /home/peter/src/swipl-devel/build/home/boot/toplevel.pl:1173
   Exception: (14) test_ffi:non_existant_pred(foo) ? 

User hit “enter” (for “creep”) at each prompt:

^  Exception: (13) call(test_ffi:non_existant_pred(foo)) ? 
(3) ... ffi_call next_solution rc=FALSE(0) success=0
(4) ... ffi_call [pass_exception] rc=0 exception(qid): error(existence_error(procedure,test_ffi:non_existant_pred/1),context(prolog_stack([frame(14,call(test_ffi:non_existant_pred/1),test_ffi:non_existant_pred(foo)),frame(13,clause(<clause>(0x5a93e008e120),3),call(test_ffi:non_existant_pred(foo))),frame(12,meta_call,0),frame(11,foreign(test_ffi:ffi_call_/2),test_ffi:ffi_call_(non_existant_pred(foo),16)),frame(10,clause(<clause>(0x5a93e03c68f0),17),plunit_call:ffi_call(non_existant_pred(foo),[pass_exception])),frame(9,clause(<clause>(0x5a93e0217050),3),toplevel_call(user:plunit_call: ...))]),_102)) exception(0): error(existence_error(procedure,test_ffi:non_existant_pred/1),context(prolog_stack([frame(14,call(test_ffi:non_existant_pred/1),test_ffi:non_existant_pred(foo)),frame(13,clause(<clause>(0x5a93e008e120),3),call(test_ffi:non_existant_pred(foo))),frame(12,meta_call,0),frame(11,foreign(test_ffi:ffi_call_/2),test_ffi:ffi_call_(non_existant_pred(foo),16)),frame(10,clause(<clause>(0x5a93e03c68f0),17),plunit_call:ffi_call(non_existant_pred(foo),[pass_exception])),frame(9,clause(<clause>(0x5a93e0217050),3),toplevel_call(user:plunit_call: ...))]),_102))
(5) ... ffi_call [pass_exception] cut_query rc=1 exception(qid): <no-exception> exception(0): <no-exception>
(8) ... ffi_call [pass_exception] return FALSE
   Exception: (11) test_ffi:ffi_call_(non_existant_pred(foo), 16) ? 
   Exception: (10) plunit_call:ffi_call(non_existant_pred(foo), [pass_exception]) ? 

First of all, thanks for the PR on the docs. It clarified several things but notably pointed at the poor sections of the docs. It triggered some more rewrites. Hopefully the picture concerning exception handling in foreign code is clear now.

I think that is wrong. My latest commit on the docs may clarify that. The bottom line is that the intend of these flags is different and the lifetime of the exception reflects that. When using PL_Q_CATCH_EXCEPTION you basically state “If an exception occurs I’ll deal with it is C(++) and discard it as far as Prolog is concerned” and the exception is cleared by PL_cut_query() or PL_close_query(). On the other hand, `PL_Q_PASS_EXCEPTION’ states “the foreign code will propagate the exception back to Prolog” As a result, Prolog does the tricky work to make the query exception survive closing the query and be the current pending exception in the outer environment. Besides this technical difference, it also defines when the debugger considers the exception caught and (thus) whether it kicks in or not.

I doubt this is true. Mainly because PL_Q_PASS_EXCEPTION makes handling the exception from a query pretty much the same as handling exceptions from any of the other API calls. This, provided that you place the try ... catch around the entire block that opens the query, uses PL_next_solution and closes/cuts the query. I think that is what you typically want anyway. The C++ cut/close query can check for the exception and throw it as C++ exception just like any of the other API calls. Note that after PL_next_solution() raises an exception, you must close the query anyway.

I just “undeprecated” it based on one of the examples where you changed this to PL_Q_PASS_EXCEPTION and I think that is not wise. It concerned a simple C main program making a call to Prolog and halt. C was obviously not willing to do any exception handling, so using PL_Q_NORMAL and let Prolog do the exception handling and (optionally) debugging seems appropriate. Updated the docs to reflect this scenario.

As (now) explained in more detail in the docs, the debugger uses PL_Q_PASS_EXCEPTION to find instances of catch/3 in outer environments to decide whether the exception is caught. If not, it tries to activate the debugger as soon as possible to provide as much as possible context. So, you can creep a couple of times to walk up the stack and then do a “retry” to re-run and find what actually creates the context for the exception. I see the docs are still wrong wrt PL_Q_CATCH_EXCEPTION as, if the debugger finds this, it assumes the exception is handled (in C/C++)

In practice, handling a Prolog exception in C(++) as part of a foreign predicate implementation is very rare. Normally you just want to propagate it. It only makes sense if the main control is in C(++). Now it does matter as using PL_Q_PASS_EXCEPTION, the exception will be considered uncaught (trapping the debugger) and PL_Q_CATCH_EXCEPTION makes it considered to be caught, which only makes sense if you want to ignore/print the exception and resume the program.

Ideally we’d like to know whether there is a C++ catch/throw around it that is intended to print/ignore the exception and continue processing. Possibly we do want to allow for both flags, i.e., make the exception available as PL_Q_PASS_EXCEPTION but tell the system we will handle it using PL_Q_CATCH_EXCEPTION

Current docs

Note that PL_Q_PASS_EXCEPTION is used by the debugger to decide whether the exception is
caught. If there is no matching catch/3 call in the current query and the query was
started using PL_Q_PASS_EXCEPTION the debugger searches the parent queries until it
either finds a matching catch/3, a query with PL_Q_CATCH_EXCEPTION (in which case it
considers the exception handled by C) or the top of the query stack (in which case it
considers the exception uncaught). Uncaught exceptions use the
library(library(prolog_stack)) to add a backtrace to the exception and start the
debugger as soon as possible if the Prolog flag debug_on_error is true.

Apologies for being so slow in fixing this … but I’ve been trying to figure out how to do this in C++ (which feels like the “natural” way for a C++ programmer):

PlQuery q(...);
try {
    int rc = q.next_solution();
      ...
} catch (PlException& ex) {
      ...
    throw;
}

and there are problems with the lifetime of the exception. I have a few ideas for solving it, but they’re somewhat ugly, so I’m hoping I can come up with something cleaner.

I think I have a solution, assuming that the following code is correct – it appears to work, but I’d like @jan 's confirmation. It does takes the exception from PL_next_solution() and wraps it in a my_exception(...) term. Error checking has been left out for clarity.

foreign_t ffi_call(term_t goal) {
  predicate_t call_pred = PL_predicate("call", 1, NULL);
  qid_t qid = PL_open_query(0, PL_Q_PASS_EXCEPTION, call_pred, goal);
  if (PL_next_solution(qid)) {
    return PL_cut_query(qid);
  }
  term_t ex = PL_exception(qid);
  if (ex) {
    term_t my_ex_wrapper = PL_new_term_ref();
    PL_cons_functor(my_ex_wrapper,
                    PL_new_functor(PL_new_atom("my_exception"), 1),
                    ex);
    PL_clear_exception();
    (void)PL_raise_exception(my_ex_wrapper); // Must be *before* PL_cut_query(qid)
  }
  (void)PL_cut_query(qid);
  return FALSE;

Instead of the call to PL_raise_exception() and returning FALSE, the code can return TRUE (after the PL_cut_query()) and the net effect is as if PL_Q_CATCH_EXCEPTION had been used in PL_open_query().

That doesn’t seem correct. If you use PL_Q_PASS_EXCEPTION you close/cut the query and propagate it. That is the deal. If you use PL_Q_CATCH_EXCEPTION you deal with it before close/cut and you do not propagate it. When using PL_Q_CATCH_EXCEPTION, you may rethrow as a C++ exception and catch that it C++ (make sure there are no Prolog environments in between).

There is no proper way to propagate a different exception. That would violate the logic to figure out whether exceptions are caught. So far I never found a compelling reason to add support for that (and I wouldn’t know how).

Note that if you are in an embedding scenario, i.e. the main control is in C++ and thus there is no Prolog environment to propagate to, things change a little. Using PL_Q_CATCH_EXCEPTION you tell the system the exception is caught and handle it before close/cut. Using PL_Q_PASS_EXCEPTION you effectively tell the system it is not caught and thus the debugger may act. You need to handle/clear it after the cut/close.

I think you do need two fragments of skeleton code that deal with these two scenarios.