UniEvent

UniEvent - Object-oriented, fast and extendable event loop abstraction framework with C++ and Perl interfaces.

Build and Install

Full build manual here

UniEvent can be built using CMake. There are several dependencies. All can be installed with CMake.

OpenSSL doesn't have its own CMake package but CMake can find it if it is installed in system.

By default UniEvent is a static library. To build a dynamic library set CMake variable LIB_TYPE to dynamic.

Documentation

UniEvent is a cross-platform extendable object-oriented event loop framework. It's also an abstraction layer on top of event loop which provides engine-independent API.

UniEvent is designed to support multiple backends (libuv is the only implemented at the moment).

Memory Management and Lifetime

All dynamic objects are managed using panda::iptr. Never delete objects manually because it can be references to them inside of the framework.

All the types that suppose to be used as heap allocated have private destrucor. It prevents any kind of lifetime errors.

Also there are aliases for all iptr<Type> to make usage shorter. They have SP suffix in the names (stands for smart pointer), e.g. LoopSP, TimerSP, TcpSP.

See more about handles lifetime in section Holding Handles.

Loop

The heart of event programming is an event loop object. This object runs the loop and polls for all registered events (handles).

You can create as many loops as you wish but you can only run one loop at a time. Each loop only polls for events registred with this loop.

LoopSP loop = new Loop();
TimerSP timer = new Timer(loop);
timer->once(1000, [loop](auto) {
    loop->stop();
});
loop->run();

By default, one loop (which is called main or default) is automatically created for you and is accessible as

    Loop::default_loop();

All event handles (watchers) will register in default loop if you don't specify a loop in their constructor.

TimerSP timer = new Timer(loop);
timer->once(1000, [](auto) { Loop::default_loop()->stop(); });
Loop::default_loop()->run();

When you run

    loop->run();

The execution flow will not return until there are no more active handles in the loop or loop stop() is called from some callback.

    loop->stop();

Each handle has a strong refence to it's event loop so that loop will not be destroyed until you loose all refs to it and all it's handles are destroyed.

The default loop is never destroyed.

Handles (Watchers)

There are a number of different handle classes to watch for different events like timers, signals, tcp and udp handles, filesystem operations, and so on. You can find detailed description of each handle class in reference.

Each handle object binds to a specific loop upon creation. Re-binding a handle to a different loop object after creation is not supported. A handle will watch for events as soon as you run the loop it was bound to.

Events

All events are dispatched using CallbackDispatcher. Usually there is a member called event on the handles that have only one event to dispatch. In other cases event-members hass suffix _event in names, e.g. read_event, connect_event, etc.

All the events has a source handle as a first argument. For example, read_event has first argument StreamSP that points to a stream that dispatched an event.

EventListener

Instead of setting callbacks for each event type

    tcp->read_event([](auto...){});
    tcp->write_event([](auto...){});

you can set an event listener object which can watch for all events types.

struct MyListener : IStreamListener {
    virtual void     on_connect (const StreamSP&, const ErrorCode&, const ConnectRequestSP&);
    virtual void     on_read    (const StreamSP&, string&, const ErrorCode&);
    virtual void     on_write   (const StreamSP&, const ErrorCode&, const WriteRequestSP&);
    virtual void     on_eof     (const StreamSP&)
    // and some others ...
};

    MyListener lstn;
    tcp->event_listener(&lstn);

Be carefull with the lifetime. Handles expect a raw pointer and does not own listeners.

The parameters are the same as for callback version.

