Project modularization using object libraries

Hello Everyone, I hope you’re all doing well!

While i have some experience developing, i’m still trying to get familiar with CMake and trying to come up with a practical layout for a kinda-large library i’m working on. It’s intended to be an open-source (not yet public though) library, ideally built statically, but allowing the end users to build dynamically as well.

The project will consist on:

  • A main library (that should build either static or shared)
  • Bindings for python and other languages
  • Probably a native app that will use such library

The library is currently structured as follows:

/CMakeLists.txt
/include/main.hpp
/src/main.cpp
/basics/CMakeLists.txt
/basics/libA/CMakeLists.txt
/basics/libA/include/basics/libA
/basics/libA/src
/basics/libB/CMakeLists.txt
/basics/libB/include/basics/libB
/basics/libB/src

/examples/CMakeLists.txt
/examples/ex01.cpp

the Main library depends on libA which in turn depends on libB (both built as OBJECT libraries).

Code looks somewhat like this:

/basic/libA/CMakeLists.txt

add_library(A OBJECT)
target_include_directories(A include/)
target_sources(A PRIVATE src/a.cpp)

/basic/libB/CMakeLists.txt

add_library(B OBJECT)
target_include_directories(B include/)
target_sources(B PRIVATE src/b.cpp)
target_link_libraries(B PUBLIC A)

/basics/CMakeLists.txt

add_subdirectory(libA)
add_subdirectory(libB)

/CMakeLists.txt

add_library(main)
target_include_directories(main include/)
target_sources(main PRIVATE src/main.cpp)
target_link_libraries(main 
    PUBLIC A
    PUBLIC B)

/examples/CMakeLists.txt

add_executable(ex01 ex01.cpp)
target_link_libraries(ex01 main)

So far I was mostly working on writing logic, and unit tests. Since I’m linking the tests directly with object libs, everything was working smoothly. Now, when I decided to build an actual example using the library, things didn’t work unless I linked the example directly with ALL object libraries as well.

When I build my example and link against my Main library, it fails to link because of unresolved symbols in libB.

Inspecting the static lib with nm confirms this, showing an U next to the symbols for which the linker complains.

Now, i’ve done some reading and I see that object libraries don’t propagate transitively, only the usage requirements from them do (at least when using only target_link_libraries).

Is there any way for OBJECT libraries to expose their build requirements and dependencies as well? I’ve read about using the TARGET_OBJECTS generator inside the add_library directive, but my understanding was that it only propagated the objects, and not the usage/interface requirements.

Is it possible (and convenient) to have this degree of modularization?
Are object libraries intended to be used this way?

Sorry for the long post, and my english, and thank you very much in advance for taking the time to read!

1 Like

You’ve chosen a tricky situation to get familiar with CMake. There is one detail you should be aware of in this case: for shared libraries on Windows, you’ll need each OBJECT library to also privately specify the export symbols for dependent OBJECT libraries because they’ll end up in the same library and need to know that they come from “the same library” rather than “from outside”.

The non-inheritibility of the objects themselves through usage requirements (to avoid adding the same object multiple times into a library and triggering duplicate symbols) was an important design decision when we added it. It is odd that A’s objects are not available in main, but maybe you’re hitting the export symbol issue mentioned above?

1 Like

Thank you for the response @ben.boeckel !

While the goal is to be compatible across windows/linux & MAC, I’m hitting this issue with CLANG in macos, and I’m on purpose no setting any CXX_VISIBILITY variable to hidden.

So, if I use the OBJECT library approach on windows i’ll need to create export macros for each object library?

If OBJECT libraries don’t support doing this? what is the best way to accomplish this level of modularization? I’ve been looking at some popular large c++ repos and see that most people just throw huge large of files on a single CMakeLists.txt, although most of them also rely on relatively old versions of CMake. Is this usually the preffered approach when building a library that consists of multiple components?

Would you mind checking if this can be an acceptable workaround? It basically required that I link all object libraries to the main one, and not only the one i’m using directly. I’m guessing this should work properly if i had say a third obj-lib c on which both a and b depended, right?

Once again, thank you very much!

My general advice regarding libraries is to only use object libraries if you have no other choice. While it may be a more familiar paradigm for some people coming from a more traditional Makefile background where you manage object files directly, a regular static library will very often be the better choice and be much simpler to work with.

3 Likes

Thank you very much @craig.scott.

Would you mind sharing what the shortcoming would be when using the approach above?

I’ve seen some articles where people build many small static libs (one for each module) and then use ar/libtool to combine them into a single shippable archive. Is this the recommended practice? Would this work if the end-user has passed -DBUILD_SHARED_LIBS?

Thanks!

You can use add_library(libname STATIC) to force a target to be a static library regardless of BUILD_SHARED_LIBS’ value.

@ben.boeckel Thanks!. I know about that, but if the end user wants to build a shared library I assume i would need a specific command to package all the static libraries inside the final shared one.

I think I read some hacks for doing this as well. But My main concern is whether the strategy previously discussed is if this a valid approach for structuring a project consisting of many modules, which will be used (in different combinations) to build both a library and an application (using different subsets of the aforementioned modules).

