Library overview

11 minute read

C11 and POSIX compatibility library

The C11 and POSIX compatibility library (liblely-libc) makes a selection of C11 features available in C99 and implements certain C99 and POSIX features that may be missing on some platforms. The library is intended to provide platform independence to C99- and C11-compliant users.

The library provides:

  • compiler feature definitions: <lely/features.h>
  • atomics: <lely/libc/stdatomic.h>
  • max_align_t: <lely/libc/stddef.h>
  • SSIZE_MAX: <lely/libc/stdint.h>
  • getdelim(), getline(), asprintf() and vasprintf(): <lely/libc/stdio.h>
  • aligned_alloc(), aligned_free() and setenv(): <lely/libc/stdlib.h>
  • strdup(), strndup() and strnlen(): <lely/libc/string.h>
  • ffs(), strcasecmp() and strncasecmp(): <lely/libc/strings.h>
  • clockid_t, ssize_t: <lely/libc/sys/types.h>
  • threads: <lely/libc/threads.h>
  • struct timespec, nanosleep(), timespec_get() and POSIX Realtime Extensions: <lely/libc/time.h>
  • char16_t and char32_t: <lely/libc/uchar.h>
  • getopt() and sleep(): <lely/libc/unistd.h>

Additionally the following C++ definitions have been backported to C++11 (and are available in the lely::compat namespace):

  • std::clock_cast() from C++20: <lely/libc/chrono.hpp>
  • std::invoke() from C++17: <lely/libc/functional.hpp>
  • std::bool_constant, std::invoke_result<>, std::invoke_result_t<>, std::is_invocable<>, std::is_invocable_r<> and std::void_t<> from C++17, and std::remove_cvref<> from C++20: <lely/libc/type_traits.hpp>
  • std::index_sequence<>, std::index_sequence_for<>, std::integer_sequence<>, std::make_index_sequence<> and std::make_integer_sequence<> from C++14: <lely/libc/utility.hpp>

Since the liblely-libc library now contains C++ features, it may be renamed to liblely-compat in the future.

Test Anything Protocol (TAP) library

The TAP library (liblely-tap) provides an implementation of the Test Anything Protocol.

The TAP library is used by all tests in the test suite (which can be run with make check).

Utilities library

The utilities library (liblely-util) provides a variety of functions and data structures used by, and shared between, other Lely core libraries.

Data structures

The utilities library implements the following data structures:

All implementations are intrusive. The user must embed a node in the struct containing the value to be stored. This is generally more performant than a non-intrusive implementation. Moreover, it is trivial to build a non-intrusive version on top of the intrusive implementation if necessary.

System support

Functions to keep track of platform-dependent and platform-independent error numbers can be found in <lely/util/errnum.h>, as well as functions to convert error numbers to human-readable strings. Convenience functions to create std::error_code error codes or throw std::system_error exceptions can be found in <lely/util/error.hpp> and <lely/util/exception.hpp>.

Diagnostic messages can be generated with the functions in <lely/util/diag.h>. These functions support user-defined message handlers and several handlers are predefined for printing diagnostic messages to screen, log files or Windows dialog boxes.

On POSIX and Windows platforms, functions are provided to run a process in the background as a daemon/service (see <lely/util/daemon.h>). Note that this functionality can be disabled when liblely-util is built.

The byte order of integers and floating-point values is platform specific. Functions to convert between native, big-endian and little-endian, and network byte order can be found in <lely/util/endian.h>.

Definitions for IEEE 754 floating-point format types are provided in <lely/util/float.h>.

File and memory management

Accessing files is generally most convenient using memory maps. I/O performance is improved and reading/writing is simplified, since the user can rely on pointer manipulation instead of repeated calls to read()/write() and seek(). Two file buffers are provided, one for reading (<lely/util/frbuf.h>) and one for writing (<lely/util/fwbuf.h>). Both are implemented using native memory maps, but provide a standard C fallback implementation if necessary. The write buffer supports atomic access: the final file is only created (or replaced) if all operations completed successfully.

A lightweight in-memory buffer with a similar API is provided in <lely/util/membuf.h>.

A generic, lock-free, single-producer, single-consumer ring buffer is provided in <lely/util/spscring.h>.

Text handling

According to the Unix philosophy, text streams are the universal interface. To ease the implementation of parsers, several lexing functions are provided in <lely/util/lex.h>. Corresponding printing functions can be found in <lely/util/print.h>.

These functions are used to implement the INI parser and printer of the configuration struct (<lely/util/config.h>), a simple string-based key-value store with sections.

The configuration struct is used to parse EDS/DCF files in the CANopen library.

Coroutines

Stackless coroutines are provided by <lely/util/coroutine.h> (and <lely/util/coroutine.hpp>).

