Did you know ... | Search Documentation: |
![]() | Overview (version 2) |
The most useful area for exploiting C++ features is type-conversion.
Prolog variables are dynamically typed and all information is passed
around using the C-interface type term_t
. In C++, term_t
is embedded in the lightweight class PlTerm
.
Constructors and operator definitions provide flexible operations and
integration with important C-types (char*
, wchar_t*
,
long
and double
), plus the C++-types (std::string
,
std::wstring
).
See also section 2.5.5.
The general philosophy for C++ classes is that a "half-created" object should not be possible - that is, the constructor should either succeed with a completely usable object or it should throw an exception. This API tries to follow that philosophy, but there are some important exceptions and caveats. (For more on how the C++ and Prolog exceptions interrelate, see section 2.18.)
The various classes (PlAtom
, PlTerm
, etc.)
are thin wrappers around the C interface's types (atom_t
,
term_t
, etc.). As such, they inherit the concept of "null"
from these types (which is abstracted as PlAtom::null
,
PlTerm::null
, etc., which typically is equivalent to
0
). Normally, you shouldn't need to check whether the
object is "fully created", but if you do, you can use the methods
is_null() or not_null().
Most of the classes have constructors that create a "complete" object. For example,
PlAtom foo("foo");
will ensure that the object foo
is useable and will
throw an exception if the atom can't be created. However, if you choose
to create an PlAtom
object from a atom_t
value, no checking is done (similarly, no checking is done if you create
a PlTerm
object from a term_t
value).
To help avoid programming errors, some of the classes do not have a
default "empty" constructor. For example, if you with to create a
PlAtom
that is uninitialized, you must explicitly use
PlAtom(PlAtom::null)
. This make some code a bit more
cumbersome because you can't omit the default constructors in struct
initalizers.
Many of the classes have a as_string() method - this might be changed
in future to to_string(), to be consistent with
std::to_string()
. However, the method names such as
as_int32_t() were chosen itnstead of to_int32_t() because they imply
that the representation is already an int32_t
, and not that
the value is converted to a int32_t
. That is, if the value
is a float, int32_t
will fail with an error rather than
(for example) truncating the floating point value to fit into a 32-bit
integer.
Many of the classes wrap long-lived items, such as atoms, functors,
predicates, or modules. For these, it's often a good idea to define them
as static
variables that get created at load time, so that
a lookup for each use isn't needed (atoms are unique, so
PlAtom("foo")
requires a lookup for an atom foo
and creates one if it isn't found).
C code sometimes creates objects "lazily" on first use:
void my_function(...) { static atom_t ATOM_foo = 0; ... if ( ! foo ) foo = PL_new_atom("foo"); ... }
For C++, this can be done in a simpler way, because C++ will call a
local “static
” constructor on first use.
void my_function(...) { static PlAtom ATOM_foo("foo"); }
The class PlTerm
(which wraps term_t
) is
the most used. Although a PlTerm
object can be created from
a term_t
value, it is intended to be used with a
constructor that gives it an initial value. The default constructor
calls PL_new_term_ref() and throws an exception if this fails.
The various constructors are described in
section 2.10.1. Note that the
default constructor is not public; to create a "variable" term, you
should use the subclass constructor PlTerm_var().
The following files are provided:
SWI-cpp2.h
Include this file to get the C++ API. It
automatically includes
SWI-cpp2-plx.h
and SWI-cpp2.cpp
, unless the
macro _SWI_CPP2_CPP_SEPARATE
is defined, in which case you
must compile SWI-cpp2.cpp
separately.
SWI-cpp2.cpp
Contains the implementations of some
methods and functions. If you wish to compile this separately, you must
define the macro _SWI_CPP2_CPP_SEPARATE
before your include
for SWI-cpp2.h
.
SWI-cpp2-plx.h
Contains the wrapper functions for the
most of the functions in
SWI-Prolog.h
. This file is not intended to be used by
itself, but is #include
d by SWI-cpp2.h
.
SWI-cpp2-atommap.h
Contains a utility class for mapping
atom-to-atom or atom-to-term.
test_cpp.cpp
, test_cpp.pl
Contains various
tests, including some longer sequences of code that can help in
understanding how the C++ API is intended to be used. In addition, there
are test_ffi.cpp
, test_ffi.pl
, which often
have the same tests written in C, without the C++ API.
The list below summarises the classes defined in the C++ interface.
term_t
(for more details on
term_t
, see
Interface
Data Types). This is a "base class" whose constructor is protected;
subclasses specify the actual contents. Additional methods allow
checking the Prolog type, unification, comparison, conversion to native
C++-data types, etc. See section
2.10.3.
The subclass constructors are as follows. If a constructor fails
(e.g., out of memory), a PlException
is thrown.
PlTerm
with constructors for building a term
that contains an atom.PlTerm
with constructors for building a term
that contains an uninstantiated variable. Typically this term is then
unified with another object.PlTerm
with constructors for building a term
from a C term_t
.PlTerm
with constructors for building a term
that contains a Prolog integer from a
long
.8PL_put_integer()
takes a long
argument.PlTerm
with constructors for building a term
that contains a Prolog integer from a int64_t
.PlTerm
with constructors for building a term
that contains a Prolog integer from a uint64_t
.PlTerm
with constructors for building a term
that contains a Prolog integer from a size_t
.PlTerm
with constructors for building a term
that contains a Prolog float.PlTerm
with constructors for building a term
that contains a raw pointer. This is mainly for backwards compatibility;
new code should use blobs.PlTerm
with constructors for building a term
that contains a Prolog string object.PlTerm
with constructors for building Prolog
lists of character integer values.PlTerm
with constructors for building Prolog
lists of one-character atoms (as atom_chars/2).PlTerm
for building and analysing Prolog lists.
Additional subclasses of PlTerm
are:
PlTerm
with constructors for building compound
terms. If there is a single string argument, then PL_chars_to_term()
or PL_wchars_to_term() is used to parse the string and create the
term. If the constructor has two arguments, the first is name of a
functor and the second is a PlTermv
with the arguments.[]
operator is overloaded to access elements in this vector. PlTermv
is used to build complex terms and provide argument-lists to Prolog
goals.PlExceptionBase
, representing a Prolog
exception. Provides methods for the Prolog communication and mapping to
human-readable text representation.
PlException
object for representing a Prolog
type_error
exception.PlException
object for representing a Prolog
domain_error
exception.PlException
object for representing a Prolog
existence_error
exception.PlException
object for representing a Prolog
permission_error
exception.std::exception
, to allow
catching
PlException
, PlExceptionFail
or PlFail
in a single "catch" clause.atom_t
) in their internal
Prolog representation for fast comparison. (For more details on
atom_t
, see
Interface
Data Types).functor_t
, which maps to the internal
representation of a name/arity pair.predicate_t
, which maps to the internal
representation of a Prolog predicate.module_t
, which maps to the internal
representation of a Prolog module.return false
instead
if failure is expected. An error can be signaled by calling
Plx_raise_exception() or one of the PL_*_error() functions and then
throwing PlFail
; but it's better style to create the error
throwing one of the subclasses of PlException
e.g.,
throw PlTypeError("int", t)
.PlException
object and throws it. If the
enclosing code doesn't intercept the exception, the PlException
object is turned back into a Prolog error when control returns to Prolog
from the PREDICATE() macros.PlException
object, so a PlExceptionFail
object is thrown. This is turned into failure by the PREDICATE() macro,
resulting in normal Prolog error handling.
The various PL_*() functions in SWI-Prolog.h
have
corresponding Plx_*() functions, defined in SWI-cpp2-plx.h
,
which is always included by SWI-cpp2.h
. There are three
kinds of wrappers:
false
,
indicating an error. The Plx_*() function checks for this and throws a PlException
object containing the error. The wrapper uses template<typename
C_t> C_t PlEx(C_t rc)
, where C_t
is the return
type of the PL_*() function. (These are defined using the PLX_WRAP()
macro.)
true
if it succeeds and false
if it fails or
has a runtime error. If it fails, the wrapper checks for a Prolog error
and throws a PlException
object containing the error. The
wrapper uses template<typename C_t> C_t PlWrap(C_t rc)
,
where C_t
is the return type of the PL_*() function. (These
are defined using the PLX_EXCE() macro.)
A few PL_*() functions do not have a corresponding Plx*() function
because they do not fit into one of these categories. For example,
PL_next_solution() has multiple return values (PL_S_EXCEPTION
,
PL_S_LAST
, etc.) if the query was opened with the
PL_Q_EXT_STATUS
flag.
Most of the PL_*() functions whose first argument is of type
term_t
, atom_t
, etc. have corresponding
methods in classes PlTerm
, PlAtom
, etc.
Important: You should use the Plx_*() wrappers only in the context of a PREDICATE() call, which will handle any C++ exceptions. Some blob callbacks can also handle an exception. Everywher else, results are unpredicatable (probably a crash).
See also section 2.5.1.
The classes all have names starting with "Pl", using CamelCase; this contrasts with the C functions that start with "PL_" and use underscores.
The wrapper classes (PlFunctor
, PlAtom
,
PlTerm
), etc. all contain a field C_
that
contains the wrapped value (functor_t
, atom_t
, term_t
respectively). If this wrapped value is needed, it should be accessed
using the unwrap() or unwrap_ptr() methods.
In some cases, it's natural to use a pointer to a wrapper class. For
those, the function PlUnwrapAsPtr() returns nullptr
if the
pointer is null; otherwise it returns the wrapped value (which itself
might be some kind of "null").
The wrapper classes (which subclass WrappedC<...>
)
all define the following methods and constants:
null
).PlAtom
,
the constructor takes an atom_t
value).C_
- the wrapped value. This can be used directly when
calling C functions, for example, if t
and a
are of type PlTerm
and PlAtom
: Plcheck_PL(PL_put_atom(t.unwrap(),a.unwrap()))
.null
- the null value (typically 0
, but
code should not rely on this).is_null()
, not_null()
- test for the
wrapped value being null
.reset()
- set the wrapped value to null
reset(new_value)
- set the wrapped value from the
wrapped type (e.g., PlTerm:;reset(term_t new_value))reset_wrapped(new_value)
- set the wrapped value from
the same type (e.g., PlTerm::reset_wrapped(PlTerm new_value))bool
operator is disabled - you should use
not_null() instead.9The reason: a bool
conversion causes ambiguity with PlAtom(PlTterm)
and PlAtom(atom_t)
.
The method unwrap() can be used to access the C_
field,
and can be used wherever a atom_t
or term_t
is
used. For example, the PL_scan_options() example code can be
written as follows. Note the use of &callback.unwrap()
to pass a pointer to the wrapped term_t
value.
PREDICATE(mypred, 2) { auto options = A2; int quoted = false; size_t length = 10; PlTerm_var callback; PlCheckFail(PL_scan_options(options, 0, "mypred_options", mypred_options, "ed, &length, &callback.unwrap())); callback.record(); // Needed if callback is put in a blob that Prolog doesn't know about. // If it were an atom (OPT_ATOM): register_ref(). <implement mypred> }
For functions in SWI-Prolog.h
that don't have a C++
equivalent in SWI-cpp2.h
, PlCheckFail() is a convenience
function that checks the return code and throws a PlFail
exception on failure or PlException
if there was an
exception. The PREDICATE() code catches PlFail
exceptions
and converts them to the foreign_t
return code for failure.
If the failure from the C function was due to an exception (e.g.,
unification failed because of an out-of-memory condition), the foreign
function caller will detect that situation and convert the failure to an
exception.
The "getter" methods for PlTerm
all throw an exception
if the term isn't of the expected Prolog type. The "getter" methods
typically start with "as", e.g. PlTerm::as_string(). There are also
other "getter" methods, such as PlTerm::get_float_ex() that wrap PL_*()
functions.
"Getters" for integers have an additionnal problem, in that C++
doesn't define the sizes of int
, long
, or
size_t
. It seems to be impossible to make an overloaded
method that works for all the various combinations of integer types on
all compilers, so there are specific methods for int64_t
,
uint64_t
, size_t
.
In some cases,it is possible to overload methods; for example, this
allows the following code without knowing the exact definition of
size_t
:
PREDICATE(p, 1) { size_t sz; A1.integer(&sz); ... }
It is strongly recommended that you enable conversion checking.
For example, with GNU C++, these options (possibly with -Werror
):
-Wconversion -Warith-conversion -Wsign-conversion
-Wfloat-conversion
.
There is an additional problem with characters - C promotes them to int
but C++ doesn't. In general, this shouldn't cause any problems, but care
must be used with the various getters for integers.
Disclaimer:
The blob API for C++ is not completely general, but is designed to make a specific use case easier to write. For other use cases, the underlying C API can still be used. The use case is:
PlBlob
, which
provides a number of fields and methods, of which a few can be
overridden in the blob (notably: write_fields(), compare_fields(),
save(), load(), and the destructor).new
operator and
passes ownership to the blob.A Prolog blob consists of five parts:
PL_blob_t
structure that defines the callbacks.
For the PL_blob_t
structure, the C++ API provides a set
of template functions that allow easily setting up the callbacks, and
defining the corresponding methods in the blob "contents" class. The C
interface allows more flexibility by allowing some of the callbacks to
default; however, the C++ API for blobs provides suitable callbacks for
all of them, so usually the programmer will specify all the template
callbacks using the
PL_BLOB_DEFINITION(blob_class,blob_name) macro.
For the data, which is subclassed from PlBlob
, the
programmer defines the various fields, a constructor that initializes
them, and a destructor. Optionally, methods can be defined for one of
more of blob compare_fields(), write_fields(), save(), load(). More
details on these are given later.
There is a mismatch between how Prolog does memory management (and
garbage collection) and how C++ does it. In particular, Prolog assumes
that cleanup will be done in the release() function associated with the
blob whereas C++ typically does cleanup in a destructor. The blob
interface gets around this mismatch by providing a default release()
function that assumes that the blob was created using PL_BLOB_NOCOPY
and manages memory using a
std::unique_ptr
.
The C blob interface has a flag that determines how memory is
managed:
PL_BLOB_NOCOPY
. The PL_BLOB_DEFINITION() macro sets
this, so Prolog does not do a call to free() when the blob is garbage
collected; instead, it lets the blob's release() free the memory, which
is done by calling the C++ destructor.
The C++ API for blobs only supports blobs with
PL_BLOB_NOCOPY
.12The
API can probably also support blobs with PL_BLOB_UNIQUE
,
but there seems to be little point in setting this flag for non-text
blobs.
TL;DR: Use PL_BLOB_DEFINITION() to define the blob with the
flag
PL_BLOB_NOCOPY
and the default PlBlob
wrappers; define your struct as a subclass of PlBlob
with
no copy constructor, move constructor, or assignment operator; create
blob using
std::unique_ptr<PlBlob>(new ...)
, call
PlTerm::unify_blob(). Optionally, define one or more of:
compare_fields(), write_fields(), save(), load() methods (these are
described after the sample code).
In this section, the blob is of type MyBlob
, a subclass
of PlBlob
.
A blob is typically created by calling a predicate that does the following:
auto ref = std::unique_ptr<PlBlob>(new
MyBlob>(...))
(std::make_unique() can't be used because it
returns type
std::unique_ptr<MyBlob>
but
PlTerm::unify_blob() requires a
std::unique_ptr<PlBlob>
and C++'s type
inferencing can't figure out that this is a covariant type).
ref.release()
to pass ownership to the Prolog blob. If you
wish to use std::make_unique<MyBlob>(), you could
instead do:
auto ref = std::make_unique<MyBlob>(...); ... // code that accesses fields in *ref std::unique_ptr<PlBlob> refb(ref.release()); // transfer ownership of ptr // from here on, can't access fields in *ref return A2.unify_blob(refb);
At this point, the blob is owned by Prolog and will be freed by its atom garbage collector, which will call the blob's destructor.
Whenever a predicate is called with the blob as an argument (e.g., as A1),
the blob can be accessed by
PlBlobv<MyBlob>::cast_check(A1.as_atom())
.
Within a method, the Prolog blob can be accessed as a term (e.g., for
constructing an error term) using the method MyBlob::symbol_term(). This
field is initialized by the call to PlTerm::unify_blob(); if
MyBlob::symbol_term() is called before a successful call to
PlTerm::unify_blob(), MyBlob::symbol_term() returns a
PlTerm_var
.
When the atom garbage collector runs, it frees the blob by first
calling the release() callback, which does delete
, which
calls the destructor MyBlob:: MyBlob(). Note that C++ destructors
are not supposed to raise exception; they also should not cause a Prolog
error, which could cause deadlock unless the real work is done in
another thread.
Often it is desired to release the resources before the garbage collector runs. To do this, the programmer can provide a "close" predicate which is the inverse of the "open" predicate that created the blob. This typically has the same logic as the destructor, except that it can raise a Prolog error.
When a blob is used in the context of a PREDICATE() macro, it can
raise a C++ exception (PlFail
or PlException
)
and the PREDICATE() code will convert it to the appropriate Prolog
failure or error; memory allocation exceptions are also handled.
Blobs have callbacks, which can run outside the context of a PREDICATE(). Their exception handling is as follows:
PlAtom::null
, which is interpreted by Prolog as
failure.
Here is minimal sample code for creating a blob that owns a
connection to a database. It has a single field (connection
)
and defines compare_fields() and write_fields().
struct MyBlob; static PL_blob_t my_blob = PL_BLOB_DEFINITION(MyBlob, "my_blob"); struct MyBlob : public PlBlob { std::unique_ptr<MyConnection> connection; std::string name_; // Used for error terms explicit MyBlob() : PlBlob(&my_blob) { } explicit MyBlob(const std::string& connection_name) : PlBlob(&my_blob), connection(std::make_unique<MyConnection>(connection_name)), name_(connection_name) { if ( !connection->open() ) throw MyBlobError("my_blob_open_error"); } PL_BLOB_SIZE ~MyBlob() noexcept { if ( !close() ) Sdprintf("Close MyBlob failed: %s", name_.c_str()); // Can't use PL_warning() } bool close() noexcept { if ( !connection ) return true; bool rc = connection->close(); connection.reset(); // Can be omitted, leaving deletion to ~MyBlob() return rc; } PlException MyBlobError(const char* error) const { return PlGeneralError(PlCompound(error, PlTermv(symbol_term()))); } int compare_fields(const PlBlob* _b_data) const override { auto b_data = static_cast<const MyBlob*>(_b_data); // See note about cast return name_.compare(b_data->name_); } bool write_fields(IOSTREAM *s, int flags) const override { if ( !Sfprintf(s, ",name=%s", name_.c_str()) ) return false; if ( !connection ) return Sfprintf(s, ",closed"); return true; } }; // %! create_my_blob(+Name: atom, -MyBlob) is semidet. PREDICATE(create_my_blob, 2) { // Allocating the blob uses std::unique_ptr<MyBlob> so that it'll be // deleted if an error happens - the auto-deletion is disabled by // ref.release() before returning success. auto ref = std::unique_ptr<PlBlob>(new MyBlob(A1.as_atom().as_string())); return A2.unify_blob(&ref); } // %! close_my_blob(+MyBlob) is det. // % Close the connection, silently succeeding if is already // % closed; throw an exception if something goes wrong. PREDICATE(close_my_blob, 1) { auto ref = PlBlobV<MyBlob>::cast_ex(A1, my_blob); if ( !ref->close() ) throw ref->MyBlobError("my_blob_close_error"); return true; }
PL_blob_t
structure with the wrapper functions and flags
set to PL_BLOB_NOCOPY
. It should be declared outside the PlBlob
class and should not be marked const
- otherwise, a runtime
error can occur.13The cause of the
runtime error is not clear, but possibly has to do with the order of
initializing globals, which is unspecified for C++.
MyBlob
struct is a subclass of PlBlob
.
See below for a discussion of the default behaviors.
MyBlob
contains a pointer to a MyConnection
object and keeps a copy of the connection's name. The MyConnection
object is handled by a std::unique_ptr
smart pointer, so
that it is automatically freed when the MyBlob
object is
freed.
PlBlob
constructor.
MyBlob
class must not provide a copy or move
constructor, nor an assignment operator (PlBlob has these as
delete
, so if you try to use one of these, you will get a
compile-time error).
PlBlob
’s constructor sets blob_t_
to
a pointer to the my_blob
definition. This is used for
run-time consistency checking by the various callback functions and for
constructing error terms (see PlBlob::symbol_term()).
PlBlob
’s acquire() is called by PlBlobV<MyBlob>::awcuire()
and fills in the symbol
field. MyBlob
must not
override this - it is not a virtual method.
MyConnection
object. If this fails, an exception is thrown.
The constructor then calls MyConnection::open() and throws an exception
if that fails. (The code would be similar if instead the constructor for MyConnection
also did an open and threw an exception on failure.)
PL_BLOB_SIZE
is boilerplate that defines a
blob_size_() method that is used when the blob is created.
throw
PlUnknownError("...")
, that will try to create a Prolog term,
which will crash because the environment for creating terms is not
available. Because there is no mechanism for reporting an
error, the destructor prints a message on failure (calling
PL_warning() would cause a crash).
PlBlob::close() calls MyConnection::close() and then frees the
object. Error handling is left to the caller because of the possibility
that this is called in the context of garbage collection. It is not
necessary to free the MyConnection
object here - if it is
not freed, the
std::unique_ptr<MyConnection>
’s
destructor would free it.
0
("equal").
The _b_data argument is of type const PlBlob*
- this is cast to const MyBlob*
using a
static_cast
. This is safe because Prolog guarantees that
PlBlobV<PlBlob>::compare() will only be called if both
blobs are of the same type.
The flags argument is the same as given to PlBlobV<PlBlob>::write(),
which is a bitwise or of zero or more of the PL_WRT_*
flags that were passed in to the caling PL_write_term() (defined
in SWI-Prolog.h
). The
flags do not have the PL_WRT_NEWLINE
bit set, so
it is safe to call PlTerm::write() and there is no need for writing a
trailing newline.
If anything in PlBlob::write_fields() throws a C++ exception, it will be caught by the calling PlBlobV<PlBlob>::write() and handled appropriately.
std::unique_ptr<PlBlob>()
creates a
MyBlob that is deleted when it goes out of scope. If an exception occurs
between the creation of the blob or if the call to unify_blob() fails,
the pointer will be automatically freed (and the
MyBlob
destructor will be called).
If PlTerm::unify_blob() is called with a pointer to a
std::unique_ptr
, it takes ownership of the object by
calling std::unique_ptr<PlBlob>::release(). This sets ref
to nullptr
, so any attempt to use ref after a
successful call to PlTerm::unify_blob() will be an error.
If you wish to create a MyBlob
object instead of a
PlBlob
object, a slightly different form is used:
auto ref = std::make_unique<MyBlob>(...); ... std::unique_ptr<PlBlob> refb(ref.release()); PlCheckFail(A2.unify_blob(&refb)); return true;
MyBlob
pointer using the
PlBlobV<MyBlob>::cast_ex() function, which will throw a
type_error
if the argument isn't a blob of the expected
type.
Passing a blob around can be inconvenient; there is an easy way to
identify a blob by an atom. An example of this is with streams, which
are identified by atoms such as user_input
.
A utility class AtomMap
is provided for this situation.
See section 2.20.4.
The C++ API remains a work in progress.
SWI-Prolog string handling has evolved over time. The functions that
create atoms or strings using char*
or wchar_t*
are "old school"; similarly with functions that get the string as
char*
or wchar_t*
. The PL_get_unify_put_[nw]chars()
family is more friendly when it comes to different input, output,
encoding and exception handling.
Roughly, the modern API is PL_get_nchars(), PL_unify_chars() and PL_put_chars() on terms. There is only half of the API for atoms as PL_new_atom_mbchars() and PL-atom_mbchars(), which take an encoding, length and char*.
For return values, char*
is dangerous because it can
point to local or stack memory. For this reason, wherever possible, the
C++ API returns a std::string
, which contains a copy of the
the string. This can be slightly less efficient that returning a
char*
, but it avoids some subtle and pervasive bugs that
even address sanitizers can't detect.16If
we wish to minimize the overhead of passing strings, this can be done by
passing in a pointer to a string rather than returning a string value;
but this is more cumbersome and modern compilers can often optimize the
code to avoid copying the return value.
Some functions require allocating string space using PL_STRINGS_MARK().
The PlStringBuffers
class provides a RAII wrapper
that ensures the matching PL_STRINGS_RELEASE() is done. The PlAtom
or PlTerm
member functions that need the string buffer use PlStringBuffers
,
and then copy the resulting string to a std::string
value.
PlStream
can be used to get a stream from a Prolog term,
or to lock the stream so that other threads cannot interleave their
output. With either usage, PlStream
is a RAII
class that ensure the matchin PL_release_stream() is done, and
also handles some subtle problems with C++ exceptions.
The methods are:
PlStream
object to an invalid stream (see PlStream::check_stream()).
IOSTREAM*
, PlStream
is implicitly converted to IOSTREAM*
.
PlStream
object contains a valid stream and throws an
exception if it doesn't. This is used to ensure that PlStream::release()
hasn't been called.
PlStream
.
For example, Sfprintf() corresponds to PlStream::printf().
The C interface to stream I/O doesn't raise a Prolog error when
there's a stream error (typically indicated by a -1 return code).
Instead, the error sets a flag on the stream and
PL_release_stream() creates the error term. The
PlStream
destructor calls PL_release_stream(); but
it's a fatal error in C++ to raise an exception in a destructor if the
destructor is invoked by stack-unwinding due to another exception,
including the pseudo-exceptions PlFail
and
PlExceptionFail
.
To get around this, the various stream I/O functions have wrapper
methods in the PlStream
class that check for an error and
call PlStream::release() to create the Prolog error, which is thrown as
a C++ error.
The destructor calls PlStream::release(), which throws a C++ exception if there is a stream error. This is outside the destructor, so it is safe - the destructor checks if the stream has been released and does nothing in that situation.
The following two code examples do essentially the same thing:
PREDICATE(name_arity, 1) { PlStream strm(Scurrent_output); strm.printf("name = %s, arity = %zd\n", A1.name().as_string().c_str(), A1.arity()); return true; }
PREDICATE(name_arity, 1) { PlStream strm(Scurrent_output); try { strm.printf("name = %s, arity = %zd\n", A1.name().as_string().c_str(), A1.arity()); } PREDICATE_CATCH({strm.release(); return false;}) return true; }
If you write the code as follows, using Sfprintf() directly, it is possible that a fatal exception will be raised on an I/O error:
PREDICATE(name_arity, 1) { PlStream strm(Scurrent_output); Sfprintf(strm, "name = %s, arity = %zd\n", A1.name().as_string().c_str(), A1.arity()); return true; // WARNING: the PlStream destructor might throw a C++ // exception on stack unwinding, giving a fatal // fatal runtime exception. }
If you don't use these, and want to throw an exception if there's an
error, the following code works because PlStream
(and the
underlying PL_acquire_stream()) can be called recursively:
{ PlStream strm(...); strm.release(); }
Many of the "opaque object handles", such as atom_t
,
term_t
, and functor_t
are integers.17Typically uintptr_t
values, which the C standard defines as “an unsigned integer type
with the property that any valid pointer to void can be converted to
this type, then converted back to pointer to void, and the result will
compare equal to the original pointer.'' As such, there is
no compile-time detection of passing the wrong handle to a function.
This leads to a problem with classes such as PlTerm
-
C++ overloading cannot be used to distinguish, for example, creating a
term from an atom versus creating a term from an integer. There are a
number of possible solutions, including:
struct
instead of an
integer.It is impractical to change the C code, both because of the amount of edits that would be required and also because of the possibility that the changes would inhibit some optimizations.
There isn't much difference between subclasses versus tags; but as a matter of design, it's better to specify things as constants than as (theoretically) variables, so the decision was to use subclasses.
The utility program swipl-ld (Win32: swipl-ld.exe) works with both C and C++ programs. See Linking embedded applications using swipl-ld for more details.
Your C++ compiler should support at least C++-17.
To avoid incompatibilities amongst the various C++ compilers' ABIs,
the object file from compiling SWI-cpp2.cpp
is not included
in the shared object libswipl
; instead, it must be compiled
along with any foreign predicate files. If the macro
_SWI_CPP2_CPP_SEPARATE
is defied before the include for
SWI-cpp2.h
), then SWI-cpp2.cpp
is not
automatically included and must be compiled separately - either by
creating a
.a
file or by adding a #include <SWI-cpp2.cpp>
to one of your source files.