The comments are treated in a funny way in the Perl XS files. Well, inside functions they are the normal C/C++ comments. But if you want to put a block comment before a function, XS won't let you just use "//" for its lines. It will say that it's a syntax error.
If you use "#" there, XS doesn't complain. It's a little funny, having two styles of comments in the same file but it kind of works. But only kind of. As it turns out, XS sees these lines simply as the preprocessor lines, and feeds them to the preprocessor. Which in turn happily ignores them. Until one of your lines happens to start with "if" or another preprocessor keywords. Then the preprocessor complains.
The complete solution here is to use "#//". This first makes XS send the line to the preprocessor, which then finds a C-style comment and ignores it.
This started as my thoughts on the field of Complex Event Processing, mostly about my OpenSource project Triceps. But now it's about all kinds of software-related things.
Monday, April 1, 2013
Thursday, March 28, 2013
Triceps Multithreading Concepts
The multithreading support has solidified to the point where I can start documenting it. The full description will have to wait until I finalize the Perl API but the concepts are pretty much settled.
The idea of the multithreading support in Triceps is to make writing the multithreaded model easier. To make writing the good code easy and writing the bad code hard. But of course you don't have to use it, you can always make your own if you wish (just as you could before now).
Without the further ado, the diagram of a multithreaded Triceps application:
The Triceps application is embodied in the class App. It's possible to have multiple Apps in one program.
Each thread has multiple parts to it. First, of course, there is the OS-level (or, technically, library-level, or Perl-level) thread where the code executes. And then there is a class that represents this thread and its place in the App. To reduce the naming conflict, this class is creatively named Triead (pronounced still "thread"). In the discussion I use the word "thread" for both concepts, the OS-level thread and the Triead, and it's usually clear from the context which one I mean. But sometimes it's particularly important to make the distinction, and then I name one or the other explicitly.
The class Triead itself is largely opaque, allowing only a few methods for introspection. But there is a control interface to it, called TrieadOwner. The Triead is visible from the outside, the TrieadOwner object is visible only in the OS thread that owns the Triead. The TrieadOwner manages the thread state and acts as the intermediary in the thread's communications with the App.
The data is passed between the threads through the Nexuses. A Nexus is unidirectional, with data going only one way, however it may have multiple writers and multiple readers. All the readers see the exact same data, with rowops going in the exact same order (well, there will be other policies in the future as well, but for now there is only one policy).
A Nexus passes through the data for multiple labels, very much like an FnReturn does (and indeed there is a special connection between them). A Nexus also allows to export the row types and table types from one thread to another.
A Nexus gets connected to the Trieads to though the Facets. A Facet is a connection point between the Nexus and the Triead. Each Facet is for either reading or writing. And there may be only one Facet between a given Nexus and a given Triead, you can't make multiple connections between them. As a consequence, a thread can't both write and read to the same Nexus, it can do only one thing. This might actually be an overly restrictive limitation and might change in the future but that's how things work now.
Each Nexus also has a direction: either direct ("downwards") or reverse ("upwards"). And yes, the reverse Nexuses allow to build the models with loops. However the loops consisting of only the direct Nexuses are not allowed, nor of only reverse Nexuses. They would mess up the flow control. The proper loops must contain a mix of direct and reverse Nexuses.
The direct Nexuses have a limited queue size and stop the writers when the queue fills up, until the data gets consumed, thus providing the flow control. The reverse Nexuses have an unlimited queue size, which allows to avoid the circular deadlocks.
Normally an App is built once and keeps running in this configuration until it stops. But there is a strong need to have the threads dynamically added and deleted too. For example, if the App running as a server and clients connect to it, each client needs to have its thread(s) added on connection and then deleted when the client disconnects. This is handled through the concept of fragments. There is no Fragment class but when you create a Triead, you can specify a fragment name for it. Then it becomes possible to shut down and dispose the threads in a fragment after the fragment's work is done.
The idea of the multithreading support in Triceps is to make writing the multithreaded model easier. To make writing the good code easy and writing the bad code hard. But of course you don't have to use it, you can always make your own if you wish (just as you could before now).
Without the further ado, the diagram of a multithreaded Triceps application:
![]() |
| Fig. 1. Triceps application. |
The Triceps application is embodied in the class App. It's possible to have multiple Apps in one program.
Each thread has multiple parts to it. First, of course, there is the OS-level (or, technically, library-level, or Perl-level) thread where the code executes. And then there is a class that represents this thread and its place in the App. To reduce the naming conflict, this class is creatively named Triead (pronounced still "thread"). In the discussion I use the word "thread" for both concepts, the OS-level thread and the Triead, and it's usually clear from the context which one I mean. But sometimes it's particularly important to make the distinction, and then I name one or the other explicitly.
The class Triead itself is largely opaque, allowing only a few methods for introspection. But there is a control interface to it, called TrieadOwner. The Triead is visible from the outside, the TrieadOwner object is visible only in the OS thread that owns the Triead. The TrieadOwner manages the thread state and acts as the intermediary in the thread's communications with the App.
The data is passed between the threads through the Nexuses. A Nexus is unidirectional, with data going only one way, however it may have multiple writers and multiple readers. All the readers see the exact same data, with rowops going in the exact same order (well, there will be other policies in the future as well, but for now there is only one policy).
A Nexus passes through the data for multiple labels, very much like an FnReturn does (and indeed there is a special connection between them). A Nexus also allows to export the row types and table types from one thread to another.
A Nexus gets connected to the Trieads to though the Facets. A Facet is a connection point between the Nexus and the Triead. Each Facet is for either reading or writing. And there may be only one Facet between a given Nexus and a given Triead, you can't make multiple connections between them. As a consequence, a thread can't both write and read to the same Nexus, it can do only one thing. This might actually be an overly restrictive limitation and might change in the future but that's how things work now.
Each Nexus also has a direction: either direct ("downwards") or reverse ("upwards"). And yes, the reverse Nexuses allow to build the models with loops. However the loops consisting of only the direct Nexuses are not allowed, nor of only reverse Nexuses. They would mess up the flow control. The proper loops must contain a mix of direct and reverse Nexuses.
The direct Nexuses have a limited queue size and stop the writers when the queue fills up, until the data gets consumed, thus providing the flow control. The reverse Nexuses have an unlimited queue size, which allows to avoid the circular deadlocks.
Normally an App is built once and keeps running in this configuration until it stops. But there is a strong need to have the threads dynamically added and deleted too. For example, if the App running as a server and clients connect to it, each client needs to have its thread(s) added on connection and then deleted when the client disconnects. This is handled through the concept of fragments. There is no Fragment class but when you create a Triead, you can specify a fragment name for it. Then it becomes possible to shut down and dispose the threads in a fragment after the fragment's work is done.
Saturday, March 16, 2013
initialization templates
Some of the helper initialization templates have been already shown in the examples, and here finally is the systematic description.
Trices has the common approach of the building of the complex objects by the chained mehod calls (shown on a fictional class Object):
Autoref<Object> o = Object::make()->addOption1(arg)->addOption2(arg);
Here Object::make() is a convenience wrapper for "new Object", because new has an inconvenient priority. The new or make() returns a pointer to the newly constructed object, and then each method in the chain returns the same pointer (this, from its standpoint) to facilitate the chaining.
While the chain executes, the pointer stays a simple pointer, not a reference. So the methods in the chain can't throw any exceptions, or the memory will leak. Instead they collect the error messages in an Errors object that has to be checked afterwards, like:
if (o->getErrors()->hasError()) ...
Note that the object gets a reference to it created first, so that on an error it would be properly destroyed.
The convenience template checkOrThrow() allows to do the check along with the chain, and if an error is found, convert it to an exception:
Autoref<Object> o = checkOrThrow(Object::make()->addOption1(arg)->addOption2(arg));
It does all the right things with the references.
Some objects need to be initialized after all the options have been set, since it's much easier to check things once and get the interaction of the options right rather than check on every option. And since the initialization might create the references to the object, to get it right, it has to be done after the "main" reference is created.
The template initialize() takes care of it:
Autoref<Object> o = initialize(Object::make()->addOption1(arg)->addOption2(arg));
For some objects the initialization can't fail (nor any other errors can be created by the options). For the others, the error needs to be checked afterwards. The template initializeOrThrow() takes care of both the initialization, the check, and exception throwing on errors:
Autoref<Object> o = initializeOrThrow(Object::make()->addOption1(arg)->addOption2(arg));
That's basically it. All these templates are defined in common/Initialize.h.
Trices has the common approach of the building of the complex objects by the chained mehod calls (shown on a fictional class Object):
Autoref<Object> o = Object::make()->addOption1(arg)->addOption2(arg);
Here Object::make() is a convenience wrapper for "new Object", because new has an inconvenient priority. The new or make() returns a pointer to the newly constructed object, and then each method in the chain returns the same pointer (this, from its standpoint) to facilitate the chaining.
While the chain executes, the pointer stays a simple pointer, not a reference. So the methods in the chain can't throw any exceptions, or the memory will leak. Instead they collect the error messages in an Errors object that has to be checked afterwards, like:
if (o->getErrors()->hasError()) ...
Note that the object gets a reference to it created first, so that on an error it would be properly destroyed.
The convenience template checkOrThrow() allows to do the check along with the chain, and if an error is found, convert it to an exception:
Autoref<Object> o = checkOrThrow(Object::make()->addOption1(arg)->addOption2(arg));
It does all the right things with the references.
Some objects need to be initialized after all the options have been set, since it's much easier to check things once and get the interaction of the options right rather than check on every option. And since the initialization might create the references to the object, to get it right, it has to be done after the "main" reference is created.
The template initialize() takes care of it:
Autoref<Object> o = initialize(Object::make()->addOption1(arg)->addOption2(arg));
For some objects the initialization can't fail (nor any other errors can be created by the options). For the others, the error needs to be checked afterwards. The template initializeOrThrow() takes care of both the initialization, the check, and exception throwing on errors:
Autoref<Object> o = initializeOrThrow(Object::make()->addOption1(arg)->addOption2(arg));
That's basically it. All these templates are defined in common/Initialize.h.
Friday, March 15, 2013
perl wrapping for the C++ objects
There is one more item in the C++ part of Triceps that I haven't touched upon yet. It's not a part of the C++ API as such but the connection between the C++ and Perl APIs. You need to bother about it if you want to write more of the components in C++ and export them into Perl.
When exporting the C++ (or any compiled language) API into Perl (or into any scripting language) there are two things to consider:
1. The script must never crash the interpterer. The interpreted program might die but the interpreter itself must never crash. If you've ever deal with wksh, you know how horrible is the debugging of such crashes.
2. Perl has its memory management by reference counting, which needs to be married with the memory management at the C++ level.
The solution to the second problem is fairly straightforward: have an intermediate wrapper structure. Perl has its reference counting for the pointer to this structure. When you construct a Perl object, return the pointer to a newly allocated instance of this structure. When the Perl reference count gows down to zero, if calls the method DESTROY for this object, and then you destroy this structure.
And inside this structure will be the C++ reference to the actual object. When the wrapper structure gets created, it gets a new reference and when the wrapper structure gets destroyed, it releases this reference.
Here is a small example of how the RowType object gets created and destroyed:
WrapRowType *
Triceps::RowType::new(...)
CODE:
RowType::FieldVec fld;
.....
Onceref<RowType> rt = new CompactRowType(fld);
Erref err = rt->getErrors();
if (!err.isNull() && !err->isEmpty()) {
setErrMsg("Triceps::RowType::new: " + err->print());
XSRETURN_UNDEF;
}
RETVAL = new WrapRowType(rt);
OUTPUT:
RETVAL
void
DESTROY(WrapRowType *self)
CODE:
delete self;
Now to the first problem. How does Perl know to call a particular XS method for a object? From the package this object is blessed to. If the package happens to be an XS package, the XS method will be called. However it's entirely possible to re-bless the object to a completely different package. If the user does this, a completely wrong method may be called and will crash when it tries to reach a wrong object at the pointer.
Things get even worse for the other arguments of the methods. For example, the argument "other" here:
int
equals(WrapRowType *self, WrapRowType *other)
Well, Perl lets you provide a code snippet that would check that the object is as expected in the typemap file. But how would that snipped know? Going just by the blessed package is unreliable.
Triceps solves this problem by placing an 8-byte magic code at the front of every wrap object. Each class has its own magic value for this field. 8 bytes allows to have great many unique codes, and is quick to check because it's just one CPU word.
This magic code is defined as:
struct WrapMagic {
char v_[8]; // 8 bytes to make a single 64-bit comparison
bool operator!=(const WrapMagic &wm) const
{
return (*(int64_t *)v_) != (*(int64_t *)wm.v_);
}
};
Then the wrapper contains the fields:
WrapMagic magic_;
Autoref<Class> ref_; // referenced value
static WrapMagic classMagic_;
The check is done with:
// returns true if the magic value is bad
bool badMagic() const
{
return magic_ != magic;
}
And the typemap entry is:
O_WRAP_OBJECT
if( sv_isobject($arg) && (SvTYPE(SvRV($arg)) == SVt_PVMG) ) {
$var = ($type)SvIV((SV*)SvRV( $arg ));
if ($var == 0 || $var->badMagic()) {
setErrMsg( \"${Package}::$func_name(): $var has an incorrect magic for $ntype\" );
XSRETURN_UNDEF;
}
} else{
setErrMsg( \"${Package}::$func_name(): $var is not a blessed SV reference to $ntype\" );
XSRETURN_UNDEF;
}
It checks that this is an object, of an XS package, and then that the pointer to the C/C++ object is not NULL, and that the value at this pointer starts with the right magic.
setErrMsg() is the Triceps finction for setting its error code, and will soon be replaced with a Perl error indication.
The repeatable build of the wrapper object is handled in Triceps with a template that you don't need to use directly and a macro that you do use. For example, the definition for the RowType wrap in wrap/Wrap.h is:
DEFINE_WRAP(RowType);
The second component, the magic definition goes into Wrap.cpp:
WrapMagic magicWrapRowType = { "RowType" };
Just make sure that the string contains no more than 7 characters.
Some objects in Triceps are reached through the special references that know both the object and its type (such as rows and row handles). For them there is a separate macro:
DEFINE_WRAP2(const RowType, Rowref, Row);
DEFINE_WRAP2(Table, Rhref, RowHandle);
The arguments are the type class, the reference class and finally the object class itself.
And there is one more twist: sometimes the object are self-contained but when you use them, you must use them only with a correct parent object. Right now there is only one such class: the Tray must be used with its correct Unit, and the Perl code checks it. In this case the wrapper has the reference to both Tray and the Unit, and is defined as:
DEFINE_WRAP_IDENT(Unit, Tray);
When exporting the C++ (or any compiled language) API into Perl (or into any scripting language) there are two things to consider:
1. The script must never crash the interpterer. The interpreted program might die but the interpreter itself must never crash. If you've ever deal with wksh, you know how horrible is the debugging of such crashes.
2. Perl has its memory management by reference counting, which needs to be married with the memory management at the C++ level.
The solution to the second problem is fairly straightforward: have an intermediate wrapper structure. Perl has its reference counting for the pointer to this structure. When you construct a Perl object, return the pointer to a newly allocated instance of this structure. When the Perl reference count gows down to zero, if calls the method DESTROY for this object, and then you destroy this structure.
And inside this structure will be the C++ reference to the actual object. When the wrapper structure gets created, it gets a new reference and when the wrapper structure gets destroyed, it releases this reference.
Here is a small example of how the RowType object gets created and destroyed:
WrapRowType *
Triceps::RowType::new(...)
CODE:
RowType::FieldVec fld;
.....
Onceref<RowType> rt = new CompactRowType(fld);
Erref err = rt->getErrors();
if (!err.isNull() && !err->isEmpty()) {
setErrMsg("Triceps::RowType::new: " + err->print());
XSRETURN_UNDEF;
}
RETVAL = new WrapRowType(rt);
OUTPUT:
RETVAL
void
DESTROY(WrapRowType *self)
CODE:
delete self;
Now to the first problem. How does Perl know to call a particular XS method for a object? From the package this object is blessed to. If the package happens to be an XS package, the XS method will be called. However it's entirely possible to re-bless the object to a completely different package. If the user does this, a completely wrong method may be called and will crash when it tries to reach a wrong object at the pointer.
Things get even worse for the other arguments of the methods. For example, the argument "other" here:
int
equals(WrapRowType *self, WrapRowType *other)
Well, Perl lets you provide a code snippet that would check that the object is as expected in the typemap file. But how would that snipped know? Going just by the blessed package is unreliable.
Triceps solves this problem by placing an 8-byte magic code at the front of every wrap object. Each class has its own magic value for this field. 8 bytes allows to have great many unique codes, and is quick to check because it's just one CPU word.
This magic code is defined as:
struct WrapMagic {
char v_[8]; // 8 bytes to make a single 64-bit comparison
bool operator!=(const WrapMagic &wm) const
{
return (*(int64_t *)v_) != (*(int64_t *)wm.v_);
}
};
Then the wrapper contains the fields:
WrapMagic magic_;
Autoref<Class> ref_; // referenced value
static WrapMagic classMagic_;
The check is done with:
// returns true if the magic value is bad
bool badMagic() const
{
return magic_ != magic;
}
And the typemap entry is:
O_WRAP_OBJECT
if( sv_isobject($arg) && (SvTYPE(SvRV($arg)) == SVt_PVMG) ) {
$var = ($type)SvIV((SV*)SvRV( $arg ));
if ($var == 0 || $var->badMagic()) {
setErrMsg( \"${Package}::$func_name(): $var has an incorrect magic for $ntype\" );
XSRETURN_UNDEF;
}
} else{
setErrMsg( \"${Package}::$func_name(): $var is not a blessed SV reference to $ntype\" );
XSRETURN_UNDEF;
}
It checks that this is an object, of an XS package, and then that the pointer to the C/C++ object is not NULL, and that the value at this pointer starts with the right magic.
setErrMsg() is the Triceps finction for setting its error code, and will soon be replaced with a Perl error indication.
The repeatable build of the wrapper object is handled in Triceps with a template that you don't need to use directly and a macro that you do use. For example, the definition for the RowType wrap in wrap/Wrap.h is:
DEFINE_WRAP(RowType);
The second component, the magic definition goes into Wrap.cpp:
WrapMagic magicWrapRowType = { "RowType" };
Just make sure that the string contains no more than 7 characters.
Some objects in Triceps are reached through the special references that know both the object and its type (such as rows and row handles). For them there is a separate macro:
DEFINE_WRAP2(const RowType, Rowref, Row);
DEFINE_WRAP2(Table, Rhref, RowHandle);
The arguments are the type class, the reference class and finally the object class itself.
And there is one more twist: sometimes the object are self-contained but when you use them, you must use them only with a correct parent object. Right now there is only one such class: the Tray must be used with its correct Unit, and the Perl code checks it. In this case the wrapper has the reference to both Tray and the Unit, and is defined as:
DEFINE_WRAP_IDENT(Unit, Tray);
Saturday, March 9, 2013
a better unit tracer
I've done a few small improvements to the Unit::Tracer in the C++ code.
The first one, I've moved the message buffer support from Unit::StringTracer into the base class Unit::Tracer. The buffer is used in pretty much any tracer, so it looks like a good idea. And if some subclass doesn't want to use it, it doesn't have to.
So, now in Unit::Tracer is a protected element for the use of subclasses:
Erref buffer_;
And a couple of methods for manipulating this buffer:
virtual Erref getBuffer();
virtual void clearBuffer();
They are virtual to let the subclasses do whatever they please, but the default implementation is exactly as it was in the StringTracer: return the buffer_ element, and put a new clean Errors object into the buffer_ reference.
The next improvement allows to add a custom row printer. Now not only the Perl code but also C++ code can print the rows in tracing (though you have to provide that C++ code that prints the interesting portion of the row or the whole row). This separation of the general trace logic and of the row printing will eventually make it to Perl too , but not yet.
There are two ways to do it. First, there is a virtual method
virtual void printRow(string &res, const RowType *rt, const Row *row);
You can re-define it in your subclass and do the printing. The job of this method is to append the information from the row row of the type rt to the result string res. Append, not replace.
The second way is by providing a pointer to a simple C-style function of the type:
typedef void RowPrinter(string &res, const RowType *rt, const Row *row);
The arguments are exactly the same as for the method. This pointer can be given to the Tracer constructor:
Tracer(RowPrinter *rp = NULL);
StringTracer(bool verbose = false, RowPrinter *rp = NULL);
StringNameTracer(bool verbose = false, RowPrinter *rp = NULL);
The default implementation of the method printRow() simply calls the function at this pointer if it's not NULL. So if you redefine this method in your subclass, the row printer function pointer will stop working.
The first one, I've moved the message buffer support from Unit::StringTracer into the base class Unit::Tracer. The buffer is used in pretty much any tracer, so it looks like a good idea. And if some subclass doesn't want to use it, it doesn't have to.
So, now in Unit::Tracer is a protected element for the use of subclasses:
Erref buffer_;
And a couple of methods for manipulating this buffer:
virtual Erref getBuffer();
virtual void clearBuffer();
They are virtual to let the subclasses do whatever they please, but the default implementation is exactly as it was in the StringTracer: return the buffer_ element, and put a new clean Errors object into the buffer_ reference.
The next improvement allows to add a custom row printer. Now not only the Perl code but also C++ code can print the rows in tracing (though you have to provide that C++ code that prints the interesting portion of the row or the whole row). This separation of the general trace logic and of the row printing will eventually make it to Perl too , but not yet.
There are two ways to do it. First, there is a virtual method
virtual void printRow(string &res, const RowType *rt, const Row *row);
You can re-define it in your subclass and do the printing. The job of this method is to append the information from the row row of the type rt to the result string res. Append, not replace.
The second way is by providing a pointer to a simple C-style function of the type:
typedef void RowPrinter(string &res, const RowType *rt, const Row *row);
The arguments are exactly the same as for the method. This pointer can be given to the Tracer constructor:
Tracer(RowPrinter *rp = NULL);
StringTracer(bool verbose = false, RowPrinter *rp = NULL);
StringNameTracer(bool verbose = false, RowPrinter *rp = NULL);
The default implementation of the method printRow() simply calls the function at this pointer if it's not NULL. So if you redefine this method in your subclass, the row printer function pointer will stop working.
Tuesday, March 5, 2013
synchronization with events
The events feel like an easier form of synchronization to use than dealing directly with the condition variables, and for the simple cases they are. However once you try to do something real-sized, it always turns out that you want to do something more while holding that mutex, not just set a flag and sleep (or the other way around, not just wake up) as the internals of an event do. And no, holding a separate mutex and waiting for an event with it doesn't work, because the separate mutex doesn't get unlocked when the thread goes to sleep.
And so far in Triceps always, always, every single time an event had become some handcrafted logic around a mutex and condition variable. There is one event in it now but it's about to be split up and converted.
Perhaps a good solution to make the events more general and more usable would be to split the mutex from the rest of the event logic, make it like the mutex-condvar pair that is sometimes called a monitor (or in the Ptwrap library term, a pmcond). Or, to turn the same idea sideways, make public not only the combined API but also the constituent parts, the mutex and the logic that is protected behind the mutex.
It has also turned out that for the auto-reset events the ability to read the event state without resetting it comes quite handy.
And so far in Triceps always, always, every single time an event had become some handcrafted logic around a mutex and condition variable. There is one event in it now but it's about to be split up and converted.
Perhaps a good solution to make the events more general and more usable would be to split the mutex from the rest of the event logic, make it like the mutex-condvar pair that is sometimes called a monitor (or in the Ptwrap library term, a pmcond). Or, to turn the same idea sideways, make public not only the combined API but also the constituent parts, the mutex and the logic that is protected behind the mutex.
It has also turned out that for the auto-reset events the ability to read the event state without resetting it comes quite handy.
Monday, March 4, 2013
more printf for the errors
I've been using strprintf() repeatedly for the error messages and exceptions, and I've come up with a better way for it.
First, Ive added a var-args version of strprintf(), in common/Strprintf.h:
string vstrprintf(const char *fmt, va_list ap);
You can use it to create strings from other functions taking the printf-like arguments.
Next go the extensions to the Errors class. The consistent theme there is "check if the Errors reference (Erref) is NULL, if it is, allocate a new Errors, and then add a formatted error message to it". So I've added the new methods not to Errors but to Erref. They check if the Erref object is NULL, allocate a new Errors object into it if needed, and then format the arguments. The simplest one is:
void f(const char *fmt, ...);
It adds a simple formatted message, always marked as an error. You use it like this:
Erref e; // initially NULL
...
e.f("a message with integer %d", n);
The message may be multi-line, it will be split appropriately, like in Errors::appendMultiline().
The next one is even more smart:
bool fAppend(Autoref<Errors> clde, const char *fmt, ...);
It first checks that the child errors object is not NULL and contains an error, and if it does then it does through the dance of allocating a new Errors object if needed, appends the formatted message, and the child errors. The message goes before the child errors, unlike the method signature. So you can use it blindly like this to do the right thing:
Autoref<Errors> checkSubObject(int idx);
...
for (int i = 0; i < sz; i++)
e.fAppend(checkSubObject(i), "error in the sub-object %d:", i);
Same as before, you can use the multi-line error messages.
Next goes the same thing for Exception:
static Exception f(const char *fmt, ...);
static Exception fTrace(const char *fmt, ...);
these are the static factory methods that create an Exception object with the message, and either without and with the stack trace. They are used like
throw Exception::f("a message with integer %d", n);
And the similar methods for construction with the nested errors:
static Exception f(Onceref<Errors> err, const char *fmt, ...);
static Exception fTrace(Onceref<Errors> err, const char *fmt, ...);
Unlike the Erref method, these work unconditionally (since their result is normally used in throw, and it's too late to do anything by that time), so you better make sure in advance that there is a child error. A typical usage would be like this:
try {
...
} catch (Exception e) {
throw Exception(e.getErrors(), "error at stage %d:", n);
}
Again, in the resulting exception the message goes before the nested errors.
First, Ive added a var-args version of strprintf(), in common/Strprintf.h:
string vstrprintf(const char *fmt, va_list ap);
You can use it to create strings from other functions taking the printf-like arguments.
Next go the extensions to the Errors class. The consistent theme there is "check if the Errors reference (Erref) is NULL, if it is, allocate a new Errors, and then add a formatted error message to it". So I've added the new methods not to Errors but to Erref. They check if the Erref object is NULL, allocate a new Errors object into it if needed, and then format the arguments. The simplest one is:
void f(const char *fmt, ...);
It adds a simple formatted message, always marked as an error. You use it like this:
Erref e; // initially NULL
...
e.f("a message with integer %d", n);
The message may be multi-line, it will be split appropriately, like in Errors::appendMultiline().
The next one is even more smart:
bool fAppend(Autoref<Errors> clde, const char *fmt, ...);
It first checks that the child errors object is not NULL and contains an error, and if it does then it does through the dance of allocating a new Errors object if needed, appends the formatted message, and the child errors. The message goes before the child errors, unlike the method signature. So you can use it blindly like this to do the right thing:
Autoref<Errors> checkSubObject(int idx);
...
for (int i = 0; i < sz; i++)
e.fAppend(checkSubObject(i), "error in the sub-object %d:", i);
Same as before, you can use the multi-line error messages.
Next goes the same thing for Exception:
static Exception f(const char *fmt, ...);
static Exception fTrace(const char *fmt, ...);
these are the static factory methods that create an Exception object with the message, and either without and with the stack trace. They are used like
throw Exception::f("a message with integer %d", n);
And the similar methods for construction with the nested errors:
static Exception f(Onceref<Errors> err, const char *fmt, ...);
static Exception fTrace(Onceref<Errors> err, const char *fmt, ...);
Unlike the Erref method, these work unconditionally (since their result is normally used in throw, and it's too late to do anything by that time), so you better make sure in advance that there is a child error. A typical usage would be like this:
try {
...
} catch (Exception e) {
throw Exception(e.getErrors(), "error at stage %d:", n);
}
Again, in the resulting exception the message goes before the nested errors.
Sunday, February 24, 2013
atomic int vs mutex
I've been wondering, how much faster is the native implementation of an atomic integer (such as Triceps imports from NSPR) versus a simple mutex-based approach (lock the mutex, change the value, unlock the mutex).
I've finally got interested enough to measure it. On my machine, compiled with GCC optimization -O3, the difference is at about 2.5-3 times. Both with and without contention, the difference is about the same. So it's faster but not hugely so.
An interesting thing is that with contention of two threads for the same mutex, Linux shows the total user CPU time about the same as the real time. With contention of two threads for the same atomic integer, the user CPU time is twice higher than the real time. It looks like Linux manages to account for the wait time properly even in these tiny increments and without adding a huge amount of overhead.
And the real time with contention of two threads for the same mutex (that is, threads doing nothing but locking-unlocking the mutex in a loop) is about 3-3.5 times higher than with the same two threads running sequentially. The same goes for the atomic integers as well.
The program I used was:
#include <stdlib.h>
#include <vector>
#include "pw/ptwrap.h"
#include "mem/Atomic.h"
using namespace Triceps;
const int NTHREADS = 2;
pw::pmutex m;
AtomicInt ai(0);
class TestThread: public pw::pwthread
{
public:
TestThread():
p_(NULL)
{ }
virtual void *execute()
{
m.lock();
m.unlock();
for (int i = 0; i < 10*1000*1000; i++) {
if (p_)
p_();
// ai.inc();
m.lock();
m.unlock();
}
}
void (*p_)();
};
std::vector<TestThread *> t;
int main(int argc, char **argv)
{
m.lock();
for (int i = 0; i < NTHREADS; i++) {
TestThread *job = new TestThread;
t.push_back(job);
job->start();
}
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
m.unlock(); // the run starts!
for (int i = 0; i < NTHREADS; i++) {
t[i]->join();
delete t[i];
}
return 0;
}
Comment, uncomment and adjust the constants as needed. The iteration count of 10 millions may be much too low for the newer faster machines.
The build command for me was:
g++ -DTRICEPS_NSPR=4 -I $TRICEPS/trunk/cpp -O3 -o x x.cpp -lnspr4 -lpthread $TRICEPS/trunk/cpp/build/libtriceps.a
Adjust the library names and TRICEPS_NSPR value as needed (for Ubuntu it would likely be "-DTRICEPS_NSPR=0 -lnspr").To try the simple-minded implementation of atomics through the mutex, remove "-DTRICEPS_NSPR".
To measure the time, run it in the time command:
time ./x
When comparing time, don't forget that when you run 2 threads, they make twice more iterations!
I've finally got interested enough to measure it. On my machine, compiled with GCC optimization -O3, the difference is at about 2.5-3 times. Both with and without contention, the difference is about the same. So it's faster but not hugely so.
An interesting thing is that with contention of two threads for the same mutex, Linux shows the total user CPU time about the same as the real time. With contention of two threads for the same atomic integer, the user CPU time is twice higher than the real time. It looks like Linux manages to account for the wait time properly even in these tiny increments and without adding a huge amount of overhead.
And the real time with contention of two threads for the same mutex (that is, threads doing nothing but locking-unlocking the mutex in a loop) is about 3-3.5 times higher than with the same two threads running sequentially. The same goes for the atomic integers as well.
The program I used was:
#include <stdlib.h>
#include <vector>
#include "pw/ptwrap.h"
#include "mem/Atomic.h"
using namespace Triceps;
const int NTHREADS = 2;
pw::pmutex m;
AtomicInt ai(0);
class TestThread: public pw::pwthread
{
public:
TestThread():
p_(NULL)
{ }
virtual void *execute()
{
m.lock();
m.unlock();
for (int i = 0; i < 10*1000*1000; i++) {
if (p_)
p_();
// ai.inc();
m.lock();
m.unlock();
}
}
void (*p_)();
};
std::vector<TestThread *> t;
int main(int argc, char **argv)
{
m.lock();
for (int i = 0; i < NTHREADS; i++) {
TestThread *job = new TestThread;
t.push_back(job);
job->start();
}
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
sched_yield();
m.unlock(); // the run starts!
for (int i = 0; i < NTHREADS; i++) {
t[i]->join();
delete t[i];
}
return 0;
}
Comment, uncomment and adjust the constants as needed. The iteration count of 10 millions may be much too low for the newer faster machines.
The build command for me was:
g++ -DTRICEPS_NSPR=4 -I $TRICEPS/trunk/cpp -O3 -o x x.cpp -lnspr4 -lpthread $TRICEPS/trunk/cpp/build/libtriceps.a
Adjust the library names and TRICEPS_NSPR value as needed (for Ubuntu it would likely be "-DTRICEPS_NSPR=0 -lnspr").To try the simple-minded implementation of atomics through the mutex, remove "-DTRICEPS_NSPR".
To measure the time, run it in the time command:
time ./x
When comparing time, don't forget that when you run 2 threads, they make twice more iterations!
Sunday, February 17, 2013
Aggregator in C++, Part 4
And finally here is the reference of actions available in the aggregator handler:
parentIndexType->groupSize(gh)
Get the size of the group. The result is of the type size_t. This is pretty much the only method of IndexType that should be called directly, and only in the aggregation. The rest of the IndexType methods should be accessed through the similar methods in the Table, and I won't even document them. However if you really, really want to, you can find the description of the other methods type/IndexType.h and call them in the aggregation as well.
gadget()->getLabel()->getType()
Get the result row type.
The rest of the actions are done by calling the Index methods on the argument index.Same as with the IndexType, the aggregation is the only place where these methods are called directly, everywhere else the equivalent actions are done through the Table methods. Because of this I've grouped their description with the aggregation and not deparately.
const IndexType *getType() const;
Get the type of this index.
RowHandle *begin() const;
Get the handle of the first row of the group, in the default order according to its first leaf index type. Note that here it's not the whole table's first leaf index type but the first leaf in the index type subtree under this index's type. All the iteration methods return NULL if there are no more rows.
RowHandle *next(const RowHandle *cur) const;
Get the handle of the next row (or NULL if that was the last one) in the default order. The NULL argument makes the NULL result.
RowHandle *last() const;
Get the handle of the last row in the group in the default order.
The rest of the methods of Index aren't really to be used directly.
Unlike the Perl API of AggregatorContext, there are no direct analogs of beginIdx() and such in C++ Index. To get them in C++, you need to translate the iteration to another index type through the Table (and of course, just like in Perl, you would need somehow to get the reference to another index type into your aggregator, and that index type better be in the subtree of the parentIndexType). To translate through the Table, you take any row from the group, usually the first one, and use it with the table methods that accept a sample row.
For example:
RowHandle *sample = index->begin();
RowHandle *rhend = table->nextGroupIdx(otherIndexType, sample);
for (RowHandle *rhit = table->firstOfGroupIdx(otherIndexType, sample); rhit != rhend; rhit = table->nextIdx(otherIndexType, rhit)) {
...
}
This concludes the discussion of the aggregators. This also concludes the description of the whole C++ API, except for the most recent additions.
parentIndexType->groupSize(gh)
Get the size of the group. The result is of the type size_t. This is pretty much the only method of IndexType that should be called directly, and only in the aggregation. The rest of the IndexType methods should be accessed through the similar methods in the Table, and I won't even document them. However if you really, really want to, you can find the description of the other methods type/IndexType.h and call them in the aggregation as well.
gadget()->getLabel()->getType()
Get the result row type.
The rest of the actions are done by calling the Index methods on the argument index.Same as with the IndexType, the aggregation is the only place where these methods are called directly, everywhere else the equivalent actions are done through the Table methods. Because of this I've grouped their description with the aggregation and not deparately.
const IndexType *getType() const;
Get the type of this index.
RowHandle *begin() const;
Get the handle of the first row of the group, in the default order according to its first leaf index type. Note that here it's not the whole table's first leaf index type but the first leaf in the index type subtree under this index's type. All the iteration methods return NULL if there are no more rows.
RowHandle *next(const RowHandle *cur) const;
Get the handle of the next row (or NULL if that was the last one) in the default order. The NULL argument makes the NULL result.
RowHandle *last() const;
Get the handle of the last row in the group in the default order.
The rest of the methods of Index aren't really to be used directly.
Unlike the Perl API of AggregatorContext, there are no direct analogs of beginIdx() and such in C++ Index. To get them in C++, you need to translate the iteration to another index type through the Table (and of course, just like in Perl, you would need somehow to get the reference to another index type into your aggregator, and that index type better be in the subtree of the parentIndexType). To translate through the Table, you take any row from the group, usually the first one, and use it with the table methods that accept a sample row.
For example:
RowHandle *sample = index->begin();
RowHandle *rhend = table->nextGroupIdx(otherIndexType, sample);
for (RowHandle *rhit = table->firstOfGroupIdx(otherIndexType, sample); rhit != rhend; rhit = table->nextIdx(otherIndexType, rhit)) {
...
}
This concludes the discussion of the aggregators. This also concludes the description of the whole C++ API, except for the most recent additions.
Aggregator in C++, Part 3
Doing a proper custom aggregator is more involved, and requires making subclasses of both Aggregator and AggregatorType. The test case aggSum shows an example of aggregator that can sum any 64-bit field.
The subclass of Aggregator contains only one method that is very similar to the BasicAggregator handler shown before:
class MySumAggregator: public Aggregator
{
public:
virtual void handle(Table *table, AggregatorGadget *gadget, Index *index,
const IndexType *parentIndexType, GroupHandle *gh, Tray *dest,
Aggregator::AggOp aggop, Rowop::Opcode opcode, RowHandle *rh);
};
void MySumAggregator::handle(Table *table, AggregatorGadget *gadget, Index *index,
const IndexType *parentIndexType, GroupHandle *gh, Tray *dest,
Aggregator::AggOp aggop, Rowop::Opcode opcode, RowHandle *rh)
{
// don't send the NULL record after the group becomes empty
if (opcode == Rowop::OP_NOP || parentIndexType->groupSize(gh) == 0)
return;
int fidx = gadget->typeAs<MySumAggregatorType>()->fieldIdx();
int64_t sum = 0;
for (RowHandle *rhi = index->begin(); rhi != NULL; rhi = index->next(rhi)) {
sum += table->getRowType()->getInt64(rhi->getRow(), fidx, 0);
}
// pick the rest of fields from the last row of the group
RowHandle *lastrh = index->last();
// build the result row; relies on the aggregator result being of the
// same type as the rows in the table
FdataVec fields;
table->getRowType()->splitInto(lastrh->getRow(), fields);
fields[fidx].setPtr(true, &sum, sizeof(sum));
gadget->sendDelayed(dest, fields, opcode);
}
The difference is that the field index is not hardcoded but taken from the aggregator type. The aggregator type is found with
gadget->typeAs<MySumAggregatorType>()
The typeAs template is a very recent addition, I've added it when writing this example. The previous (and still available) equivalent way to do it was to call gadget->getType() and then cast it to the pointer to the proper subclass. The method fieldIdx() is a custom addition to the MySumAggregatorType, not inherited from any base class.
The version of AggregatorGadget::sendDelayed() used here is different:
void sendDelayed(Tray *dest, FdataVec &data, Rowop::Opcode opcode) const;
I've actually added it after writing the first version of this post, then edited the post. It conveniently handles the construction of the row from fields and sending it.
Then the aggregator type needs to be defined with a fixed set of inherited virtual methods plus any needed custom parts.
class MySumAggregatorType: public AggregatorType
{
public:
// @param fname - field name to sum on
MySumAggregatorType(const string &name, const string &fname):
AggregatorType(name, NULL),
fname_(fname),
fidx_(-1)
{ }
// the copy constructor works fine
// (might set the non-NULL row type, but it will be overwritten
// during initialization)
// constructor for deep copy
// (might set the non-NULL row type, but it will be overwritten
// during initialization)
MySumAggregatorType(const MySumAggregatorType &agg, HoldRowTypes *holder):
AggregatorType(agg, holder),
fname_(agg.fname_),
fidx_(agg.fidx_)
{ }
virtual AggregatorType *copy() const
{
return new MySumAggregatorType(*this);
}
virtual AggregatorType *deepCopy(HoldRowTypes *holder) const
{
return new MySumAggregatorType(*this, holder);
}
virtual bool equals(const Type *t) const
{
if (this == t)
return true; // self-comparison, shortcut
if (!AggregatorType::equals(t))
return false;
const MySumAggregatorType *sumt = static_cast<const MySumAggregatorType *>(t);
if (fname_ != sumt->fname_)
return false;
return true;
}
virtual bool match(const Type *t) const
{
if (this == t)
return true; // self-comparison, shortcut
if (!AggregatorType::match(t))
return false;
const MySumAggregatorType *sumt = static_cast<const MySumAggregatorType *>(t);
if (fname_ != sumt->fname_)
return false;
return true;
}
virtual AggregatorGadget *makeGadget(Table *table, IndexType *intype) const
{
return new AggregatorGadget(this, table, intype);
}
virtual Aggregator *makeAggregator(Table *table, AggregatorGadget *gadget) const
{
return new MySumAggregator;
}
virtual void initialize(TableType *tabtype, IndexType *intype)
{
const RowType *rt = tabtype->rowType();
setRowType(rt); // the result has the same type as the argument
fidx_ = rt->findIdx(fname_);
if (fidx_ < 0)
errors_.fAppend(new Errors(rt->print()), "Unknown field '%s' in the row type:", fname_.c_str());
else {
if (rt->fields()[fidx_].arsz_ != RowType::Field::AR_SCALAR
|| rt->fields()[fidx_].type_->getTypeId() != Type::TT_INT64)
errors_.fAppend(new Errors(rt->print()),
"Field '%s' is not an int64 scalar in the row type:", fname_.c_str());
}
AggregatorType::initialize(tabtype, intype);
}
// called from the handler
int fieldIdx() const
{
return fidx_;
}
protected:
string fname_; // name of the field to sum, must be an int64
int fidx_; // index of field named fname_
};
The constructor accepts the aggregator name and the name of the field on which it will sum. The field name will be translated to field index during initialization, and made available to the MySumAggregator::handler() via the method fieldIdx(). The aggregator type starts with the result row type of NULL, with the actual row type set during initialization. This ability to set the row type later had the latent existence all along, but it's the first time I've made it explicit. It hasn't propagated to the Perl API yet. The idea here is that the result row type of this aggregator is always equal to the input row type, so rather than specifying the result type explicitly and then having to check it for compatibility, why not just take the table's row type when it becomes available? And it works beautifully.
The copy constructor and the constructor with HoldRowTypes are the implementations of the virtual methods copy() and deepCopy(). The deepCopy() is yet another recent addition used in the multithreaded support. I'll describe it in detail later, for now just Do It Like This.
The methods match() and equals() follow the same general shape as everywhere else. makeGadget() creates a generic gadget, and makeAggregator() creates an instance of aggregator for each group.
The interesting stuff starts happening in the initialization. The row type gets found from the table and set as the result type. Then the aggregation field is found in the row type and checked for being of the proper type. Its index is remembered for the later use.
errors_.fAppend() is a new part of the Erref API that makes the error construction more convenient. It is smart enough to check the reference for NULL and allocate a new Errors if so, then append a printf-formatted message and nested errors to it. I've been adding the direct printf-formatting for both Errors and Exceptions, this part of the API is to be documented yet.
The subclass of Aggregator contains only one method that is very similar to the BasicAggregator handler shown before:
class MySumAggregator: public Aggregator
{
public:
virtual void handle(Table *table, AggregatorGadget *gadget, Index *index,
const IndexType *parentIndexType, GroupHandle *gh, Tray *dest,
Aggregator::AggOp aggop, Rowop::Opcode opcode, RowHandle *rh);
};
void MySumAggregator::handle(Table *table, AggregatorGadget *gadget, Index *index,
const IndexType *parentIndexType, GroupHandle *gh, Tray *dest,
Aggregator::AggOp aggop, Rowop::Opcode opcode, RowHandle *rh)
{
// don't send the NULL record after the group becomes empty
if (opcode == Rowop::OP_NOP || parentIndexType->groupSize(gh) == 0)
return;
int fidx = gadget->typeAs<MySumAggregatorType>()->fieldIdx();
int64_t sum = 0;
for (RowHandle *rhi = index->begin(); rhi != NULL; rhi = index->next(rhi)) {
sum += table->getRowType()->getInt64(rhi->getRow(), fidx, 0);
}
// pick the rest of fields from the last row of the group
RowHandle *lastrh = index->last();
// build the result row; relies on the aggregator result being of the
// same type as the rows in the table
FdataVec fields;
table->getRowType()->splitInto(lastrh->getRow(), fields);
fields[fidx].setPtr(true, &sum, sizeof(sum));
gadget->sendDelayed(dest, fields, opcode);
}
The difference is that the field index is not hardcoded but taken from the aggregator type. The aggregator type is found with
gadget->typeAs<MySumAggregatorType>()
The typeAs template is a very recent addition, I've added it when writing this example. The previous (and still available) equivalent way to do it was to call gadget->getType() and then cast it to the pointer to the proper subclass. The method fieldIdx() is a custom addition to the MySumAggregatorType, not inherited from any base class.
The version of AggregatorGadget::sendDelayed() used here is different:
void sendDelayed(Tray *dest, FdataVec &data, Rowop::Opcode opcode) const;
I've actually added it after writing the first version of this post, then edited the post. It conveniently handles the construction of the row from fields and sending it.
Then the aggregator type needs to be defined with a fixed set of inherited virtual methods plus any needed custom parts.
class MySumAggregatorType: public AggregatorType
{
public:
// @param fname - field name to sum on
MySumAggregatorType(const string &name, const string &fname):
AggregatorType(name, NULL),
fname_(fname),
fidx_(-1)
{ }
// the copy constructor works fine
// (might set the non-NULL row type, but it will be overwritten
// during initialization)
// constructor for deep copy
// (might set the non-NULL row type, but it will be overwritten
// during initialization)
MySumAggregatorType(const MySumAggregatorType &agg, HoldRowTypes *holder):
AggregatorType(agg, holder),
fname_(agg.fname_),
fidx_(agg.fidx_)
{ }
virtual AggregatorType *copy() const
{
return new MySumAggregatorType(*this);
}
virtual AggregatorType *deepCopy(HoldRowTypes *holder) const
{
return new MySumAggregatorType(*this, holder);
}
virtual bool equals(const Type *t) const
{
if (this == t)
return true; // self-comparison, shortcut
if (!AggregatorType::equals(t))
return false;
const MySumAggregatorType *sumt = static_cast<const MySumAggregatorType *>(t);
if (fname_ != sumt->fname_)
return false;
return true;
}
virtual bool match(const Type *t) const
{
if (this == t)
return true; // self-comparison, shortcut
if (!AggregatorType::match(t))
return false;
const MySumAggregatorType *sumt = static_cast<const MySumAggregatorType *>(t);
if (fname_ != sumt->fname_)
return false;
return true;
}
virtual AggregatorGadget *makeGadget(Table *table, IndexType *intype) const
{
return new AggregatorGadget(this, table, intype);
}
virtual Aggregator *makeAggregator(Table *table, AggregatorGadget *gadget) const
{
return new MySumAggregator;
}
virtual void initialize(TableType *tabtype, IndexType *intype)
{
const RowType *rt = tabtype->rowType();
setRowType(rt); // the result has the same type as the argument
fidx_ = rt->findIdx(fname_);
if (fidx_ < 0)
errors_.fAppend(new Errors(rt->print()), "Unknown field '%s' in the row type:", fname_.c_str());
else {
if (rt->fields()[fidx_].arsz_ != RowType::Field::AR_SCALAR
|| rt->fields()[fidx_].type_->getTypeId() != Type::TT_INT64)
errors_.fAppend(new Errors(rt->print()),
"Field '%s' is not an int64 scalar in the row type:", fname_.c_str());
}
AggregatorType::initialize(tabtype, intype);
}
// called from the handler
int fieldIdx() const
{
return fidx_;
}
protected:
string fname_; // name of the field to sum, must be an int64
int fidx_; // index of field named fname_
};
The constructor accepts the aggregator name and the name of the field on which it will sum. The field name will be translated to field index during initialization, and made available to the MySumAggregator::handler() via the method fieldIdx(). The aggregator type starts with the result row type of NULL, with the actual row type set during initialization. This ability to set the row type later had the latent existence all along, but it's the first time I've made it explicit. It hasn't propagated to the Perl API yet. The idea here is that the result row type of this aggregator is always equal to the input row type, so rather than specifying the result type explicitly and then having to check it for compatibility, why not just take the table's row type when it becomes available? And it works beautifully.
The copy constructor and the constructor with HoldRowTypes are the implementations of the virtual methods copy() and deepCopy(). The deepCopy() is yet another recent addition used in the multithreaded support. I'll describe it in detail later, for now just Do It Like This.
The methods match() and equals() follow the same general shape as everywhere else. makeGadget() creates a generic gadget, and makeAggregator() creates an instance of aggregator for each group.
The interesting stuff starts happening in the initialization. The row type gets found from the table and set as the result type. Then the aggregation field is found in the row type and checked for being of the proper type. Its index is remembered for the later use.
errors_.fAppend() is a new part of the Erref API that makes the error construction more convenient. It is smart enough to check the reference for NULL and allocate a new Errors if so, then append a printf-formatted message and nested errors to it. I've been adding the direct printf-formatting for both Errors and Exceptions, this part of the API is to be documented yet.
Aggregator in C++, part 2
Building an aggregator is a fairly complex subject, so next I want to show a working example. It turns out that I've never actually written an aggregator in C++ before, other than some tiny fragments; I was always working through Perl. So I've written some more interesting examples just now. The full text can be found in the unit test file cpp/table/t_Agg.cpp.
First, if your aggregator is truly stateless and fully hardcoded, the easier way to do it as by defining a plain function with the same handler arguments and building a BasicAggregatorType with it. And here is one that sums the values of an int64 field (the test case aggBasicSum):
void sumC(Table *table, AggregatorGadget *gadget, Index *index,
const IndexType *parentIndexType, GroupHandle *gh, Tray *dest,
Aggregator::AggOp aggop, Rowop::Opcode opcode, RowHandle *rh)
{
// don't send the NULL record after the group becomes empty
if (opcode == Rowop::OP_NOP || parentIndexType->groupSize(gh) == 0)
return;
int64_t sum = 0;
for (RowHandle *rhi = index->begin(); rhi != NULL; rhi = index->next(rhi)) {
sum += table->getRowType()->getInt64(rhi->getRow(), 2, 0); // field c at idx 2
}
// pick the rest of fields from the last row of the group
RowHandle *lastrh = index->last();
// build the result row; relies on the aggregator result being of the
// same type as the rows in the table
FdataVec fields;
table->getRowType()->splitInto(lastrh->getRow(), fields);
fields[2].setPtr(true, &sum, sizeof(sum)); // set the field c from the sum
Rowref res(gadget->getLabel()->getType(), fields);
gadget->sendDelayed(dest, res, opcode);
}
...
Autoref<TableType> tt = TableType::make(rt1)
->addSubIndex("Hashed", HashedIndexType::make( // will be the default index
(new NameSet())->add("e")
)->addSubIndex("Fifo", FifoIndexType::make()
->setAggregator(new BasicAggregatorType("aggr", rt1, sumC))
)
);
...
As I've told before, you create the BasicAggregatorType by giving it the aggregator name, aggregator result row type, and the handler function.
In this case the handler function is completely hardcoded. It works on the int64 field at index 2. The row type I used in this example is:
row {
uint8[10] a,
int32[] b,
int64 c,
float64 d,
string e,
}
So the field is actually named "c", and that's why the aggregator function is named "sumC". But since in this case everything is known in advance, to make it more efficient, the look-up of the field by name has been skipped, and the field index has been pre-computed and placed into the function.
The general outline of the aggregator is exactly the same as in Perl: check for an empty group, then iterate through all the rows in the group and compute a the sum, fill the rest of the fields from the last row, and send the result. The difference is that there is no AggregatorContext, and the calls are done directly on the bits and pieces received as arguments.
The group size is computed as
parentIndexType->groupSize(gh)
This is pretty much the reason for parentIndexType and gh to be passed as arguments to the handler. Why it's done like this is a long story. It allows an individual index (as in instance, not type!) to not care about the number of rows in it. Remember, a group may have multiple "parallel" indexes defined on it, with all of them containing the same rows, and thus the same number of rows. The group handle (gh) contains the common information about the group, including the group size. And the parent index type is the one that manages the group handles inside it, and knows how to extract the information from them.
The iteration is done as usual with the begin() and next(), only called directly on the index. It will return NULL after the last row. The last row can also be found directly with index->last(); on an empty group it would return NULL, so be careful with that.
The input row type for reading the rows from the group is found as:
table->getRowType()
The result row is built in this example by copying the last row and replacing one field. The data from the last row is split into FdataVec (the data itself is not copied at this point but the data descriptors in the construction vector are made to point to the data in the original row). Then the descriptor for the field "c" is changed to point to the computed sum. Then the new row is built from the descriptor.
In this particular case the type of the input rows and of the result rows is the same, so either could have been used to construct the result rows. There are two ways to find the result type:
gadget()->getType()->getRowType()
gadget->getLabel()->getType()
They are exactly the same, just there are two paths leading to the same object.
Finally, the constructed row is sent. sendDelayed() takes care of constructing the rowop from the components.
Potentially, you could even call directly
sendDelayed(dest, gadget->getLabel()->getType()->makeRow(fields), opcode);
However the caveat is that if the table's enqueuing mode is set to EM_IGNORE (which is an old idea, nowadays everyone should use EM_CALL and eventually there will be no other way than EM_CALL, but for now it could happen), the rows would leak. The reason for that leak is that sendDelayed() will skip the rowop creation with EM_IGNORE, and thus will skip the reference creation, and the row would never have a reference to it created and would never be freed. Creating an explicit reference makes sure that the row will always be cleaned up when it becomes unused.
First, if your aggregator is truly stateless and fully hardcoded, the easier way to do it as by defining a plain function with the same handler arguments and building a BasicAggregatorType with it. And here is one that sums the values of an int64 field (the test case aggBasicSum):
void sumC(Table *table, AggregatorGadget *gadget, Index *index,
const IndexType *parentIndexType, GroupHandle *gh, Tray *dest,
Aggregator::AggOp aggop, Rowop::Opcode opcode, RowHandle *rh)
{
// don't send the NULL record after the group becomes empty
if (opcode == Rowop::OP_NOP || parentIndexType->groupSize(gh) == 0)
return;
int64_t sum = 0;
for (RowHandle *rhi = index->begin(); rhi != NULL; rhi = index->next(rhi)) {
sum += table->getRowType()->getInt64(rhi->getRow(), 2, 0); // field c at idx 2
}
// pick the rest of fields from the last row of the group
RowHandle *lastrh = index->last();
// build the result row; relies on the aggregator result being of the
// same type as the rows in the table
FdataVec fields;
table->getRowType()->splitInto(lastrh->getRow(), fields);
fields[2].setPtr(true, &sum, sizeof(sum)); // set the field c from the sum
Rowref res(gadget->getLabel()->getType(), fields);
gadget->sendDelayed(dest, res, opcode);
}
...
Autoref<TableType> tt = TableType::make(rt1)
->addSubIndex("Hashed", HashedIndexType::make( // will be the default index
(new NameSet())->add("e")
)->addSubIndex("Fifo", FifoIndexType::make()
->setAggregator(new BasicAggregatorType("aggr", rt1, sumC))
)
);
...
As I've told before, you create the BasicAggregatorType by giving it the aggregator name, aggregator result row type, and the handler function.
In this case the handler function is completely hardcoded. It works on the int64 field at index 2. The row type I used in this example is:
row {
uint8[10] a,
int32[] b,
int64 c,
float64 d,
string e,
}
So the field is actually named "c", and that's why the aggregator function is named "sumC". But since in this case everything is known in advance, to make it more efficient, the look-up of the field by name has been skipped, and the field index has been pre-computed and placed into the function.
The general outline of the aggregator is exactly the same as in Perl: check for an empty group, then iterate through all the rows in the group and compute a the sum, fill the rest of the fields from the last row, and send the result. The difference is that there is no AggregatorContext, and the calls are done directly on the bits and pieces received as arguments.
The group size is computed as
parentIndexType->groupSize(gh)
This is pretty much the reason for parentIndexType and gh to be passed as arguments to the handler. Why it's done like this is a long story. It allows an individual index (as in instance, not type!) to not care about the number of rows in it. Remember, a group may have multiple "parallel" indexes defined on it, with all of them containing the same rows, and thus the same number of rows. The group handle (gh) contains the common information about the group, including the group size. And the parent index type is the one that manages the group handles inside it, and knows how to extract the information from them.
The iteration is done as usual with the begin() and next(), only called directly on the index. It will return NULL after the last row. The last row can also be found directly with index->last(); on an empty group it would return NULL, so be careful with that.
The input row type for reading the rows from the group is found as:
table->getRowType()
The result row is built in this example by copying the last row and replacing one field. The data from the last row is split into FdataVec (the data itself is not copied at this point but the data descriptors in the construction vector are made to point to the data in the original row). Then the descriptor for the field "c" is changed to point to the computed sum. Then the new row is built from the descriptor.
In this particular case the type of the input rows and of the result rows is the same, so either could have been used to construct the result rows. There are two ways to find the result type:
gadget()->getType()->getRowType()
gadget->getLabel()->getType()
They are exactly the same, just there are two paths leading to the same object.
Finally, the constructed row is sent. sendDelayed() takes care of constructing the rowop from the components.
Potentially, you could even call directly
sendDelayed(dest, gadget->getLabel()->getType()->makeRow(fields), opcode);
However the caveat is that if the table's enqueuing mode is set to EM_IGNORE (which is an old idea, nowadays everyone should use EM_CALL and eventually there will be no other way than EM_CALL, but for now it could happen), the rows would leak. The reason for that leak is that sendDelayed() will skip the rowop creation with EM_IGNORE, and thus will skip the reference creation, and the row would never have a reference to it created and would never be freed. Creating an explicit reference makes sure that the row will always be cleaned up when it becomes unused.
Friday, February 15, 2013
Aggregator in C++, part 1
I've described the AggregatorType and AggregatorGadget before. Now, the last piece of the puzzle is the Aggregator class.
An object of subclass of AggregatorType defines the logic of the aggregation, and there is one of them per a TableType. An object of subclass of AggregatorGadget provides the aggregation output of a Table, and there is one of them per table. An object of subclass of Aggregator keeps the state of an aggregation group, and there is one of them per Index that holds a group in the table.
Repeating what I wrote before:
"The purpose of the Aggregator object created by AggregatorType's makeAggregator() is to keep the state of the group. If you're doing an additive aggregation, it allows you to keep the previous results. If you're doing the optimization of the deletes, it allows you to keep the previous sent row.
What if your aggregator keeps no state? You still have to make an Aggregator for every group, and no, you can't just return NULL, and no, they are not reference-countable, so you have to make a new copy of it for every group (i.e. for every call of makeAggergator()). This looks decidedly sub-optimal, and eventually I'll get around to straighten it out. The good news though is that most of the real aggerators keep the state anyway, so it doesn't matter much."
Once again, the Aggregator object doesn't normally do the logic by itself, it calls its AggregatorType for the logic. It only provides storage for one group's state and lets the AggergatorType logic use that storage.
The Aggregator is defined in table/Aggregator.h. It defines two things: the constants and the virtual method.
The constants are in the enum Aggregator::AggOp, same as defined before in Perl:
AO_BEFORE_MOD,
AO_AFTER_DELETE,
AO_AFTER_INSERT,
AO_COLLAPSE,
Their meaning is also the same. They can be converted to and from the string representation with methods
static const char *aggOpString(int code, const char *def = "???");
static int stringAggOp(const char *code);
They work the same way as the other constant conversion methods.
Notably, there is no explicit Aggregator constructor. The base class Aggregator doesn't have any knowledge of the group state, so the default constructor is good enough. You add this knowledge of the state in your subclass, and there you define your own constructor. Whether the constructor takes any arguments or not is completely up to you, since your subclass of AggregatorType will be calling that constructor.
And the actual work then happens in the method
virtual void handle(Table *table, AggregatorGadget *gadget, Index *index,
const IndexType *parentIndexType, GroupHandle *gh, Tray *dest,
AggOp aggop, Rowop::Opcode opcode, RowHandle *rh);
Your subclass absolutely has to define this method because it's abstract in the base class. The arguments provide the way to get back to the table, its components and its type, so you don't have to pass them through your Aggregator constructor and keep them in your instance, this saves memory.
Notably absent is the pointer to the AggregatorType. This is because the AggregatorGadget already has this pointer, so all you need to to is call
gadget->getType();
You might also want to cast it to your aggregator's type in case if you plan to call your custom methods, like:
MyAggregatorType *at = (MyAggregatorType *)gadget->getType();
Some of the argument types, Index and GroupHandle, have not been described yet, and will be described shortly.
But in general the method handle() is expected to work very much like the aggregator handler does in Perl. It's called at the same times (and indeed the Perl handler is called from the C++-level of the handler), with the same aggop and opcode. The rh is still the current modified row.
A bit of a difference is that the Perl way hides the sensitive arguments in an AggregatorContext while in C++ they are passed directly.
The job of the handle() is to do whatever is necessary, possibly iterate through the group, possibly use the previous state, produce the new state (and possibly remember it), then send the rowop to the gadget. This last step usually is:
gadget->sendDelayed(dest, newrow, opcode);
Here gadget is the AggregatorGadget from the arguments, opcode also helpfully comes from the arguments (and if it's OP_NOP, there is no need to send anything, it will be thrown away anyway), and newrow is the newly computed result Row (not RowHandle!). Obviously, the type of the result row must match the output row type of the AggregatorGadget, or all kinds of bad things will happen. dest is also an argument of handle(), and is a tray that will hold the aggregator results until a proper time when they can be sent. The "delayed" part of "sendDelayed" means exactly that: the created rowops are not sent directly to the gadget's output but are collected in a Tray until the Table decides that it's the right time to send them.
You absolutely must not use the Gadget's default method send(), use only sendDelayed(). To help with this discipline, AggregatorGadget hides the send() method and makes only the sendDelayed() visible.
An object of subclass of AggregatorType defines the logic of the aggregation, and there is one of them per a TableType. An object of subclass of AggregatorGadget provides the aggregation output of a Table, and there is one of them per table. An object of subclass of Aggregator keeps the state of an aggregation group, and there is one of them per Index that holds a group in the table.
Repeating what I wrote before:
"The purpose of the Aggregator object created by AggregatorType's makeAggregator() is to keep the state of the group. If you're doing an additive aggregation, it allows you to keep the previous results. If you're doing the optimization of the deletes, it allows you to keep the previous sent row.
What if your aggregator keeps no state? You still have to make an Aggregator for every group, and no, you can't just return NULL, and no, they are not reference-countable, so you have to make a new copy of it for every group (i.e. for every call of makeAggergator()). This looks decidedly sub-optimal, and eventually I'll get around to straighten it out. The good news though is that most of the real aggerators keep the state anyway, so it doesn't matter much."
Once again, the Aggregator object doesn't normally do the logic by itself, it calls its AggregatorType for the logic. It only provides storage for one group's state and lets the AggergatorType logic use that storage.
The Aggregator is defined in table/Aggregator.h. It defines two things: the constants and the virtual method.
The constants are in the enum Aggregator::AggOp, same as defined before in Perl:
AO_BEFORE_MOD,
AO_AFTER_DELETE,
AO_AFTER_INSERT,
AO_COLLAPSE,
Their meaning is also the same. They can be converted to and from the string representation with methods
static const char *aggOpString(int code, const char *def = "???");
static int stringAggOp(const char *code);
They work the same way as the other constant conversion methods.
Notably, there is no explicit Aggregator constructor. The base class Aggregator doesn't have any knowledge of the group state, so the default constructor is good enough. You add this knowledge of the state in your subclass, and there you define your own constructor. Whether the constructor takes any arguments or not is completely up to you, since your subclass of AggregatorType will be calling that constructor.
And the actual work then happens in the method
virtual void handle(Table *table, AggregatorGadget *gadget, Index *index,
const IndexType *parentIndexType, GroupHandle *gh, Tray *dest,
AggOp aggop, Rowop::Opcode opcode, RowHandle *rh);
Your subclass absolutely has to define this method because it's abstract in the base class. The arguments provide the way to get back to the table, its components and its type, so you don't have to pass them through your Aggregator constructor and keep them in your instance, this saves memory.
Notably absent is the pointer to the AggregatorType. This is because the AggregatorGadget already has this pointer, so all you need to to is call
gadget->getType();
You might also want to cast it to your aggregator's type in case if you plan to call your custom methods, like:
MyAggregatorType *at = (MyAggregatorType *)gadget->getType();
Some of the argument types, Index and GroupHandle, have not been described yet, and will be described shortly.
But in general the method handle() is expected to work very much like the aggregator handler does in Perl. It's called at the same times (and indeed the Perl handler is called from the C++-level of the handler), with the same aggop and opcode. The rh is still the current modified row.
A bit of a difference is that the Perl way hides the sensitive arguments in an AggregatorContext while in C++ they are passed directly.
The job of the handle() is to do whatever is necessary, possibly iterate through the group, possibly use the previous state, produce the new state (and possibly remember it), then send the rowop to the gadget. This last step usually is:
gadget->sendDelayed(dest, newrow, opcode);
Here gadget is the AggregatorGadget from the arguments, opcode also helpfully comes from the arguments (and if it's OP_NOP, there is no need to send anything, it will be thrown away anyway), and newrow is the newly computed result Row (not RowHandle!). Obviously, the type of the result row must match the output row type of the AggregatorGadget, or all kinds of bad things will happen. dest is also an argument of handle(), and is a tray that will hold the aggregator results until a proper time when they can be sent. The "delayed" part of "sendDelayed" means exactly that: the created rowops are not sent directly to the gadget's output but are collected in a Tray until the Table decides that it's the right time to send them.
You absolutely must not use the Gadget's default method send(), use only sendDelayed(). To help with this discipline, AggregatorGadget hides the send() method and makes only the sendDelayed() visible.
Friday, February 8, 2013
little table type additions
As I'm working on the multithreading, this has created some little additions that have been missed before.
First, I've added a method to copy a whole table type. In Perl it is:
$newtt = $oldtt->copy();
In C++ it is:
TableType *copy();
This copies the table type, along with copying all the index types in it, since each table type must have its own instances of the index types. The copied table is always uninitialized.
In case if the table type collected errors, the errors aren't copied, and you should not copy such a table type. This caveat applies to the C++ code, since the Perl code would not let you create a table type with errors, it would fail on these errors immediately.
Second, in the Perl implementation of AggregatorType I've added a method to get back the row type:
$rt = $aggtype->getRowType();
First, I've added a method to copy a whole table type. In Perl it is:
$newtt = $oldtt->copy();
In C++ it is:
TableType *copy();
This copies the table type, along with copying all the index types in it, since each table type must have its own instances of the index types. The copied table is always uninitialized.
In case if the table type collected errors, the errors aren't copied, and you should not copy such a table type. This caveat applies to the C++ code, since the Perl code would not let you create a table type with errors, it would fail on these errors immediately.
Second, in the Perl implementation of AggregatorType I've added a method to get back the row type:
$rt = $aggtype->getRowType();
Wednesday, January 30, 2013
another approach to the weak references
I've recently developed another idea about the weak references. So, let's look at the basic problem:
There is an object A that has a reference to object B.
A->B
The object B needs to be able to reach back to the object A. But it can't have a reference because that would create a reference cycle. So instead it has to have some kind of weak reference. In the single-threaded situation it can be as simple as a plain pointer that gets reset to NULL when the object A is destroyed, like for example the Triceps Label has a pointer to its Unit.
There could be lots of objects B per object A. So when the object A gets destroyed, it needs to take care to clear the weak references in all the objects B. Which is kind of annoying. And it easily gets worse if the connection is not direct but multi-level, say A->B->C and then C having a back reference back to A.
However there is a way around it. Create a single object W that will keep a weak reference back to A. Then all the objects B can just keep a reference to W. And of course A would also keep a reference to W. When A is destroyed, it will reset the single weak reference in W.
A------------>W
\-->B1-->/
\-->B2-->/
\-->B3-->/
Then the logic for creating and resetting the object W can be placed into a base class and then reused all over the place. The object W could even hold the pieces of information that can be still useful after the object A goes away, such as the name of the object A for the error messages.
There is an object A that has a reference to object B.
A->B
The object B needs to be able to reach back to the object A. But it can't have a reference because that would create a reference cycle. So instead it has to have some kind of weak reference. In the single-threaded situation it can be as simple as a plain pointer that gets reset to NULL when the object A is destroyed, like for example the Triceps Label has a pointer to its Unit.
There could be lots of objects B per object A. So when the object A gets destroyed, it needs to take care to clear the weak references in all the objects B. Which is kind of annoying. And it easily gets worse if the connection is not direct but multi-level, say A->B->C and then C having a back reference back to A.
However there is a way around it. Create a single object W that will keep a weak reference back to A. Then all the objects B can just keep a reference to W. And of course A would also keep a reference to W. When A is destroyed, it will reset the single weak reference in W.
A------------>W
\-->B1-->/
\-->B2-->/
\-->B3-->/
Then the logic for creating and resetting the object W can be placed into a base class and then reused all over the place. The object W could even hold the pieces of information that can be still useful after the object A goes away, such as the name of the object A for the error messages.
Tuesday, January 29, 2013
Table in C++
The Table is defined in table/Table.h. It inherits from Gadget, with the table's output label being the gadget's output label. Naturally, it's an Starget and usable from one thread only.
It's constructor is not public, and it's created from the TableType with its method makeTable():
Autoref<Table> t = tabType->makeTable(unit, Table::EM_CALL, "t");
The arguments are the unit where the table will belong, the enqueueing mode for its output label (this is a legacy argument and will go away soon), and the name of the table.
For the reference, that TableType method is:
Onceref<Table> makeTable(Unit *unit, Gadget::EnqMode emode, const string &name) const;
The table has a large number of methods, grouped into multiple subsets.
EnqMode getEnqMode() const;
const string &getName() const;
Unit *getUnit() const;
Label *getLabel() const;
These methods are inherited from the Gadget. The only special thing to remember is that getLabel() returns the table's output label. Technically, getName() has an overriding implementation in the Table, to return the table's name while its output label has the suffix ".out" appended to it.
const TableType *getType() const;
const RowType *getRowType() const;
const RowHandleType *getRhType() const;
Get back the type of the table, of its rows, and its row handles.
Label *getInputLabel() const;
Label *getPreLabel() const;
Label *getDumpLabel() const;
Label *getAggregatorLabel(const string &agname) const;
Get the assorted labels. The aggregator label getter takes the name of the aggregator (as was defined in the TableType) as an argument.
FnReturn *fnReturn() const;
Get the FnReturn of this table's outputs. It gets created and remembered on the first call, and the subsequent calls return the same object. It has a few labels with the fixed names: "out", "pre" and "dump", and a label for each aggregator with the aggregator's name. It could throw an Exception if you name an aggregator to conflict with one of the fixed labels, which you should not. The return's name will be "tableName.fret".
Next go the operations on the table (and of course the table may also be modified by sending rowops to its input label).
RowHandle *makeRowHandle(const Row *row) const;
Create a row handle for a row. Remember, the row handles are reference-counted, and also have the special kind of references with Rhref. So the returned pointer should be stored in an Rhref. The row handle created will not be inserted into the table yet.
bool insert(RowHandle *rh);
Insert a row handle into the table. This invokes all the row replacement policies along the way. If the handle is already in the table, does nothing and returns false. May also return false if a replacement policy refuses the row, but in practice there are no such refusing policies yet. Otherwise returns true.
It may throw an Exception. It may throw by itself if the row handle doesn't belong to this table or propagate the exception up: since the execution involves calling the output labels and such, an exception might be thrown from there.
bool insertRow(const Row *row);
The version that combines the row handle construction and insertion. Unlike Perl, in C++ this method is named differently instead of overloading. The comments about the replacement policies and return code, and about exceptions apply here too.
void remove(RowHandle *rh);
Remove a row handle from the table. If the handle is not in the table, silently does nothing. May throw an Exception.
bool deleteRow(const Row *row);
Find a matching row and delete it. Returns true if the row was found and removed, false if not found. May throw an Exception.
void clear(size_t limit = 0);
Clear the table by removing all the rows from it. The removed rows are sent as usual to the "pre" and "out" labels. If the limit is not 0, no more than that number of the rows will be removed. The rows are removed in the usual order of the first leaf index.
Next go the iteration methods. The rule of thumb is that for them a NULL row handle pointer means "end of iteration" or "not found" (or sometimes "bad arguments"). And they can handle the NULL row handles OK on the input, just returning NULL on the output.
RowHandle *begin() const;
Get the first row handle in the default order of the first leaf index. If the table is empty, returns NULL.
RowHandle *beginIdx(IndexType *ixt) const;
Get the first handle in the order of a particular index. The index type must belong to this table's type. For an incorrect index type it returns NULL (perhaps in the future this will be changed to an exception).
RowHandle *next(const RowHandle *cur) const;
RowHandle *nextIdx(IndexType *ixt, const RowHandle *cur) const;
Get the next row handle in the order of the default or specific index. Returns NULL after the last handle. It's safe to pass the current row handle as NULL, the result will be NULL, and also on any other error.
RowHandle *firstOfGroupIdx(IndexType *ixt, const RowHandle *cur) const;
RowHandle *lastOfGroupIdx(IndexType *ixt, const RowHandle *cur) const;
Get the first or last row handle in the same group as the current row according to a non-leaf index. The NULL current handle will cause NULL returned. See the details in the description of the Perl API.
RowHandle *nextGroupIdx(IndexType *ixt, const RowHandle *cur) const;
Get the first row handle of the next group. The return will be NULL if the current group was the last one, or if the current handle is NULL.
Next go the size operations:
size_t size() const;
Get the number of rows currently in the table.
size_t groupSizeIdx(IndexType *ixt, const RowHandle *what) const;
Get the size of the group where the handle belongs according to a non-leaf index. If any arguments are wrong, returns 0. The row handle doesn't have to be in the table. If it isn't in the table, the method will find the group where the row would belong if it were inserted and return its current size.
size_t groupSizeRowIdx(IndexType *ixt, const Row *what) const;
A convenience version that makes a row handle from a row, finds the group size and disposes of the handle.
Next go the finding methods:
RowHandle *find(const RowHandle *what) const;
RowHandle *findIdx(IndexType *ixt, const RowHandle *what) const;
Find the handle of a matching row according to the default (first leaf) or the specific index, or return NULL if not found.
RowHandle *findRow(const Row *what) const;
RowHandle *findRowIdx(IndexType *ixt, const Row *what) const;
The convenience versions that create a temporary row handle and then perform the search.
Next goes the dump API that sends the whole contents of the table to the "dump" label, thus making any labels connected to it perform an implicit iteration over the table.
void dumpAll(Rowop::Opcode op = Rowop::OP_INSERT) const;
void dumpAllIdx(IndexType *ixt, Rowop::Opcode op = Rowop::OP_INSERT) const;
The dump can go in the order of default or specific index. The opcode argument is used for the rowops sent on the dump label. Using the argument index type of NULL makes dumpAllIdx() use the default index and work just like DumpAll(). In the furute there probably will be methods that dump only a group of records.
As usual, the general logic of the methods matches the Perl API unless said otherwise. Please refer to the Perl API description for the details and examples.
It's constructor is not public, and it's created from the TableType with its method makeTable():
Autoref<Table> t = tabType->makeTable(unit, Table::EM_CALL, "t");
The arguments are the unit where the table will belong, the enqueueing mode for its output label (this is a legacy argument and will go away soon), and the name of the table.
For the reference, that TableType method is:
Onceref<Table> makeTable(Unit *unit, Gadget::EnqMode emode, const string &name) const;
The table has a large number of methods, grouped into multiple subsets.
EnqMode getEnqMode() const;
const string &getName() const;
Unit *getUnit() const;
Label *getLabel() const;
These methods are inherited from the Gadget. The only special thing to remember is that getLabel() returns the table's output label. Technically, getName() has an overriding implementation in the Table, to return the table's name while its output label has the suffix ".out" appended to it.
const TableType *getType() const;
const RowType *getRowType() const;
const RowHandleType *getRhType() const;
Get back the type of the table, of its rows, and its row handles.
Label *getInputLabel() const;
Label *getPreLabel() const;
Label *getDumpLabel() const;
Label *getAggregatorLabel(const string &agname) const;
Get the assorted labels. The aggregator label getter takes the name of the aggregator (as was defined in the TableType) as an argument.
FnReturn *fnReturn() const;
Get the FnReturn of this table's outputs. It gets created and remembered on the first call, and the subsequent calls return the same object. It has a few labels with the fixed names: "out", "pre" and "dump", and a label for each aggregator with the aggregator's name. It could throw an Exception if you name an aggregator to conflict with one of the fixed labels, which you should not. The return's name will be "tableName.fret".
Next go the operations on the table (and of course the table may also be modified by sending rowops to its input label).
RowHandle *makeRowHandle(const Row *row) const;
Create a row handle for a row. Remember, the row handles are reference-counted, and also have the special kind of references with Rhref. So the returned pointer should be stored in an Rhref. The row handle created will not be inserted into the table yet.
bool insert(RowHandle *rh);
Insert a row handle into the table. This invokes all the row replacement policies along the way. If the handle is already in the table, does nothing and returns false. May also return false if a replacement policy refuses the row, but in practice there are no such refusing policies yet. Otherwise returns true.
It may throw an Exception. It may throw by itself if the row handle doesn't belong to this table or propagate the exception up: since the execution involves calling the output labels and such, an exception might be thrown from there.
bool insertRow(const Row *row);
The version that combines the row handle construction and insertion. Unlike Perl, in C++ this method is named differently instead of overloading. The comments about the replacement policies and return code, and about exceptions apply here too.
void remove(RowHandle *rh);
Remove a row handle from the table. If the handle is not in the table, silently does nothing. May throw an Exception.
bool deleteRow(const Row *row);
Find a matching row and delete it. Returns true if the row was found and removed, false if not found. May throw an Exception.
void clear(size_t limit = 0);
Clear the table by removing all the rows from it. The removed rows are sent as usual to the "pre" and "out" labels. If the limit is not 0, no more than that number of the rows will be removed. The rows are removed in the usual order of the first leaf index.
Next go the iteration methods. The rule of thumb is that for them a NULL row handle pointer means "end of iteration" or "not found" (or sometimes "bad arguments"). And they can handle the NULL row handles OK on the input, just returning NULL on the output.
RowHandle *begin() const;
Get the first row handle in the default order of the first leaf index. If the table is empty, returns NULL.
RowHandle *beginIdx(IndexType *ixt) const;
Get the first handle in the order of a particular index. The index type must belong to this table's type. For an incorrect index type it returns NULL (perhaps in the future this will be changed to an exception).
RowHandle *next(const RowHandle *cur) const;
RowHandle *nextIdx(IndexType *ixt, const RowHandle *cur) const;
Get the next row handle in the order of the default or specific index. Returns NULL after the last handle. It's safe to pass the current row handle as NULL, the result will be NULL, and also on any other error.
RowHandle *firstOfGroupIdx(IndexType *ixt, const RowHandle *cur) const;
RowHandle *lastOfGroupIdx(IndexType *ixt, const RowHandle *cur) const;
Get the first or last row handle in the same group as the current row according to a non-leaf index. The NULL current handle will cause NULL returned. See the details in the description of the Perl API.
RowHandle *nextGroupIdx(IndexType *ixt, const RowHandle *cur) const;
Get the first row handle of the next group. The return will be NULL if the current group was the last one, or if the current handle is NULL.
Next go the size operations:
size_t size() const;
Get the number of rows currently in the table.
size_t groupSizeIdx(IndexType *ixt, const RowHandle *what) const;
Get the size of the group where the handle belongs according to a non-leaf index. If any arguments are wrong, returns 0. The row handle doesn't have to be in the table. If it isn't in the table, the method will find the group where the row would belong if it were inserted and return its current size.
size_t groupSizeRowIdx(IndexType *ixt, const Row *what) const;
A convenience version that makes a row handle from a row, finds the group size and disposes of the handle.
Next go the finding methods:
RowHandle *find(const RowHandle *what) const;
RowHandle *findIdx(IndexType *ixt, const RowHandle *what) const;
Find the handle of a matching row according to the default (first leaf) or the specific index, or return NULL if not found.
RowHandle *findRow(const Row *what) const;
RowHandle *findRowIdx(IndexType *ixt, const Row *what) const;
The convenience versions that create a temporary row handle and then perform the search.
Next goes the dump API that sends the whole contents of the table to the "dump" label, thus making any labels connected to it perform an implicit iteration over the table.
void dumpAll(Rowop::Opcode op = Rowop::OP_INSERT) const;
void dumpAllIdx(IndexType *ixt, Rowop::Opcode op = Rowop::OP_INSERT) const;
The dump can go in the order of default or specific index. The opcode argument is used for the rowops sent on the dump label. Using the argument index type of NULL makes dumpAllIdx() use the default index and work just like DumpAll(). In the furute there probably will be methods that dump only a group of records.
As usual, the general logic of the methods matches the Perl API unless said otherwise. Please refer to the Perl API description for the details and examples.
Thursday, January 24, 2013
FnBinding in C++
FnBinding is defined in sched/FnBinding.h, and substantially matches the Perl version. It inherits from Starget, and can be used in only one thread.
Like many other classes, it has the constructor and the static make() function:
FnBinding(const string &name, FnReturn *fn);
static FnBinding *make(const string &name, FnReturn *fn);
The binding is constructed on a specific FnReturn and gets (references) the RowSetType from it. The FnReturn must be initialized before it can be used to create the bindings. It can be used with any matching FnReturn, not just the one it was constructed with.
It's generally constructed in a chain fashion:
Autoref<FnBinding> bind = FnBinding::make(fn)
->addLabel("lb1", lb1, true)
->addLabel("lb2", lb2, false);
Each method in the chain returns the same FnBinding object. The method addLabel() adds one concrete label that gets connected to the FnReturn's label by name. The other chainable method is withTray() which switches the mode of collecting the resulting rowops in a tray rather than calling them immediately.
The errors encountered during the chained construction are remembered and can be read later with the method:
Erref getErrors() const;
You must check the bindings for errors before using it. A binding with errors may not be used.
Or you can use the checkOrThrow() wrapper from common/Initialize.h to automatically convert any detected errors to an Exception:
Autoref<FnBinding> bind = checkOrThrow(FnBinding::make(fn)
->addLabel("lb1", lb1, true)
->addLabel("lb2", lb2, false)
->withTray(true)
);
FnBinding *addLabel(const string &name, Autoref<Label> lb, bool autoclear);
Adds a label to the binding. The name must match a name from the FnReturn, and there may be only one label bound to a name (some names from the return may be left unbound). The label must have a type matching the named FnReturn's label. The autoclear flag enables the automatic clearing of the label (and also forgetting it in the Unit) when the binding gets destroyed. This allows to create and destroy the bindings dynamically as needed. So, basically, if you've created a label just for the binding, use autoclear==true. If you do a binding to a label that exists in the model by itself and can be used without the binding, use autoclear==false.
In principle, nothing stops you from adding more labels later (though you can't remove nor replace the labels that are already added). Just make sure that their types match the expected ones.
Not all the available names have to get the labels added. Some (or all) may be left without labels. Any rowops coming to the undefined ones will be simply ignored.
The labels in the FnBinding may belong to a different Unit than the FnReturn. This allows to use the FnReturn/FnBinding coupling to connect the units.
FnBinding *withTray(bool on);
Changes the tray collection mode, the true argument enables it, false disables. Can be done at any time, not just at construction. Disabling the tray mode discards the current tray. If the tray mode is enabled, whenever the binding is pushed onto a return and the rowops come into it, the labels in this binding won't be called immediately but they would adopt the incoming rowops, and the result will be queued into a tray, to be executed later.
Onceref<Tray> swapTray();
Used with the tray collection mode, normally after some rowops have been collected in the tray. Returns the current tray and replaces it in the binding with a new clean tray. You can call the returned tray afterwards. If the tray mode is not enabled, will return NULL, and won't create a new tray.
Tray *getTray() const;
Get the current tray. You can use and modify the tray contents in any usual way. If the tray mode is not enabled, will return NULL.
void callTray();
A convenience combination method that swaps the tray and calls it. This method is smart about the labels belonging to different units. Each rowop in the tray is called with its proper unit, that is found from the rowop's label. Mixing the labels of multiple units in one binding is probably still not such a great idea, but it works anyway.
const string &getName() const;
Get back the binding's name.
RowSetType *getType() const;
Get the type of the binding. It will be the same row set type object as created in the FnReturn that was used to construct this FnBinding.
int size() const;
Get the number of labels in the row set type (of all available labels, not just the ones that have been added).
const RowSetType::NameVec &getLabelNames() const;
const RowSetType::RowTypeVec &getRowTypes() const;
const string *getLabelName(int idx) const;
RowType *getRowType(const string &name) const;
RowType *getRowType(int idx) const;
The convenience wrappers that translate to the same methods in the RowSetType.
Label *getLabel(const string &name) const;
int findLabel(const string &name) const;
Label *getLabel(int idx) const;
Methods similar to FnReturn that allow to translate the names to indexes and get the labels by name or index. The same return values, the index -1 is returned for an unknown name, and a NULL label pointer is returned for an unknown name, incorrect index and a undefined label at a correct name or index.
typedef vector<Autoref<Label> > LabelVec;
const LabelVec &getLabels() const;
Return all the labels as a vector. This is an internal vector of the class, so only a const reference is returned. The elements for undefined labels will contain NULLs.
typedef vector<bool> BoolVec;
const BoolVec &getAutoclear() const;
Return the vector of the autoclear flags for the labels.
bool isAutoclear(const string &name) const;
Get the autoclear flag for a label by name. If the name is unknown, will quietly return false.
bool equals(const FnReturn *t) const;
bool match(const FnReturn *t) const;
bool equals(const FnBinding *t) const;
bool match(const FnBinding *t) const;
Similarly to the FnReturn, the convenience methods that compare the types between the FnReturns and FnBindings. They really translate to the same methods on the types of the returns or bindings.
Like many other classes, it has the constructor and the static make() function:
FnBinding(const string &name, FnReturn *fn);
static FnBinding *make(const string &name, FnReturn *fn);
The binding is constructed on a specific FnReturn and gets (references) the RowSetType from it. The FnReturn must be initialized before it can be used to create the bindings. It can be used with any matching FnReturn, not just the one it was constructed with.
It's generally constructed in a chain fashion:
Autoref<FnBinding> bind = FnBinding::make(fn)
->addLabel("lb1", lb1, true)
->addLabel("lb2", lb2, false);
Each method in the chain returns the same FnBinding object. The method addLabel() adds one concrete label that gets connected to the FnReturn's label by name. The other chainable method is withTray() which switches the mode of collecting the resulting rowops in a tray rather than calling them immediately.
The errors encountered during the chained construction are remembered and can be read later with the method:
Erref getErrors() const;
You must check the bindings for errors before using it. A binding with errors may not be used.
Or you can use the checkOrThrow() wrapper from common/Initialize.h to automatically convert any detected errors to an Exception:
Autoref<FnBinding> bind = checkOrThrow(FnBinding::make(fn)
->addLabel("lb1", lb1, true)
->addLabel("lb2", lb2, false)
->withTray(true)
);
FnBinding *addLabel(const string &name, Autoref<Label> lb, bool autoclear);
Adds a label to the binding. The name must match a name from the FnReturn, and there may be only one label bound to a name (some names from the return may be left unbound). The label must have a type matching the named FnReturn's label. The autoclear flag enables the automatic clearing of the label (and also forgetting it in the Unit) when the binding gets destroyed. This allows to create and destroy the bindings dynamically as needed. So, basically, if you've created a label just for the binding, use autoclear==true. If you do a binding to a label that exists in the model by itself and can be used without the binding, use autoclear==false.
In principle, nothing stops you from adding more labels later (though you can't remove nor replace the labels that are already added). Just make sure that their types match the expected ones.
Not all the available names have to get the labels added. Some (or all) may be left without labels. Any rowops coming to the undefined ones will be simply ignored.
The labels in the FnBinding may belong to a different Unit than the FnReturn. This allows to use the FnReturn/FnBinding coupling to connect the units.
FnBinding *withTray(bool on);
Changes the tray collection mode, the true argument enables it, false disables. Can be done at any time, not just at construction. Disabling the tray mode discards the current tray. If the tray mode is enabled, whenever the binding is pushed onto a return and the rowops come into it, the labels in this binding won't be called immediately but they would adopt the incoming rowops, and the result will be queued into a tray, to be executed later.
Onceref<Tray> swapTray();
Used with the tray collection mode, normally after some rowops have been collected in the tray. Returns the current tray and replaces it in the binding with a new clean tray. You can call the returned tray afterwards. If the tray mode is not enabled, will return NULL, and won't create a new tray.
Tray *getTray() const;
Get the current tray. You can use and modify the tray contents in any usual way. If the tray mode is not enabled, will return NULL.
void callTray();
A convenience combination method that swaps the tray and calls it. This method is smart about the labels belonging to different units. Each rowop in the tray is called with its proper unit, that is found from the rowop's label. Mixing the labels of multiple units in one binding is probably still not such a great idea, but it works anyway.
const string &getName() const;
Get back the binding's name.
RowSetType *getType() const;
Get the type of the binding. It will be the same row set type object as created in the FnReturn that was used to construct this FnBinding.
int size() const;
Get the number of labels in the row set type (of all available labels, not just the ones that have been added).
const RowSetType::NameVec &getLabelNames() const;
const RowSetType::RowTypeVec &getRowTypes() const;
const string *getLabelName(int idx) const;
RowType *getRowType(const string &name) const;
RowType *getRowType(int idx) const;
The convenience wrappers that translate to the same methods in the RowSetType.
Label *getLabel(const string &name) const;
int findLabel(const string &name) const;
Label *getLabel(int idx) const;
Methods similar to FnReturn that allow to translate the names to indexes and get the labels by name or index. The same return values, the index -1 is returned for an unknown name, and a NULL label pointer is returned for an unknown name, incorrect index and a undefined label at a correct name or index.
typedef vector<Autoref<Label> > LabelVec;
const LabelVec &getLabels() const;
Return all the labels as a vector. This is an internal vector of the class, so only a const reference is returned. The elements for undefined labels will contain NULLs.
typedef vector<bool> BoolVec;
const BoolVec &getAutoclear() const;
Return the vector of the autoclear flags for the labels.
bool isAutoclear(const string &name) const;
Get the autoclear flag for a label by name. If the name is unknown, will quietly return false.
bool equals(const FnReturn *t) const;
bool match(const FnReturn *t) const;
bool equals(const FnBinding *t) const;
bool match(const FnBinding *t) const;
Similarly to the FnReturn, the convenience methods that compare the types between the FnReturns and FnBindings. They really translate to the same methods on the types of the returns or bindings.
Tuesday, January 1, 2013
Streaming function helper classes
A couple more of helper classes are defined in sched/FnReturn.h.
ScopeFnBind does a scoped pushing and popping of a binding on an FnReturn. Its only method is the constructor:
ScopeFnBind(Onceref<FnReturn> ret, Onceref<FnBinding> binding);
It's used as:
{
ScopeFnBind autobind(ret, binding);
...
}
It will pop the binding at the end of the block. An unpleasant feature is that if the return stack get messed up, it will throw an Exception from a destructor, which is a big no-no in C++. However since normally in the C++ code the Triceps Exception is essentially an abort, this works good enough. If you make the Exception catchable, such as when calling the C++ code from an interpreter, you better make very sure that the stack can not get corrupted, or do not use ScopeFnBind.
AutoFnBind is a further extension of the scoped binding. It does three additional things: It allows to push multiple bindings on multiple returns as a group, popping them all on destruction. It's a reference-counted Starget object, which allows the scope to be more than one block. It also has a more controllable way of dealing with the exceptions. This last two properties allow to use it from the Perl code, making the scope of a Perl block, not C++ block, and to pass the exceptions properly back to Perl.
AutoFnBind();
AutoFnBind *make();
The constructor just creates an empty object which then gets filled with bindings.
AutoFnBind *add(Onceref<FnReturn> ret, Autoref<FnBinding> binding);
Add a binding, in a chainable fashion. The simple-minded of using the AutoFnBind is:
{
Autoref<AutoFnBind> bind = AutoFnBind::make()
->add(ret1, binding1)
->add(ret2, binding2);
...
}
However if any of these add()s throw an Exception, this will leave an orphaned AutoFnBind object, since the throwing would happen before it has a chance to do the reference-counting. So the safer way to use it is:
{
Autoref<AutoFnBind> bind = new AutoFnBind;
bind
->add(ret1, binding1)
->add(ret2, binding2);
...
}
Then the AutoFnBind will be reference-counted first, and if an add() throws later, this will cause a controlled destruction of the Autoref and of AutoFnBind.
But it's not the end of the story yet. The throws on destruction are still a possibility. To catch them, use an explicit clearing:
void clear();
Pops all the bindings. If any Exceptions get thrown, they can get caught nicely. It tries to be real smart, going through all the bindings in the backwards order and popping each one of them. If a pop() throws an exception, its information will be collected but clear() will then continue going through the whole list. At the end of the run it will make sure that it doesn't have any references to anything any more, and then will re-throw any collected errors as a single Exception. This cleans up the things as much as possible and as much as can be handled, but the end result will still not be particularly clean: the returns that got their stacks corrupted will still have their stacks corrupted, and some very serious application-level cleaning will be needed to continue. Probably a better choice would be to destroy everything and restart from scratch. But at least it allows to get safely to this point of restarting from scratch.
So, the full correct sequence will be:
{
Autoref<AutoFnBind> bind = new AutoFnBind;
bind
->add(ret1, binding1)
->add(ret2, binding2);
...
bind->clear() ;
}
Or if any code in "..." can throw anything, then something like (not tested, so use with caution):
{
Autoref<AutoFnBind> bind = new AutoFnBind;
bind
->add(ret1, binding1)
->add(ret2, binding2);
try {
...
} catch (Triceps::Exception e) {
try {
bind->clear() ;
} catch (Triceps::Exception ee) {
e->getErrors()->append("Unbinding errors triggered by the last error:", ee->getErrors());
}
throw;
} catch (exception e) {
bind->clear() ;
throw;
}
}
It tries to be nice if the exception thrown from "..." was a Triceps one, and add nicely any errors from the binding clearing to it.
Finally, a little about how the Perl AutoFnBind translates to the C++ AutoFnBind:
The Perl constructor creates the C++-level object and adds the bindings to it. If any of them throw, it destroys everything nicely and translates the Exception to Perl. Otherwise it saves a reference to the AutoFnBind in a wrapper object that gets returned to Perl.
The Perl destructor then first clears the AutoFnBind and catches if there is any Exception. However there is just no way to return a Perl exception from a Perl destructor, so it juts prints the error on stderr and calls exit(1). If no exception was thrown, the AutoFnBind gets destroyed nicely by removing the last reference.
For the nicer handling, there is a Perl-level method clear() that does the clearing and translates the exception to Perl.
ScopeFnBind does a scoped pushing and popping of a binding on an FnReturn. Its only method is the constructor:
ScopeFnBind(Onceref<FnReturn> ret, Onceref<FnBinding> binding);
It's used as:
{
ScopeFnBind autobind(ret, binding);
...
}
It will pop the binding at the end of the block. An unpleasant feature is that if the return stack get messed up, it will throw an Exception from a destructor, which is a big no-no in C++. However since normally in the C++ code the Triceps Exception is essentially an abort, this works good enough. If you make the Exception catchable, such as when calling the C++ code from an interpreter, you better make very sure that the stack can not get corrupted, or do not use ScopeFnBind.
AutoFnBind is a further extension of the scoped binding. It does three additional things: It allows to push multiple bindings on multiple returns as a group, popping them all on destruction. It's a reference-counted Starget object, which allows the scope to be more than one block. It also has a more controllable way of dealing with the exceptions. This last two properties allow to use it from the Perl code, making the scope of a Perl block, not C++ block, and to pass the exceptions properly back to Perl.
AutoFnBind();
AutoFnBind *make();
The constructor just creates an empty object which then gets filled with bindings.
AutoFnBind *add(Onceref<FnReturn> ret, Autoref<FnBinding> binding);
Add a binding, in a chainable fashion. The simple-minded of using the AutoFnBind is:
{
Autoref<AutoFnBind> bind = AutoFnBind::make()
->add(ret1, binding1)
->add(ret2, binding2);
...
}
However if any of these add()s throw an Exception, this will leave an orphaned AutoFnBind object, since the throwing would happen before it has a chance to do the reference-counting. So the safer way to use it is:
{
Autoref<AutoFnBind> bind = new AutoFnBind;
bind
->add(ret1, binding1)
->add(ret2, binding2);
...
}
Then the AutoFnBind will be reference-counted first, and if an add() throws later, this will cause a controlled destruction of the Autoref and of AutoFnBind.
But it's not the end of the story yet. The throws on destruction are still a possibility. To catch them, use an explicit clearing:
void clear();
Pops all the bindings. If any Exceptions get thrown, they can get caught nicely. It tries to be real smart, going through all the bindings in the backwards order and popping each one of them. If a pop() throws an exception, its information will be collected but clear() will then continue going through the whole list. At the end of the run it will make sure that it doesn't have any references to anything any more, and then will re-throw any collected errors as a single Exception. This cleans up the things as much as possible and as much as can be handled, but the end result will still not be particularly clean: the returns that got their stacks corrupted will still have their stacks corrupted, and some very serious application-level cleaning will be needed to continue. Probably a better choice would be to destroy everything and restart from scratch. But at least it allows to get safely to this point of restarting from scratch.
So, the full correct sequence will be:
{
Autoref<AutoFnBind> bind = new AutoFnBind;
bind
->add(ret1, binding1)
->add(ret2, binding2);
...
bind->clear() ;
}
Or if any code in "..." can throw anything, then something like (not tested, so use with caution):
{
Autoref<AutoFnBind> bind = new AutoFnBind;
bind
->add(ret1, binding1)
->add(ret2, binding2);
try {
...
} catch (Triceps::Exception e) {
try {
bind->clear() ;
} catch (Triceps::Exception ee) {
e->getErrors()->append("Unbinding errors triggered by the last error:", ee->getErrors());
}
throw;
} catch (exception e) {
bind->clear() ;
throw;
}
}
It tries to be nice if the exception thrown from "..." was a Triceps one, and add nicely any errors from the binding clearing to it.
Finally, a little about how the Perl AutoFnBind translates to the C++ AutoFnBind:
The Perl constructor creates the C++-level object and adds the bindings to it. If any of them throw, it destroys everything nicely and translates the Exception to Perl. Otherwise it saves a reference to the AutoFnBind in a wrapper object that gets returned to Perl.
The Perl destructor then first clears the AutoFnBind and catches if there is any Exception. However there is just no way to return a Perl exception from a Perl destructor, so it juts prints the error on stderr and calls exit(1). If no exception was thrown, the AutoFnBind gets destroyed nicely by removing the last reference.
For the nicer handling, there is a Perl-level method clear() that does the clearing and translates the exception to Perl.
FnReturn in C++
FnReturn, defined in sched/FnReturn.h, is generally constructed similarly to the RowSetType:
ret = initializeOrThrow(FnReturn::make(unit, name)
->addLabel("lb1", rt1)
->addFromLabel("lb2", lbX)
);
Or of course piece-meal. As it gets built, it actually builds a RowSetType inseide itself.
FnReturn(Unit *unit, const string &name);
static FnReturn *make(Unit *unit, const string &name);
The constructor and convenience wrapper. The unit will be remembered only as a pointer, not reference, to avoid the reference loops. However this pointer will be used to construct the internal labels. So until the FnReturn is fully initialized, you better make sure that the Unit object has a reference and doesn't get freed. FnReturn is an Starget, and must be used in only one thread.
const string &getName() const;
Unit *getUnitPtr() const;
const string &getUnitName() const;
Get back the information from the constructor. Just like for the label, it reminds you that the Unit is only available as a pointer, not reference, here. The FnReturn also have a concept of clearing: it has the special labels inside, and once any of these labels gets cleared, the FnReturn is also cleared by setting the unit pointer to NULL and forgetting the FnContext (more on that one later). So after the FnReturn is cleared, getUnitPtr() will return NULL. And again similar to the Label, there is a convenience function to get the unit name for informational printouts. When FnReturn is cleared, it returns the same "[fn return cleared]".
FnReturn *addFromLabel(const string &lname, Autoref<Label>from);
Add a label to the return by chaining it off another label. lname is the name within the return. The full name of the label will be return_name.label_name. The label names within a return must be unique and not empty, or it will be returned as an initialization error. The label type will be copied (actually, referenced) from the from label, and the new label will be automatically chained off it. The labels can be added only until the return is initialized, or it will throw an Exception.
FnReturn *addLabel(const string &lname, const_Autoref<RowType>rtype);
Add a new independen label to the return. Works very similar to addFromLabel, only uses the explicit row type and doesn't chain to anything. The label can be later found with getLabel() and either chained off something or used to send the rows to it explicitly. The labels can be added only until the return is initialized.
class FnContext: public Starget
{
public:
virtual ~FnContext();
virtual void onPush(const FnReturn *fret) = 0;
virtual void onPop(const FnReturn *fret) = 0;
};
FnReturn *setContext(Onceref<FnContext> ctx);
Set the context with handlers for the pushing and popping of the bindings in the FnReturn. Triceps generally tries to follow the C++ tradition of using the virtual methods for the callbacks, with the user then subclassing the base class and replacing the callback methods. However subclassing FnReturn is extremely inconvenient, because it gets connected to the other objects in a quite complicated way. So the solution is to make a separate context class for the callbacks, and then connect it. By the way, FnContext is not a subclass of FnReturn but a separate top-level class. I.e. NOT Triceps::FnReturn::FnContext but Triceps::FnContext. The callbacks will be called just before the binding is pushed or popped, but after the check for the correctness of the push or pop. They can be used to adjust the state of the streaming function by pushing or popping its stack of local variables, like was shown in the Perl examples.The context can be set only until the return is initialized.
template<class C> C *contextIn() const;
Get back the context. Since the context will be a subclass of FnContext, this also handles the correct type casting. Use it like:
Autoref<MyFnCtx> ctx = fret1->contextIn<MyFnCtx>();
The type is converted using the static_cast, and you need to know the correct type in advance, or your program will break in some horrible ways. If the context has not been set, it will return a NULL.
void initialize();
Initialize the FnReturn. Very similar to the Type classes, it will collect the errors in an Errors object that has to be checked afterwards, and an FnReturn with errors must not be used. The initialization can be called repeatedly with no ill effects. After initialization the structure of the return (labels and context) can not be changed any more.
Erref getErrors() const;
Get the errors detected. Normally called after initialization but can also be called at any stage, as the errors are collected all the way through the object construction.
bool isInitialized() const;
Check whether the return is initialized.
RowSetType *getType() const;
Get the type of the return, which gets built internally by the return. The names of the row types in the set will be the same as the names of labels in the return, and their order will also be the same. This call can be made only after initialization, or it will throw an Exception.
int size() const;
Get the number of labels in the return. Can be called at any time.
const RowSetType::NameVec &getLabelNames() const;
const RowSetType::RowTypeVec &getRowTypes() const;
const string *getLabelName(int idx) const;
RowType *getRowType(const string &name) const;
RowType *getRowType(int idx) const;
Get the piecemeal information about the label names and types. These are really the convenience wrappers around the RowSetType. Note that they return pointers to be able to return NULL on the argument that is out of range. A somewhat special feature is that even though the row set type can be read only after initialization (after it becomes frozen and can not be messed with any more), these wrappers work at any time, even when the return is being built.
bool equals(const FnReturn *t) const;
bool match(const FnReturn *t) const;
bool equals(const FnBinding *t) const;
bool match(const FnBinding *t) const;
Convenience wrappers that compare the equality or match of the underlying row set types.
Label *getLabel(const string &name) const;
int findLabel(const string &name) const;
Label *getLabel(int idx) const;
Get the label by name or index, or the index of the label by name. Return a NULL pointer or -1 index on an invalid argument.
typedef vector<Autoref<RetLabel> > ReturnVec;
const ReturnVec &getLabels() const;
Get the whole set of labels. FnReturn::RetLabel is a special private label type with undisclosed internals. You need to treat these labels as being a plain Label.
void push(Onceref<FnBinding> bind);
Push a binding on the return stack. The return must be initialized, and the binding must be of a matching type, or an Exception will be thrown. The reference to the binding will be kept until it's popped.
void pushUnchecked(Onceref<FnBinding> bind);
Similar to push(), only the type of the binding is not checked. This is an optimization for the automatically generated code that does all the type checks up front at the generation time. The manually written code probably should not be using it.
void pop(Onceref<FnBinding> bind);
Pop a binding from the return stack. The binding argument specifies, which binding is expected to be popped. It's not strictly necessary but allows to catch any mess-ups with the return stack early. If the stack is empty or the top binding is not the same as the argument, throws an Exception.
void pop();
The unchecked version. It still checks and throws if the stack is empty. This method may come handy occasionally, but in general the checked version should be preferred. Pretty much the only reason to use it would be if you try to restore after a major error and want to pop everything from all your FnReturns untill their stacks become empty. But there is much trouble with this kind of restoration.
int bindingStackSize() const;
Get the size of the return stack (AKA the stack of bindings). Useful for debugging.
typedef vector<Autoref<FnBinding> > BindingVec;
const BindingVec &bindingStack() const;
Get the current return stack. Useful for debugging.
ret = initializeOrThrow(FnReturn::make(unit, name)
->addLabel("lb1", rt1)
->addFromLabel("lb2", lbX)
);
Or of course piece-meal. As it gets built, it actually builds a RowSetType inseide itself.
FnReturn(Unit *unit, const string &name);
static FnReturn *make(Unit *unit, const string &name);
The constructor and convenience wrapper. The unit will be remembered only as a pointer, not reference, to avoid the reference loops. However this pointer will be used to construct the internal labels. So until the FnReturn is fully initialized, you better make sure that the Unit object has a reference and doesn't get freed. FnReturn is an Starget, and must be used in only one thread.
const string &getName() const;
Unit *getUnitPtr() const;
const string &getUnitName() const;
Get back the information from the constructor. Just like for the label, it reminds you that the Unit is only available as a pointer, not reference, here. The FnReturn also have a concept of clearing: it has the special labels inside, and once any of these labels gets cleared, the FnReturn is also cleared by setting the unit pointer to NULL and forgetting the FnContext (more on that one later). So after the FnReturn is cleared, getUnitPtr() will return NULL. And again similar to the Label, there is a convenience function to get the unit name for informational printouts. When FnReturn is cleared, it returns the same "[fn return cleared]".
FnReturn *addFromLabel(const string &lname, Autoref<Label>from);
Add a label to the return by chaining it off another label. lname is the name within the return. The full name of the label will be return_name.label_name. The label names within a return must be unique and not empty, or it will be returned as an initialization error. The label type will be copied (actually, referenced) from the from label, and the new label will be automatically chained off it. The labels can be added only until the return is initialized, or it will throw an Exception.
FnReturn *addLabel(const string &lname, const_Autoref<RowType>rtype);
Add a new independen label to the return. Works very similar to addFromLabel, only uses the explicit row type and doesn't chain to anything. The label can be later found with getLabel() and either chained off something or used to send the rows to it explicitly. The labels can be added only until the return is initialized.
class FnContext: public Starget
{
public:
virtual ~FnContext();
virtual void onPush(const FnReturn *fret) = 0;
virtual void onPop(const FnReturn *fret) = 0;
};
FnReturn *setContext(Onceref<FnContext> ctx);
Set the context with handlers for the pushing and popping of the bindings in the FnReturn. Triceps generally tries to follow the C++ tradition of using the virtual methods for the callbacks, with the user then subclassing the base class and replacing the callback methods. However subclassing FnReturn is extremely inconvenient, because it gets connected to the other objects in a quite complicated way. So the solution is to make a separate context class for the callbacks, and then connect it. By the way, FnContext is not a subclass of FnReturn but a separate top-level class. I.e. NOT Triceps::FnReturn::FnContext but Triceps::FnContext. The callbacks will be called just before the binding is pushed or popped, but after the check for the correctness of the push or pop. They can be used to adjust the state of the streaming function by pushing or popping its stack of local variables, like was shown in the Perl examples.The context can be set only until the return is initialized.
template<class C> C *contextIn() const;
Get back the context. Since the context will be a subclass of FnContext, this also handles the correct type casting. Use it like:
Autoref<MyFnCtx> ctx = fret1->contextIn<MyFnCtx>();
The type is converted using the static_cast, and you need to know the correct type in advance, or your program will break in some horrible ways. If the context has not been set, it will return a NULL.
void initialize();
Initialize the FnReturn. Very similar to the Type classes, it will collect the errors in an Errors object that has to be checked afterwards, and an FnReturn with errors must not be used. The initialization can be called repeatedly with no ill effects. After initialization the structure of the return (labels and context) can not be changed any more.
Erref getErrors() const;
Get the errors detected. Normally called after initialization but can also be called at any stage, as the errors are collected all the way through the object construction.
bool isInitialized() const;
Check whether the return is initialized.
RowSetType *getType() const;
Get the type of the return, which gets built internally by the return. The names of the row types in the set will be the same as the names of labels in the return, and their order will also be the same. This call can be made only after initialization, or it will throw an Exception.
int size() const;
Get the number of labels in the return. Can be called at any time.
const RowSetType::NameVec &getLabelNames() const;
const RowSetType::RowTypeVec &getRowTypes() const;
const string *getLabelName(int idx) const;
RowType *getRowType(const string &name) const;
RowType *getRowType(int idx) const;
Get the piecemeal information about the label names and types. These are really the convenience wrappers around the RowSetType. Note that they return pointers to be able to return NULL on the argument that is out of range. A somewhat special feature is that even though the row set type can be read only after initialization (after it becomes frozen and can not be messed with any more), these wrappers work at any time, even when the return is being built.
bool equals(const FnReturn *t) const;
bool match(const FnReturn *t) const;
bool equals(const FnBinding *t) const;
bool match(const FnBinding *t) const;
Convenience wrappers that compare the equality or match of the underlying row set types.
Label *getLabel(const string &name) const;
int findLabel(const string &name) const;
Label *getLabel(int idx) const;
Get the label by name or index, or the index of the label by name. Return a NULL pointer or -1 index on an invalid argument.
typedef vector<Autoref<RetLabel> > ReturnVec;
const ReturnVec &getLabels() const;
Get the whole set of labels. FnReturn::RetLabel is a special private label type with undisclosed internals. You need to treat these labels as being a plain Label.
void push(Onceref<FnBinding> bind);
Push a binding on the return stack. The return must be initialized, and the binding must be of a matching type, or an Exception will be thrown. The reference to the binding will be kept until it's popped.
void pushUnchecked(Onceref<FnBinding> bind);
Similar to push(), only the type of the binding is not checked. This is an optimization for the automatically generated code that does all the type checks up front at the generation time. The manually written code probably should not be using it.
void pop(Onceref<FnBinding> bind);
Pop a binding from the return stack. The binding argument specifies, which binding is expected to be popped. It's not strictly necessary but allows to catch any mess-ups with the return stack early. If the stack is empty or the top binding is not the same as the argument, throws an Exception.
void pop();
The unchecked version. It still checks and throws if the stack is empty. This method may come handy occasionally, but in general the checked version should be preferred. Pretty much the only reason to use it would be if you try to restore after a major error and want to pop everything from all your FnReturns untill their stacks become empty. But there is much trouble with this kind of restoration.
int bindingStackSize() const;
Get the size of the return stack (AKA the stack of bindings). Useful for debugging.
typedef vector<Autoref<FnBinding> > BindingVec;
const BindingVec &bindingStack() const;
Get the current return stack. Useful for debugging.
More about Label construction
I forgot to tell it explicitly in the post on the Labels in C++, but the label construction in C++ works just the same as in Perl: it makes the Unit remember the label right away. So there is no need to call Unit::rememberLabel() manually. On the other hand, if you do not want the label to be remembered by its unit (why?), the only way to achieve that is to call Unit::forgetLabel() after its construction.
Sunday, December 30, 2012
RowSetType
RowSetType, defined in types/RowSetType.h, is another item that is not visible in Perl. Maybe it will be in the future but at the moment things look good enough without it. It has been added for 1.1.0 and expresses the type ("return type" if you want to be precise) of a streaming function (FnReturn and FnBinding classes). Naturally, it's a sequence of the row types, and despite the word "set", the order matters.
A RowSetType is one of these objects that gets assembled from many parts and then initialized, like this:
Autoref<RowSetType> rst = initializeOr Throw(RowSetType::make()
->addRow("name1", rt1)
->addRow("name2", rt2)
);
The function, or actually template, initializeOrThrow() itself is also a new addition, that I'll describe in detail later.
Of course, nothing stops you from adding the row types one by one, in a loop or in some other way, and then calling initialize() manually. And yes, of course you can keep a reference to a row set type as soon as it has been constructed, not waiting for initialization. You could do instead:
Autoref<RowSetType> rst = new RowSetType();
rst->addRow("name1", rt1);
rst->addRow("name2", rt2);
rst->initialize();
if (rst->getErrors()->hasError()) {
...
}
You could use the initializeOrThrow() template here as well, just I also wanted to show the way for the manual handling of the errors. And you can use the new or make() interchangeably too.
All that the initialization does is fixate the row set, forbid the addition of the further row types to it. Which kind of makes sense at the moment but I'm not so sure about the future, in the future the dynamically expandable row sets might come useful. We'll see when we get there.
RowSetType();
static RowSetType *make();
Construct a row set type. The method make() is just a wrapper around the constructor that is more convenient to use with the following ->addRow(), because of the way the operator priorities work in C++. Like any other type, RowSetType is unnamed by itself, and takes no constructor arguments. Like any other type, RowSetType is an Mtarget and can be shared between multiple threads after it has been initialized.
RowSetType *addRow(const string &rname, const_Autoref<RowType>rtype);
Add a row type to the set. All the row types are named, and all the names must be unique within the set. The order of the addition matters too. See the further explanation of why it does in the description of the FnReturn. If this method detects an error (such as duplicate names), it will append the error to the internal Errors object, that can be read later by getErrors(). A type with errors must not be used.
The row types may not be added after the row set type has been initialized.
void initialize();
Initialize the type. Any detected errors can be read afterwards with getErrors(). The repeated calls of initialize() are ignored.
bool isInitialized() const;
Check whether the type has been initialized.
typedef vector<string> NameVec;
const NameVec &getRowNames() const;
typedef vector<Autoref<RowType> > RowTypeVec;
const RowTypeVec &getRowTypes() const;
Read back the contents of the type. The elements will go in the order they were added.
int size() const;
Read the number of row types in the set.
int findName(const string &name) const;
Translate the row type name to index (i.e. the order in which it was added, starting from 0).Returns -1 on an invalid name.
RowType *getRowType(const string &name) const;
Find the type by name. Returns NULL on an invalid name.
const string *getRowTypeName(int idx) const;
RowType *getRowType(int idx) const;
Read the data by index. These methods check that the index is in the valid range, and otherwise return NULL.
The usual methods inherited from Type also work: getErrors(), equals(), match(), printTo().
The row set types are considered equal if they contain the equal row types with equal names going in the same order. They are considered matching if they contain matching row types going in the same order, with any names. If the match condition seems surprising to you, think of it as "nothing will break if one type is substituted for another at execution time".
void addError(const string &msg);
Erref appendErrors();
The ways to add extra errors to the type's errors. It's for convenience of the users of this type, the thinking being that since we already have one Errors object, can as well use it for everything, and also keep all the errors reported in the order of the fields, rather than first all the errors from the type then all the errors from its user. The FnReturn and FnBinding use it.
A RowSetType is one of these objects that gets assembled from many parts and then initialized, like this:
Autoref<RowSetType> rst = initializeOr Throw(RowSetType::make()
->addRow("name1", rt1)
->addRow("name2", rt2)
);
The function, or actually template, initializeOrThrow() itself is also a new addition, that I'll describe in detail later.
Of course, nothing stops you from adding the row types one by one, in a loop or in some other way, and then calling initialize() manually. And yes, of course you can keep a reference to a row set type as soon as it has been constructed, not waiting for initialization. You could do instead:
Autoref<RowSetType> rst = new RowSetType();
rst->addRow("name1", rt1);
rst->addRow("name2", rt2);
rst->initialize();
if (rst->getErrors()->hasError()) {
...
}
You could use the initializeOrThrow() template here as well, just I also wanted to show the way for the manual handling of the errors. And you can use the new or make() interchangeably too.
All that the initialization does is fixate the row set, forbid the addition of the further row types to it. Which kind of makes sense at the moment but I'm not so sure about the future, in the future the dynamically expandable row sets might come useful. We'll see when we get there.
RowSetType();
static RowSetType *make();
Construct a row set type. The method make() is just a wrapper around the constructor that is more convenient to use with the following ->addRow(), because of the way the operator priorities work in C++. Like any other type, RowSetType is unnamed by itself, and takes no constructor arguments. Like any other type, RowSetType is an Mtarget and can be shared between multiple threads after it has been initialized.
RowSetType *addRow(const string &rname, const_Autoref<RowType>rtype);
Add a row type to the set. All the row types are named, and all the names must be unique within the set. The order of the addition matters too. See the further explanation of why it does in the description of the FnReturn. If this method detects an error (such as duplicate names), it will append the error to the internal Errors object, that can be read later by getErrors(). A type with errors must not be used.
The row types may not be added after the row set type has been initialized.
void initialize();
Initialize the type. Any detected errors can be read afterwards with getErrors(). The repeated calls of initialize() are ignored.
bool isInitialized() const;
Check whether the type has been initialized.
typedef vector<string> NameVec;
const NameVec &getRowNames() const;
typedef vector<Autoref<RowType> > RowTypeVec;
const RowTypeVec &getRowTypes() const;
Read back the contents of the type. The elements will go in the order they were added.
int size() const;
Read the number of row types in the set.
int findName(const string &name) const;
Translate the row type name to index (i.e. the order in which it was added, starting from 0).Returns -1 on an invalid name.
RowType *getRowType(const string &name) const;
Find the type by name. Returns NULL on an invalid name.
const string *getRowTypeName(int idx) const;
RowType *getRowType(int idx) const;
Read the data by index. These methods check that the index is in the valid range, and otherwise return NULL.
The usual methods inherited from Type also work: getErrors(), equals(), match(), printTo().
The row set types are considered equal if they contain the equal row types with equal names going in the same order. They are considered matching if they contain matching row types going in the same order, with any names. If the match condition seems surprising to you, think of it as "nothing will break if one type is substituted for another at execution time".
void addError(const string &msg);
Erref appendErrors();
The ways to add extra errors to the type's errors. It's for convenience of the users of this type, the thinking being that since we already have one Errors object, can as well use it for everything, and also keep all the errors reported in the order of the fields, rather than first all the errors from the type then all the errors from its user. The FnReturn and FnBinding use it.
Subscribe to:
Posts (Atom)