To give you an idea of the modules I have:

  • An Expected template class that is used by almost every other module
  • A simple event loop
  • An http client that works on top of such loop
  • A logger that is used by almost every other module as well
  • Business-logic specific components that depends on all of the modules mentioned below

Thank you once again folks!

Just created the account to say thank you. I was looking for a simple solution to build a library which depends on many other modules I would like to let the user compile as a standalone or as the whole big library. So now I can focus on defining the dependencies for the standalone parts. Thank you very much, I wasn’t able to find something like this on StackOverflow!

Would you mind sharing what the shortcoming would be when using the approach above?

+1. Apologies for the :ghost:post, but also curious about the reasons, since someone pointed me to this comment as a point against using object libs. VTK appears to use OBJECT libs for all modules; ITK and ParaView do not (AFAICT).

Object libraries have some differences to static libraries which can catch people out.

  • They only add their objects to things that link to them directly. Usage requirements are carried transitively to consumers (meaning things like PUBLIC compile flags and header search paths can propagate to consumers of consumers), but the objects themselves do not. A relatively common query we see in the forums and in CMake’s issue tracker is people mistakenly expecting objects to be added transitively. In comparison, static libraries work intuitively when linked through multiple levels of dependencies. CMake will propagate linking them as needed, even when something links to them as PRIVATE (static libraries need to propagate their used symbols up to the shared library, module library or executable).
  • Adding objects to a shared library, module library or executable has a different effect to linking a static library into a shared library, module library or executable. When linking a static library, the linker will only need to go looking in the static library for symbols it has not yet been able to resolve. If nothing uses a symbol, that symbol won’t be added to the binary from the static library (unless you add linker flags to specifically ask for that behavior, which should be rare). Adding objects is different, it puts all the objects’ symbols into the linker’s “need to resolve these” bucket. Depending on linker flags, the symbols provided by objects might not be discarded and therefore can still be present in the final binary, even if nothing actually uses them. I’ve seen projects take advantage of this to add code which is useful when run from within gdb, but not otherwise executed by anything in the program (think: pretty-printing custom types).

There are probably more reasons I’ve temporarily forgotten, but those are the two main ones that come to mind.

2 Likes

OBJECT libraries are an implementation detail of “kit” builds. Basically it allows for decoupling the target properties from the resulting target artifact so that multiple sets of target objects can live in a single shared library at the end (in order to reduce runtime loader work on HPC machines where MPI and shared filesystems can be a significant overhead). It’s complicated and something I only recommend following if you really need it (i.e., a customer reported a problem and this complexity is worth how much it solves the problem at hand).

ParaView uses this same code via the VTK API.

1 Like

I followed the example code describing how target_link_libraries() works with OBJECT libraries and built a small project myself, with a CMakeLists.txt file I copy-pasted from the example.

The example only describes the CMakeLists.txt file, so each of my source files calls a function from a source file it depends on:

// a.c
void a() {}

// obj.c
void a(); // target_link_libraries(obj PUBLIC A)
void obj() { a(); }

// b.c
void obj(); // target_link_libraries(B PUBLIC obj)
void b() { obj(); }

// main.c
void b(); // target_link_libraries(main B)
int main() { b(); }

// obj2.c
void obj(); // `target_link_libraries(obj2 PUBLIC obj)`
void obj2() { obj(); }

// main2.c
void obj2(); // `target_link_libraries(main2 obj2)`
int main() { obj2(); }

make main builds properly, but make main2 crashes the build with

~/cmake-test/build$ make VERBOSE=1 main2
[ 83%] Building C object CMakeFiles/main2.dir/main2.c.o
/usr/bin/clang -DA -DOBJ  -Os -DNDEBUG -MD -MT CMakeFiles/main2.dir/main2.c.o -MF CMakeFiles/main2.dir/main2.c.o.d -o CMakeFiles/main2.dir/main2.c.o -c /home/jhemphill/cmake-test/main2.c
[100%] Linking C executable main2
/usr/bin/cmake -E cmake_link_script CMakeFiles/main2.dir/link.txt --verbose=1
/usr/bin/clang -Os -DNDEBUG CMakeFiles/main2.dir/main2.c.o CMakeFiles/obj2.dir/obj2.c.o -o main2  -Wl,-rpath,/home/jhemphill/cmake-test/build libA.so 
/usr/bin/ld: CMakeFiles/obj2.dir/obj2.c.o: in function `obj2':
obj2.c:(.text+0x11): undefined reference to `obj'

I feel like this is as close to a copy-paste of the official documentation as I can build, and it fails because cmake doesn’t link main2 to obj. Is this somehow not a faithful representation of the given example, or of the real-world use-case that it represents?

A and B are SHARED. Did you make them OBJECT libraries?

No, A and B are marked as SHARED in my CMakeLists.txt file, just as they are in the documentation.

I think this makes sense; maybe the tutorial needs updated? OBJECT libraries only add their objects to targets which directly link to them; other ways of access only get the usage requirements. This ends up working:

target_link_libraries(obj2 INTERFACE obj2.o $<TARGET_OBJECTS:obj> $<TARGET_OBJECTS:obj2.o>)

Cc: @betsy.mcphail

Yep, that works!