Cpp2 exceptions

This is the old c++ interface:

matthias@DESKTOP-A2T8IFC:~/exception$ cat likes.cpp
#include "SWI-cpp.h"
#include <iostream>

int main(int argc, char** argv)
{
    PlEngine e(argc, argv) ;
    PlTermv noargs(0) ;
    PlQuery q("license1", noargs) ;

    try
    { q.next_solution() ;
    }

    catch(PlException& ex)
    { std::cerr << (char*) ex << std::endl ;
    }
    return 0 ;
}

Running it:

matthias@DESKTOP-A2T8IFC:~/exception$ swipl-ld likes.cpp
matthias@DESKTOP-A2T8IFC:~/exception$ ./a.out
Welcome to SWI-Prolog (threaded, 64 bits, version 9.1.2-4-g65fab31ed)
(etc.)
For built-in help, use ?- help(Topic). or ?- apropos(Word).
error(existence_error(procedure,license1/0),context(system:'$c_call_prolog'/0,_6776))

This is the new one:

matthias@DESKTOP-A2T8IFC:~/exception$ cat likes2.cpp
#include "SWI-cpp2.h"
#include <iostream>

int main(int argc, char** argv)
{
    PlEngine e(argc, argv) ;
    PlTermv noargs(0) ;
    PlQuery q("license1", noargs) ;

    try
    { if(!q.next_solution())
        std::cerr << "query failed" << std::endl ;
    }

    catch(PlException& ex)
    { std::cerr << "caught exception" << std::endl ;
      std::cerr << ex.as_string() << std::endl ;
    }
    return 0 ;
}

Compilation and run:

matthias@DESKTOP-A2T8IFC:~/exception$ swipl-ld likes2.cpp
matthias@DESKTOP-A2T8IFC:~/exception$ ./a.out
Welcome to SWI-Prolog (threaded, 64 bits, version 9.1.2-4-g65fab31ed)
(etc.)
For built-in help, use ?- help(Topic). or ?- apropos(Word).

query failed
matthias@DESKTOP-A2T8IFC:~/exception$

What do I need to do to catch the exception? I played around with the flags (optional 3rd argument to PlQuery), it didn’t really help.

I think calling PlCheck(PL_next_solution()). I also think this is wrong and the old should still work. Let us wait for @peter.ludemann :slight_smile:

This one just terminates the program, and I think PlCheck is void, so that I cannot obtain the return value telling me if the query succeeds or fails.

matthias@DESKTOP-A2T8IFC:~/exception$ cat likes2.cpp
#include "SWI-cpp2.h"
#include <iostream>

int main(int argc, char** argv)
{
    PlEngine e(argc, argv) ;
    PlTermv noargs(0) ;
    PlQuery q("license1", noargs) ;

    try
    { PlCheck(q.next_solution()) ;
    }

    catch(PlException& ex)
    { std::cerr << "caught exception" << std::endl ;
      std::cerr << ex.as_string() << std::endl ;
    }
    return 0 ;
}

Output

matthias@DESKTOP-A2T8IFC:~/exception$ ./a.out
(welcome etc.)

terminate called after throwing an instance of 'PlFail'
Aborted

Let’s see if @peter.ludemann can help.

I am at the airport, so cannot help very much at the moment …
I tried to preserve compatibility with the old interface but it’s possible that I got this wrong. There should be some examples that could help you in the test cases.

It seems the current interface upsets many of the “embedding” scenarios. When writing a foreign predicate in C++, all magic is done using the PREDICATE() wrapper to eventually get the proper exception. I.e., a failure from an API is mapped to throw PlFail, which the PREDICATE() wrapper translates into return false, which causes the Prolog runtime system to find the raised Prolog exceptions and perform a Prolog throw/1.

This used to be different, where the C++ API wrappers checked the false return code and when associated with an exception they packaged the Prolog exception as a C++ exception. As a result, a Prolog API could return false for logical failure or throw a C++ exception that represents the Prolog exception.

This requires some thought. As is, most failures throw PlFail. This should be caught, after which we must inspect the Prolog environment to verify whether there is an exception or logical failure. It probably requires some creativity to make this look natural again …

PlFail is new and separate from PlException. Both are caught in PREDICATE. PlFail shouldn’t be part of any API methods; it should be only a convenience, for avoiding Golang-style cascading error checks. (See also the definition of PlCheck) These should be documented; if not clear, please tell mec what is confusing.

I probably won’t be able to say anything definitive for 24 hours or so

This here sort-of works:

#include "SWI-cpp2.h"
#include <iostream>

