Tamer language & library manual

Tamer home - Compiler manual


C++ extensions for event-driven programming


#include <tamer/tamer.hh> 
using namespace tamer; 

class event<T0, T1, T2, T3> { public: event(); operator bool() const; bool empty() const; void trigger(const T0 &v0, const T1 &v1, const T2 &v2, const T3 &v3); void at_trigger(const event<> &trigger_evt); }

class rendezvous<I0, I1> { public: rendezvous(); unsigned nevents() const; unsigned nready() const; unsigned nwaiting() const; bool join(I0 &
i0, I1 &i1); }

event<T0, T1, T2, T3> make_event(rendezvous<I0, I1> &
rendezvous, const I0 &i0, const I1 &i1, T0 &s0, T1 &s1, T2 &s2, T3 &s3); event<T0, T1, T2, T3> make_event(rendezvous<I0> &rendezvous, const I0 &i0, T0 &s0, T1 &s1, T2 &s2, T3 &s3); event<T0, T1, T2, T3> make_event(rendezvous<> &rendezvous, T0 &s0, T1 &s1, T2 &s2, T3 &s3); ... event<> make_event(rendezvous<I0, I1> &rendezvous, const I0 &i0, const I1 &i1); event<> make_event(rendezvous<I0> &rendezvous, const I0 &i0); event<> make_event(rendezvous<> &rendezvous);

tamed void user_function(...) { tvars { ... }; }

twait { ... }

twait(rendezvous<I0, I1> &
rendezvous, I0 &i0, I1 &i1); twait(rendezvous<I0> &rendezvous, I0 &i0); twait(rendezvous<> &rendezvous);


Tamer is a set of libraries and C++ language extensions designed to simplify the practice of event-driven programming.

This manual page gives an overview of the Tamer abstractions, and describes the user-accessible Tamer methods and functions. Most Tamer programs will also use the tamer(1) preprocessor, which converts programs using twait into conventional C++.


Tamer introduces four related abstractions for handling concurrency: events, wait points, rendezvous, and safe local variables.

