Did you know ... | Search Documentation: |
Argument Passing and Control |
If Prolog encounters a foreign predicate at run time it will call a
function specified in the predicate definition of the foreign predicate.
The arguments 1, ... , <arity> pass the
Prolog arguments to the goal as Prolog terms. Foreign functions should
be declared of type
foreign_t
.
All the arguments to a foreign predicate must be of type
term_t
. The only operation that is allowed with an argument
to a foreign predicate is unification; for anything that might
over-write the term, you must use a copy created by
PL_copy_term_ref().
For an example, see PL_unify_list().
Deterministic foreign functions return with either TRUE
(success) or FALSE
(failure).215SWI-Prolog.h
defines the macros PL_succeed
and PL_fail
to
return with success or failure. These macros should be considered
deprecated. The foreign function may raise an exception
using
PL_raise_exception()
or one of the shorthands for commonly used exceptions such as PL_type_error().
Note that the C language does not provide exception handling and
therefore the functions that raise an exception return (with the value FALSE
).
Functions that raise an exception must return FALSE
.
By default foreign predicates are deterministic. Using the
PL_FA_NONDETERMINISTIC
attribute (see PL_register_foreign())
it is possible to register a predicate as a non-deterministic predicate.
Writing non-deterministic foreign predicates is slightly more
complicated as the foreign function needs context information for
generating the next solution. Note that the same foreign function should
be prepared to be simultaneously active in more than one goal. Suppose
the natural_number_below_n/2 is a non-deterministic foreign predicate,
backtracking over all natural numbers lower than the first argument. Now
consider the following predicate:
quotient_below_n(Q, N) :- natural_number_below_n(N, N1), natural_number_below_n(N, N2), Q =:= N1 / N2, !.
In this predicate the function natural_number_below_n/2 simultaneously generates solutions for both its invocations.
Non-deterministic foreign functions should be prepared to handle three different calls from Prolog:
PL_FIRST_CALL
)PL_REDO
)PL_PRUNED
)(term_t)0
.
Both the context information and the type of call is provided by an
argument of type control_t
appended to the argument list
for deterministic foreign functions. The macro PL_foreign_control()
extracts the type of call from the control argument. The foreign
function can pass a context handle using the PL_retry*()
macros and extract the handle from the extra argument using the
PL_foreign_context*()
macro.
return
_PL_retry(n)
.
See also PL_succeed().return _PL_retry_address(n)
.
See also
PL_succeed().PL_PRUNED
case and should be aware that the other
arguments are not valid in this case.PL_FIRST_CALL
the context value is 0L. Otherwise it is the
value returned by the last PL_retry()
associated with this goal (both if the call type is PL_REDO
or PL_PRUNED
).Fetch the Prolog predicate that is executing this function. Note that if the predicate is imported, the returned predicate refers to the final definition rather than the imported predicate; i.e., the module reported by PL_predicate_info() is the module in which the predicate is defined rather than the module where it was called. See also PL_predicate_info().
Note: If a non-deterministic foreign function returns using PL_succeed()
or PL_fail(), Prolog assumes the foreign function has cleaned its
environment. No call with control argument PL_PRUNED
will follow.
The code of figure 5 shows a skeleton for a non-deterministic foreign predicate definition.
typedef struct /* define a context structure */ { ... } context; foreign_t my_function(term_t a0, term_t a1, control_t handle) { struct context * ctxt; switch( PL_foreign_control(handle) ) { case PL_FIRST_CALL: if ( !(ctxt = malloc(sizeof *ctxt)) ) return PL_resource_error("memory"); <initialize ctxt> break; case PL_REDO: ctxt = PL_foreign_context_address(handle); break; case PL_PRUNED: ctxt = PL_foreign_context_address(handle); ... free(ctxt); return TRUE; } <find first/next solution from ctxt> ... // We fail */ if ( <no_solution> ) { free(ctx); return FALSE; } // We succeed without a choice point */ if ( <last_solution> ) { free(ctx); return TRUE; } // We succeed with a choice point */ PL_retry_address(ctxt); }
Starting with SWI-Prolog 8.5.5 we provide an experimental interface that allows using a SWI-Prolog engine for asynchronous processing. The idea is that an engine that calls a foreign predicate which would need to block may be suspended and later resumed. For example, consider an application that listens to a large number of network connections (sockets). SWI-Prolog offers three scenarios to deal with this:
As is, these features can only used through the foreign language interface. It was added after discussion with with Mattijs van Otterdijk aiming for using SWI-Prolog together with Rust's asynchronous programming support. Note that this feature is related to the engine API as described in section 11. It uis different though. Where the Prolog engine API allows for communicating with a Prolog engine, the facilities of this section merely allow an engine to suspend, to be resumed later.
To prepare a query for asynchronous usage we first create an engine
using PL_create_engine().
Next, we create a query in the engine using
PL_open_query()
with the flags PL_Q_ALLOW_YIELD
and
PL_Q_EXT_STATUS
. A foreign predicate that needs to be
capable of suspending must be registered using PL_register_foreign()
and the flags
PL_FA_VARARGS
and PL_FA_NONDETERMINISTIC
;
i.e., only non-det predicates can yield. This is no restriction as
non-det predicate can always return TRUE
to indicate
deterministic success. Finally, PL_yield_address()
allows the predicate to yield control, preparing to resume similar to PL_retry_address()
does for non-deterministic results. PL_next_solution()
returns PL_S_YIELD
if a predicate calls PL_yield_address()
and may be resumed by calling
PL_next_solution()
using the same query id (qid). We illustrate the above using
some example fragments.
First, let us create a predicate that can read the available input
from a Prolog stream and yield if it would block. Note that our
predicate
must the PL_FA_VARARGS
interface, which implies
the first argument is in a0, the second in a0+1
,
etc.216the other foreign
interfaces do not support the yield API.
/** read_or_block(+Stream, -String) is det. */ #define BUFSIZE 4096 static foreign_t read_or_block(term_t a0, int arity, void *context) { IOSTREAM *s; switch(PL_foreign_control(context)) { case PL_FIRST_CALL: if ( PL_get_stream(a0, &s, SIO_INPUT) ) { Sset_timeout(s, 0); break; } return FALSE; case PL_RESUME: s = PL_foreign_context_address(context); break; case PL_PRUNED: return PL_release_stream(s); default: assert(0); return FALSE; } char buf[BUFSIZE]; size_t n = Sfread(buf, sizeof buf[0], sizeof buf / sizeof buf[0], s); if ( n == 0 ) // timeout or error { if ( (s->flags&SIO_TIMEOUT) ) PL_yield_address(s); // timeout: yield else return PL_release_stream(s); // raise error } else { return ( PL_release_stream(s) && PL_unify_chars(a0+1, PL_STRING|REP_ISO_LATIN_1, n, buf) ); } }
This function must be registered using PL_register_foreign():
PL_register_foreign("read_or_block", 2, read_or_block, PL_FA_VARARGS|PL_FA_NONDETERMINISTIC);
Next, create an engine to run handle_connection/1 on a Prolog stream. Note that we omitted most of the error checking for readability. Also note that we must make our engine current using PL_set_engine() before we can interact with it.
qid_t start_connection(IOSTREAM *c) { predicate_t p = PL_predicate("handle_connection", 1, "user"); PL_engine_t e = PL_create_engine(NULL); PL_engine_t old; if ( PL_set_engine(e, &old) ) { term_t av = PL_new_term_refs(1); PL_unify_stream(av+0, c); qid_t q = PL_open_query(e, NULL, PL_Q_CATCH_EXCEPTION| PL_Q_ALLOW_YIELD| PL_Q_EXT_STATUS, p, av); PL_set_engine(old, NULL); return q; } /* else error */ }
Finally, our foreign code must manage this engine. Normally it will do so together with many other engines. First, we write a function that runs a query in the engine to which it belongs.217Possibly, future versions of PL_next_solution() may do that although the value is in general limited because interacting with the arguments of the query requires the query's engine to be current anyway.
int PL_engine_next_solution(qid_t qid) { PL_engine_t old; int rc; if ( PL_set_engine(PL_query_engine(qid), &old) == PL_ENGINE_SET ) { rc = PL_next_solution(qid); PL_set_engine(old, NULL); } else rc = FALSE; return rc; }
Now we can simply handle a connection using the loop below which restarts the query as long as it yields. Realistic code manages multiple queries and will (in this case) use the POSIX poll() or select() interfaces to activate the next query that can continue without blocking.
int rc; do { rc = PL_engine_next_solution(qid); } while( rc == PL_S_YIELD );
After the query completes it must be closed using PL_close_query() or PL_cut_query(). The engine may be destroyed using PL_engine_destroy() or reused for a new query.
PL_S_YIELD
. A subsequent call to PL_next_solution()
on the same query calls the foreign predicate again with the control
status set to
PL_RESUME
, after which PL_foreign_context_address()
retrieves the address passed to this function. The state of the Prolog
engine is maintained, including term_t
handles. If the
passed address needs to be invalidated the predicate must do so when
returning either
TRUE
or FALSE
. If the engine terminates the
predicate the predicate is called with status PL_PRUNED
, in
which case the predicate must cleanup.TRUE
when called from inside a foreign predicate if
the query that (indirectly) calls this foreign predicate can yield using PL_yield_address().
Returns FALSE
when either there is no current query or the
query cannot yield.Discussion
Asynchronous processing has become popular with modern programming languages, especially those aiming at network communication. Asynchronous processing uses fewer resources than threads while avoiding most of the complications associated with thread synchronization if only a single thread is used to manage the various states. The lack of good support for destructive state updates in Prolog makes it attractive to use threads for dealing with multiple inputs. The fact that Prolog discourages using shared global data such as dynamic predicates typically makes multithreaded code easy to manage.
It is not clear how much scalability we gain using Prolog engines instead of Prolog threads. The only difference between the two is the operating system task. Prolog engines are still rather memory intensive, mainly depending on the stack sizes. Global garbage collection (atoms and clauses) need to process all the stacks of all the engines and thus limit scalability.
One possible future direction is to allow all (possibly) blocking Prolog predicates to use the yield facility and provide a Prolog API to manage sets of engines that use this type of yielding. As is, these features are designed to allow SWI-Prolog for cooperating with languages that provide asynchronous functions.