Disclaimer:
The blob API for C++ is not completely general, but is designed to make a specific use case easier to write. For other use cases, the underlying C API can still be used. The use case is:
PlBlob
, which
provides a number of fields and methods, of which a few can be
overridden in the blob (notably: write_fields(), compare_fields(),
save(), load(), and the destructor).new
operator and
passes ownership to the blob.A Prolog blob consists of five parts:
PL_blob_t
structure that defines the callbacks.
For the PL_blob_t
structure, the C++ API provides a set
of template functions that allow easily setting up the callbacks, and
defining the corresponding methods in the blob "contents" class. The C
interface allows more flexibility by allowing some of the callbacks to
default; however, the C++ API for blobs provides suitable callbacks for
all of them, so usually the programmer will specify all the template
callbacks using the
PL_BLOB_DEFINITION(blob_class,blob_name) macro.
For the data, which is subclassed from PlBlob
, the
programmer defines the various fields, a constructor that initializes
them, and a destructor. Optionally, methods can be defined for one of
more of blob compare_fields(), write_fields(), save(), load(). More
details on these are given later.
There is a mismatch between how Prolog does memory management (and
garbage collection) and how C++ does it. In particular, Prolog assumes
that cleanup will be done in the release() function associated with the
blob whereas C++ typically does cleanup in a destructor. The blob
interface gets around this mismatch by providing a default release()
function that assumes that the blob was created using PL_BLOB_NOCOPY
and manages memory using a
std::unique_ptr
.
The C blob interface has a flag that determines how memory is
managed:
PL_BLOB_NOCOPY
. The PL_BLOB_DEFINITION() macro sets
this, so Prolog does not do a call to free() when the blob is garbage
collected; instead, it lets the blob's release() free the memory, which
is done by calling the C++ destructor.
The C++ API for blobs only supports blobs with
PL_BLOB_NOCOPY
.12The
API can probably also support blobs with PL_BLOB_UNIQUE
,
but there seems to be little point in setting this flag for non-text
blobs.
TL;DR: Use PL_BLOB_DEFINITION() to define the blob with the
flag
PL_BLOB_NOCOPY
and the default PlBlob
wrappers; define your struct as a subclass of PlBlob
with
no copy constructor, move constructor, or assignment operator; create
blob using
std::unique_ptr<PlBlob>(new ...)
, call
PlTerm::unify_blob(). Optionally, define one or more of:
compare_fields(), write_fields(), save(), load() methods (these are
described after the sample code).
In this section, the blob is of type MyBlob
, a subclass
of PlBlob
.
A blob is typically created by calling a predicate that does the following:
auto ref = std::unique_ptr<PlBlob>(new
MyBlob>(...))
(std::make_unique() can't be used because it
returns type
std::unique_ptr<MyBlob>
but
PlTerm::unify_blob() requires a
std::unique_ptr<PlBlob>
and C++'s type
inferencing can't figure out that this is a covariant type).
ref.release()
to pass ownership to the Prolog blob. If you
wish to use std::make_unique<MyBlob>(), you could
instead do:
auto ref = std::make_unique<MyBlob>(...); ... // code that accesses fields in *ref std::unique_ptr<PlBlob> refb(ref.release()); // transfer ownership of ptr // from here on, can't access fields in *ref return A2.unify_blob(refb);
At this point, the blob is owned by Prolog and will be freed by its atom garbage collector, which will call the blob's destructor.
Whenever a predicate is called with the blob as an argument (e.g., as A1),
the blob can be accessed by
PlBlobv<MyBlob>::cast_check(A1.as_atom())
.
Within a method, the Prolog blob can be accessed as a term (e.g., for
constructing an error term) using the method MyBlob::symbol_term(). This
field is initialized by the call to PlTerm::unify_blob(); if
MyBlob::symbol_term() is called before a successful call to
PlTerm::unify_blob(), MyBlob::symbol_term() returns a
PlTerm_var
.
When the atom garbage collector runs, it frees the blob by first
calling the release() callback, which does delete
, which
calls the destructor MyBlob:: MyBlob(). Note that C++ destructors
are not supposed to raise exception; they also should not cause a Prolog
error, which could cause deadlock unless the real work is done in
another thread.
Often it is desired to release the resources before the garbage collector runs. To do this, the programmer can provide a "close" predicate which is the inverse of the "open" predicate that created the blob. This typically has the same logic as the destructor, except that it can raise a Prolog error.
When a blob is used in the context of a PREDICATE() macro, it can
raise a C++ exception (PlFail
or PlException
)
and the PREDICATE() code will convert it to the appropriate Prolog
failure or error; memory allocation exceptions are also handled.
Blobs have callbacks, which can run outside the context of a PREDICATE(). Their exception handling is as follows:
PlAtom::null
, which is interpreted by Prolog as
failure.
Here is minimal sample code for creating a blob that owns a
connection to a database. It has a single field (connection
)
and defines compare_fields() and write_fields().
struct MyConnection { std::string name; explicit MyConnection(); explicit MyConnection(const std::string& _name); ~MyConnection() { } bool open(); bool close() noexcept; void portray(PlStream& strm) const; }; struct MyBlob; static PL_blob_t my_blob = PL_BLOB_DEFINITION(MyBlob, "my_blob"); struct MyBlob : public PlBlob { std::unique_ptr<MyConnection> connection; explicit MyBlob() : PlBlob(&my_blob) { } explicit MyBlob(const std::string& connection_name) : PlBlob(&my_blob), connection(std::make_unique<MyConnection>(connection_name)) { if ( !connection->open() ) throw MyBlobError("my_blob_open_error"); } PL_BLOB_SIZE ~MyBlob() noexcept { if ( !close() ) Sdprintf("***ERROR: Close MyBlob failed: %s\n", name().c_str()); // Can't use PL_warning() } inline std::string name() const { return connection ? connection->name : ""; } bool close() noexcept { if ( !connection ) return true; bool rc = connection->close(); connection.reset(); // Can be omitted, leaving deletion to ~MyBlob() return rc; } PlException MyBlobError(const char* error) const { return PlGeneralError(PlCompound(error, PlTermv(symbol_term()))); } int compare_fields(const PlBlob* _b_data) const override { auto b_data = static_cast<const MyBlob*>(_b_data); // See note about cast return name().compare(b_data->name()); } bool write_fields(IOSTREAM *s, int flags) const override { PlStream strm(s); strm.printf(","); return write_fields_only(strm); } bool write_fields_only(PlStream& strm) const { if ( connection ) connection->portray(strm); else strm.printf("closed"); return true; } bool portray(PlStream& strm) const { strm.printf("MyBlob("); write_fields_only(strm); strm.printf(")"); return true; } }; // %! create_my_blob(+Name: atom, -MyBlob) is semidet. PREDICATE(create_my_blob, 2) { // Allocating the blob uses std::unique_ptr<MyBlob> so that it'll be // deleted if an error happens - the auto-deletion is disabled by // ref.release() before returning success. auto ref = std::unique_ptr<PlBlob>(new MyBlob(A1.as_atom().as_string())); return A2.unify_blob(&ref); } // %! close_my_blob(+MyBlob) is det. // % Close the connection, silently succeeding if is already // % closed; throw an exception if something goes wrong. PREDICATE(close_my_blob, 1) { auto ref = PlBlobV<MyBlob>::cast_ex(A1, my_blob); if ( !ref->close() ) throw ref->MyBlobError("my_blob_close_error"); return true; } // %! portray_my_blob(+Stream, +MyBlob) is det. // % Hook predicate for // % user:portray(MyBlob) :- // % blob(MyBlob, my_blob), !, // % portray_my_blob(current_output, MyBlob). PREDICATE(portray_my_blob, 2) { auto ref = PlBlobV<MyBlob>::cast_ex(A2, my_blob); PlStream strm(A1, 0); return ref->portray(strm); }
PL_blob_t
structure with the wrapper functions and flags
set to PL_BLOB_NOCOPY
. It should be declared outside the PlBlob
class and should not be marked const
- otherwise, a runtime
error can occur.13The cause of the
runtime error is not clear, but possibly has to do with the order of
initializing globals, which is unspecified for C++.
MyBlob
struct is a subclass of PlBlob
.
See below for a discussion of the default behaviors.
MyBlob
contains a pointer to a MyConnection
object and keeps a copy of the connection's name. The MyConnection
object is handled by a std::unique_ptr
smart pointer, so
that it is automatically freed when the MyBlob
object is
freed.
PlBlob
constructor.
MyBlob
class must not provide a copy or move
constructor, nor an assignment operator (PlBlob has these as
delete
, so if you try to use one of these, you will get a
compile-time error).
PlBlob
’s constructor sets blob_t_
to
a pointer to the my_blob
definition. This is used for
run-time consistency checking by the various callback functions and for
constructing error terms (see PlBlob::symbol_term()).
PlBlob
’s acquire() is called by PlBlobV<MyBlob>::awcuire()
and fills in the symbol
field. MyBlob
must not
override this - it is not a virtual method.
MyConnection
object. If this fails, an exception is thrown.
The constructor then calls MyConnection::open() and throws an exception
if that fails. (The code would be similar if instead the constructor for MyConnection
also did an open and threw an exception on failure.)
PL_BLOB_SIZE
is boilerplate that defines a
blob_size_() method that is used when the blob is created.
throw
PlUnknownError("...")
, that will try to create a Prolog term,
which will crash because the environment for creating terms is not
available. Because there is no mechanism for reporting an
error, the destructor prints a message on failure (calling
PL_warning() would cause a crash).
PlBlob::close() calls MyConnection::close() and then frees the
object. Error handling is left to the caller because of the possibility
that this is called in the context of garbage collection. It is not
necessary to free the MyConnection
object here - if it is
not freed, the
std::unique_ptr<MyConnection>
’s
destructor would free it.
0
("equal").
The _b_data argument is of type const PlBlob*
- this is cast to const MyBlob*
using a
static_cast
. This is safe because Prolog guarantees that
PlBlobV<PlBlob>::compare() will only be called if both
blobs are of the same type.
The flags argument is the same as given to PlBlobV<PlBlob>::write(),
which is a bitwise or of zero or more of the PL_WRT_*
flags that were passed in to the caling PL_write_term() (defined
in SWI-Prolog.h
). The
flags do not have the PL_WRT_NEWLINE
bit set, so
it is safe to call PlTerm::write() and there is no need for writing a
trailing newline.
If anything in PlBlob::write_fields() throws a C++ exception, it will be caught by the calling PlBlobV<PlBlob>::write() and handled appropriately.
std::unique_ptr<PlBlob>()
creates a
MyBlob that is deleted when it goes out of scope. If an exception occurs
between the creation of the blob or if the call to unify_blob() fails,
the pointer will be automatically freed (and the
MyBlob
destructor will be called).
If PlTerm::unify_blob() is called with a pointer to a
std::unique_ptr
, it takes ownership of the object by
calling std::unique_ptr<PlBlob>::release(). This sets ref
to nullptr
, so any attempt to use ref after a
successful call to PlTerm::unify_blob() will be an error.
If you wish to create a MyBlob
object instead of a
PlBlob
object, a slightly different form is used:
auto ref = std::make_unique<MyBlob>(...); ... std::unique_ptr<PlBlob> refb(ref.release()); PlCheckFail(A2.unify_blob(&refb)); return true;
MyBlob
pointer using the
PlBlobV<MyBlob>::cast_ex() function, which will throw a
type_error
if the argument isn't a blob of the expected
type.
Passing a blob around can be inconvenient; there is an easy way to
identify a blob by an atom. An example of this is with streams, which
are identified by atoms such as user_input
.
A utility class AtomMap
is provided for this situation.
See section 2.20.4.