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 1.17.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 invoked 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 1.17.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.