- Documentation
- Reference manual
- Packages
- A C++ interface to SWI-Prolog
- A C++ interface to SWI-Prolog (Version 2)
- A C++ interface to SWI-Prolog
2.5.7 Blobs
Nomenclature warning:
There are two different release()
functions:
- The release() callback for a blob (see the definition of
PL_blob_t
). - std::unique_ptr::release(), which passes ownership of a
unique_ptr
.
Disclaimer:
The blob API for C++ is not completely general, but is designed to make common use cases easy to write. For other use cases, the underlying C API can still be used. The use case is:
- 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 will not be subclassed.
- The blob contains the foreign object or a pointer to it (e.g., a database connection or 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. This “create” predicate uses
std::unique_ptr
to manage the blob (that is, the blob is created using the new operator and is not created on the stack). - Optionally, there can be a predicate that deletes the foreign object, such as a file or database connection close.
- The blob can be garbage collected, althought this might require calling the predicate that deletes the foreign object first. There is no provision for handling “weak references” (e.g., a separate lookup table or cache for the foreign objects).
- The blob must have a default constructor that sets all the fields to appropriate initial values.11This is used by the load() callback; the default implementation for a C++ blob is to throw an error.
- The blob's constructor throws an exception and cleans up any resources if it cannot create the blob.12This 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
new
operator and passes ownership to the blob. More complex behavior is possible, using PlAtom::register_ref() and PlAtom::unregister_ref(). - The blob's lifetime is controlled by Prolog and its destructor is invoked 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_t
structure that defines the callbacks. The PL_BLOB_DEFINITION() macro is typically used to create this, with the callbacks pointing to methods in the C++ blob. - A structure that contains the blob data. This must have a
constructor that references the
PL_blob_t
structure, and optionally a virtual destructor. ThePL_BLOB_SIZE
macro is used to define some required methods. - A “create” or “open” predicate that unifies
one of its arguments with a newly created blob that contains the foreign
object. The blob is created using the new operator (not
on the stack) and managed with
std::unique_ptr
. - (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 the
PL_BLOB_DEFINITION(blob_class,blob_name) macro, which references
a set of template functions that allow easily setting up the callbacks.
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, using the PL_BLOB_DEFINITION() macro.
For the data, which is subclassed from PlBlob
, the
programmer defines the various fields, a constructor that initializes
them, and a destructor. Optionally, override methods can be defined for
one of more of the methods PlBlob::compare_fields(), PlBlob::write_fields(),
PlBlob::save(), PlBlob::load(), PlBlob::pre_delete().
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() callback 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() callback that assumes that the blob was created
using PL_BLOB_NOCOPY
and manages memory using a
std::unique_ptr
.13This release()
function has nothing to do with std::unique_ptr::release().
More details on this are in
section 2.5.7.1.
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 will call the C++ destructor when the blob is garbage
collected. (This call is done indirectly, using a callback that is
registeered with Prolog.)
The C++ API for blobs only supports blobs with
PL_BLOB_NOCOPY
.14The
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.7.1 A review of C++ features used by the API
Some slightly obscure features of C++ are used with PlBlob
and
ContextType
, and can easily cause subtle bugs or memory
leaks if not used carefully.
When a C++ object is created, its memory is allocated (either on the stack or on the heap using new), and the constructors are called in this order:
- the base class's constructor (possibly specified in the intialization list)
- the constructors for all the fields (possibly specified by an initial value and/or being in the initialization list)
- the object's constructor.
There are special forms of the constructor for copying, moving, and
assigning. The “copy constructor” has a signature Type(const
Type&
and is used when an object is created by copying, for
example by assignment or passing the object on the stack in a function
call. The “move constructor” has the signature Type(Type&&
and is equivalent to the copy constructor for the new object followed by
the destructor for the old object. (Assignment is usually allowed to
default but can also be specified).
Currently, the copy and move constructors are not used, so it is best to explicitly mark them as not existing:
Type(const Type&) = delete; Type(Type&&) = delete; Type& operator =(const Type&) = delete; Type& operator =(Type&&) = delete;
A constructor may throw an exception - good programming style is to not leave a “half constructed” object but to throw an exception. Destructors are not allowed to throw exceptions,15because the destructor might be invoked by another exception, and C++ has no mechanism for dealing with a second exception. which complicates the API somewhat.
More details about constructors and destructors can be found in the FAQs for constructors and destructors.
Many classes or types have a constructor that simply assigns a
default value (e.g., 0 for int
) and the destructor does
nothing. In particular, the destructor for a pointer does nothing, which
can lead to memory leaks. To avoid memory leaks, the smart pointer
std::unique_ptr
16The
name “unique” is to distinguish this from a “shared” pointer.
A shared pointer can share ownership with multiple pointers and the
pointed-to object is deleted only when all pointers to the object have
been deleted. A unique pointer allows only a single pointer, so the
pointed-to object is deleted when the unique pointer is deleted.
can be used, whose destructor deletes its managed object. Note that std::unique_ptr
does not enforce single ownership; it merely makes single ownership easy
to manage and it detects most common mistakes, for example by not having
copy constructor or assignment operator.
For example, in the following, the implicit destructor for p
does nothing, so there will be a memory leak when a Ex1
object is deleted:
class Ex1 { public: Ex1() : p(new int) { } int *p; };
To avoid a memory leak, the code could be changed to this:
class Ex1 { public: Ex1() p(new int) { } ~Ex1() { delete p; } int *p; };
but it is easier to do the following, where the destructor for
std::unique_ptr
will free the memory:
class Ex1 { public: Ex1() p(new int) { } std::unique_ptr<int> p; };
The same concept applies to objects that are created in code - if a
C++ object is created using new, the programmer must
manage when its destructor is called. In the following, if the call to
data->validate()
fails, there will be a memory
leak:
MyData *foo(int some_value) { MyData *data = new MyData(...); data->some_field = some_value; if (! data->validate() ) throw std::runtime_error("Failed to validate data"); return data; }
Ths could fixed by adding delete data
before throwing
the runtime_error
; but this doesn't handle the situation of data->validate()
throwing an exception (which would require a catch/throw). Instead, it's
easiser to use std::unique_ptr
, which takes care of every
return or exception path:
MyData *foo(int some_value) { std::unique_ptr<MyData> data(new MyData(...)); data->some_field = some_value; if (! data->validate() ) throw std::runtime_error("Failed to validate data"); return data.release(); // don't delete the new MyData }
The destructor for std::unique_ptr
will delete the data
when it goes out of scope (in this case, by return or throw) unless the
std::unique_ptr::release() method is called.17The
call to unique_ptr<MYData>::release
doesn't call the destructor; it can be called using std::unique_ptr::get_deleter().
In the code above, the throw
will cause the
unique_ptr
’s destructor to be called, which will free
the data; but the data will not be freed in the return
statement because of the unique_ptr::release(). Using this style,
a pointer to data on the heap can be managed as easily as data on the
stack. The current C++ API for blobs takes advantage of this - in
particular, there are two methods for unifying a blob:
- PlTerm::unify_blob(const PlBlob* blob) - does no memory management
- PlTerm::unify_blob(std::unique_std<PlBlob>* blob) - if unification fails or raises an error, the memory is automatically freed; otherwise the memory's ownership is transferred to Prolog, which may garbage collect the blob by calling the blob's destructor. Note that this uses a pointer to the pointer, so that PlTerm::unify_blob() can modify it.
unique_ptr
allows specifying the delete function. For
example, the following can be used to manage memory created with PL_malloc():
std::unique_ptr<void, decltype(&PL_free)> ptr(PL_malloc(...), &PL_free);
or, when memory is allocated within a PL_*() function (in this case, using the Plx_*() wrapper for PL_get_nchars()):
size_t len; char *str = nullptr; Plx_get_nchars(t, &len, &str.get(), BUF_MALLOC|CVT_ALL|CVT_WRITEQ|CVT_VARIABLE|REP_UTF8|CVT_EXCEPTION); std::unique_ptr<char, decltype(&PL_free)> _str(str, &PL_free);
The current C++ API assumes that the C++ blob is allocated on the
heap. If the programmer wishes to use the stack, they can use std::unique_ptr
to automatically delete the object if an error is thrown -
PlTerm::unify_blob(std::unique_ptr<PlBlob>*)
prevents the automatic deletion if unification succeeds.
A unique_ptr
needs a bit of care when it is passed as an
argument. The unique_ptr::get() method can be used to get the “raw” pointer;
the delete must not be used with this pointer. Or, the unique_ptr::release()
method can be used to transfer ownership without calling the object's
destructor.
Using unique_ptr::release() is a bit incovenient, so instead
the
unique_ptr
can be passed as a pointer (or a reference).
This does not create a new scope, so the pointer must be assigned to a
local variable. For example, the code for unify_blob() is
something like:
bool PlTerm::unify_blob(std::unique_ptr<PlBlob>* b) const { std::unique_ptr<PlBlob> blob(std::move(*b)); if ( !unify_blob(blob.get()) ) return false; (void)blob.release(); return true; }
The line declaration for blob
uses the “move
constructor” to set the value of a newly scoped variable (std::move(*b)
is a cast, so unique_ptr
’s move constructor is used).
This has the same effect as calling b->reset()
,
so from this point on,
b
has the value nullptr
.
Alternatively, the local unique_ptr
could be set by
std::unique_ptr<PlBlob> blob(b->release());
or
std::unique_ptr<PlBlob> blob; blob.swap(*b);
If the call to PlTerm::unify_blob()
fails or throws an exception, the virtual destructor for blob
is called. Otherwise, the call to blob.release()
prevents the destructor from being called - Prolog now owns the blob
object and can call its destructor when the garbage collector reclaims
it.
2.5.7.2 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 a
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.7.3 The life of a PlBlob
In this section, the blob is of type MyBlob
, a subclass
of PlBlob
. (Example code is given in section
2.5.7.5)
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>(...))}
or
auto ref = std::make_unique<MyBlob>(...);
- After the fields of the blob are filled in:
return PlTerm::unify_blob(&ref);
If unification fails or throws an exception, the object is automatically freed and its destructor is called.
If make_unique() was used to create the pointer, you need to call PlTerm::unify_blob() as follows, because C++'s type inferencing can't figure out that this is a covariant type:
std::unique_ptr<PlBlob> refb(ref.release()); // refb now "owns" the ptr - from here on, ref == nullptr return A2.unify_blob(&refb);
If unification succeeds, Prolog calls:
- PlBlobV<MyBlob>acquire(), which calls
- MyBlob::acquire(), which sets the field MyBlob::symbol_,
which is usually accessed using the method MyBlob::symbol_term().
If this all succeeds, PlTerm::unify_blob(ref)
calls
ref->release()
to pass ownership of the blob to Prolog (when the blob is eventually garbage collected, the blob's destructor will be called).
At this point, the blob is owned by Prolog and may be freed by its
atom garbage collector, which will call the blob's destructor (if the
blob shouldn't be deleted, it can override the the PlBlob::pre_delete()
method to return false
).
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 that 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.7.4 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
the exception 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:
- void PlBlob::acquire()
- , which is called from PlBlobV<MyBlob>::acquire(), can throw a C++ exception. The programmer cannot override this.
- int PlBlob::compare_fields(const PlBlob *_b)
- , 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.
- bool PlBlob::write_fields(IOStream *s, int flags)
- , 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.
- void PlBlob::PlBlob::save(IOStream *fd)
- can throw a C++ exception, including PlFail().
- PlAtom PlBlob::PlBlob::load(IOSTREAM *fd)
- can throw a C++ exception, which is converted to a return value of
PlAtom::null
, which is interpreted by Prolog as failure. - bool PlBlob::PlBlob::pre_delete()
- , which is called from PlBLobV<MyBLOB>::release(),
can return
false
(or throw aPlException
orPlExceptinFailBase
, which will be interpreted as a return value offalse
), resulting in the blob not being garbage collected, and the destructor not being called. Note that this doesn't work well with final clean-up atom garbage collection, which disregards the return value and also doesn't respect the ordering of blob dependencies (e.g., if an iterator blob refers to a file-like blob, the file-like blob might be deleted before the iterator is deleted).This code runs in the
gc
thread. The only PL_*() function that can safely be called are PL_unregister_atom() (which is what PlAtom::unregister_ref() calls).
2.5.7.5 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 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() inside unify_blob() 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); }
2.5.7.6 Discussion of the sample PlBlob code
- PL_BLOB_DEFINITION(MyBlob, "my_blob") creates a
PL_blob_t
structure with the wrapper functions and flags set toPL_BLOB_NOCOPY
. It should be declared outside thePlBlob
class and should not be markedconst
- otherwise, a runtime error can occur.18The 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
MyBlob
struct is a subclass ofPlBlob
. See below for a discussion of the default behaviors.MyBlob
contains a pointer to aMyConnection
object and keeps a copy of the connection's name. TheMyConnection
object is handled by astd::unique_ptr
smart pointer, so that it is automatically freed when theMyBlob
object is freed.- A default constructor is defined - this is needed for the
load() and save() methods; it invokes the
PlBlob
constructor. - The
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 setsblob_t_
to a pointer to themy_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>::acquire() and fills in thesymbol_
field.MyBlob
must not override this - it is not a virtual method. Thesymbol_
field can be accessed by PlBlob::symbol_term().- 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 - this can be
checked with PlTerm::is_variable().
- The MyBlob(connection_name) constructor creates a
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 forMyConnection
also did an open and threw an exception on failure.) - The
PL_BLOB_SIZE
is 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.19It
isn't enough to just catch exceptions; for example, if the code throws
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, 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_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.
- 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 qsave_program/[1,2]).20The
C API defaults would save the internal form of the blob, which is
probably not what you want, so the C++ API throws an error as its
default.
- 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 theMyBlob
destructor will be called).PlTerm::unify_blob() is called with a pointer to a
std::unique_ptr
, which takes ownership of the object by calling std::unique_ptr<PlBlob>::release() and passes the pointer to Prolog, which then owns it. This also sets ref tonullptr
, so any attempt to use ref after a call to PlTerm::unify_blob() will be an error.If you wish to create a
MyBlob
object instead of aPlBlob
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;
- close_my_blob/1
predicate:
- The argument is turned into a
MyBlob
pointer using the PlBlobV<MyBlob>::cast_ex() function, which will throw atype_error
if 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.7.7 Identifying blobs by atoms
Passing a Prolog blob around can be inconvenient; it is easier if a
blob can be identified 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.16.4.