This is an article I’ve been wanting to sit down and write for a few years now. I first started developing software in the late ’90s and got myself a Borland C++ compiler, which I quickly realized was only really going to work on Windows. I made a few small command-line applications at first and then started experimenting with graphical applications. I loved the creative process, but was disappointed by many of the tools. At the time, I didn’t really move beyond adapting simple examples.
After that I got a little distracted with Linux, packaging applications, and porting to a new 64-bit architecture as a Gentoo developer. That was a lot of fun, and I learned a lot about dependencies, security updates, shared libraries, and how bad many scientists were at writing build systems. Throughout this period, I also learned about being part of an extended online community and had the opportunity to work with a lot of very dedicated and skilled people.
C++ and native development
Ultimately, I realized I wanted to develop software, and really wanted to develop in a native language where I had access to the hardware. At the time I used Linux as my main operating system, but also worked with people using Windows and Mac OS X on a regular basis. I wanted them to be able to use the software I developed, and didn’t want to write it three times. This was when I started aligning on a software stack, largely influenced by KDE and the opportunity I had to do a Google Summer of Code project with them. It is now about nine years since I did that Google Summer of Code project, and I am largely using the same stack with some additions/updates to facilitate collaborative, open source, cross-platform development.
C++ is a standardized language with a number of powerful open source compilers and many proprietary compilers that span everything from embedded systems to the biggest supercomputers on the planet. In my work, we regularly target both extremes of the spectrum as well as a lot of work in the desktop/laptop space. I think it is still one of the most diverse languages, with higher level abstraction than C, yet close enough to the metal to get great performance. It also has access to many APIs often exposed through C interfaces and can even interact with FORTRAN when properly coerced.
C++ can be written in a portable way, but there are many platform-specific APIs, toolkits, and language extensions. There are also a number of ways to build a C++ application. I started off with simple handwritten Makefiles, but it soon became obvious that maintaining these for even simple projects was tedious and error prone. I started looking at Autotools, and later SCONS, before coming across CMake right around the time KDE was looking at making the switch.
CMake is a meta build system generator, meaning it doesn’t build anything directly itself. You use the CMake language to specify how your project should be built, and can define exceptions for specific platforms where necessary. This might sound a little tedious too, and it can be, but you get something pretty huge in return—it will generate a build system for your environment. If you want to use Visual Studio, go ahead. If you want Makefiles, great. If you prefer the new Ninja system, you can use that. I normally use Ninja coupled with something called CodeBlocks that gets me Qt Creator integration.
Once you have something that can be built, you want to consider how you might abstract the windowing system. I went through quite a few abstractions here too, including Java Swing, GTKMM (C++ wrappers around GTK), wxWidgets, and a few I think I may have blocked out. I settled on Qt, which used to have a big disadvantage that it was under the GPLv2 license, and so your code must also use the GPL unless you paid for the commercial license. It was always dual-licensed, but I never really liked that approach. It was far better than any of the other abstractions I tried, and felt pragmatic to me, natural, and it had a large community of friendly open source developers in KDE.
Another great thing I always got from Qt was a native look and feel. The abstraction works hard to use native widgets, file dialogs, color pickers, etc. As the toolkit evolved it was extended to support Android and iOS, and it was relicensed as LGPL after Nokia acquired Trolltech. It also has an agreement in place with the KDE e.V. assuring that it will always be free, which provides protections for the future.
Version control, code review
I started out with CVS, and we would generally do code review after things were merged in Gentoo and KDE. This worked pretty well, but required disciplined developers who kept up with the commit messages on the commit list. Switching to Subversion things remained pretty similar, but the tool felt easier to use and had a little more atomicity.
For me, the big revolution was moving to Git. At first, we used it as a simple drop-in replacement with the ability to locally stage commits before pushing them. Later we started looking at more integrated code review, trying out a few solutions. For a while we used Gerrit, but I never felt like the interface was intuitive enough, nor did I like its focus on single commits.
Most of my work now uses GitHub or GitLab, both of which have a strong focus on pull/merge requests that look at branches containing one or more commits. This lets us separate things into separate commits where it makes sense, publish them as they are developed, and request review when it is ready for integration. I like the granularity where I can switch to individual commits to understand why a group of changes were made or look at the diff for the branch. Line-by-line comments allow focused review, and high-level discussion can take place in the main tab.
Automated building and testing
Another important aspect of cross-platform development is automated building and testing. This also ends up getting into building/packaging dependencies, as we rarely write code that doesn’t reuse other libraries. This is another area where CMake provides quite a bit of help, and our practices are evolving.
I think this is one of the more difficult aspects of cross platform development, and some platforms are more difficult than others. The traditional approach has been to use cron jobs to automate build initiation, and drive dashboard scripts on the host that primarily use CMake via CTest to automate builds. They also generate build artifacts that are uploaded to CDash.
Many projects have something we call superbuilds. These automate the building and staging of project dependencies before finally building the project we are interested in. They enable us to ensure everything is built with the right flags and the correct versions. They also interact with CPack in some cases to create installers for different platforms. CDash will display build summaries, automatically created installers, test results, etc.
Cross-platform development is challenging. It’s important to see the results of changes you make on other platforms within hours (or days, at worst). A project needs to consider cross-platform as part of its workflow to achieve optimal results. There is a lot more I could say about our approach, but this article is already very long. You can achieve great results with native code, and scientific applications often have a number of additional dependencies. Once you throw supercomputers and embedded devices into the mix, things become very interesting.