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.4.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.17.)
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 using the PlTerm_term_t
constructor).
To help avoid programming errors, most 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 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.9.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
but does not include SWI-cpp2.cpp
.
SWI-cpp2.cpp
Contains the implementations of some
methods and functions. It must be compiled as-is or included in the
foreign predicate's source file. Alternatively, it can be included with
each include of
SWI-cpp2.h
with this macro definition:
#define _SWI_CPP2_CPP_inline inline
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
.
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.9.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
.10PL_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.std::exception
, 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.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.PlException
object, so a PlExceptionFail
object is thrown. This is turned into failure by the PREDICATE()
macro, resulting in normal Prolog error handling.The required C++ function header and registration of a predicate is arranged through a macro called PREDICATE().
The various PL_*() functions in SWI-Prolog.h
have
corresponding Plx_*() functions. 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 PlExce(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.
See also section 2.4.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).
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.C_,a.C_))
.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 valuebool
operator is turned off - you should use
not_null() instead.11The reason: a bool
conversion causes ambiguity with PlAtom(PlTterm)
and PlAtom(atom_t)
.
The C_
field 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.C_
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; PlCheck_L(PL_scan_options(options, 0, "mypred_options", mypred_options, "ed, &length, &callback.C_)); 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
, PlCheck_PL() 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. Where possible, the
"getters" have the same name as the underlying type; but this isn't
possible for types such as int
or float
, so
for these the name is prepended with "as_".
"Getters" for integers have an additionnal problem, in that C++
doesn't define the sizes of int
and long
, nor
for
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.
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*.
However, there is no native "string" type in C++; the char*
strings can be automatically cast to string. If a C++ interface provides
only std::string
arguments or return values, that can
introduce some inefficiency; therefore, many of the functions and
constructors allow either a char*
or std::string
as a value (also wchar_t*
or std::wstring
.
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.12If
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.
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 istntead 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 "opaque object handles", such as atom_t
,
term_t
, and functor_t
are integers.13Typically 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
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. You can do this in three ways:
SWI-cpp2.cpp
separately.#include SWI-cpp2.cpp
to one of the foreign
predicate files.#include SWI-cpp2.h%
, add
#define _SWI_CPP2_CPP_inline inline #include <SWI-cpp2.cpp>
This will cause the compiler to attempt to inline all the functions and methods, even those that are rarely used, resulting in some code bloat.