CMake Tutorial

Generating LCM type bindings with CMake

This tutorial will walk you through writing a CMakeLists.txt to generate bindings for your LCM message types. Please note that this is not meant to serve as a general CMake tutorial. This tutorial assumes that you are already familiar with CMake.

Note

This tutorial assumes that you are using CMake 3.1 or later. Some of the features used, especially those related to the creation of a convenient INTERFACE library for C++ bindings, are not available in older versions of CMake. If you are using an older version of CMake, you may need to refer to the implementation details of LCM’s helper functions, found in lcmUtilities.cmake, in order to manually accomplish the tasks that the helper functions would normally do. Note that all of the helper functions require CMake 3.1 or later on Windows, and that it is much more difficult to use lcm-gen on Windows from within CMake prior to CMake 3.1.

Initial Setup

The very first thing you’ll want to do is to find LCM and include its “use file”. (A “use file” is a CMake script provided with some packages that adds utility functions for using that package. Many, including LCM’s, provide additional documentation of their utility functions in the use file itself.) Depending on how you will be using LCM, as well as personal preference, this can be done in your project’s root CMakeLists.txt. This is also a good time to look for additional language components you may want, such as Python and Java. (If you will not need bindings for these languages, you can omit those parts. Alternatively, you may want to make them REQUIRED.)

find_package(lcm REQUIRED)
include(${LCM_USE_FILE})

find_package(PythonInterp)
find_package(Java)

if(JAVA_FOUND)
  include(UseJava)
endif()

The rest of the CMake logic we will show will typically go in the CMakeLists.txt that is located with your LCM type files.

A Simple Example

We’ll start with a very simple example that generates a STATIC library of C language bindings:

lcm_wrap_types(
  C_SOURCES c_sources
  C_HEADERS c_headers
  my_type_1.lcm
  my_type_2.lcm
  ...
)

lcm_add_library(my_lcmtypes C STATIC ${c_sources} ${c_headers})
target_include_directories(my_lcmtypes INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)

The first function creates rules to invoke lcm-gen to generate the binding source files. The names c_sources and c_headers are the names of variables that will receive these lists of files. You can see where we use them again to create the C library. We chose these names as they are both simple and clear, but you can use whatever names you like.

The second function creates a C library, which is STATIC, from the specified sources. Including the header files is optional, but may be beneficial to some IDE’s. This has a couple advantages over plain old add_library. First, it will link the library to LCM for you. Second, and more important, it will set up an additional target to ensure that all of the named bindings have been generated before the sources are compiled. This is important for some generators when one LCM type references another; otherwise, the build tool might try to compile the source file for one type before the header for the referenced type has been generated, resulting in a spurious build error.

Before we call it a day, it would be nice if consumers could find the library’s headers. We do that by adding an interface include directory, like so:

target_link_libraries(my_lcmtypes INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)

Note the use of $<BUILD_INTERFACE> to tell CMake that the include directory should not be included in the installed version of the library. This prevents details of your build from leaking into the install. We’ll handle the include directory for the installed library differently. (If there are no local consumers of your library, you can skip this step.)

The include directory should be the directory where the bindings are generated. Since we didn’t pass a DESTINATION to lcm_wrap_types, it defaulted to the current subdirectory of the build tree (CMAKE_CURRENT_BINARY_DIR).

Shared C Libraries

Static libraries are ugly. They inflate the size of every binary that consumes them, and they can’t be replaced with updated versions without recompiling every consumer.

Of course, as you probably know, creating a shared library on Windows, or if using ELF hidden symbol visibility (which you really, really should be doing), requires a) marking those parts of your API that should be exported, and b) specifying how to mark an exported function. You’ve almost certainly seen headers peppered with preprocessor symbols like LCM_EXPORT, and you probably know that the correct definition of such symbols depends on both the platform and whether you are building or consuming the library.

Fortunately, lcm-gen can take care of the first item for you with only a very little hand-holding, and as you probably already know, CMake has an excellent utility, generate_export_header, for the second. Let’s update our example to use them:

lcm_wrap_types(
  C_EXPORT my_lcmtypes
  C_SOURCES c_sources
  C_HEADERS c_headers
  my_type_1.lcm
  my_type_2.lcm
  ...
)

lcm_add_library(my_lcmtypes C ${c_sources} ${c_headers})
generate_export_header(my_lcmtypes)
target_include_directories(my_lcmtypes INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)

Note the addition of C_EXPORT to our invocation of lcm_wrap_types. This is designed to work with generate_export_header, and will specify both the symbol name and export header name as created by generate_export_header. In this instance, we are using the library name, but you can use whatever you like by passing the same name to C_EXPORT (lcm_wrap_types) and BASE_NAME (generate_export_header).

Note also that we didn’t replace STATIC with SHARED, but rather just removed it. Just like a regular call to add_library, this will generate a library that is either static or shared depending on if BUILD_SHARED_LIBS is enabled. This approach is often preferred as it allows the user to decide if they want static libraries or shared libraries, but you can also use SHARED to enforce building of a shared library.

C++

We probably want to generate at least C++ bindings also. Let’s do that now:

lcm_wrap_types(
  C_EXPORT my_lcmtypes
  C_SOURCES c_sources
  C_HEADERS c_headers
  CPP_HEADERS cpp_headers
  my_type_1.lcm
  my_type_2.lcm
  ...
)

# ...logic for C library...

lcm_add_library(my_lcmtypes-cpp CPP ${cpp_headers})
target_include_directories(my_lcmtypes-cpp INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)

None of this should look surprising after seeing how the C library was created. The additional argument to lcm_wrap_types, CPP_HEADERS, serves both to tell lcm_wrap_types to generate C++ bindings, and what variable should receive the list of C++ headers. C_SOURCES and C_HEADERS work very much the same way; by omitting both, we can skip generation of C bindings. (Unlike CPP_HEADERS, C_SOURCES and C_HEADERS are coupled; it is an error to specify one without the other.)

“But wait,” you may be thinking to yourself, “aren’t LCM C++ bindings header only?” Indeed they are, which is why this “library” is really an INTERFACE library. An INTERFACE library in CMake is just a fancy way of hanging usage requirements (like the target_include_directories, above) off of a target so that consumers can consume a logical target with the same convenience that they can consume targets that have actually binary objects backing them.

There is one critical caveat to this, however. Just like our C library needed to ensure that the bindings are generated before the sources are built, consumers of the C++ bindings need to ensure that the bindings are actually built before the consumer.

CMake 3.3 added the ability to hang dependencies off of INTERFACE libraries, and lcm_add_library will do this for you for the C++ library just as for the C library. Earlier versions of CMake lack this wonderfully convenient feature, but fortunately, LCM can help us out. If your project might be build with CMake earlier than 3.3, using lcm_target_link_libraries when linking a consumer to a C++ bindings library such as shown above will automagically set up a dependency to ensure that the bindings are generated before trying to build the consumer. Just use lcm_target_link_libraries with the same arguments as you would use target_link_libraries. (If your project enforces use of CMake 3.3 or later, or if the bindings you are consuming are build by an external project, just use target_link_libraries.)

Python and Java

C and C++ are great, but you may well have users that want to consume your LCM types with Python or Java. Just as when we added C++, we’ll start by adding some additional arguments to lcm_wrap_types. Depending on whether you generate these bindings always, or opportunistically when Python and/or Java are available, you may want to make this logic conditional, as in the approach shown here:

if(PYTHONINTERP_FOUND)
  set(python_args PYTHON_SOURCES python_sources)
endif()
if(JAVA_FOUND)
  set(java_args JAVA_SOURCES java_sources)
endif()

lcm_wrap_types(
  C_EXPORT my_lcmtypes
  C_SOURCES c_sources
  C_HEADERS c_headers
  CPP_HEADERS cpp_headers
  ${python_args}
  ${java_args}
  my_type_1.lcm
  my_type_2.lcm
  ...
)

If you require Python and/or Java, you can of course simply inline the corresponding arguments as is done for C and C++. Note that the main reason to require that Python is found is so that we can match its version in order to install things to the right place. If you have some other means of determining the correct install location, you could skip requiring that Python is found.

Python doesn’t need to “build” anything (and we’ll come back to installation in a moment), so for now, the only other change is to build the JAR:

if(JAVA_FOUND)
  add_jar(my_lcmtypes-jar
    OUTPUT_NAME my_lcmtypes
    INCLUDE_JARS lcm-java
    SOURCES ${java_sources}
  )
endif()

As before, if you require Java, you can omit the JAVA_FOUND check.

Installing Everything

First, let’s revisit our variable names:

if(PYTHONINTERP_FOUND)
  set(python_args PYTHON_SOURCES python_install_sources)
endif()
if(JAVA_FOUND)
  set(java_args JAVA_SOURCES java_sources)
endif()

lcm_wrap_types(
  C_EXPORT my_lcmtypes
  C_SOURCES c_sources
  C_HEADERS c_install_headers
  CPP_HEADERS cpp_install_headers
  ${python_args}
  ${java_args}
  my_type_1.lcm
  my_type_2.lcm
  ...
)

Note that we’ve added _install_ to a few of the variable names. While this is in no way necessary (recall that the variable names can be whatever you like), it serves to clearly indicate which variables hold the names of files that need to be installed, and which are only files to be compiled.

We’ll skip repeating the rest of the existing logic, but don’t forget to change the names of these variables in the other locations they appear.

Now, we’ll install everything:

lcm_install_headers(DESTINATION include
  ${CMAKE_CURRENT_BINARY_DIR}/my_lcmtypes_export.h
  ${c_install_headers}
  ${cpp_install_headers}
)

if(PYTHONINTERP_FOUND)
  lcm_install_python(${python_install_sources})
endif()

install(TARGETS my_lcmtypes my_lcmtypes-cpp
  EXPORT ${PROJECT_NAME}Targets
  RUNTIME DESTINATION bin
  LIBRARY DESTINATION lib${LIB_SUFFIX}
  ARCHIVE DESTINATION lib${LIB_SUFFIX}
  INCLUDES DESTINATION include
)

if(JAVA_FOUND)
  install_jar(my_lcmtypes-jar share/java)
endif()

The two LCM-provided helper functions we used here, lcm_install_headers and lcm_install_python, are convenience functions that will preserve subdirectory components (particularly, the package name subdirectory of the C++ headers) of the files being installed. Additionally, lcm_install_python chooses the correct destination directory by default. We also use INCLUDES DESTINATION when installing the libraries to set the interface include directories on the libraries. This directory should match the DESTINATION to which the headers are installed.

Note that we specified an EXPORT for the C/C++ libraries, but do not show installing the target exports file or creating build-tree exports. This is because these procedures a) are not specific to LCM, and b) must be done in a single location for the entire project. For similar reasons, we also did not show exporting the JAR file. (LCM itself may be used as an example of these tasks.)

Note that exporting JAR files requires CMake 3.7 or later, or copying UseJava.cmake into your project. See LCM itself for an example of the latter. (Note that consumers of exported JAR’s don’t require CMake 3.7; the created export files are perfectly usable with much older versions of CMake.)

Other Useful Tidbits

We did not cover every possible argument to lcm_wrap_types. Most of the options accepted by lcm-gen are available through lcm_wrap_types. See lcmUtilities.cmake and lcm-gen --help for more information on what options are available.