Not all functionality of the C-interface is provided, but as
PlTerm
and term_t
are essentially the same
thing with type-conversion between the two (using the unwrap() method),
this interface can be freely mixed with the functions defined for plain
C. For checking return codes from C functions, it is recommended to use
PlCheckFail() or PlCheck_PL().
Using this interface rather than the plain C-interface requires a
little more resources. More term-references are wasted (but reclaimed on
return to Prolog or using PlFrame
). Use of some
intermediate types (functor_t
etc.) is not supported in the
current interface, causing more hash-table lookups. This could be fixed,
at the price of slighly complicating the interface.
Global terms and atoms need to be handled slightly differently in C++ than in C - see section 2.20.3
Exceptions are normal Prolog terms that are handled specially by the
PREDICATE macro when they are used by a C++ throw
, and
converted into Prolog exceptions. The exception term may not be unbound;
that is, throw(_) must raise an error. The C++ code and underlying C
code do not explicitly check for the term being a variable, and
behaviour of raising an exception that is an unbound term is undefined,
including the possibility of causing a crash or corrupting data.
The Prolog exception term error(Formal, _) is special. If the 2nd
argument of error/2
is undefined, and the term is thrown, the system finds the catcher (if
any), and calls the hooks in library(prolog_stack) to add the context
and stack trace information when appropriate. That is, throw
PlDomainError(Domain,Culprit)
ends up doing the same thing as
calling
PL_domain_error(Domain,Culprit)
which internally
calls
PL_raise_exception() and returns control back to Prolog.
The VM handling of calling to C finds the FALSE
return
code, checks for the pending exception and propagates the exception into
the Prolog environment. As the term references (term_t
)
used to create the exception are lost while returning from the foreign
function we need some way to protect them. That is done using a global term_t
handle that is allocated at the epoch of Prolog.
PL_raise_exception() sets this to the term using PL_put_term().
PL_exception(0) returns the global exception term_t
if it is bound and 0 otherwise.
Special care needs to be taken with data backtracking using
PL_discard_foreign_frame() or PL_close_query() because
that will invalidate the exception term. So, between raising the
exception and returning control back to Prolog we must make sure not to
do anything that invalidates the exception term. If you suspect
something like that to happen, use the debugger with a breakpoint on
__do_undo__LD() defined in pl-wam.c
.
In order to always preserve Prolog exceptions and return as quickly as possible to Prolog on an exception, some of the C++ classes can throw an exception in their destructor. This is theoretically a dangerous thing to do, and can lead to a crash or program termination if the destructor is envoked as part of handling another exception.
Sometimes it is convenient to put constant terms and atoms as global
variables in a file (with a static
qualifier), so that they
are only created (and looked up) cone. This is fine for atoms and
functors, which can be created by something like this:
static PlAtom ATOM_foo("foo"); static PlFunctor FUNCTOR_ff_2("ff", 2);
C++ makes no guarantees about the order of creating global variables
across "translation units" (that is, individual C++ files), but the
Prolog runtime ensures that the necessary initialization has been done
to allow PlAtom
and PlFunctor
objects to be
created. However, to be safe, it is best to put such global variables
inside functions - C++ will initialize them on their firstuse.
Global Terms need a bit of care. For one thing, terms are ephemeral,
so it is wrong to have a PlTerm
static variable - instead,
a
PlRecord
must be used, which will provide a fresh copy of
the term using PlRecord::term(). There is no guarantee that the Prolog
runtime has initialized everything needed for creating entries in the
recorded database (see
Recorded
database). Therefore, global recorded terms must be wrapped inside a
function. C++ will call the constructor upon first use. For example:
static PlTerm term_foo_bar() { static PlRecord r(PlCompound("foo", PlTermv(PlTerm_atom("bar"))).record()); return r.term(); }
The include file SWI-cpp2-atommap.h
contains a templated
class
AtomMap
for mapping atoms to atoms or terms. The typical
use case is for when it is desired to open a database or stream and,
instead of passing around the blob, an atom can be used to identify the
blob.
The keys in the map must be standard Prolog atoms and not blobs - the code depends on the fact that an atom has a unique ID.
The AtomMap
is thread-safe (it contains a mutex). It
also takes care of reference counts for both the key and the value. Here
is a typical use case:
static AtomMap<PlAtom, PlAtom> map_atom_my_blob("alias", "my_blob"); // look up an entry: auto value = map_atom_my_blob(A1.as_atom()); PlCheckFail(value.not_null()); // insert an entry: map_atom_my_blob.insert(A1.as_atom(), A2.as_atom()); // remove an entry: map_atom_my_blob.erase(A1.as_atom());
The constructor and methods are as follows:
AtomMap
.
The ValueType and StoredValueType specify what
type you wish for the value. Currently, two value types are supported:
PlAtom
- the StoredValueType should be PlAtom
.PlTerm
- the StoredValueType shoud be PlRecord
(because the term needs to be put on the global stack).
permission_error
if the value
is already in the map, unless the value is identical to the value in the
map. The insert() method converts the value to the StoredValueType
.
The insertion code takes care of atom reference counts.
StoredValueType
to ValueType
.
The mechanisms outlined in this document can be used for static linking with the SWI-Prolog kernel using swipl-ld(1). In general the C++ linker should be used to deal with the C++ runtime libraries and global constructors.
The current interface can be entirely defined in the .h
file using inlined code. This approach has a few advantages: as no C++
code is in the Prolog kernel, different C++ compilers with different
name-mangling schemas can cooperate smoothly. However, inlining
everything can lead to code bloat, so the larger functions and methods
have been put into a .cpp
file that can be either compiled
separately (by the same compiler as used by the foreign predicate) or
inlined as if it were part of the .h
file.
Also, changes to the header file have no consequences to binary compatibility with the SWI-Prolog kernel. This makes it possible to have different versions of the header file with few compatibility consequences.
As of 2023-04, some details remain to be decided, mostly to do with
encodings. A few methods have a PlEncoding
optional
parameter (e.g., PlTerm::as_string()), but this hasn't yet been extended
to all methods that take or return a string. Also, the details of how
the default encoding is set have not yet been decided.
As of 2023-04, the various error convenience classes do not fully
match what the equivalent C functions do. That is, throw
PlInstantiationError(A1)
does not result in the same context and
traceback information that would happen from
Plx_instantiation_error(A1.unwrap()); throw PlFail()
. See
section 2.20.2.
The Plx_*() wrappers may require small adjustments in whether their
return values require [[nodiscard]]
or whether their return
values should be treated as an error.
The implementation of PlException
is likely to change
somewhat in the future. Currently, to ensure that the exception term has
a sufficient lifetime, it is serialized using PL_record_external().
In future, if this proves unnecessary, the term will be stored as-is.
The API will not change if this implementation detail changes.