Each event object represents a future occurrence, such as the completion of a network read. When the expected occurrence actually happens -- for instance, a packet arrives -- the event is triggered via its trigger method. Each active event is associated with exactly one rendezvous object, which represents a set of related events. A function can block until some event on a rendezvous occurs using a twait special form. For example, this function uses Tamer to print "Done!" after 10 seconds have elapsed. During that 10 seconds, the function blocks, allowing other application code to continue processing.

  tamed void f() { 
      twait { tamer::at_delay_sec(10, make_event()); } 


The make_event function allocates an event of type event<T*>, where T* is a sequence of zero to four types determined by make_event’s arguments. This event’s trigger method has the signature void trigger(T*). Calling trigger(v*) marks the event as having occurred, and passes zero to four results v*, which are called trigger values, to whomever is expecting the event. The types of v* must match the event’s types T*. For example:

rendezvous<> r; int i = 0; event<int> e = make_event(r, i); e.trigger(100); assert(i == 100); // assertion will succeed

When triggered, e’s int trigger value is stored in i, the trigger slot passed earlier to make_event. The type of i is echoed in e’s type event<int>.

event objects may be freely assigned, passed as arguments, and copied via copy constructors -- essentially, treated as primitive values. They are automatically reference counted. Each event may be triggered at most once. An event that hasn’t triggered yet is called active. Triggering an active event makes it empty; triggering an empty event has no effect.

Tamer automatically triggers an active event when its last reference goes out of scope. This case is considered a programming error, however, and a message is printed to indicate the abnormal trigger.

Wait Points and Rendezvous

The wait point language extension, written twait, blocks the calling function until one or more events are triggered. Blocking causes a function to return to its caller, but its execution point and safe local variables are preserved in memory. When an expected event occurs, the function unblocks and resumes processing at the wait point. By that time, of course, the function’s original caller may have returned. Any function containing a wait point is marked with the tamed keyword, which informs the caller that the function can block.

The first, and more common, form of wait point is written twait { statements; }. This executes the statements, then blocks until all events created by make_event calls in the statements have triggered. (Within the statements, make_event is redefined to a macro that automatically supplies a rendezvous argument.) For example, code like twait { tamer::at_delay_sec(10, make_event()); } should be read as execute tamer::at_delay_sec(10, make_event()), then block until the created event has triggered -- or, since tamer::at_delay_sec triggers its event argument after the given number of seconds has passed, simply as block for 10 seconds.

The second, more flexible form of wait point explicitly names a rendezvous object, which specifies the set of expected events relevant to the wait point. Every event object associates with one rendezvous. A wait point twait(r) unblocks when any one of rendezvous<> r’s events occurs. Unblocking consumes the event and restarts the blocked function. The twait() form can also return information about which event occurred. A rendezvous of type rendezvous<I*>, where I* is zero to two types, associates event IDs of type(s) I* with events. The make_event function specifies event IDs as well as trigger slots. A twait(r, i*) statement sets the variable(s) i* to the ID(s) of the unblocking event. The type(s) of i* must match the type(s) of the rendezvous.

rendezvous objects have private copy constructors and assignment operators, preventing them from being copied.

A tamed function’s caller resumes when the called function either returns or blocks. To allow its caller to distinguish returning from blocking, a tamed function will often accept an event argument, which it triggers when it returns. This trigger signals the function’s completion. Here is a tamed function that blocks, then returns an integer:

tamed void blockf(event<int> done) { ... block ... done.trigger(200); }

A caller will most likely use twait to wait for blockf to return, and so become tamed itself. Waiting for events thus trickles up the call stack until a caller doesn’t care whether its callee returns or blocks.

When an event e is triggered, Tamer enqueues a trigger notification for e’s event ID on e’s rendezvous r. This step also unblocks any function blocked on twait(r). Conversely, twait(r) checks for any queued trigger notifications r. If one exists, it is dequeued and returned. Otherwise, the function blocks at that wait point; it will unblock and recheck the rendezvous once someone triggers a corresponding event. The top-level event loop cycles through unblocked functions, calling them in some order.

Multiple functions cannot simultaneously block on the same rendezvous.

Safe Local Variables

Finally, safe local variables are variables whose values are preserved across wait points. The programmer marks local variables as safe by enclosing them in a tvars{} block, which preserves their values in a heap-allocated closure. Function parameters are always safe. Unsafe local variables have indeterminate values after a wait point. The C++ compiler will often give you an uninitialized-variable warning when a variable needs to be made safe.


The event template class represents future occurrences. The template takes zero to four type arguments, which represent the types of the event’s trigger values. In the following, T0-T3 are the template arguments of the event type. If given, these type arguments must be copy-constructible and assignable.


Creates an empty event. Trigger attempts on the event are ignored; e.empty() returns true.

template <typename R, [typename I0, typename I1]> 
event<T0, T1, T2, T3>::event(R &r, [const I0 &i0, const I1 &i1,] 
                             T0 &s0, T1 &s1, T2 &s2, T3 &s3) 
... event<>::event(R &r, [const I0 &i0, const I1 &i1]) 

Creates an event on rendezvous r with optional event IDs i0 and i1 and trigger slots s0...s3. Each event type has similar constructors whose slot arguments s* match the template arguments.

event<T*>::event(const event<T*> &e) 
event<T*> &event<T*>::operator=(const event<T*> &e) 

Events may be safely copied and assigned. After an assignment e1 = e2, the event objects e1 and e2 refer to the same underlying occurrence; for example, triggering either causes both to become empty.

event<T*>::operator bool() const 

Returns true if the event is active. Empty events return false.

bool event<T*>::empty() const 

Returns true if the event is empty, meaning it was created empty or has already been triggered. e.empty() is equivalent to !(bool)e.

void event<T0, T1, T2, T3>::trigger(const T0 &v0, const T1 &v1, 
                                    const T2 &v2, const T3 &v3) 
... void event<>::trigger() 

Triggers the event. If the event is empty, this does nothing; otherwise, it assigns the event’s trigger slots (defined at creation time) to the trigger values v0...v3 and wakes the relevant blocked closure, if any. Events become empty after they are triggered. Each event type has a trigger method whose value arguments v* have types that match the template arguments.

void event<T*>::at_trigger(const event<> &trigger_evt) 

Registers trigger_evt for cancel notification. If this event is already empty, trigger_evt is triggered immediately. Otherwise, trigger_evt is triggered when this event is triggered.

event<> event<T*>::bind_all() const 

Creates a version of this event that has no trigger slots. The returned event refers to the same occurrence as this event, so triggering either event makes both events appear empty. The returned event has no trigger slots, however, and bind_all().trigger() will leave this event’s slots unchanged.


The rendezvous template class groups related events. The template takes zero to two type arguments, which represent the types of the rendezvous’s event IDs. In the following, I0 and I1 are the template arguments of the rendezvous type. If given, these type arguments must be copy-constructible and assignable.


Creates a new rendezvous with no outstanding events.

unsigned rendezvous<I*>::nevents() const 

Returns the count of outstanding events. This includes events that have not yet triggered, and events that have triggered, but the trigger notification has not been collected yet.

unsigned rendezvous<I*>::nready() const 

Returns the count of ready events. An event is ready if it has been triggered, but the trigger notification has not been collected yet. The rendezvous<I*>::join method will return true only if nready() is greater than 0.

unsigned rendezvous<I*>::nwaiting() const

Returns the count of waiting events. An event is waiting if it has not yet triggered.

bool rendezvous<I0, I1>::join(I0 &i0, I1 &i1) 
bool rendezvous<I0>::join(I0 &i0) 
bool rendezvous<>::join() 

Collects a trigger notification, if any events have triggered but have not yet been collected. If a trigger notification is available, sets the event ID argument(s) i0 and i1, if any, to the collected event’s ID(s) and returns true. Otherwise, returns false. The twait special forms are built around calls to rendezvous<I*>::join.

void rendezvous<I*>::clear() 

Removes all pending events from this rendezvous. Any active events on this rendezvous are effectively triggered, calling their at_trigger() notifiers and making the events themselves empty. After clear(), the rendezvous’s nevents() method returns 0.


These functions manipulate events generically, for example by returning one event that triggers two others.

event<> distribute(const event<> &e1, const event<> &e2) 

Returns an event that distributes trigger operations over e1 and e2. Triggering the returned event will trigger both e1 and e2 automatically. The returned event is empty if and only if both e1 and e2 are empty.

event<> bind(const event<T0> &e, const T0 &v0) 

Returns an event that, when triggered, will call e.trigger(v0).

event<T0> unbind(const event<> &e) 

Returns an event that, when triggered, will call e.trigger(). The returned event’s trigger value is ignored.


The driver class handles Tamer’s fundamental events: timers, signals, and file descriptors. Most programs will use the single driver::main object, which is accessed through top-level functions as follows.

void at_fd_read(int fd, const event<int> &e) 
void at_fd_read(int fd, const event<> &e) 

Triggers event e when fd becomes readable, or when fd is closed or encounters an error, whichever comes first. fd must be a valid file descriptor less than FD_SETSIZE. In the version taking event<int>, the trigger value is 0 when fd becomes readable, and a negative error code otherwise.

void at_fd_write(int fd, const event<int> &e) 
void at_fd_write(int fd, const event<> &e) 

Triggers event e when fd becomes writable. fd must be a valid file descriptor less than FD_SETSIZE. The trigger value is as for at_fd_read().

void at_time(const timeval &expiry, const event<> &e) 

Triggers event e on, or very soon after, time expiry.

void at_delay(const timeval &delay, const event<> &e) 

Triggers event e after at least delay time has passed. All delays are measured relative to the timestamp now().

void at_delay(double delay, const event<> &e) 

Triggers event e after at least delay seconds have passed.

void at_delay_sec(int delay, const event<> &e) 

Triggers event e after at least delay seconds have passed.

void at_delay_msec(int delay, const event<> &e) 

Triggers event e after at least delay milliseconds have passed.

void at_signal(int signal, const event<> &e) 

Triggers event e if the signal occurs. The event is not triggered directly inside the signal handler. Rather, the signal handler marks the signal’s occurrence, then blocks the signal from further delivery. The signal remains blocked until e has been triggered and any corresponding closure has run (and possibly registered another event to catch the signal). Thus, programmers can safely catch signals without race conditions.

void at_asap(const event<> &e) 

Triggers event e on the next execution of Tamer’s main loop.

const timeval &now() 

Returns the current cached timestamp.

void once() 

Runs through the driver’s event loop once. First, the driver removes any empty timer and file descriptor events. Then, the driver calls select and possibly blocks, waiting for the next event. Then, the driver triggers and runs the appropriate signal events, file descriptor events, timer events, and ASAP events. Each path through the event loop resets now() to the correct current value.

void loop() 

Equivalent to while (1) once();.


These functions integrate timeouts, signals, and other forms of canceling into existing events. For example:

  int i;  rendezvous<> r;
  event<int> e = add_timeout(delay, make_event(r, i));
The event on r is triggered on the first of the following events.

  • e is triggered. i is set to e’s trigger value.
  • delay seconds elapse. i is set to -ETIMEDOUT (or, equivalently, tamer::outcome::timeout).

Cancel adapters are available for timeouts and signals.

event<int> add_timeout(const timeval &delay, event<int> e) 
event<int> add_timeout_sec(int delay, event<int> e) 
event<int> add_timeout_msec(int delay, event<int> e) 

Returns a timeout-adapted version of e. When the returned event is triggered, e is triggered with the same trigger value. If, however, the timeout of delay expires first, then e is triggered with value -ETIMEDOUT.

event<int> add_signal(int signal, event<int> e) 
event<int> add_signal(const std::vector<int> &signals, event<int> e) 

Returns a signal-adapted version of e. When the returned event is triggered, e is triggered with the same trigger value. If, however, the signal (or one of the signals) happens first, then e is triggered with value -EINTR.

There is also a set of cancel adapters that don’t set e’s trigger value. For example:

  int i(-1);  rendezvous<> r;
  event<int> e = with_timeout(delay, make_event(r, i));
The event on r is triggered on the first of the following events.

  • e is triggered. i is set to e’s trigger value.
  • delay seconds elapse. i retains its initial value.

This style of cancel adapter can handle any event type, not just event<int>.

event<T*> with_timeout(const timeval &delay, event<T*> e) 
event<T*> with_timeout_sec(int delay, event<T*> e) 
event<T*> with_timeout_msec(int delay, event<T*> e) 
event<T*> with_signal(int signal, event<T*> e) 
event<T*> with_signal(const std::vector<int> &signals, event<T*> e) 

Return cancel-adapted versions of e. These functions are analogous to the add_ versions above, but do not set any trigger values to indicate whether the event triggered successfully.

event<T*> with_timeout(const timeval &delay, event<T*> e, int &result) 
event<T*> with_timeout_sec(int delay, event<T*> e, int &result) 
event<T*> with_timeout_msec(int delay, event<T*> e, int &result) 
event<T*> with_signal(int signal, event<T*> e, int &result) 
event<T*> with_signal(const std::vector<int> &signals, event<T*> e, 
                      int &result) 

Return cancel-adapted versions of e. When e triggers, the result variable is set to one of the following constants to indicate why:
if e triggered successfully.
if e timed out.
if e was interrupted by a signal.

The constants tamer::outcome::{success, timeout, signal} may be used instead of the error values.


Tamer’s support for file I/O is available via #include <tamer/fd.hh>. Variants of the main I/O system calls are provided, most of them nonblocking. See tamer_fd(3).


The existing fd wrappers are only truly nonblocking for pipe, socket, and network I/O. The functions will block on disk I/O.

The Tamer interface differs in several ways from the interface described in Events Can Make Sense by Krohn et al. First, all Tamer classes and functions are declared in the tamer namespace. using namespace tamer; will bring them into the global namespace. Second, Tamer events are created with make_event (rather than mkevent), which more closely follows the C++ standard library’s style. Third, Tamer primitive events are registered with functions at_time, at_fd_read, and at_fd_write rather than timer and wait_on_fd; the at_ convention will generalize better to future classes of events. Finally, tamed functions in Tamer are declared using code like tamed void f(), not tamed f().

The Tamer interface also differs substantially from that of Tame, which is distributed as part of sfslite.


Eddie Kohler <kohler@cs.ucla.edu>
Based on joint work on Tame with Maxwell Krohn <krohn@mit.edu> and Frans Kaashoek <kaashoek@mit.edu>


tamer(1), tamer_fd(3)

Events Can Make Sense. Maxwell Krohn, Eddie Kohler, and Frans Kaashoek. In Proc. USENIX 2007 Annual Technical Conference. Also available at http://www.cs.ucla.edu/~kohler/pubs/krohn07events.pdf

The SFSlite libraries for writing asynchronous programs include the original Tame processor and libraries. The SFSlite libraries are larger and more full-featured than Tamer, but also harder to use. SFSlite is available at http://www.okws.org/doku.php?id=sfslite

Tamer home