- Documentation
- Reference manual
- Packages
- A C++ interface to SWI-Prolog
2.5.6 Blobs
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:
- The blob contains the foreign object (e.g., contains a pointer to a database connection), plus optionally some other data.
- The blob is created by a predicate that makes the foreign object and stores it (or a pointer to it) within the blob - for example, making a connection to a database or compiling a regular expression into an internal form.
- Optionally, there is a predicate that deletes the foreign object, such as a file or database connection close().
- The blob will not be subclassed.
- The blob is defined as a subclass of
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). - The blob must have a default constructor that sets all the fields to appropriate initial values.10This is used by the load() method, which by default throws an error.
- The blob's constructor throws an exception and cleans up any resources if it cannot create the blob.11This is not a strong requirement, but the code is simpler if this style is used.
- The foreign object can be deleted when the blob is deleted. That is,
the foreign object is created using the
newoperator and passes ownership to the blob. - The blob's allocation is controlled by Prolog and its destructor is envoked when the blob is garbage collected. Optionally, the predicate that deletes the foreign object deletes the foreign object and the Prolog garbage collector only frees the blob.
A Prolog blob consists of five parts:
- A
PL_blob_tstructure that defines the callbacks. - A structure that contains the blob data.
- A "create" or "open" predicate that unifies one of its arguments with a newly created blob that contains the foreign object.
- (Optionally) a "close" predicate that does the opposite of the "create" or "open" predicate.
- Predicates that manipulate the foreign object (e.g., for a file-like object, these could be read, write, seek, etc.).
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.
2.5.6.1 How to define a blob using C++
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).
2.5.6.2 The life of a PlBlob
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:
- Creates the blob using
auto ref = std::unique_ptr<PlBlob>(new MyBlob>(...))(std::make_unique() can't be used because it returns typestd::unique_ptr<MyBlob>but PlTerm::unify_blob() requires astd::unique_ptr<PlBlob>and C++'s type inferencing can't figure out that this is a covariant type). - Calls PlTerm::unify_blob(ref), using PlCheckFail() to throw an
exception if it fails. This, in turn, calls:
- PlBlobV<MyBlob>acquire(), which calls
- MyBlob::acquire(), which sets MyBlob::symbol_ (MyBlob::symbol_
is usually accessed using the method MyBlob::symbol_term()). If this all
succeeds, PlTerm::unify_blob(ref) calls
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.
2.5.6.3 C++ exceptions and blobs
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:
- acquire(), which is called from PlBlobV<MyBlob>::acquire(), can throw a C++ exception.
- compare_fields(), which is called from PlBlobV<MyBlob>::compare(), should not throw an exception. A Prolog error won't work as it uses "raw pointers" and thus a GC or stack shift triggered by creating the exception will upset the system.
- write_fields(), which is called from PlBlobV<MyBlob>::write(), can throw an exception, just like code inside a PREDICATE(). In particular, you can wrap calls to Sfprintf() in PlCheckFail(), although the calling context will check for errors on the stream, so checking the Sfprintf() result isn't necessary.
- save() can throw a C++ exception, including PlFail().
- load() can throw a C++ exception, which is converted to a return
value of
PlAtom::null, which is interpreted by Prolog as failure.
2.5.6.4 Sample PlBlob code
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 MyBlob;
static PL_blob_t my_blob = PL_BLOB_DEFINITION(MyBlob, "my_blob");
struct MyBlob : public PlBlob
{ std::unique_ptr<MyConnection> connection;
std::string name_; // Used for error terms
explicit MyBlob()
: PlBlob(&my_blob) { }
explicit MyBlob(const std::string& connection_name)
: PlBlob(&my_blob),
connection(std::make_unique<MyConnection>(connection_name)),
name_(connection_name)
{ if ( !connection->open() )
throw MyBlobError("my_blob_open_error");
}
PL_BLOB_SIZE
~MyBlob() noexcept
{ if ( !close() )
Sdprintf("Close MyBlob failed: %s", name_.c_str()); // Can't use PL_warning()
}
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
{ if ( !Sfprintf(s, ",name=%s", name_.c_str()) )
return false;
if ( !connection )
return Sfprintf(s, ",closed");
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;
}
2.5.6.5 Discussion of the sample PlBlob code
- PL_BLOB_DEFINITION(MyBlob, "my_blob") creates a
PL_blob_tstructure with the wrapper functions and flags set toPL_BLOB_NOCOPY. It should be declared outside thePlBlobclass and should not be markedconst- 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++. - The
MyBlobstruct is a subclass ofPlBlob. See below for a discussion of the default behaviors.MyBlobcontains a pointer to aMyConnectionobject and keeps a copy of the connection's name. TheMyConnectionobject is handled by astd::unique_ptrsmart pointer, so that it is automatically freed when theMyBlobobject is freed.- A default constructor is defined - this is needed for the load() and
save() methods; it invokes the
PlBlobconstructor. - The
MyBlobclass must not provide a copy or move constructor, nor an assignment operator (PlBlob has these asdelete, so if you try to use one of these, you will get a compile-time error). PlBlob’s constructor setsblob_t_to a pointer to themy_blobdefinition. 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 thesymbolfield.MyBlobmust not override this - it is not a virtual method.- PlBlob::symbol_term() Creates a term from the blob, for use in error
terms. It is always safe to use this; if the symbol hasn't been set
(because acquire() hasn't been called), symbol_term() returns a "var"
term.
- The MyBlob(connection_name) constructor creates a
MyConnectionobject. 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 forMyConnectionalso did an open and threw an exception on failure.) - The
PL_BLOB_SIZEis boilerplate that defines a blob_size_() method that is used when the blob is created. - The destructor MyBlob() is called when the blob is released by
the garbage collector and in turn calls the MyBlob::close(), throwing
away the result. If there is an error, a message is printed because
there is no other way report the error. For this reason, it is preferred
that the program explicitly calls the
close_my_blob/1
predicate, which can raise an error. One way of doing this is by using
the at_halt/1
hook.
- The MyBlob::close() method is called by either the destructor or by
the close_my_blob/1
predicate. Because it can be called by the garbage collector, which does
not provide the usual environment and which may also be in a different
thread, the only Prolog function that can be called is
PlAtom::unregister_ref(); and the MyBlob::close() method must not throw
an exception.14It isn't enough to
just catch exceptions; for example, if the code does
throw PlUnknownErro("..."), 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
MyConnectionobject here - if it is not freed, thestd::unique_ptr<MyConnection>’s destructor would free it. - PlBlob::MyBlobError() is a convenience method for creating errror
terms.
- PlBlob::compare_fields() makes the blob comparison function more
deterministic by comparing the name fields; if the names are the same,
the comparison will be done by comparing the addresses of the blobs
(which is the default behavior for blobs defined using the C API).
PlBlob::compare_fields() is called by PlBlobV<PlBlob>::compare(),
which provides the default comparison if PlBlob::compare_fields()
returns
0("equal").The _b_data argument is of type
const PlBlob*- this is cast toconst MyBlob*using astatic_cast. This is safe because Prolog guarantees that PlBlobV<PlBlob>::compare() will only be called if both blobs are of the same type. - PlBlob::write_fields() outputs the name and the status of the
connection, in addition to the default of outputting the blob type and
its address. This is for illustrative purposes only; an alternative is
to have a my_blob_properties/2
predicate to provide the information.
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 inSWI-Prolog.h). The flags do not have thePL_WRT_NEWLINEbit 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.
- PlBlob::save() and PlBlob::load() are not defined, so the defaults
are used - they throw an error on an attempt to save the blob (e.g., by
using
indexqsave_program/[1,2]qsave_program/[1,2]).15If these weren't specified, the defaults would save the internal form of the blob, which is probably not what you want.
- create_my_blob/2
predicate:
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 theMyBlobdestructor 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 tonullptr, so any attempt to use ref after a successful call to PlTerm::unify_blob() will be an error.If you wish to create a
MyBlobobject instead of aPlBlobobject, 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;
- close_my_blob/1
predicate:
- The argument is turned into a
MyBlobpointer using the PlBlobV<MyBlob>::cast_ex() function, which will throw atype_errorif the argument isn't a blob of the expected type. - The MyBlob::close() method is called - if it fails, a Prolog error
is thrown.
- The argument is turned into a
2.5.6.6 Identifying blobs by atoms
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.