Stackfull coroutines, or fibers, are provided by <lely/util/fiber.h> (and <lely/util/fiber.hpp>). On Windows, the implementation uses native fibers. On other platforms, the implementation is based on setjmp() and longjmp(). Calling environments with a separate step can be created with mkjmp() from <lely/util/mkjmp.h>.

Other

The ubiquitous ABS(), MIN(), MAX() and ALIGN() macros are provided by <lely/util/util.h>. This header also contains a powerof2() macro, and the countof() and structof() macros (similar to ARRAY_SIZE() and container_of() in the Linux kernel).

Bit counting and manipulation functions are provided in <lely/util/bits.h> and <lely/util/bitset.h>.

Comparison functions for integers, pointers and strings can be found in <lely/util/cmp.h>.

<lely/util/invoker.hpp> defines make_invoker(), which can be used to create function objects containing a Callable and its arguments.

This is used by the event library to wrap function calls in asynchronous tasks.

An abstract class providing the C++ BasicLockable interface is defined in <lely/util/mutex.hpp>. This header also contains UnlockGuard, a class similar to std::lock_guard, but used to ensure a mutex is unlocked for the duration of a scoped block.

A generic type that can represent both the result of a successful operation or the reason for failure is provided in <lely/util/result.hpp>.

This type is used to hold the result of futures in the event library.

Stop tokens, which provide a generic mechanism for cancelling (sequences of) tasks, are provided by <lely/util/stop.h> and <lely/util/stop.hpp>. The API is based on <stop_token> in C++20.

Convenience functions to compare and manipulate timespec structs can be found in <lely/util/time.h>.

16-bit Unicode string functions are provided in <lely/util/string.h>. These functions are equivalent to the corresponding standard library functions, except that they operate on strings of type char16_t instead of char.

Event library

The event library (liblely-ev) provides an event loop and asynchronous futures and promises. It is based on the asynchronous model from Boost.Asio and the proposed C++ Extensions for Networking.

New I/O library

The I/O library (liblely-io2) provides asynchronous I/O.

Note: Currently only timers, signal handlers and CAN devices are supported. Once it reaches feature parity with the old I/O library (liblely-io), the old library will be removed. The new library may be renamed to liblely-io in the future.

CANopen library

The CANopen library (liblely-co) is a CANopen implementation for both masters and slaves. Most of the functionality of CiA 301, 302, 305, 306, 309 and 315 is provided.

Note: The CANopen library provides a C++ interface, but it is deprecated. Please use the C++ CANopen application library (liblely-coapp) when writing CANopen applications in C++.

Contrary to most other CANopen stacks, this implementation is completely passive; the library does not perform any I/O (besides maybe reading some files from disk), it does not create threads nor does it access the system clock. Instead, it relies on the user to send and receive CAN frames and update the clock. This allows the library to be easily embedded in a wide variety of applications.

The library is also asynchronous. Issuing a request is always a non-blocking operation. If the request is confirmed, the API accepts a callback function which is invoked once the request completes (with success or failure). This allows the stack to run in a single thread, even when processing dozens of simultaneous requests (not uncommon for an NMT master).

CAN network object

The interface between the CANopen stack and the CAN bus (and system clock) is provided by the CAN network object (can_net_t) from the CAN library (liblely-can). When the CANopen stack needs to send a CAN frame, it hands it over to a CAN network object, which in turn invokes a user-defined callback function to write the frame to the bus. Similarly, when a user reads a CAN frame from the bus, he gives it to a CAN network object, which in turn distributes it to the registered receivers in the CANopen stack. Additionally, the user periodically checks the current time and tells the CAN network object, which then executes the actions for timers that have elapsed.

Object dictionary

The object dictionary is the central concept of the CANopen protocol. It contains almost the entire state of a CANopen device, including process data and communication parameters. Communication between nodes consists primarily of reading from and writing to each others object dictionary. Even writing CANopen applications is mostly a matter of configuring the object dictionary.

Together with the node-ID, the object dictionary is managed by a CANopen device object (co_dev_, defined in <lely/co/dev.h>). Although it is possible to construct the object dictionary from scratch in C, it is more convenient to read it from an Electronic Data Sheet (EDS) or Device Configuration File (DCF) (see CiA 306). Functions to parse EDS/DCF files can be found in <lely/co/dcf.h>.

Embedded devices often do not have the resources to parse an EDS/DCF file at runtime. Using the DCF-to-C tool it is possible to create a C file containing a static device descriptipn, which can be compiled with the application. The static object dictionary can then be converted to a dynamic one at runtime (see <lely/co/sdev.h>).

Service objects

