2 A C++ interface to SWI-Prolog (Version 2)
AllApplicationManualNameSummaryHelp

  • Documentation
    • Reference manual
    • Packages
      • A C++ interface to SWI-Prolog
        • A C++ interface to SWI-Prolog (Version 2)
          • Summary of changes between Versions 1 and 2
          • Introduction (version 2)
          • The life of a PREDICATE (version 2)
          • Overview (version 2)
            • Design philosophy of the classes
            • Summary of files
            • Summary of classes
            • Wrapper functions
            • Naming conventions, utility functions and methods (version 2)
            • Limitations of the interface
            • Linking embedded applications using swipl-ld
          • Examples (version 2)
          • Rational for changes from version 1 (version 2)
          • Porting from version 1 to version 2
          • The class PlFail (version 2)
          • The class PlTerm (version 2)
          • The class PlTermv (version 2)
          • The class PlAtom - Supporting Prolog constants (version 2)
          • Classes for the recorded database: PlRecord and PlRecordExternalCopy
          • Unification and foreign frames (version 2)
          • The class PlRegister (version 2)
          • The class PlQuery (version 2)
          • The PREDICATE and PREDICATE_NONDET macros (version 2)
          • Exceptions (version 2)
          • Embedded applications (version 2)
          • Considerations (version 2)
          • Conclusions (version 2)

2.4 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).

2.4.1 Design philosophy of the classes

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().

2.4.2 Summary of files

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 #included 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.

2.4.3 Summary of classes

The list below summarises the classes defined in the C++ interface.

PlTerm
Generic Prolog term that wraps 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_atom
Subclass of PlTerm with constructors for building a term that contains an atom.
PlTerm_var
Subclass of PlTerm with constructors for building a term that contains an uninstantiated variable. Typically this term is then unified with another object.
PlTerm_term_t
Subclass of PlTerm with constructors for building a term from a C term_t.
PlTerm_integer
Subclass of PlTerm with constructors for building a term that contains a Prolog integer from a long.10PL_put_integer() takes a long argument.
PlTerm_int64
Subclass of PlTerm with constructors for building a term that contains a Prolog integer from a int64_t.
PlTerm_uint64
Subclass of PlTerm with constructors for building a term that contains a Prolog integer from a uint64_t.
PlTerm_size_t
Subclass of PlTerm with constructors for building a term that contains a Prolog integer from a size_t.
PlTerm_float
Subclass of PlTerm with constructors for building a term that contains a Prolog float.
PlTerm_pointer
Subclass of PlTerm with constructors for building a term that contains a raw pointer. This is mainly for backwards compatibility; new code should use blobs.
PlTerm_string
Subclass of PlTerm with constructors for building a term that contains a Prolog string object.
PlTerm_list_codes
Subclass of PlTerm with constructors for building Prolog lists of character integer values.
PlTerm_chars
Subclass of PlTerm with constructors for building Prolog lists of one-character atoms (as atom_chars/2).
PlTerm_tail
SubClass of PlTerm for building and analysing Prolog lists.

Additional subclasses of PlTerm are:

PlCompound
Subclass of 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.
PlTermv
Vector of Prolog terms. See PL_new_term_refs(). The [] operator is overloaded to access elements in this vector. PlTermv is used to build complex terms and provide argument-lists to Prolog goals.
PlException
Subclass of std::exception, representing a Prolog exception. Provides methods for the Prolog communication and mapping to human-readable text representation.
PlTerm PlTypeError()
Creates a PlException object for representing a Prolog type_error exception.
PlTerm PlDomainError()
Creates a PlException object for representing a Prolog domain_error exception.
PlTerm PlExistenceError()
Creates a PlException object for representing a Prolog existence_error exception.
PlTerm PlPermissionError()
Creates a PlExceptionobject for representing a Prolog permission_error exception.
PlAtom
Allow for manipulating atoms (atom_t) in their internal Prolog representation for fast comparison. (For more details on atom_t, see Interface Data Types).
PlFunctor
A wrapper for functor_t, which maps to the internal representation of a name/arity pair.
PlPredicate
A wrapper for predicate_t, which maps to the internal representation of a Prolog predicate.
PlModule
A wrapper for module_t, which maps to the internal representation of a Prolog module.
PlQuery
Represents opening and enumerating the solutions to a Prolog query.
PlFail
Can be thrown to short-circuit processing and return failure to Prolog. Performance-critical code should use 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
If a call to Prolog results in an error, the C++ interface converts the error into a PlException object and throws it. If the enclosing code doesn't intercept the exception, the PlException object is turned back into a Prolog error.
PlExceptionFail
In some situations, a Prolog error cannot be turned into a PlException object, so a PlExceptionFail object is thrown. This is turned into failure by the PREDICATE() macro, resulting in normal Prolog error handling.
PlFrame
This utility-class can be used to discard unused term-references as well as to do‘data-backtracking’.
PlEngine
This class is used in embedded applications (applications where the main control is held in C++). It provides creation and destruction of the Prolog environment.
PlRegister
The encapsulation of PL_register_foreign() is defined to be able to use C++ global constructors for registering foreign predicates.

The required C++ function header and registration of a predicate is arranged through a macro called PREDICATE().

2.4.4 Wrapper functions

The various PL_*() functions in SWI-Prolog.h have corresponding Plx_*() functions. There are three kinds of wrappers:

  • "as-is" - the PL_*() function cannot cause an error. If it has a return value, the caller will want to use it. (These are defined using the PLX_ASIS() and PLX_VOID() macros.)

  • "exception wrapper" - the PL_*() function can return 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.)

  • "success, failure, or error" - the PL_*() function can return 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.

2.4.5 Naming conventions, utility functions and methods (version 2)

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:

  • default constructor (sets the wrapped value to null)
  • constructor that takes the wrapped value (e.g., for 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 value
  • The bool 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,
                            &quoted, &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.

2.4.6 Limitations of the interface

The C++ API remains a work in progress.

2.4.6.1 Strings

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.

2.4.6.2 Object handles

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:

  • A subclass for each kind of initializer;
  • A tag for each kind of intializer;
  • Change the the C code to use a 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.

2.4.7 Linking embedded applications using swipl-ld

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:

  • Compile SWI-cpp2.cpp separately.
  • Add #include SWI-cpp2.cpp to one of the foreign predicate files.
  • Wherever you have #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.