Back Home

Transport Layer

The transport layer is a very important feature for ZCM. With this subsystem, end-users can craft their own custom transport implementations to transfer zcm messages. This is achieved with a generic transport interface that specifies the services a transport must provide and how ZCM will use a given transport.

Built-in Transports

With this interface contract, we can implement a wide set of transports. Many common transport protocols have a ZCM variant built-in to the library. The following table shows the built-in transports and the URLs that can be used to summon the transport:

Type URL Format Example Usage
Inter-thread inproc zcm_create("inproc"), zcm_create("inproc://mysubnet")
Inter-process (IPC) ipc://<ipc-subnet> zcm_create("ipc"), zcm_create("ipc://mysubnet")
Nonblocking Inter-thread nonblock-inproc zcm_create("nonblock-inproc")
UDP Multicast udpm://<ipaddr>:<port>?ttl=<ttl> zcm_create("udpm://239.255.76.67:7667?ttl=0")
UDP Unicast udp://<ipaddr>:<sub_port>:<pub_port>?ttl=<ttl> zcm_create("udp://127.0.0.1:9000:9001?ttl=0")
Serial serial://<path-to-device>?baud=<baud> zcm_create("serial:///dev/ttyUSB0?baud=115200")
CAN can://<interface>?msgid=<id> zcm_create("can://can0?msgid=65536")

When no url is provided (i.e. zcm_create(NULL)), the ZCM_DEFAULT_URL environment variable is queried for a valid url.

Custom Transports

While these built-in transports are enough for many applications, there are many situations that can benefit from a custom transport protocol. For this, we can use the transport API. In fact, the transport API is first-class. All transports, even built-in ones, use this API. There is nothing special about the built-in transports. You can even configure a custom transport to use a URL just like above.

The transport API is a C89-style interface. While it is possible to expose this interface to other languages, we don't currently provide this capability. However, you can still implement a custom transport using the C89 API and access it from any other language just like the built-ins.

There are two variants of this interface. One variant is for implementing blocking-style transports, and the other is for non-blocking transports. In most cases on a linux system, the blocking interface is most convenient, but the non-blocking interface can also be used if desired.

All of the core transport code is contained in zcm/transport.h and reading it is highly recommended.

Core Datastructs

To pass message data between the core-library and the transport implementations, the following datastruct is employed:

struct zcm_msg_t
{
    uint64_t utime;  /* 0 means invalid (caller should compute its own utime) */
    const char *channel;
    size_t len;
    char *buf;
};

To implement a polymorphic interface with only C89 code, we use a hand-rolled virtual-table of function pointers to the type. The following struct represents this virtual-table:

struct zcm_trans_methods_t
{
    size_t  (*get_mtu)(zcm_trans_t *zt);
    int     (*sendmsg)(zcm_trans_t *zt, zcm_msg_t msg);
    int     (*recvmsg_enable)(zcm_trans_t *zt, const char *channel, bool enable);
    int     (*recvmsg)(zcm_trans_t *zt, zcm_msg_t *msg, int timeout);
    int     (*update)(zcm_trans_t *zt);
    void    (*destroy)(zcm_trans_t *zt);
};

To make everything work, we need a basetype that is aware of the virtual-table and understands whether it is a blocking or non-blocking style transport. Here is this type:

struct zcm_trans_t
{
    enum zcm_type trans_type;
    zcm_trans_methods_t *vtbl;
};

Creating custom transports requires a bit of pointer coercion, but otherwise is fairly straightforward. Here's is an outline of a typical implementation:

typedef struct
{
    enum zcm_type trans_type;
    zcm_trans_methods_t *vtbl;
    /* transport specific data here */
} my_transport_t;

/* implement the API methods here:
    my_transport_get_mtu
    my_transport_sendmsg
    my_transport_recvmsg_enable
    my_transport_recvmsg
    my_transport_update
    my_transport_destroy
*/

static zcm_trans_methods_t methods = {
    my_transport_get_mtu,
    my_transport_sendmsg,
    my_transport_recvmsg_enable,
    my_transport_recvmsg,
    my_transport_update,
    my_transport_destroy
};

zcm_trans_t *my_transport_create(zcm_url_t *url)
{
    my_transport_t *trans;
    /* construct trans here */

    trans->trans_type = ZCM_BLOCKING;  /* or ZCM_NONBLOCKING */
    trans->vtbl = &methods;            /* setting the virtual-table defined above */

    return (zcm_trans_t *) trans;
}

IMPORTANT: The my_transport_t struct must perfectly mirror the start of zcm_trans_t.

IMPORTANT: The my_transport_create must set the trans_type and vtbl fields

Blocking Transport API Semantics

Non-blocking API Semantics

General Note: None of the non-blocking methods must be thread-safe. This API is designed for single-thread, non-blocking, and minimalist transports (such as those found in embedded). In order to make nonblocking transports friendly to embedded systems, memory for subscriptions is allocated at compile time. You can control the maximum number of subscriptions by defining the preprocessor variable, ZCM_NONBLOCK_SUBS_MAX. By default, this number is 512.

Registering a Transport

Once we've implemented a new transport, we can register its create function with ZCM. Registering a transport allows users to create it using a url just like the built-in transports. To register, we simply need to call the following function (from zcm/transport_registrar.h):

bool zcm_transport_register(const char *name, const char *desc, zcm_trans_create_func *creator);

The name parameter is the simply the name of the transport protocol to use in the url.

If we wanted to register the transport we created above, we could write:

zcm_transport_register("myt", "A custom example transport", my_transport_create);

Now, we can create new my_transport instances with:

zcm_create("myt://endpoint-for-my?param1=abcd&param2=dfg&param3=yui");

This will issue a call to our my_transport_create function with a zcm_url_t

We even can go a step further and register transports in a static-context (i.e. before main)! This can be achieved we either a compiler-specific attribute (in C) or with a static object constructor in C++. The zcm_transport_register function is designed to be static-context safe. To learn more about how to do this in C++, take a look at zcm/transport/transport_zmq_local.cpp

Lastly, ZCM supports using a transport without registering. In this mode, URL parsing is not supported. Instead, the user manually constructs a custom zcm_trans_t* and passes it directly to the core library with the following function:

zcm_t *zcm_create_from_trans(zcm_trans_t *zt);

Blocking vs Non-Blocking Message Handling

In most ways, the distinction between blocking and non-blocking transports are completely transparent to the end user. Internally, ZCM operates differently depending on the type of transport it's using. This allows all of the following to work identically:

However, like all abstractions, this one is not completely leak-free. Despite our best efforts, we were unable to design a reasonable approach to message dispatching. Rather than try to force a broken abstraction that would behave differently depending on mode, we opted to make this difference explicit with distinct API methods.

For the blocking case, there are the following approaches:

For the non-blocking case, there is a single approach:

To prevent errors, the internal library checks that the API method matches the transport type.

Closing Thoughts

While incredibly powerful, the transport API can be tricky to understand and use as a beginner. This appears to primarily be the incidental complexity of our requirements:

For the aspiring transport developer, we recommend reading the following core sources:

It's also great to browse the implementations of built-in transports:

Finally, we love contributions! Check out Contributing.


Back Home