The CANopen stack implements the following services:

  • Process data object (PDO), including Multiplex PDO (MPDO)
    • Receive-PDO (RPDO): (co_rpdo_t, defined in <lely/co/rpdo.h>)
    • Transmit-PDO (TPDO): (co_tpdo_t, defined in <lely/co/tpdo.h>)
  • Service data object (SDO)
    • Server-SDO (CSDO): (co_ssdo_t, defined in <lely/co/ssdo.h>)
    • Client-SDO (CSDO): (co_csdo_t, defined in <lely/co/csdo.h>)
  • Synchronization object (SYNC): (co_sync_t, defined in <lely/co/sync.h>)
  • Time stamp object (TIME): (co_time_t, defined in <lely/co/time.h>)
  • Emergency object (EMCY): (co_emcy_t, defined in <lely/co/emcy.h>)
  • Network management (NMT): (co_nmt_t, defined in <lely/co/nmt.h>)
  • Layer setting services (LSS): (co_lss_t, defined in <lely/co/lss.h>)

While it is possible to create these services by hand, it is almost always more convenient to let them be managed by a single Network Management (NMT) service object. An NMT object manages the state of a CANopen device and creates and destroys the other services as needed.

Like all CANopen services, the NMT service gets its configuration from the object dictionary. A typical CANopen application therefore consists of

  • creating the CAN network object;
  • loading the object dictionary form an EDS/DCF file;
  • creating an NMT service;
  • and, finally, processing CAN frames in an event loop.

Note, however, that a newly created NMT service starts out in the ‘Initialisation’ state and does not create any services or perform any communication. This allows the application to register callback functions before the node becomes operational. The NMT service, including the entire boot-up sequence, can be started by giving it the ‘reset node’ command.

CAN frame tunneling

Sometimes a CANopen application runs on a device which does not have direct access to a CAN bus. CiA 315 defines a protocol for tunneling CAN frames over wired or Wireless Transmission Media (WTM). The interface for this protocol can be found in <lely/co/wtm.h>. Additionally, the CAN-to-UDP tool can be run as a daemon or service to act as a proxy between a CAN bus and UDP.

C++ CANopen application library

The C++ CANopen application library (liblely-coapp) provides a high-level application interface in C++ for liblely-co. It is intended to simplify the development of CANopen master applications (and, to a lesser extent, slaves) by providing a driver model and combining it with an event loop (from liblely-ev).

CAN library

The CAN library (lely-can provides) generic CAN frame definitions, conversion functions for different drivers and an interface designed to simplify CAN frame handling.

The generic CAN message type is struct can_msg (defined in <lely/can/msg.h>). This struct can represent both CAN and CAN FD frames (if CAN FD support is enabled). The can_msg struct does not correspond to any specific driver or interface. Conversion functions to and from IXXAT VCI and SocketCAN can be found in <lely/can/vci.h> and <lely/can/socket.h>, respectively.

The CAN bus is a broadcast bus, so each node receives all messages. Applications typically only listen to messages with a specific CAN identifier. The CAN network interface (can_net_t, defined in <lely/can/net.h>) assists the user with processing CAN frames by allowing callback functions to be registered for individual CAN identifiers. Additionally, callback functions can be registered to be invoked at specific times. The CAN network interface is passive, relying on the application to feed it CAN frames or notify it of the current time. A C++ interface can be found in <lely/can/net.hpp>.

When processing CAN frames it is often necessary to buffer incoming or outgoing messages. A thread-safe, lock-free circular buffer is provided in <lely/can/buf.h> (and <lely/can/buf.hpp>).

Old I/O library (deprecated)

Note: The old I/O library is deprecated and should not be used in new projects. It will be removed once the new I/O library (liblely-io2) reaches feature parity.

The I/O library (liblely-io) provides a uniform, platform-independent I/O interface for CAN devices, regular files, serial I/O devices and network sockets. Depending on the platform, some or all of these devices can be polled for events, allowing the use of the reactor pattern.

All I/O devices are represented by the same handle (io_handle_t, defined in <lely/io/io.h>), providing a uniform interface. This handle is a reference-counted wrapper around a platform-specific file descriptor or handle. A C++ interface for I/O device handles can be found in <lely/io/io.hpp>.

The following I/O devices are supported:

  • CAN devices (<lely/io/can.h> and `<lely/io/can.hpp>)
  • regular files (<lely/io/file.h> and <lely/io/file.hpp>`)
  • pipes (<lely/io/pipe.h> and <lely/io/pipe.hpp>)
  • serial I/O devices (<lely/io/serial.h> and <lely/io/serial.hpp>)
  • network sockets (<lely/io/sock.h> and <lely/io/sock.hpp>)

Additionally, serial I/O device attributes can be manipulated with the functions in <lely/io/attr.h>, while network addresses and interfaces can be queried and manipulated with the functions in <lely/io/addr.h> and <lely/io/if.h>`, respectively.

Depending on the platform, some or all of the I/O devices can be polled for events. Combined with non-blocking access, this allows an application to use the reactor pattern to concurrently handle multiple I/O channels. The polling interface is provided by <lely/io/poll.h> (and <lely/io/poll.hpp>).

Updated: