The Event Loop API supports the event-driven programming model, which is favoured in Legato (but not forced). Each thread that uses this system has a central event loop which calls event handler functions in response to event reports.
Software components register their event handler functions with the event system (either directly through the Event Loop API or indirectly through other APIs that use the Event Loop API) so the central event loop knows the functions to call in response to defined events.
Every event loop has an event queue, which is a queue of events waiting to be handled by that event loop.
The following different usage patterns are supported by the Event Loop API:
Deferred Function Calls
Dispatching Function Execution to Other Threads
Publish-Subscribe Events
Layered Publish-Subscribe Handlers
Other Legato C Runtime Library APIs using the event loop include:
File Descriptor Monitor API
Timer API
Command Line Arguments API
Signals API
Low-Level Messaging API
A basic Event Queue usage is to queue a function for the Event Loop to call later (when that function gets to the head of the Event Queue) by calling le_event_QueueFunction()
.
This code sample has a component initialization function queueing another function to be call later, by the process's main thread when the Event Loop is running. Two parameters are needed by the deferred function. The third is just filled with NULL and ignored by the deferred function.
Deferred function calls are useful when implementing APIs with asynchronous result call-backs. If an error is detected before the API function returns, it can't just call the call-back directly, because it could cause re-entrancy problems in the client code or cause recursive loops. Instead of forcing the API function to return an error code in special cases (which will increase the client's code complexity and may leak API implementation details to the client), the API function can defers executing the call-back until later by queuing an error handling function onto the Event Queue.
In multi-threaded programs, sometimes the implementor needs to ask another thread to run a function because:
To assist with this, the Event Loop API provides le_event_QueueFunctionToThread()
. It works the same as le_event_QueueFunction(), except that it queues the function onto a specific thread's Event Queue.
If the other thread isn't running the Event Loop, then the queued function will never be executed.
This code sample shows two arguments started by the process's main thread, and executed in the background by a low-priority thread. The result is reported back to the client through a completion callback running in the same thread that requested that the computation be performed.
In the publish-subscribe pattern, someone publishes information and if anyone cares about that information, they subscribe to receive it. The publisher doesn't have to know whether anything is listening, or how many subscribers might be listening. Likewise, the subscribers don't have to know whether anything is publishing or how many publishers there might be. This decouples publishers and subscribers.
Subscribers add handlers for events and wait for those handlers to be executed.
Publishers report events.
When an event report reaches the front of an Event Queue, the Event Loop will pop it from the queue and call any handlers that have been registered for that event.
Events are identified using an Event ID created by calling le_event_CreateId()
before registering an handler for that event or report. Any thread within the process with an Event ID can register a handler or report events.
Event reports can carry a payload. The size and format of the payload depends on the type of event. For example, reports of temperature changes may need to carry the new temperature. To support this, le_event_CreateId()
takes the payload size as a parameter.
To report an event, the publisher builds their report payload in their own buffer and passes a pointer to that buffer (and its size) to le_event_Report()
:
This results in the report getting queued to the Event Queues of all threads with handlers registered for that event ID.
To register a handler, the subscriber calls le_event_AddHandler()
.
When an event report reaches the front of a thread's Event Queue, that thread's Event Loop reads the report and then:
Another opaque pointer, called the context pointer can be set for the handler using le_event_SetContextPtr()
. When the handler function is called, it can call le_event_GetContextPtr() to fetch the context pointer.
Finally, le_event_RemoveHandler() can be used to remove an event handler registration, if necessary.
If a handler is removed after the report for that event has been added to the event queue, but before the report reaches the head of the queue, then the handler will not be called.
If you need to implement an API that allows clients to register "handler" functions to be called-back after a specific event occurs, the Event Loop API provides some special help.
You can have the Event Loop call your handler function (the first-layer handler), to unpack specified items from the Event Report and call the client's handler function (the second-layer handler).
For example, you could create a "Temperature Sensor API" that allows its clients to register handler functions to be called to handle changes in the temperature, like this:
The implementation could look like this:
This approach gives strong type checking of both handler references and handler function pointers in code that uses this Temperature Sensor API.
Sometimes you need to report an event where the report payload is pointing to a reference-counted object allocated from a memory pool (see Dynamic Memory Allocation API). Memory leaks and/or crashes can result if its is sent through the Event Loop API without telling the Event Loop API it's pointing to a reference counted object. If there are no subscribers, the Event Loop API iscards the reference without releasing it, and the object is never be deleted. If multiple handlers are registered, the reference could be released by the handlers too many times. Also, there are other, subtle issues that are nearly impossible to solve if threads terminate while reports containing pointers to reference-counted objects are on their Event Queues.
To help with this, the functions le_event_CreateIdWithRefCounting()
and le_event_ReportWithRefCounting()
have been provided. These allow a pointer to a reference-counted memory pool object to be sent as the payload of an Event Report.
le_event_ReportWithRefCounting()
passes ownership of one reference to the Event Loop API, and when the handler is called, it receives ownership for one reference. It then becomes the handler's responsibility to release its reference (using le_mem_Release()) when it's done.
le_event_CreateIdWithRefCounting()
is used the same way as le_event_CreateId(), except that it doesn't require a payload size as the payload is always known from the pointer to a reference-counted memory pool object. Only Event IDs created using le_event_CreateIdWithRefCounting() can be used with le_event_ReportWithRefCounting().
All functions in this API are thread safe.
Each thread can have only one Event Loop. The main thread in every Legato process will always run an Event Loop after it's run the component initialization functions. As soon as all component initialization functions have returned, the main thread will start processing its event queue.
When a function is called to "Add" an event handler, that handler is associated with the calling thread's Event Loop. If the calling thread doesn't run its Event Loop, the event reports will pile up in the queue, never getting serviced and never releasing their memory. This will appear in the logs as event queue growth warnings.
If a client starts its own thread (e.g., by calling le_thread_Create() ), then that thread will not automatically run an Event Loop. To make it run an Event Loop, it must call le_event_RunLoop()
(which will never return).
If a thread running an Event Loop terminates, the Legato framework automatically deregisters any handlers and deletes the thread's Event Loop, its Event Queue, and any event reports still in that Event Queue.
Many legacy programs written on top of POSIX APIs will have previously built their own event loop using poll(), select(), or some other blocking functions. It may be difficult to refactor this type of event loop to use the Legato event loop instead.
Two functions are provided to assist integrating legacy code with the Legato Event Loop:
le_event_GetFd()
- Fetches a file descriptor that can be monitored using some variant of poll() or select() (including epoll). It will appear readable when the Event Loop needs servicing.le_event_ServiceLoop()
- Services the event loop. This should be called if the file descriptor returned by le_event_GetFd() appears readable to poll() or select().In an attempt to avoid starving the caller when there are a lot of things that need servicing on the Event Loop, le_event_ServiceLoop()
will only perform one servicing step (i.e., call one event handler function) before returning, regardless of how much work there is to do. It's the caller's responsibility to check the return code from le_event_ServiceLoop() and keep calling until it indicates that there is no more work to be done.
A logging keyword can be enabled to view a given thread's event handling activity. The keyword name depends on the thread and process name where the thread is located. For example, the keyword "P/T/events" controls logging for a thread named "T" running inside a process named "P".
Copyright (C) Sierra Wireless Inc. Use of this work is subject to license.