Event listener does not disable callbacks, i.e. if both event listener object and callbacks are set to a handle, both will be called (first callbacks, then event listener's method).

Event listener is convenient when some object makes use of several handles and wants to listen events from them. Then instead of setting many callbacks it can set itself as a listener for those handles. This saves cpu time and can make the code clearer.

    struct MyClass : IStreamListener, ITimerListener {

    TimerSP timer;
    TcpSP stream;

    MyClass() {
        timer = new Timer();
        timer->start(10);
        timer->event_listener(this);

        tcp = new Tcp();
        tcp->connect(host, port);
        tcp->event_listener(this);
    }

    void on_timer(const TimerSP&) {}

    void on_connect(const StreamSP&, const ErrorCode&, const ConnectRequestSP&) {}

Event Exceptions

See error handling policy to understand the place of exceptions in the library. UniEvent catch all exceptions from event callbacks. It neither ignores them nor passes to any other callback. After any exception is thrown loop stops and rethrow the original exception from Loop::run. So any exception forces loop to stop and it is expected to terminate the whole. You can catch it and run the loop again although. Loop object is correct and fully operational after stop() caused by an exception.

Some events can be trigered by a destructor. I.e, remove_handle_event is called from ~Handle(). Since all destructors are noexcept(true) you should not throw any exception from remove_handle_event listeners.

See detailed information in the documentation of the corresponding classes.

Holding Handles

UniEvent does not hold a strong reference for created handle objects. You must hold them by yourself otherwise no events will be watched for.

    Timer::create_once(1, [](auto){}); // timer is destroyed immediately as you didn't hold it
    Loop::default_loop->run(); // no events to watch, run() will return immediately

Correct way is

    TimerSP timer = Timer::create_once(1, [](auto){}); // keep a strong reference
    Loop::default_loop->run();

The important exception for this rule are streams with request queues (i.e. Tcp). If a request is in progress the handle that created this request is alive till the end of request processing.

{
    TcpSP client = new Tcp();
    client->connect("127.0.0.1", 8080);
    client->write("q");
}
// client is still alive. it will die after write("q") succeeds or fails

See Request Queue for more information about this feature.

Usually you have an object to place a refence to handle to. However sometimes it is convenient to capture handle in callback. Keep in mind that if you do this

    TimerSP timer = new Timer(loop);
    timer->start(1, [timer](auto){
        //...
        if (smth) { timer->stop(); }
    });

yes, you will hold the timer, however you will create a cyclic reference timer -> callback -> timer and thus create a memory leak. This code will stop the timer but will never destroy it. To remove cyclic reference, you need to break the cycle

    TimerSP timer = new Timer(loop);
    timer->start(1, [timer](auto) mutable {
        //...
        if (smth) {
            timer->stop();
            timer.reset(); // reset iptr, not the timer
        }
    });

Notice that lambda has to be mutable for this. Or you can remove callback from the timer handle.

    TimerSP timer = new Timer(loop);
    timer->start(1, [timer](auto) {
        //...
        if (smth) {
            timer->stop();
            timer->event.remove_all();
        }
    });

Request Queue

All classes that interit Stream (Tcp, Pipe, Tty) support request queue. It means that you can call methods one after another without waiting of result of previous. Usual situation is connect somwhere then write data and then close the connection. Traditional way is chain of callbacks:

TcpSP client = new Tcp();
client->connect("127.0.0.1", 8080);
client->connect_event.add([client](auto, auto, auto) {
    client->write("q", [&](auto, auto& err, auto) {
        client->disconnect();
    });
});

Nested callbacks are hard to control and process errors. It is also an ugly code. The solution is internal queue.

TcpSP client = new Tcp();
client->connect("127.0.0.1", 8080);
client->write("q");
client->disconnet();

Actual write won't start until connect request finishes, and disconenct closes connection only after all data from write is sent. If any step fails all callbacks from other requests receives std::errc::operation_canceled as ErrorCode. The queue also holds a strong reference to Stream so it won't be deleted untill all requests are processed even if all other referens are lost.

Async DNS

UniEvent supports for trully asyncronous resolve (async DNS). It does not use any threads and based on c-ares.

You may use it indirectly, for example via tcp->connect(host, port) method or directly like this:

    ResolverSP resolver = new Resolver(loop);
    resolver->resolve('myhost.com', [](const AddrInfo&, const std::error_code&, const RequestSP&) {
        // process result here
    });
    loop->run();

For more details, see Resolver, Tcp.

Filesystem Async Operations

UniEvent provides a wide variety of cross-platform sync and async file system operations. For example:

    Fs::stat(file, [](auto stat, auto err) {
        ...
    });

    Fs::mkstemp(template, [](auto path, auto fd, auto err) {
        ...
    });

    Loop::default_loop()->run();

See Fs for details.

Fork

UniEvent is completely fork-aware. It automatically does all the stuff needed after fork. You can just fork() and run any loop after that including the ones that were in use in the master process.

However keep in mind, that if you run the same loop in child process that was running in master process, you should probably remove the master's process event handles to avoid double execution. This is especially important for I/O handles like tcp, udp, pipe and so on, because no usable behaviour will occur if you watch for the same descriptior with 2 or more different handles.

That's why it is often more convenient to run different loop in child process than to remove all master's recources from the same loop.

    # master
    LoopSP loop  = new Loop();
    TimerSP timer = Timer::create(1000, [](TimerSP){}, loop);
    TcpSP tcp   = new Tcp();
    //...

    fork();

    #child
    LoopSP child_loop = new Loop();
    //...add events
    child_loop->run();

Threads

Event loops and handles are NOT thread-safe, you can't run or access the same loop/handles in different threads. The only exception is unievent::Async handle which is thread-safe and designed for inter-thread communication.

Loop::default_loop is thread-local (different in each thread), so you can safely create handles and run default loop in each thread.

Utility Functions

UniEvent provides a number of cross-platform utility functions described in function.md. Almost all of them are syncronous and not related to an event loop.