SWI-Prolog is well suitable for writing a wide range of complete applications without introducing other languages into the system and an even wider range when linking C/C++ coded shared objects to access external resources, in real-life Prolog is often embedded in systems written in other languages. The reasons vary. Re-use of existing code, expertise in the development team or external requirements are commonly encountered motivations.
Embedding Prolog is not a logical choice in our view. An important part of the power of Prolog can be found in its development system where retrying goals and reloading patched code on the running system speedup development considerably. In embedded system these features are often lost or severely limited due to lack of access to the interactive Prolog toplevel or inability of the rest of the application to stay synchronised with the dynamic nature of the Prolog part of the application.
If you have to embed there are several options for doing so, each with specific advantages and disadvantages.
This packages consists of the following components:
cpp_interface.pl
and typedef.pl
define directives that allow you to specify the predicates that are
callable from C++ and their types. Only specified predicates can be
called and only with matching types. Restricting what can be called
greatly improves security when used in a server setting. Section
4 describes these declarations.
cpp_codegen.pl
defines the code generator. The
code generator is used to create the C++ source for a proxy class that
is used in the C++ client to talk to Prolog. Section
6 describes generating the C++ proxy.
cpp_server.pl
defines the
Prolog server. See section 8 for
details.
SWI-proxy.cpp
and SWI-Proxy.h
provide
the base classes for the client proxy.
The technique used in this package are not new nor unique. Inter-language communication has been a topic in ICT for a long period, resulting in various widespread and well established solutions. The power of this solution is that it is tailured to Prolog's features such as non-deterministic predicates, lightweight, simple and fast. The weakness is of course being bound to Prolog and, currently, C++. Proxy generators can be defined for other languages, reusing most of the infrastructure except for the details of the code generation.
The design can work with other Prolog systems. The server exploits multi-threading, but with some limitations this can be changed to run in a single thread. The proxy generator is portable with some effort and it is also possible to generate the proxy with SWI-Prolog and use it with a server written in another Prolog system. The proxy itself is pure C++, knowing nothing about Prolog.
The interface definition defines the C++ callable predicates as well
as their types and modes. The interface only deals with ground terms.
The type language syntax is defined in the library typedef.pl
and is based on the Mycroft/O'Keefe type language.
|
(vertical bar)
symbol.1The design allows for
limited polymorphism, but this is not yet part of the current
implementation. A single definition is a term whose
arguments define the types of the arguments.
There are three primitive types: integer
, float
and atom
.
Valid type declarations for our C++ interface do not use polymorphism and a fully expanded type definition consists of structures and primitive types. The argument names for compound types are derived from the type-name and usually bound to a real type using a type-alias. Here is an example:
:- type first_name = atom. :- type last_name = atom. :- type age = integer. :- type person -> person(first_name, last_name, age).
The callable predicates are specified using the library
cpp_interface.pl
, which defines two directives.
void
method on the C++
proxy class. If the predicate fails this is mapped to a C++ exception.
This is the default.int
method on the C++ proxy
class returning FALSE
if the predicate fails and TRUE
if it succeeds.The examples below depend on the type examples above.
:- cpp_callable version(-atom) = [one], find_person_younger_than(+age, -person) = [zero_or_more]. version('0.0'). find_person_younger_than(MaxAge, person(FirstName, LastName, Age)) :- person(FirstName, LastName, Age), Age =< MaxAge.
Compound data that is to be communicated to Prolog is represented as a C++ class. This class must provide methods to fetch the components for use as a predicate input argument and with a method to create fill an instance of this class for predicate output arguments. These methods are:
long
, (for Prolog integers) double
(for Prolog
floats) and the C++ std class string
for atoms.
For each named field (see section 4)
a function must be provided that extracts the field and returns the
appropriate type. For atom typed fields the return value can be an std string
or a plain C char*
.
Below is a possible implementation for the above defined person class.
class person { public: char *first_name; char *last_name; int age; person() { first_name = NULL; last_name = NULL; age = -1; }; ~person() { if ( first_name ) free(first_name); if ( last_name ) free(last_name); } char *get_first_name() const { return first_name; } char *get_last_name() const { return last_name; } long get_age() const { return age; } void initialize(string fn, string ln, long years) { if ( first_name ) free(first_name); if ( last_name ) free(last_name); first_name = strdup(fn.c_str()); last_name = strdup(ln.c_str()); age = years; } };
The C++ proxy class is automatically generated from the Prolog
declarations using the library cpp_codegen.pl
. To generate
the code load this library in a Prolog process that has all the
cpp_callable/1
and type declarations in place and invoke the predicate
cpp_server_code/2:
MyProxy
.
Primitive data are the Prolog types integer, float and atom.
Compound data is represented as a compound term in Prolog and, unless renamed using cpp_type/2, an equally named class in C++.
The proxy for a non-deterministic predicates is a subclass of
PlQuery
. The name of the class is the name of the
predicate, unless modified using the as(Name)
attribute
with cpp_callable/1.
A query is started by creating an instance of this class using a pointer
to the proxy as argument. The only method defined on this class is
::next_solution(). This method uses the same arguments as the proxy
methods that represent deterministic queries. The following example
fetches all functors with arity 3 defined in Prolog:
:- use_module(library(typedef)). :- use_module(library(cpp_interface)). :- cpp_callable current_functor(-atom, +integer) = [zero_or_more].
#include <iostream> #include "myproxy.h> int main(int argc, char **argv) { MyProxy proxy("localhost", 4224); try { between q(&proxy); string name; while ( q.next_solution(name, 3) ) { cout << name << endl; } } catch ( PlException &ex ) { cerr << (char *)ex; } return 0; }
Non-deterministic queries are initiated by creating an instance of its class. The query is said to be open as long as the query object is not destroyed. New queries, both deterministic and non-deterministic can be started while another query is still open. The nested query however must be closed before more solutions can be asked from the query it is nested in.
The example below computes a table of all square roots for the numbers 1 to 100 using prolog to generate the numbers on backtracking using between/3 and access to sqrt/2. First the Prolog code, followed by the C++ code.
:- use_module(library(typedef)). :- use_module(library(cpp_interface)). :- cpp_callable between(+integer, +integer, -integer) = [zero_or_more], sqrt(+float, -float). sqrt(In, Out) :- Out is sqrt(In).
#include <iostream> #include "myproxy.h> int main(int argc, char **argv) { SqrtProxy proxy("localhost", 4224); try { between q(&proxy); long l = 1; long h = 100; long i; while ( q.next_solution(l, h, i) ) { double ifloat = (double)i; double rval; proxy.sqrt(ifloat, rval); cout << "sqrt(" << i << ") = " << rval << endl; } } catch ( PlException &ex ) { cerr << ex; } return 0; }
For running the server we need a Prolog process with the actual
predicates and their declarations loaded. We load the library
cpp_server
and invoke cpp_server/1:
cpp_accept
that accepts new connections and,
for each new connection, starts a new thread that handles the queries
for the client. Options include:
The base-classes for the runtime system are installed in the
SWI-Prolog include directory as SWI-proxy.cpp
and its
header
SWI-proxy.h
. These files are not compiled into a
library. Considering compatibility between different compilers and
compilation models (threading, etc.) it is thought to be easier to
include this code into the target project using the source-code.
The directory examples (installed as
.../pl/doc/packages/examples/cppproxy
) contains some
stand-alone examples as well as a README
explaining how to
compile and run the examples.
The current implementation is a demonstrator. Issues to be resolved in future versions of this package include
The system is designed to be portable using any modern C++ compiler. It has been tested on Linux using g++ 3.3.4 and MS-Windows using MSVC 6.
Installation on Unix system uses the commonly found configure,
make and make install sequence. SWI-Prolog should be
installed before building this package. If SWI-Prolog is not installed
as pl, the environment variable PL
must be set to
the name of the SWI-Prolog executable. Installation is now accomplished
using:
% ./configure % make % make install
This installs the foreign library serialize
in
$PLBASE/lib/$PLARCH
and the Prolog library files in
$PLBASE/library
and the files SWI-proxy.cpp
and
SWI-proxy.h
in $PLBASE/include
, where
$PLBASE
refers to the SWI-Prolog `home-directory'.
If you have successfully installed the system from source you can install this package using
% nmake /f Makefile.mak % nmake /f Makefile.mak install
If not, compile serialize.c using the command below and install the files by hand or using the makefile after setting the variable PLBASE to the base of the installed Prolog system.
% plld -o serialize serialize.c