int main(int argc, char** argv)
{
    PlEngine e(argc, argv) ;
    PlTermv noargs(0) ;
    PlQuery q("license1", noargs, PL_Q_PASS_EXCEPTION | PL_Q_EXT_STATUS) ;

    try
    { int r = q.next_solution() ;
      if(r == PL_S_FALSE)
        std::cerr << "failed" << std::endl ;

      if(r == PL_S_TRUE)
        std::cerr << "query successful" << std::endl ;

      if(r = PL_S_EXCEPTION)
      { std::cerr << "returned with an exception" << std::endl ;
        PlException_qid ex ;
        std::cerr << ex.as_string() << std::endl ;
      }
    }

    catch(PlException& ex)
    { std::cerr << "caught exception" << std::endl ;
      std::cerr << ex.as_string() << std::endl ;
    }
    return 0 ;
}

It’s not pretty because of the message about current_prolog_flag, but I get some information.

matthias@DESKTOP-A2T8IFC:~/exception$ ./a.out
(welcome)

returned with an exception
Thread 1 (main): foreign predicate system:current_prolog_flag/2 did not clear exception:
        error(existence_error(procedure,license1/0),context(system: $c_call_prolog/0,_6776))
'$c_call_prolog'/0: Unknown procedure: license1/0
  However, there are definitions for:
        license/0
        license/1
        license/2
1 Like

That could be the theory. It seems the implementation is quite different though :frowning: Just scanning SWI-cpp2.h we see a lot of throw PlFail. Most APIs are also wrapped into chkex(), which is defined as

inline bool
PlTerm::chkex(int rc)
{ if ( rc )
    return rc;
  throw PlFail();
}

So, a failure of the API gets a PlFail exception. For embedding we’d assume something like

inline bool
PlTerm::chkex(int rc)
{ term_t ex;
  if ( !rc && (ex=PL_exception(0)) )
    throw PlException(ex);
  return rc;
}

Enjoy your trip!

I had a few minutes to look into this … and I’m not sure what’s going on.

I ran one of the hello_call tests in test_cpp.pl manually:

?- catch(hello_call(writeln_wrong(hello(世界四))), E, true).
E = error(existence_error(procedure, writeln_wrong/1), context(system:call/1, _)).

which seems to be what you expect from your code.

The body of hello_call/1 is pretty simple (and also wrong – see below):

PREDICATE(hello_call, 1)
{ PlCheck(PlCall(A1));
  return true;
}

If you look into that, you’ll see that PlCall() is:

int
PlCall(const std::string& predicate, const PlTermv& args, int flags = PL_Q_PASS_EXCEPTION)
{ PlQuery q(predicate, args, flags);
  return q.next_solution();
}

I shouldn’t have used PlCheck() with this … the idea behind PlCheck() is that if there’s a “false” return code, it throws PlFail() – if the “false” was called by an exception, then the exception information has already been recorded, so the top level of PREDICATE will just return false and the normal exception handling will be done in Prolog. However, in the case of PlQuery::next_solution(), which wraps PL_next_solution(), there are results that can indicate failure that aren’t “false” (e.g. PL_S_EXCEPTION).

(So, I lied when I said that the API shouldn’t use PlCheck() – it does in a few places, but in contexts where the failure is due to an exception)

In this particular situation, it seems that the call to next_solution() returns PL_S_FALSE, so perhaps there’s a bug in PL_next_solution()? (I think it should have returned PL_S_EXCEPTION)

Here’s my modified code mgondan1_likes2.cpp:

#include "SWI-cpp2.h"
#include <iostream>

int main(int argc, char** argv)
{
    PlEngine e(argc, argv) ;
    PlQuery q("license1", PlTermv(), PL_Q_PASS_EXCEPTION) ;

    try
    { int rc = q.next_solution();
      const char *ex_str;
      switch ( rc )
      { case PL_S_EXCEPTION: ex_str = "PL_S_EXCEPTION"; break;
        case PL_S_FALSE:     ex_str = "PL_S_FALSE";     break;
        case PL_S_TRUE:      ex_str = "PL_S_TRUE";      break;
        case PL_S_LAST:      ex_str = "PL_S_LAST";      break;
        case PL_S_YIELD:     ex_str = "PL_S_YIELD";     break;
        default:             ex_str = "????";           break;
      }
      std::cerr << "query failed: " << rc << ": " << ex_str << std::endl ;
    }

    catch(PlException& ex)
    { std::cerr << "caught exception" << std::endl ;
      std::cerr << ex.as_string() << std::endl ;
    }
    return 0 ;
}
$ swipl-ld mgondan1_likes2.cpp && ./a.out
Welcome to SWI-Prolog (threaded, 64 bits, version 9.1.0-61-g1d24d79ed-DIRTY)
SWI-Prolog comes with ABSOLUTELY NO WARRANTY. This is free software.
Please run ?- license. for legal details.

For online help and background, visit https://www.swi-prolog.org
For built-in help, use ?- help(Topic). or ?- apropos(Word).

query failed: 0: PL_S_FALSE

As a general design philosophy, constructors can throw exceptions if they can’t properly construct an object (e.g., resource error or type error). This can be either PlFail or PlException, depending on the situation. The complication is that PlFail can have an underlying Prolog exception (this is part of the design philosophy in SWI-Prolog.h foreign language interface), and I’ve made PlFail and PlException separate. Perhaps I should make PlFail a subclass of PlException? … I need to think about this, and want to also solicit other people’s opinions. (A separate issue is whether we should retain compatibility with existing “version 1” C++ code.)
I wrote a bit about this in the “Design philosophy of the classes” section of the documentation.

PlFail has another use, which is to allow a return-to-Prolog when failure occurs, e.g. PlCheck(A1.unify_integer(0)) instead of if (!A1.unify_integer(0)) return false. Note that this also covers the situation where the method call creates a Prolog exception – the appropriate handling is done by a combination of PREDICATE and the Prolog foreign language interface.

I hope nobody is in a hurry for me to resolve this – it appears that I changed my design approach but didn’t go carefully through PlQuery and related interfaces.

I’ll re-read my code and try to do a proper fix for things; but I won’t have much time for this until the end of the year.

I am not.

Still desire correct code before incorrect faster code or buggy code rushed into usage. :slightly_smiling_face:

I’ll use cpp1 in the meantime, no rush.

Nope. You need to add PL_Q_EXT_STATUS to get the extended status messages. Without it uses the usual TRUE/FALSE return with an exception in PL_exception(0) if PL_Q_PASS_EXCEPTION is passed (otherwise the exception can be retrieved using PL_exception(qid), but is not passed into the environment). The extended status was implemented when yield was added. As we needed one extra anyway, this was a good moment to also return the other useful status indicators :slight_smile:

Back to the original issue, I don’t think it is too bad. As is, wrapped C APIs now mostly throw PlFail, regardless of whether an exception happened. That is fine, as the PREDICATE wrapper turns this into return with FALSE to Prolog and all works as expected. It does not work if we are not defining a Prolog predicate in C++ and (thus) there is no PREDICATE wrapper.

I created a branch “exceptions” with a (partial) solution to this problem. Partial in the sense that not all paths are covered. It makes @mgondan1’s original code work again. And still passes @peter.ludemann’s tests.

I think the design should be that C++ methods and classes should always propagate the original Prolog exception. I’m not sure about PlFail. Possibly it should only be thrown by PlCheck()? Surely (back to partial), there are several places that are not ready for embedding. See e.g., PlTermv

  { if ( size_ && a0_ == PlTerm::null )
      throw PlFail();

If PL_new_term_refs() fails, we know there is a Prolog exception and should forward that. We could use the (new) PlCheck(0) here. That will forward the current Prolog exception or throw PlFail (which should not happen).

The reasoning behind the C API is that it returns success or failure. On failure it could be logical failure or an error. Logical failure only applies to a part of the API such as the *_unify_* functions and PL_next_solution(). Then there is the PL_get_() group that returns silent failure if the input is incorrect. The C++ version should be using the derived PL_get__ex() group that fails with an exception. That is another area with issues. For example

inline bool PlTerm::operator ==(int64_t v) const
{ int64_t v0;

  if ( PL_get_int64(C_, &v0) )
    return v0 == v;

  throw PlTypeError("integer", *this);
}

That should use PL_get_int64_ex(), as there are at least three errors (instantiation, type and int64 range). I think this should be:

inline bool PlTerm::operator ==(int64_t v) const
{ int64_t v0;

  PlCheck(PL_get_int64_ex(C_, &v0));
  
  return v0 == v;
}

I’ll merge this with what I’m working on, plus the bugs that @jan has noted. (Hopefully by year-end)

Amongst other things, I’ve created an enum to encapsulate the return codes from next_solution(), similar to PlEncoding, plus some convenience functions for checking the result. Unfortunately, C++ does automatic conversions between enums and integers, so it’s not obvious to me how to cause a compile-time error if the code does a “bool”-like check on the result of next_solution() (which is complicated by the use of PL_Q_PASS_EXCEPTION etc). This will require at least another cup of coffee and more thought before I propose a solution … I might be able to produce a safer version by encapsulating PlQueryRc in a class and removing the bool operator but this will require some reading of the C++ documentations plus some experimentation with the C++ compilers.

That would explain why version 1 of the API works and version 2 doesn’t (with my misunderstanding of the flag).

I completely agree; in this case, we’re trying to avoid a “buggy design”.

1 Like

Note that in most standard solutions we simply want this.

  try
  { while (q.next_solution()) )
      <handle answer>
  } catch ...

The extended return codes are not often required. The distinction between an answer and the last answer is probably rarely interesting.

Using PL_Q_PASS_EXCEPTION is probably what you want as it makes exception handling consistent with the rest of the API. Note that after an exception we need to return control to Prolog or use PL_clear_exception(). If we are not defining a predicate there is no way to return, so we should call PL_clear_exception() before continuing.

Leaving aside the question of what next_solution() should return for the moment, there’s a more fundamental question of how exceptions are handled.

Take, for example, PlTerm::as_float(). It returns a double, so it must throw an exception if the term isn’t a float. The code is:

double PlTerm::as_float() const
{ double v;
  PlCheck(PL_get_float_ex(C_, &v));
  return v;
}

where the PlCheck does throw PlFail() if PL_get_float_ex() returns false. The PlFail() is caught by PREDICATE and turned into return false to the foreign language interface, which recognizes that an exception has been set (by PL_get_float_ex()), and that exception can be caught by catch/3.

On the other hand, consider PlTerm::arity() … there is no PL_get_name_arity_ex(), so the code uses PL_get_name_arity() and throws a PlException:

size_t PlTerm::arity() const
{ size_t arity;
  if ( !PL_get_name_arity(C_, nullptr, &arity) )
    throw PlTypeError("compound", *this);
  return arity;
}

This is caught by PREDICATE, which calls PL_raise_exception() to pass the PlTypeError to Prolog.

The PlFail and PlException stuff works fine if the C++ code doesn’t want to do any error checking (with try-catch). But if you want a try-catch around the method call (e.g., with q.next_solution()), then things become more complicated because you have to know whether the underlying method can throw PlFail (which requires an additional check for whether there’s an exception) or PlException. In other words, the code becomes like this (untested):

try
{  while (q.next_solution()) )
      <handle answer>
} catch ( PlFail& )
{ if ( PL_exception(0) )
  {  <handle Prolog exception>
  } else
  }  <handle next_solution() failed>
} catch ( PlException& )
{ <handle C++ exception>
}

I don’t want to burden every method call with an additional call to PL_exception(0); but it would be good if we could simplify the error handling where we want to catch the exception and not simply pass it to Prolog.

[This was written in a bit of a hurry; I hope it makes sense.]

This is why my branch changes PlCheck() to throw the real exception if one is pending. That causes some more work for the failure path and even a bit more if there is an exception, but that shouldn’t matter too much (exception recovery in C++ seems slow anyway).

The added PlCheckEx() returns the boolean value, throwing only if there is an exception. That is intended for all API wrappers that may both return logical success/failure and exceptions. PlCheck should be used for wrappers that have no logical failure path, e.g., most of the API that creates something.

Yes, that looks reasonable (I wasn’t able to look at your “exception” branch until just now).
However, I’d like to think about it a bit more before making this change.

A simple benchmark showed a performance penalty of 15x to 20x between return false and throw PlFail() and the subsequent handling within PREDICATE. (There’s no overhead by having a try-catch if nothing is thrown.
I presume (but haven’t measured) that the cost of the call to PL_exception(0) is small compared to the cost of the throw/catch, so the extra work in the failure path shouldn’t matter. The performance-obsessed programmer can still use return PL_raise_exception(…).

I think that PlCheckEx() and the modified PlCheck() make sense – I’ll add some tests to verify that they behave the way @mgondan1 expects (and he’s not alone – I ran into something similar but don’t follow up), plus add/fix some documentation. [Also, some minor changes to @jan’s code for efficiency, plus cover all the paths]

The issues of how best to handle the various return codes from PlQuery::next_solution() will need a bit more thought – it might make sense to have a second version of next_solution() for PL_Q_EXT_STATUS.

The flags are a bit-wise OR, but their descriptions seem to say that they are mutually distinct (e.g., it’s not clear what PL_Q_PASS_EXCEPTION|PL_Q_CATCH_EXCEPTION does).

Should I specify PL_Q_PASS_EXCEPTION or PL_Q_NORMAL|PL_Q_PASS_EXCEPTION?

(With the code in the exceptions branch, I don’t seem to have a way of turning off the traceback message - it seems to be triggered before the throw PlException_qid() happens … PL_Q_CATCH_EXCEPTION blocks the traceback message but also seems to make the exception unavailable.)